226 lines
7.5 KiB
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>
|