Assign printers to a MACHINE and let whichever PC controls it install them, with the observed half kept strictly apart from the assigned half: a collector report can never write an assignment row. Adoption is explicit. Adds a singular relationship type so defaultprinter can mean exactly one, client scripts that converge a bay's queues and correct a drifted one in place, and a wave gate so a fleet-wide driver rollout cannot stampede its own share. On the container side: the compose stack never persisted the instance directory, so a rebuild discarded plugins.json and every upload. It is a volume now, the image ships instance/ owned by the app user so the mountpoint is writable, and the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended since 8.0 went end of life. The image also could not build at all: the frontend stage never copied the plugin staging script it runs. Adds a topology migration guide covering IIS to Docker and back.
135 lines
4.4 KiB
Vue
135 lines
4.4 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">
|
|
<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, onMounted } from 'vue'
|
|
import { settingsApi } from '@/api'
|
|
import api from '@/api'
|
|
|
|
const RANGES_KEY = 'computers_routableranges'
|
|
|
|
const ranges = ref('')
|
|
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 || ''
|
|
} 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>
|