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:
@@ -1,36 +1,51 @@
|
||||
// Global enable/disable flags for optional asset identifiers, read from the
|
||||
// settings table. Loaded once and shared across components. Defaults to
|
||||
// enabled when a flag is missing so a fresh install shows the identifiers.
|
||||
// Per-type enable/disable flags for optional asset identifiers, read from the
|
||||
// settings table. Keys follow identifier_<name>_<assettype>_enabled. A legacy
|
||||
// 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 { settingsApi } from '../api'
|
||||
|
||||
const identifierflags = reactive({
|
||||
gaugelabreference: true,
|
||||
maintenancereference: true,
|
||||
fqdn: true,
|
||||
// scope[name][assettype] = boolean. legacy[name] = boolean (old global flag).
|
||||
const state = reactive({
|
||||
scope: {},
|
||||
legacy: {},
|
||||
loaded: false
|
||||
})
|
||||
|
||||
let inflight = null
|
||||
|
||||
export function useIdentifierFlags() {
|
||||
if (!identifierflags.loaded && !inflight) {
|
||||
function loadFlags() {
|
||||
if (!state.loaded && !inflight) {
|
||||
inflight = settingsApi.list()
|
||||
.then(({ data }) => {
|
||||
const map = {}
|
||||
;(data.data || []).forEach(s => { map[s.key] = s.value })
|
||||
if ('identifier_gaugelabreference_enabled' in map) {
|
||||
identifierflags.gaugelabreference = map.identifier_gaugelabreference_enabled !== false
|
||||
}
|
||||
if ('identifier_maintenancereference_enabled' in map) {
|
||||
identifierflags.maintenancereference = map.identifier_maintenancereference_enabled !== false
|
||||
}
|
||||
if ('identifier_fqdn_enabled' in map) {
|
||||
identifierflags.fqdn = map.identifier_fqdn_enabled !== false
|
||||
}
|
||||
identifierflags.loaded = true
|
||||
;(data.data || []).forEach(s => {
|
||||
const match = /^identifier_(.+?)(?:_(equipment|computer|printer|network_device))?_enabled$/.exec(s.key)
|
||||
if (!match) return
|
||||
const name = match[1]
|
||||
const assettype = match[2]
|
||||
if (assettype) {
|
||||
if (!state.scope[name]) state.scope[name] = {}
|
||||
state.scope[name][assettype] = s.value !== false
|
||||
} else {
|
||||
state.legacy[name] = s.value !== false
|
||||
}
|
||||
})
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -68,11 +68,11 @@
|
||||
<span class="info-label">Name</span>
|
||||
<span class="info-value">{{ equipment.name }}</span>
|
||||
</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-value mono">{{ equipment.gaugelabreference }}</span>
|
||||
</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-value mono">{{ equipment.maintenancereference }}</span>
|
||||
</div>
|
||||
@@ -240,7 +240,7 @@ import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const route = useRoute()
|
||||
const identifierflags = useIdentifierFlags()
|
||||
const { isEnabled } = useIdentifierFlags()
|
||||
|
||||
const loading = ref(true)
|
||||
const equipment = ref(null)
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" v-if="identifierflags.gaugelabreference || identifierflags.maintenancereference">
|
||||
<div class="form-group" v-if="identifierflags.gaugelabreference">
|
||||
<div class="form-row" v-if="isEnabled('gaugelabreference', 'equipment') || isEnabled('maintenancereference', 'equipment')">
|
||||
<div class="form-group" v-if="isEnabled('gaugelabreference', 'equipment')">
|
||||
<label for="gaugelabreference">Gauge Lab Reference</label>
|
||||
<input
|
||||
id="gaugelabreference"
|
||||
@@ -46,7 +46,7 @@
|
||||
<small class="form-help">Authoritative gauge lab asset reference (if tracked)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="identifierflags.maintenancereference">
|
||||
<div class="form-group" v-if="isEnabled('maintenancereference', 'equipment')">
|
||||
<label for="maintenancereference">Maintenance Reference</label>
|
||||
<input
|
||||
id="maintenancereference"
|
||||
@@ -358,7 +358,7 @@ import Modal from '../../components/Modal.vue'
|
||||
import { currentTheme } from '../../stores/theme'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const identifierflags = useIdentifierFlags()
|
||||
const { isEnabled } = useIdentifierFlags()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -111,6 +111,14 @@
|
||||
<span class="info-label">Serial Number</span>
|
||||
<span class="info-value mono">{{ device.serialnumber || '-' }}</span>
|
||||
</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">
|
||||
<span class="info-label">Vendor</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 { networkApi } from '../../api'
|
||||
import AssetRelationships from '../../components/AssetRelationships.vue'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const { isEnabled } = useIdentifierFlags()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -54,6 +54,27 @@
|
||||
</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-group">
|
||||
<label for="locationid">Location</label>
|
||||
@@ -229,6 +250,9 @@ import {
|
||||
assetsApi,
|
||||
businessunitsApi
|
||||
} from '../../api'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const { isEnabled } = useIdentifierFlags()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -240,6 +264,8 @@ const form = ref({
|
||||
assetnumber: '',
|
||||
name: '',
|
||||
serialnumber: '',
|
||||
gaugelabreference: '',
|
||||
maintenancereference: '',
|
||||
statusid: '',
|
||||
locationid: '',
|
||||
businessunitid: '',
|
||||
@@ -332,6 +358,8 @@ async function loadDevice() {
|
||||
form.value.assetnumber = data.assetnumber || ''
|
||||
form.value.name = data.name || ''
|
||||
form.value.serialnumber = data.serialnumber || ''
|
||||
form.value.gaugelabreference = data.gaugelabreference || ''
|
||||
form.value.maintenancereference = data.maintenancereference || ''
|
||||
form.value.statusid = data.statusid || ''
|
||||
form.value.locationid = data.locationid || ''
|
||||
form.value.businessunitid = data.businessunitid || ''
|
||||
@@ -365,6 +393,8 @@ async function submitForm() {
|
||||
assetnumber: form.value.assetnumber,
|
||||
name: form.value.name || null,
|
||||
serialnumber: form.value.serialnumber || null,
|
||||
gaugelabreference: form.value.gaugelabreference || null,
|
||||
maintenancereference: form.value.maintenancereference || null,
|
||||
statusid: form.value.statusid || null,
|
||||
locationid: form.value.locationid || null,
|
||||
businessunitid: form.value.businessunitid || null,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="hero-content">
|
||||
<div class="hero-title">
|
||||
<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 class="hero-meta">
|
||||
<span class="badge badge-lg badge-info">Computer</span>
|
||||
@@ -57,7 +57,7 @@
|
||||
<span class="info-label">Name</span>
|
||||
<span class="info-value">{{ computer.name }}</span>
|
||||
</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-value mono">{{ computer.computer.hostname }}</span>
|
||||
</div>
|
||||
@@ -65,6 +65,14 @@
|
||||
<span class="info-label">Serial Number</span>
|
||||
<span class="info-value mono">{{ computer.serialnumber }}</span>
|
||||
</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>
|
||||
|
||||
@@ -210,7 +218,7 @@ import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const route = useRoute()
|
||||
const identifierflags = useIdentifierFlags()
|
||||
const { isEnabled } = useIdentifierFlags()
|
||||
|
||||
const loading = ref(true)
|
||||
const computer = ref(null)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input
|
||||
id="hostname"
|
||||
@@ -55,6 +55,28 @@
|
||||
</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-group">
|
||||
<label for="machinetypeid">PC Type *</label>
|
||||
@@ -276,7 +298,7 @@ import Modal from '../../components/Modal.vue'
|
||||
import { currentTheme } from '../../stores/theme'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const identifierflags = useIdentifierFlags()
|
||||
const { isEnabled } = useIdentifierFlags()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -301,6 +323,8 @@ const form = ref({
|
||||
alias: '',
|
||||
hostname: '',
|
||||
serialnumber: '',
|
||||
gaugelabreference: '',
|
||||
maintenancereference: '',
|
||||
machinetypeid: '',
|
||||
statusid: '',
|
||||
vendorid: '',
|
||||
@@ -371,6 +395,8 @@ onMounted(async () => {
|
||||
alias: pc.name && pc.name.toUpperCase() !== 'NONE' ? pc.name : '',
|
||||
hostname: ext.hostname || '',
|
||||
serialnumber: pc.serialnumber || '',
|
||||
gaugelabreference: pc.gaugelabreference || '',
|
||||
maintenancereference: pc.maintenancereference || '',
|
||||
machinetypeid: ext.computertypeid || '',
|
||||
statusid: pc.statusid || '',
|
||||
vendorid: ext.vendorid || '',
|
||||
@@ -423,6 +449,8 @@ async function savePC() {
|
||||
assetnumber: form.value.machinenumber,
|
||||
hostname: form.value.hostname || null,
|
||||
serialnumber: form.value.serialnumber || null,
|
||||
gaugelabreference: form.value.gaugelabreference || null,
|
||||
maintenancereference: form.value.maintenancereference || null,
|
||||
computertypeid: form.value.machinetypeid || null,
|
||||
statusid: form.value.statusid || null,
|
||||
vendorid: form.value.vendorid || null,
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<span class="info-label">Windows Name</span>
|
||||
<span class="info-value mono">{{ printer.printer.windowsname }}</span>
|
||||
</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-value mono">{{ printer.printer.hostname }}</span>
|
||||
</div>
|
||||
@@ -75,6 +75,14 @@
|
||||
<span class="info-label">Serial Number</span>
|
||||
<span class="info-value mono">{{ printer.serialnumber }}</span>
|
||||
</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>
|
||||
|
||||
@@ -248,7 +256,7 @@ import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const route = useRoute()
|
||||
const identifierflags = useIdentifierFlags()
|
||||
const { isEnabled } = useIdentifierFlags()
|
||||
|
||||
const loading = ref(true)
|
||||
const printer = ref(null)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input
|
||||
id="hostname"
|
||||
@@ -61,6 +61,28 @@
|
||||
</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-group">
|
||||
<label for="machinetypeid">Printer Type *</label>
|
||||
@@ -273,7 +295,7 @@ import Modal from '../../components/Modal.vue'
|
||||
import { currentTheme } from '../../stores/theme'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const identifierflags = useIdentifierFlags()
|
||||
const { isEnabled } = useIdentifierFlags()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -293,6 +315,8 @@ const form = ref({
|
||||
alias: '',
|
||||
hostname: '',
|
||||
serialnumber: '',
|
||||
gaugelabreference: '',
|
||||
maintenancereference: '',
|
||||
machinetypeid: '',
|
||||
statusid: '',
|
||||
vendorid: '',
|
||||
@@ -462,6 +486,8 @@ onMounted(async () => {
|
||||
alias: printer.name && printer.name.toUpperCase() !== 'NONE' ? printer.name : '',
|
||||
hostname: ext.hostname || '',
|
||||
serialnumber: printer.serialnumber || '',
|
||||
gaugelabreference: printer.gaugelabreference || '',
|
||||
maintenancereference: printer.maintenancereference || '',
|
||||
machinetypeid: ext.printertypeid || '',
|
||||
statusid: printer.statusid || '',
|
||||
vendorid: ext.vendorid || '',
|
||||
@@ -520,6 +546,8 @@ async function savePrinter() {
|
||||
windowsname: form.value.machinenumber || null,
|
||||
hostname: form.value.hostname || null,
|
||||
serialnumber: form.value.serialnumber || null,
|
||||
gaugelabreference: form.value.gaugelabreference || null,
|
||||
maintenancereference: form.value.maintenancereference || null,
|
||||
printertypeid: form.value.machinetypeid || null,
|
||||
statusid: form.value.statusid || null,
|
||||
vendorid: form.value.vendorid || null,
|
||||
|
||||
@@ -382,50 +382,35 @@
|
||||
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
Enable or disable optional asset identifiers. When disabled, the identifier
|
||||
is hidden from asset forms and detail pages across the system.
|
||||
Enable or disable optional asset identifiers per asset type. When disabled
|
||||
for a type, the identifier is hidden from that type's forms and detail
|
||||
pages across the system.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Gauge Lab Reference</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.identifier_gaugelabreference_enabled }"
|
||||
@click="toggleSetting('identifier_gaugelabreference_enabled')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Maintenance Reference</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.identifier_maintenancereference_enabled }"
|
||||
@click="toggleSetting('identifier_maintenancereference_enabled')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</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 class="table-container">
|
||||
<table class="identifier-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th v-for="col in assetTypeCols" :key="col.key">{{ col.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in identifierRows" :key="row.name">
|
||||
<td class="identifier-name">{{ row.label }}</td>
|
||||
<td v-for="col in assetTypeCols" :key="col.key">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: matrixValue(row.name, col.key) }"
|
||||
@click="toggleIdentifier(row.name, col.key)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -463,13 +448,33 @@ const settings = reactive({
|
||||
saml_acs_url: '',
|
||||
saml_allow_local_login: true,
|
||||
saml_auto_create_users: true,
|
||||
saml_admin_group: '',
|
||||
// Asset identifiers
|
||||
identifier_gaugelabreference_enabled: true,
|
||||
identifier_maintenancereference_enabled: true,
|
||||
identifier_fqdn_enabled: true
|
||||
saml_admin_group: ''
|
||||
})
|
||||
|
||||
// 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 saving = ref(false)
|
||||
const testingEmail = ref(false)
|
||||
@@ -530,6 +535,8 @@ async function loadSettings() {
|
||||
for (const setting of data.data) {
|
||||
if (setting.key in settings) {
|
||||
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) {
|
||||
@@ -545,6 +552,41 @@ async function toggleSetting(key) {
|
||||
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) {
|
||||
try {
|
||||
saving.value = true
|
||||
@@ -797,4 +839,31 @@ onMounted(loadSettings)
|
||||
margin-top: -0.5rem;
|
||||
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>
|
||||
|
||||
@@ -328,6 +328,8 @@ def create_computer():
|
||||
assetnumber=data['assetnumber'],
|
||||
name=data.get('name'),
|
||||
serialnumber=data.get('serialnumber'),
|
||||
gaugelabreference=data.get('gaugelabreference'),
|
||||
maintenancereference=data.get('maintenancereference'),
|
||||
assettypeid=computer_type.assettypeid,
|
||||
statusid=data.get('statusid', 1),
|
||||
locationid=data.get('locationid'),
|
||||
@@ -424,7 +426,8 @@ def update_computer(computer_id: int):
|
||||
changes = {}
|
||||
|
||||
# Update asset fields
|
||||
asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid',
|
||||
asset_fields = ['assetnumber', 'name', 'serialnumber', 'gaugelabreference',
|
||||
'maintenancereference', 'statusid',
|
||||
'locationid', 'businessunitid', 'mapx', 'mapy', 'notes', 'isactive']
|
||||
for key in asset_fields:
|
||||
if key in data:
|
||||
|
||||
@@ -324,6 +324,8 @@ def create_network_device():
|
||||
assetnumber=data['assetnumber'],
|
||||
name=data.get('name'),
|
||||
serialnumber=data.get('serialnumber'),
|
||||
gaugelabreference=data.get('gaugelabreference'),
|
||||
maintenancereference=data.get('maintenancereference'),
|
||||
assettypeid=network_type.assettypeid,
|
||||
statusid=data.get('statusid', 1),
|
||||
locationid=data.get('locationid'),
|
||||
@@ -406,7 +408,8 @@ def update_network_device(device_id: int):
|
||||
changes = {}
|
||||
|
||||
# Update asset fields
|
||||
asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid',
|
||||
asset_fields = ['assetnumber', 'name', 'serialnumber', 'gaugelabreference',
|
||||
'maintenancereference', 'statusid',
|
||||
'locationid', 'businessunitid', 'mapx', 'mapy', 'notes', 'isactive']
|
||||
for key in asset_fields:
|
||||
if key in data:
|
||||
|
||||
@@ -285,6 +285,8 @@ def create_printer():
|
||||
assetnumber=data['assetnumber'],
|
||||
name=data.get('name'),
|
||||
serialnumber=data.get('serialnumber'),
|
||||
gaugelabreference=data.get('gaugelabreference'),
|
||||
maintenancereference=data.get('maintenancereference'),
|
||||
assettypeid=printer_type.assettypeid,
|
||||
statusid=data.get('statusid', 1),
|
||||
locationid=data.get('locationid'),
|
||||
@@ -364,8 +366,9 @@ def update_printer(printer_id: int):
|
||||
http_code=409
|
||||
)
|
||||
|
||||
# Update asset fields (gauge lab / maintenance refs are equipment-only)
|
||||
asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid',
|
||||
# Update asset fields (optional identifiers gated per-type in Settings)
|
||||
asset_fields = ['assetnumber', 'name', 'serialnumber', 'gaugelabreference',
|
||||
'maintenancereference', 'statusid',
|
||||
'locationid', 'businessunitid', 'mapx', 'mapy',
|
||||
'notes', 'isactive']
|
||||
for key in asset_fields:
|
||||
|
||||
@@ -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
|
||||
for d in defaults:
|
||||
if not Setting.query.filter_by(key=d['key']).first():
|
||||
|
||||
@@ -17,6 +17,16 @@ SETTINGS_CACHE_TTL = 300 # 5 minutes
|
||||
# exposed in plaintext. Sending it back on update is treated as "unchanged".
|
||||
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:
|
||||
return 'password' in key or 'token' in key or 'secret' in key
|
||||
@@ -155,29 +165,22 @@ def create_setting():
|
||||
@jwt_required()
|
||||
def seed_default_settings():
|
||||
"""Seed default settings if they don't exist."""
|
||||
defaults = [
|
||||
# Asset identifier feature toggles (global, per identifier)
|
||||
# Asset identifier feature toggles, per identifier AND per asset type.
|
||||
# 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',
|
||||
'valuetype': 'boolean',
|
||||
'category': 'identifiers',
|
||||
'description': 'Show the Gauge Lab Reference identifier on assets'
|
||||
},
|
||||
{
|
||||
'key': 'identifier_maintenancereference_enabled',
|
||||
'value': 'true',
|
||||
'valuetype': 'boolean',
|
||||
'category': 'identifiers',
|
||||
'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'
|
||||
},
|
||||
'description': f'Show the {label} identifier on {assettype} assets',
|
||||
}
|
||||
for name, label in IDENTIFIER_LABELS.items()
|
||||
for assettype in IDENTIFIER_ASSETTYPES
|
||||
]
|
||||
|
||||
defaults = identifierdefaults + [
|
||||
# Zabbix integration
|
||||
{
|
||||
'key': 'zabbix_enabled',
|
||||
|
||||
Reference in New Issue
Block a user