The USB kiosk had a feature flag and nothing else. A flag is a decision, not a credential: with the kiosk on, anyone who could reach the app could move a drive between checked-out and checked-in and sign it with a badge they guessed. Same reasoning that closed the parts kiosk, smaller blast radius - which is why Darkwing did not raise it and why it was worth asking about anyway. Both kiosks now share one mechanism. The device token moved out of the printedparts api wrapper into a request interceptor that attaches X-Kiosk-Token by URL prefix, with one localStorage key per kiosk - they are separate screens in separate rooms, and a shared token would mean provisioning one grants the other. claimKioskToken() is shared too, so both views take the token from a ?token= URL once and strip it from the address bar identically. Then the part that matters more than either fix. I told you to set the new settings in the UI, and that was impossible: every plugin settings page carries a hardcoded key list, and nothing had added them. printedparts_kiosk_enabled, printedparts_kiosk_token and usb_kiosk_token were declared, seeded and read correctly, and rendered by no page - changeable only by writing SQL. tests/test_plugin_settings_reachable.py now fails when any declared setting appears nowhere in its plugin's frontend. It found seven MORE that were already in that state, all pre-existing: collector_corporate_ranges computers_configpolicies_enabled computers_machinelink_alerts computers_quietreporthours geenforce_reportstaleminutes printers_dashboardpercent warranty_expiringdays All seven now have fields. I first parked them in a KNOWN_GAPS ratchet, then fixed them and deleted the ratchet - a backlog list in a new check is a way of agreeing the check does not apply yet. There is no backlog; the check is absolute. Two of my own mistakes on the way, both caught by tests rather than by reading: the blanket rename of the USB guard also rewrote the call inside _kiosk_open, so it called itself until the stack ran out; and the first version of the reachable check skipped all 17 plugins because it looked for get_settings() when the method is get_settings_defaults() - it passed by testing nothing, which is the exact shape it exists to catch. Suite 2336, frontend 316, build clean. Scope: product
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>
|