Make gauge/maintenance identifiers per-asset-type and extend to all types

Optional asset identifiers (gauge lab reference, maintenance reference, FQDN)
were global per-identifier and only surfaced on equipment. Now they are
toggleable per asset type and rendered on every asset type.

- settings: replace 3 global identifier toggles with a per-type matrix. New
  keys identifier_<name>_<assettype>_enabled (3 identifiers x 4 types).
  IDENTIFIER_LABELS / IDENTIFIER_ASSETTYPES constants drive the seed (API seed
  and CLI seed settings).
- composable: identifierSettings now exposes isEnabled(name, assettype),
  per-type flag winning over the legacy global key, defaulting on.
- backend writes: computers, network, printers asset create + update now
  accept gaugelabreference and maintenancereference (equipment already did).
  Reads already flowed through Asset.to_dict.
- frontend: Settings page renders an identifier x asset-type toggle matrix.
  Equipment, PC, printer, network forms and detail pages show gauge/maintenance
  (and FQDN where applicable) gated by isEnabled(name, type).

Legacy global identifier_<name>_enabled keys are still honored as a fallback
for older installs. SystemSettings toggles upsert (create on 404) so a deploy
that has not re-seeded still works on first toggle.

144 tests pass, naming/style check green, frontend builds. Verified live:
matrix renders, PC form shows the fields, PUT persists gauge/maintenance on a
PC and reads back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 16:34:50 -04:00
parent b567de14ac
commit 37ffb4add5
15 changed files with 330 additions and 109 deletions

View File

@@ -1,36 +1,51 @@
// Global enable/disable flags for optional asset identifiers, read from the // Per-type enable/disable flags for optional asset identifiers, read from the
// settings table. Loaded once and shared across components. Defaults to // settings table. Keys follow identifier_<name>_<assettype>_enabled. A legacy
// enabled when a flag is missing so a fresh install shows the identifiers. // global key identifier_<name>_enabled is honored as a fallback for older
// installs. Missing = enabled, so a fresh install shows every identifier.
import { reactive } from 'vue' import { reactive } from 'vue'
import { settingsApi } from '../api' import { settingsApi } from '../api'
const identifierflags = reactive({ // scope[name][assettype] = boolean. legacy[name] = boolean (old global flag).
gaugelabreference: true, const state = reactive({
maintenancereference: true, scope: {},
fqdn: true, legacy: {},
loaded: false loaded: false
}) })
let inflight = null let inflight = null
export function useIdentifierFlags() { function loadFlags() {
if (!identifierflags.loaded && !inflight) { if (!state.loaded && !inflight) {
inflight = settingsApi.list() inflight = settingsApi.list()
.then(({ data }) => { .then(({ data }) => {
const map = {} ;(data.data || []).forEach(s => {
;(data.data || []).forEach(s => { map[s.key] = s.value }) const match = /^identifier_(.+?)(?:_(equipment|computer|printer|network_device))?_enabled$/.exec(s.key)
if ('identifier_gaugelabreference_enabled' in map) { if (!match) return
identifierflags.gaugelabreference = map.identifier_gaugelabreference_enabled !== false const name = match[1]
} const assettype = match[2]
if ('identifier_maintenancereference_enabled' in map) { if (assettype) {
identifierflags.maintenancereference = map.identifier_maintenancereference_enabled !== false if (!state.scope[name]) state.scope[name] = {}
} state.scope[name][assettype] = s.value !== false
if ('identifier_fqdn_enabled' in map) { } else {
identifierflags.fqdn = map.identifier_fqdn_enabled !== false state.legacy[name] = s.value !== false
} }
identifierflags.loaded = true })
state.loaded = true
}) })
.catch(() => { identifierflags.loaded = true }) .catch(() => { state.loaded = true })
} }
return identifierflags }
// True when identifier `name` should show on `assettype`. Per-type flag wins,
// then the legacy global flag, then default-on.
function isEnabled(name, assettype) {
const perType = state.scope[name]
if (perType && assettype in perType) return perType[assettype]
if (name in state.legacy) return state.legacy[name]
return true
}
export function useIdentifierFlags() {
loadFlags()
return { state, isEnabled }
} }

View File

@@ -68,11 +68,11 @@
<span class="info-label">Name</span> <span class="info-label">Name</span>
<span class="info-value">{{ equipment.name }}</span> <span class="info-value">{{ equipment.name }}</span>
</div> </div>
<div class="info-row" v-if="identifierflags.gaugelabreference && equipment.gaugelabreference"> <div class="info-row" v-if="isEnabled('gaugelabreference', 'equipment') && equipment.gaugelabreference">
<span class="info-label">Gauge Lab Reference</span> <span class="info-label">Gauge Lab Reference</span>
<span class="info-value mono">{{ equipment.gaugelabreference }}</span> <span class="info-value mono">{{ equipment.gaugelabreference }}</span>
</div> </div>
<div class="info-row" v-if="identifierflags.maintenancereference && equipment.maintenancereference"> <div class="info-row" v-if="isEnabled('maintenancereference', 'equipment') && equipment.maintenancereference">
<span class="info-label">Maintenance Reference</span> <span class="info-label">Maintenance Reference</span>
<span class="info-value mono">{{ equipment.maintenancereference }}</span> <span class="info-value mono">{{ equipment.maintenancereference }}</span>
</div> </div>
@@ -240,7 +240,7 @@ import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '../../composables/identifierSettings'
const route = useRoute() const route = useRoute()
const identifierflags = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()
const loading = ref(true) const loading = ref(true)
const equipment = ref(null) const equipment = ref(null)

View File

@@ -34,8 +34,8 @@
</div> </div>
</div> </div>
<div class="form-row" v-if="identifierflags.gaugelabreference || identifierflags.maintenancereference"> <div class="form-row" v-if="isEnabled('gaugelabreference', 'equipment') || isEnabled('maintenancereference', 'equipment')">
<div class="form-group" v-if="identifierflags.gaugelabreference"> <div class="form-group" v-if="isEnabled('gaugelabreference', 'equipment')">
<label for="gaugelabreference">Gauge Lab Reference</label> <label for="gaugelabreference">Gauge Lab Reference</label>
<input <input
id="gaugelabreference" id="gaugelabreference"
@@ -46,7 +46,7 @@
<small class="form-help">Authoritative gauge lab asset reference (if tracked)</small> <small class="form-help">Authoritative gauge lab asset reference (if tracked)</small>
</div> </div>
<div class="form-group" v-if="identifierflags.maintenancereference"> <div class="form-group" v-if="isEnabled('maintenancereference', 'equipment')">
<label for="maintenancereference">Maintenance Reference</label> <label for="maintenancereference">Maintenance Reference</label>
<input <input
id="maintenancereference" id="maintenancereference"
@@ -358,7 +358,7 @@ import Modal from '../../components/Modal.vue'
import { currentTheme } from '../../stores/theme' import { currentTheme } from '../../stores/theme'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '../../composables/identifierSettings'
const identifierflags = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -111,6 +111,14 @@
<span class="info-label">Serial Number</span> <span class="info-label">Serial Number</span>
<span class="info-value mono">{{ device.serialnumber || '-' }}</span> <span class="info-value mono">{{ device.serialnumber || '-' }}</span>
</div> </div>
<div class="info-row" v-if="isEnabled('gaugelabreference', 'network_device') && device.gaugelabreference">
<span class="info-label">Gauge Lab Reference</span>
<span class="info-value mono">{{ device.gaugelabreference }}</span>
</div>
<div class="info-row" v-if="isEnabled('maintenancereference', 'network_device') && device.maintenancereference">
<span class="info-label">Maintenance Reference</span>
<span class="info-value mono">{{ device.maintenancereference }}</span>
</div>
<div class="info-row"> <div class="info-row">
<span class="info-label">Vendor</span> <span class="info-label">Vendor</span>
<span class="info-value">{{ device.networkdevice?.vendorname || '-' }}</span> <span class="info-value">{{ device.networkdevice?.vendorname || '-' }}</span>
@@ -181,6 +189,9 @@ import { Network, Router, Shield, Wifi, Camera, Server, Server as Rack, Globe }
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import { networkApi } from '../../api' import { networkApi } from '../../api'
import AssetRelationships from '../../components/AssetRelationships.vue' import AssetRelationships from '../../components/AssetRelationships.vue'
import { useIdentifierFlags } from '../../composables/identifierSettings'
const { isEnabled } = useIdentifierFlags()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -54,6 +54,27 @@
</div> </div>
</div> </div>
<div class="form-row" v-if="isEnabled('gaugelabreference', 'network_device') || isEnabled('maintenancereference', 'network_device')">
<div class="form-group" v-if="isEnabled('gaugelabreference', 'network_device')">
<label for="gaugelabreference">Gauge Lab Reference</label>
<input
id="gaugelabreference"
v-model="form.gaugelabreference"
type="text"
class="form-control"
/>
</div>
<div class="form-group" v-if="isEnabled('maintenancereference', 'network_device')">
<label for="maintenancereference">Maintenance Reference</label>
<input
id="maintenancereference"
v-model="form.maintenancereference"
type="text"
class="form-control"
/>
</div>
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="locationid">Location</label> <label for="locationid">Location</label>
@@ -229,6 +250,9 @@ import {
assetsApi, assetsApi,
businessunitsApi businessunitsApi
} from '../../api' } from '../../api'
import { useIdentifierFlags } from '../../composables/identifierSettings'
const { isEnabled } = useIdentifierFlags()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -240,6 +264,8 @@ const form = ref({
assetnumber: '', assetnumber: '',
name: '', name: '',
serialnumber: '', serialnumber: '',
gaugelabreference: '',
maintenancereference: '',
statusid: '', statusid: '',
locationid: '', locationid: '',
businessunitid: '', businessunitid: '',
@@ -332,6 +358,8 @@ async function loadDevice() {
form.value.assetnumber = data.assetnumber || '' form.value.assetnumber = data.assetnumber || ''
form.value.name = data.name || '' form.value.name = data.name || ''
form.value.serialnumber = data.serialnumber || '' form.value.serialnumber = data.serialnumber || ''
form.value.gaugelabreference = data.gaugelabreference || ''
form.value.maintenancereference = data.maintenancereference || ''
form.value.statusid = data.statusid || '' form.value.statusid = data.statusid || ''
form.value.locationid = data.locationid || '' form.value.locationid = data.locationid || ''
form.value.businessunitid = data.businessunitid || '' form.value.businessunitid = data.businessunitid || ''
@@ -365,6 +393,8 @@ async function submitForm() {
assetnumber: form.value.assetnumber, assetnumber: form.value.assetnumber,
name: form.value.name || null, name: form.value.name || null,
serialnumber: form.value.serialnumber || null, serialnumber: form.value.serialnumber || null,
gaugelabreference: form.value.gaugelabreference || null,
maintenancereference: form.value.maintenancereference || null,
statusid: form.value.statusid || null, statusid: form.value.statusid || null,
locationid: form.value.locationid || null, locationid: form.value.locationid || null,
businessunitid: form.value.businessunitid || null, businessunitid: form.value.businessunitid || null,

View File

@@ -16,7 +16,7 @@
<div class="hero-content"> <div class="hero-content">
<div class="hero-title"> <div class="hero-title">
<h1>{{ computer.assetnumber }}</h1> <h1>{{ computer.assetnumber }}</h1>
<span v-if="identifierflags.fqdn && computer.computer?.hostname" class="hero-alias">{{ computer.computer.hostname }}</span> <span v-if="isEnabled('fqdn', 'computer') && computer.computer?.hostname" class="hero-alias">{{ computer.computer.hostname }}</span>
</div> </div>
<div class="hero-meta"> <div class="hero-meta">
<span class="badge badge-lg badge-info">Computer</span> <span class="badge badge-lg badge-info">Computer</span>
@@ -57,7 +57,7 @@
<span class="info-label">Name</span> <span class="info-label">Name</span>
<span class="info-value">{{ computer.name }}</span> <span class="info-value">{{ computer.name }}</span>
</div> </div>
<div class="info-row" v-if="identifierflags.fqdn && computer.computer?.hostname"> <div class="info-row" v-if="isEnabled('fqdn', 'computer') && computer.computer?.hostname">
<span class="info-label">Hostname</span> <span class="info-label">Hostname</span>
<span class="info-value mono">{{ computer.computer.hostname }}</span> <span class="info-value mono">{{ computer.computer.hostname }}</span>
</div> </div>
@@ -65,6 +65,14 @@
<span class="info-label">Serial Number</span> <span class="info-label">Serial Number</span>
<span class="info-value mono">{{ computer.serialnumber }}</span> <span class="info-value mono">{{ computer.serialnumber }}</span>
</div> </div>
<div class="info-row" v-if="isEnabled('gaugelabreference', 'computer') && computer.gaugelabreference">
<span class="info-label">Gauge Lab Reference</span>
<span class="info-value mono">{{ computer.gaugelabreference }}</span>
</div>
<div class="info-row" v-if="isEnabled('maintenancereference', 'computer') && computer.maintenancereference">
<span class="info-label">Maintenance Reference</span>
<span class="info-value mono">{{ computer.maintenancereference }}</span>
</div>
</div> </div>
</div> </div>
@@ -210,7 +218,7 @@ import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '../../composables/identifierSettings'
const route = useRoute() const route = useRoute()
const identifierflags = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()
const loading = ref(true) const loading = ref(true)
const computer = ref(null) const computer = ref(null)

View File

@@ -34,7 +34,7 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group" v-if="identifierflags.fqdn"> <div class="form-group" v-if="isEnabled('fqdn', 'computer')">
<label for="hostname">Hostname</label> <label for="hostname">Hostname</label>
<input <input
id="hostname" id="hostname"
@@ -55,6 +55,28 @@
</div> </div>
</div> </div>
<div class="form-row" v-if="isEnabled('gaugelabreference', 'computer') || isEnabled('maintenancereference', 'computer')">
<div class="form-group" v-if="isEnabled('gaugelabreference', 'computer')">
<label for="gaugelabreference">Gauge Lab Reference</label>
<input
id="gaugelabreference"
v-model="form.gaugelabreference"
type="text"
class="form-control"
/>
</div>
<div class="form-group" v-if="isEnabled('maintenancereference', 'computer')">
<label for="maintenancereference">Maintenance Reference</label>
<input
id="maintenancereference"
v-model="form.maintenancereference"
type="text"
class="form-control"
/>
</div>
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="machinetypeid">PC Type *</label> <label for="machinetypeid">PC Type *</label>
@@ -276,7 +298,7 @@ import Modal from '../../components/Modal.vue'
import { currentTheme } from '../../stores/theme' import { currentTheme } from '../../stores/theme'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '../../composables/identifierSettings'
const identifierflags = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -301,6 +323,8 @@ const form = ref({
alias: '', alias: '',
hostname: '', hostname: '',
serialnumber: '', serialnumber: '',
gaugelabreference: '',
maintenancereference: '',
machinetypeid: '', machinetypeid: '',
statusid: '', statusid: '',
vendorid: '', vendorid: '',
@@ -371,6 +395,8 @@ onMounted(async () => {
alias: pc.name && pc.name.toUpperCase() !== 'NONE' ? pc.name : '', alias: pc.name && pc.name.toUpperCase() !== 'NONE' ? pc.name : '',
hostname: ext.hostname || '', hostname: ext.hostname || '',
serialnumber: pc.serialnumber || '', serialnumber: pc.serialnumber || '',
gaugelabreference: pc.gaugelabreference || '',
maintenancereference: pc.maintenancereference || '',
machinetypeid: ext.computertypeid || '', machinetypeid: ext.computertypeid || '',
statusid: pc.statusid || '', statusid: pc.statusid || '',
vendorid: ext.vendorid || '', vendorid: ext.vendorid || '',
@@ -423,6 +449,8 @@ async function savePC() {
assetnumber: form.value.machinenumber, assetnumber: form.value.machinenumber,
hostname: form.value.hostname || null, hostname: form.value.hostname || null,
serialnumber: form.value.serialnumber || null, serialnumber: form.value.serialnumber || null,
gaugelabreference: form.value.gaugelabreference || null,
maintenancereference: form.value.maintenancereference || null,
computertypeid: form.value.machinetypeid || null, computertypeid: form.value.machinetypeid || null,
statusid: form.value.statusid || null, statusid: form.value.statusid || null,
vendorid: form.value.vendorid || null, vendorid: form.value.vendorid || null,

View File

@@ -63,7 +63,7 @@
<span class="info-label">Windows Name</span> <span class="info-label">Windows Name</span>
<span class="info-value mono">{{ printer.printer.windowsname }}</span> <span class="info-value mono">{{ printer.printer.windowsname }}</span>
</div> </div>
<div class="info-row" v-if="identifierflags.fqdn && printer.printer?.hostname"> <div class="info-row" v-if="isEnabled('fqdn', 'printer') && printer.printer?.hostname">
<span class="info-label">Hostname / FQDN</span> <span class="info-label">Hostname / FQDN</span>
<span class="info-value mono">{{ printer.printer.hostname }}</span> <span class="info-value mono">{{ printer.printer.hostname }}</span>
</div> </div>
@@ -75,6 +75,14 @@
<span class="info-label">Serial Number</span> <span class="info-label">Serial Number</span>
<span class="info-value mono">{{ printer.serialnumber }}</span> <span class="info-value mono">{{ printer.serialnumber }}</span>
</div> </div>
<div class="info-row" v-if="isEnabled('gaugelabreference', 'printer') && printer.gaugelabreference">
<span class="info-label">Gauge Lab Reference</span>
<span class="info-value mono">{{ printer.gaugelabreference }}</span>
</div>
<div class="info-row" v-if="isEnabled('maintenancereference', 'printer') && printer.maintenancereference">
<span class="info-label">Maintenance Reference</span>
<span class="info-value mono">{{ printer.maintenancereference }}</span>
</div>
</div> </div>
</div> </div>
@@ -248,7 +256,7 @@ import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '../../composables/identifierSettings'
const route = useRoute() const route = useRoute()
const identifierflags = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()
const loading = ref(true) const loading = ref(true)
const printer = ref(null) const printer = ref(null)

View File

@@ -37,7 +37,7 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group" v-if="identifierflags.fqdn"> <div class="form-group" v-if="isEnabled('fqdn', 'printer')">
<label for="hostname">Hostname (FQDN)</label> <label for="hostname">Hostname (FQDN)</label>
<input <input
id="hostname" id="hostname"
@@ -61,6 +61,28 @@
</div> </div>
</div> </div>
<div class="form-row" v-if="isEnabled('gaugelabreference', 'printer') || isEnabled('maintenancereference', 'printer')">
<div class="form-group" v-if="isEnabled('gaugelabreference', 'printer')">
<label for="gaugelabreference">Gauge Lab Reference</label>
<input
id="gaugelabreference"
v-model="form.gaugelabreference"
type="text"
class="form-control"
/>
</div>
<div class="form-group" v-if="isEnabled('maintenancereference', 'printer')">
<label for="maintenancereference">Maintenance Reference</label>
<input
id="maintenancereference"
v-model="form.maintenancereference"
type="text"
class="form-control"
/>
</div>
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="machinetypeid">Printer Type *</label> <label for="machinetypeid">Printer Type *</label>
@@ -273,7 +295,7 @@ import Modal from '../../components/Modal.vue'
import { currentTheme } from '../../stores/theme' import { currentTheme } from '../../stores/theme'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '../../composables/identifierSettings'
const identifierflags = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -293,6 +315,8 @@ const form = ref({
alias: '', alias: '',
hostname: '', hostname: '',
serialnumber: '', serialnumber: '',
gaugelabreference: '',
maintenancereference: '',
machinetypeid: '', machinetypeid: '',
statusid: '', statusid: '',
vendorid: '', vendorid: '',
@@ -462,6 +486,8 @@ onMounted(async () => {
alias: printer.name && printer.name.toUpperCase() !== 'NONE' ? printer.name : '', alias: printer.name && printer.name.toUpperCase() !== 'NONE' ? printer.name : '',
hostname: ext.hostname || '', hostname: ext.hostname || '',
serialnumber: printer.serialnumber || '', serialnumber: printer.serialnumber || '',
gaugelabreference: printer.gaugelabreference || '',
maintenancereference: printer.maintenancereference || '',
machinetypeid: ext.printertypeid || '', machinetypeid: ext.printertypeid || '',
statusid: printer.statusid || '', statusid: printer.statusid || '',
vendorid: ext.vendorid || '', vendorid: ext.vendorid || '',
@@ -520,6 +546,8 @@ async function savePrinter() {
windowsname: form.value.machinenumber || null, windowsname: form.value.machinenumber || null,
hostname: form.value.hostname || null, hostname: form.value.hostname || null,
serialnumber: form.value.serialnumber || null, serialnumber: form.value.serialnumber || null,
gaugelabreference: form.value.gaugelabreference || null,
maintenancereference: form.value.maintenancereference || null,
printertypeid: form.value.machinetypeid || null, printertypeid: form.value.machinetypeid || null,
statusid: form.value.statusid || null, statusid: form.value.statusid || null,
vendorid: form.value.vendorid || null, vendorid: form.value.vendorid || null,

View File

@@ -382,50 +382,35 @@
<div class="setting-group"> <div class="setting-group">
<p class="setting-description"> <p class="setting-description">
Enable or disable optional asset identifiers. When disabled, the identifier Enable or disable optional asset identifiers per asset type. When disabled
is hidden from asset forms and detail pages across the system. for a type, the identifier is hidden from that type's forms and detail
pages across the system.
</p> </p>
<div class="setting-row"> <div class="table-container">
<label class="toggle-label"> <table class="identifier-matrix">
<span>Gauge Lab Reference</span> <thead>
<button <tr>
class="toggle-btn" <th>Identifier</th>
:class="{ active: settings.identifier_gaugelabreference_enabled }" <th v-for="col in assetTypeCols" :key="col.key">{{ col.label }}</th>
@click="toggleSetting('identifier_gaugelabreference_enabled')" </tr>
:disabled="saving" </thead>
> <tbody>
<span class="toggle-slider"></span> <tr v-for="row in identifierRows" :key="row.name">
</button> <td class="identifier-name">{{ row.label }}</td>
</label> <td v-for="col in assetTypeCols" :key="col.key">
</div> <button
class="toggle-btn"
<div class="setting-row"> :class="{ active: matrixValue(row.name, col.key) }"
<label class="toggle-label"> @click="toggleIdentifier(row.name, col.key)"
<span>Maintenance Reference</span> :disabled="saving"
<button >
class="toggle-btn" <span class="toggle-slider"></span>
:class="{ active: settings.identifier_maintenancereference_enabled }" </button>
@click="toggleSetting('identifier_maintenancereference_enabled')" </td>
:disabled="saving" </tr>
> </tbody>
<span class="toggle-slider"></span> </table>
</button>
</label>
</div>
<div class="setting-row">
<label class="toggle-label">
<span>FQDN / Hostname</span>
<button
class="toggle-btn"
:class="{ active: settings.identifier_fqdn_enabled }"
@click="toggleSetting('identifier_fqdn_enabled')"
:disabled="saving"
>
<span class="toggle-slider"></span>
</button>
</label>
</div> </div>
</div> </div>
</div> </div>
@@ -463,13 +448,33 @@ const settings = reactive({
saml_acs_url: '', saml_acs_url: '',
saml_allow_local_login: true, saml_allow_local_login: true,
saml_auto_create_users: true, saml_auto_create_users: true,
saml_admin_group: '', saml_admin_group: ''
// Asset identifiers
identifier_gaugelabreference_enabled: true,
identifier_maintenancereference_enabled: true,
identifier_fqdn_enabled: true
}) })
// Asset identifier matrix: identifier x asset type. Keys follow
// identifier_<name>_<assettype>_enabled. Missing = enabled (default on).
const identifierRows = [
{ name: 'gaugelabreference', label: 'Gauge Lab Reference' },
{ name: 'maintenancereference', label: 'Maintenance Reference' },
{ name: 'fqdn', label: 'FQDN / Hostname' }
]
const assetTypeCols = [
{ key: 'equipment', label: 'Equipment' },
{ key: 'computer', label: 'PC' },
{ key: 'printer', label: 'Printer' },
{ key: 'network_device', label: 'Network' }
]
const identifierMatrix = reactive({})
function identifierKey(name, assettype) {
return `identifier_${name}_${assettype}_enabled`
}
function matrixValue(name, assettype) {
const key = identifierKey(name, assettype)
return key in identifierMatrix ? identifierMatrix[key] : true
}
const loading = ref(true) const loading = ref(true)
const saving = ref(false) const saving = ref(false)
const testingEmail = ref(false) const testingEmail = ref(false)
@@ -530,6 +535,8 @@ async function loadSettings() {
for (const setting of data.data) { for (const setting of data.data) {
if (setting.key in settings) { if (setting.key in settings) {
settings[setting.key] = setting.value settings[setting.key] = setting.value
} else if (/^identifier_.+_(equipment|computer|printer|network_device)_enabled$/.test(setting.key)) {
identifierMatrix[setting.key] = setting.value !== false
} }
} }
} catch (e) { } catch (e) {
@@ -545,6 +552,41 @@ async function toggleSetting(key) {
await saveSetting(key, newValue) await saveSetting(key, newValue)
} }
// Toggle a per-type identifier flag. The key may not be seeded yet on older
// installs, so fall back to creating it when the update returns 404.
async function toggleIdentifier(name, assettype) {
const key = identifierKey(name, assettype)
const newValue = !matrixValue(name, assettype)
try {
saving.value = true
error.value = ''
success.value = ''
try {
await settingsApi.update(key, newValue)
} catch (e) {
if (e.response?.status === 404) {
await settingsApi.create({
key,
value: newValue,
valuetype: 'boolean',
category: 'identifiers',
description: `Show the ${name} identifier on ${assettype} assets`
})
} else {
throw e
}
}
identifierMatrix[key] = newValue
success.value = 'Setting saved'
setTimeout(() => { success.value = '' }, 2000)
} catch (e) {
error.value = e.response?.data?.message || 'Failed to save setting'
console.error(e)
} finally {
saving.value = false
}
}
async function saveSetting(key, value) { async function saveSetting(key, value) {
try { try {
saving.value = true saving.value = true
@@ -797,4 +839,31 @@ onMounted(loadSettings)
margin-top: -0.5rem; margin-top: -0.5rem;
margin-left: 0; margin-left: 0;
} }
.identifier-matrix {
width: 100%;
border-collapse: collapse;
}
.identifier-matrix th,
.identifier-matrix td {
padding: 0.6rem 0.75rem;
text-align: center;
border-bottom: 1px solid var(--border);
}
.identifier-matrix th:first-child,
.identifier-matrix td.identifier-name {
text-align: left;
}
.identifier-matrix th {
color: var(--text-light);
font-weight: 600;
font-size: 0.9rem;
}
.identifier-matrix .identifier-name {
color: var(--text);
}
</style> </style>

View File

@@ -328,6 +328,8 @@ def create_computer():
assetnumber=data['assetnumber'], assetnumber=data['assetnumber'],
name=data.get('name'), name=data.get('name'),
serialnumber=data.get('serialnumber'), serialnumber=data.get('serialnumber'),
gaugelabreference=data.get('gaugelabreference'),
maintenancereference=data.get('maintenancereference'),
assettypeid=computer_type.assettypeid, assettypeid=computer_type.assettypeid,
statusid=data.get('statusid', 1), statusid=data.get('statusid', 1),
locationid=data.get('locationid'), locationid=data.get('locationid'),
@@ -424,7 +426,8 @@ def update_computer(computer_id: int):
changes = {} changes = {}
# Update asset fields # Update asset fields
asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid', asset_fields = ['assetnumber', 'name', 'serialnumber', 'gaugelabreference',
'maintenancereference', 'statusid',
'locationid', 'businessunitid', 'mapx', 'mapy', 'notes', 'isactive'] 'locationid', 'businessunitid', 'mapx', 'mapy', 'notes', 'isactive']
for key in asset_fields: for key in asset_fields:
if key in data: if key in data:

View File

@@ -324,6 +324,8 @@ def create_network_device():
assetnumber=data['assetnumber'], assetnumber=data['assetnumber'],
name=data.get('name'), name=data.get('name'),
serialnumber=data.get('serialnumber'), serialnumber=data.get('serialnumber'),
gaugelabreference=data.get('gaugelabreference'),
maintenancereference=data.get('maintenancereference'),
assettypeid=network_type.assettypeid, assettypeid=network_type.assettypeid,
statusid=data.get('statusid', 1), statusid=data.get('statusid', 1),
locationid=data.get('locationid'), locationid=data.get('locationid'),
@@ -406,7 +408,8 @@ def update_network_device(device_id: int):
changes = {} changes = {}
# Update asset fields # Update asset fields
asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid', asset_fields = ['assetnumber', 'name', 'serialnumber', 'gaugelabreference',
'maintenancereference', 'statusid',
'locationid', 'businessunitid', 'mapx', 'mapy', 'notes', 'isactive'] 'locationid', 'businessunitid', 'mapx', 'mapy', 'notes', 'isactive']
for key in asset_fields: for key in asset_fields:
if key in data: if key in data:

View File

@@ -285,6 +285,8 @@ def create_printer():
assetnumber=data['assetnumber'], assetnumber=data['assetnumber'],
name=data.get('name'), name=data.get('name'),
serialnumber=data.get('serialnumber'), serialnumber=data.get('serialnumber'),
gaugelabreference=data.get('gaugelabreference'),
maintenancereference=data.get('maintenancereference'),
assettypeid=printer_type.assettypeid, assettypeid=printer_type.assettypeid,
statusid=data.get('statusid', 1), statusid=data.get('statusid', 1),
locationid=data.get('locationid'), locationid=data.get('locationid'),
@@ -364,8 +366,9 @@ def update_printer(printer_id: int):
http_code=409 http_code=409
) )
# Update asset fields (gauge lab / maintenance refs are equipment-only) # Update asset fields (optional identifiers gated per-type in Settings)
asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid', asset_fields = ['assetnumber', 'name', 'serialnumber', 'gaugelabreference',
'maintenancereference', 'statusid',
'locationid', 'businessunitid', 'mapx', 'mapy', 'locationid', 'businessunitid', 'mapx', 'mapy',
'notes', 'isactive'] 'notes', 'isactive']
for key in asset_fields: for key in asset_fields:

View File

@@ -361,6 +361,18 @@ def seed_settings():
}, },
] ]
# Asset identifier toggles, per identifier AND per asset type (ADR-001).
from shopdb.core.api.settings import IDENTIFIER_LABELS, IDENTIFIER_ASSETTYPES
for name, label in IDENTIFIER_LABELS.items():
for assettype in IDENTIFIER_ASSETTYPES:
defaults.append({
'key': f'identifier_{name}_{assettype}_enabled',
'value': 'true',
'valuetype': 'boolean',
'category': 'identifiers',
'description': f'Show the {label} identifier on {assettype} assets',
})
created = 0 created = 0
for d in defaults: for d in defaults:
if not Setting.query.filter_by(key=d['key']).first(): if not Setting.query.filter_by(key=d['key']).first():

View File

@@ -17,6 +17,16 @@ SETTINGS_CACHE_TTL = 300 # 5 minutes
# exposed in plaintext. Sending it back on update is treated as "unchanged". # exposed in plaintext. Sending it back on update is treated as "unchanged".
SECRET_MASK = '********' SECRET_MASK = '********'
# Optional asset identifiers and the asset types they can be toggled on.
# Drives per-type seed keys and the Settings matrix UI. The asset type names
# match the AssetType.assettype values seeded by each plugin.
IDENTIFIER_LABELS = {
'gaugelabreference': 'Gauge Lab Reference',
'maintenancereference': 'Maintenance Reference',
'fqdn': 'FQDN / hostname',
}
IDENTIFIER_ASSETTYPES = ['equipment', 'computer', 'printer', 'network_device']
def _is_secret(key: str) -> bool: def _is_secret(key: str) -> bool:
return 'password' in key or 'token' in key or 'secret' in key return 'password' in key or 'token' in key or 'secret' in key
@@ -155,29 +165,22 @@ def create_setting():
@jwt_required() @jwt_required()
def seed_default_settings(): def seed_default_settings():
"""Seed default settings if they don't exist.""" """Seed default settings if they don't exist."""
defaults = [ # Asset identifier feature toggles, per identifier AND per asset type.
# Asset identifier feature toggles (global, per identifier) # Key format: identifier_<name>_<assettype>_enabled (boolean). Admins pick
# which optional identifiers show on which asset types. See ADR-001.
identifierdefaults = [
{ {
'key': 'identifier_gaugelabreference_enabled', 'key': f'identifier_{name}_{assettype}_enabled',
'value': 'true', 'value': 'true',
'valuetype': 'boolean', 'valuetype': 'boolean',
'category': 'identifiers', 'category': 'identifiers',
'description': 'Show the Gauge Lab Reference identifier on assets' 'description': f'Show the {label} identifier on {assettype} assets',
}, }
{ for name, label in IDENTIFIER_LABELS.items()
'key': 'identifier_maintenancereference_enabled', for assettype in IDENTIFIER_ASSETTYPES
'value': 'true', ]
'valuetype': 'boolean',
'category': 'identifiers', defaults = identifierdefaults + [
'description': 'Show the Maintenance Reference identifier on assets'
},
{
'key': 'identifier_fqdn_enabled',
'value': 'true',
'valuetype': 'boolean',
'category': 'identifiers',
'description': 'Show the FQDN / hostname identifier on assets'
},
# Zabbix integration # Zabbix integration
{ {
'key': 'zabbix_enabled', 'key': 'zabbix_enabled',