Files
shopdb-flask/plugins/computers/frontend/views/CollectorSettings.vue

226 lines
7.5 KiB
Vue

<template>
<div>
<div class="page-header">
<h2>Asset reporter</h2>
</div>
<p class="setting-description">
Shop-floor PCs report what they are to this server. The script below is
generated with THIS site's values, so it downloads ready to deploy - there
is nothing in it to find and edit.
</p>
<div class="card form-card">
<h3>Collector behaviour</h3>
<div class="form-group">
<label>Corporate ranges (CIDR)</label>
<input v-model="extra.collector_corporate_ranges" type="text"
class="form-control" placeholder="10.0.0.0/8, 192.168.0.0/16" />
<p class="field-hint">
Comma-separated CIDRs naming this site's routable networks. The server
picks a PC's primary address from these when the agent reports several.
</p>
</div>
<div class="form-group">
<label>Quiet report hours</label>
<input v-model.number="extra.computers_quietreporthours" type="number"
min="1" class="form-control" />
<p class="field-hint">
Hours without a collector report before a PC counts as silent on the
dashboard.
</p>
</div>
<div class="form-group">
<label class="check-row">
<input type="checkbox" v-model="machinelinkAlerts" />
<span>Alert when a PC takes over another PC's machine</span>
</label>
</div>
<div class="form-group">
<label class="check-row">
<input type="checkbox" v-model="configPoliciesEnabled" />
<span>Configuration and business policies</span>
</label>
<p class="field-hint">
Master switch for the RMM-style policy pages. Off by default.
</p>
</div>
<button class="btn btn-primary" :disabled="savingExtra" @click="saveExtra">
{{ savingExtra ? 'Saving...' : 'Save' }}
</button>
</div>
<div class="card form-card">
<div v-if="message" class="settings-success">{{ message }}</div>
<div v-if="error" class="error-message">{{ error }}</div>
<div class="form-group">
<label>Routable ranges</label>
<input v-model="ranges" type="text" class="form-control"
placeholder="10.20.0.0/23,10.21.4.0/26" />
<p class="field-hint">
Comma-separated CIDRs for this site's corporate network. A bay with two
NICs - a private controller NIC and a routable one - reports the
address in these ranges. Leave it empty and the PC reports whichever
NIC carries the default route, which is correct at most sites and needs
no configuration.
</p>
</div>
<button class="btn btn-primary" @click="save" :disabled="saving">
{{ saving ? 'Saving...' : 'Save' }}
</button>
</div>
<div class="card form-card">
<h3>Download the reporter</h3>
<p class="field-hint">
Stamped with this server's URL and the ranges above, and with the version
that generated it, so a script found on a bay can be traced back here.
Re-download after upgrading ShopDB.
</p>
<button class="btn btn-secondary" @click="download" :disabled="downloading">
{{ downloading ? 'Generating...' : 'Download Report-AssetToShopDB.ps1' }}
</button>
<p v-if="digest" class="field-hint mono">
SHA-256 {{ digest }}
</p>
<p class="field-hint">
<strong>The collector key is not in this file, deliberately.</strong> It
lands on every shop-floor PC, and a token spread across hundreds of bays
cannot be rotated quietly. Mint a token scoped to
<code>collector.ingest</code> and provision it as
<code>HKLM:\SOFTWARE\GE\ShopDB</code> value <code>CollectorKey</code> -
the adoption guide has worked examples for Intune, DSC and GE-Enforce.
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { settingsApi } from '@/api'
import api from '@/api'
const RANGES_KEY = 'computers_routableranges'
const ranges = ref('')
// The rest of the plugin's settings. They were declared and seeded but rendered
// nowhere, so a site could only change them with SQL - see
// tests/test_plugin_settings_reachable.py, written after that shipped twice.
const EXTRA_KEYS = [
'collector_corporate_ranges',
'computers_quietreporthours',
'computers_machinelink_alerts',
'computers_configpolicies_enabled',
]
const extra = ref({
collector_corporate_ranges: '',
computers_quietreporthours: 24,
computers_machinelink_alerts: 'false',
computers_configpolicies_enabled: 'false',
})
const savingExtra = ref(false)
// Settings store strings; bridge the checkboxes rather than letting a boolean
// round-trip into the string 'True'.
function boolSetting(key) {
return computed({
get: () => String(extra.value[key]).toLowerCase() === 'true',
set: (on) => { extra.value[key] = on ? 'true' : 'false' },
})
}
const machinelinkAlerts = boolSetting('computers_machinelink_alerts')
const configPoliciesEnabled = boolSetting('computers_configpolicies_enabled')
async function saveExtra() {
savingExtra.value = true
error.value = ''
message.value = ''
try {
for (const key of EXTRA_KEYS) {
await settingsApi.update(key, String(extra.value[key]))
}
message.value = 'Saved.'
} catch (saveError) {
error.value = saveError?.response?.data?.data?.error?.message || 'Save failed.'
} finally {
savingExtra.value = false
}
}
const saving = ref(false)
const downloading = ref(false)
const digest = ref('')
const message = ref('')
const error = ref('')
onMounted(async () => {
try {
const response = await settingsApi.list({ category: 'computers' })
const row = (response.data.data || []).find(entry => entry.key === RANGES_KEY)
if (row) ranges.value = row.value || ''
for (const entry of response.data.data || []) {
if (EXTRA_KEYS.includes(entry.key)) extra.value[entry.key] = entry.value
}
} catch (loadError) {
error.value = 'Could not load settings'
console.error(loadError)
}
})
async function save() {
saving.value = true
message.value = ''
error.value = ''
try {
await settingsApi.update(RANGES_KEY, String(ranges.value ?? ''))
message.value = 'Saved. Re-download the script so it carries the new ranges.'
} catch (saveError) {
error.value = saveError.response?.data?.data?.error?.message || 'Save failed'
} finally {
saving.value = false
}
}
async function download() {
downloading.value = true
error.value = ''
try {
// responseType text: this is a script, not JSON, and the hash the server
// publishes is of exactly these bytes.
const response = await api.get('/computers/client-script', { responseType: 'text' })
digest.value = response.headers['x-script-sha256'] || ''
const blob = new Blob([response.data], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'Report-AssetToShopDB.ps1'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
} catch (downloadError) {
error.value = downloadError.response?.status === 403
? 'Only an administrator can download the reporter'
: 'Could not generate the script'
console.error(downloadError)
} finally {
downloading.value = false
}
}
</script>
<style scoped>
.mono { font-family: monospace; word-break: break-all; }
.form-card h3 { margin-top: 0; }
</style>