diff --git a/frontend/src/views/geenforce/ManifestEditor.vue b/frontend/src/views/geenforce/ManifestEditor.vue index 81e1b3d..8a52704 100644 --- a/frontend/src/views/geenforce/ManifestEditor.vue +++ b/frontend/src/views/geenforce/ManifestEditor.vue @@ -8,6 +8,25 @@ then Publish to ship it to PCs. Nothing reaches a PC until you publish.

+
+ How this works +
+

A manifest is the desired state for a PC type - the + list of what should be installed. shopdb serves it; each PC checks + itself every cycle and re-installs anything that drifted (uninstalled, + corrupted, or overwritten). That is the "self-heal".

+

Each entry is one action (install an app, copy a + file, write a registry value, run a script) plus a detection + rule that tells the PC whether it is already correct - if not, the + action runs. Entry order is execution order.

+

Targeting narrows an entry: the fleet-wide + common manifest uses PC types to hit a subset; a per-type + manifest already only runs on its own type, so it shows just the niche + gate it needs (CMM version, a bay number, a subtype).

+

Full guide: docs/GE-ENFORCE.md.

+
+
+
{{ error }}
{{ notice }}
@@ -151,7 +170,10 @@ @click="moveEntry(index, 1)" >Down - {{ entry.Name }} + + {{ entry.Name }} +
{{ describeEntry(entry) }}
+ {{ entry.Type }} {{ entry.DetectionMethod || 'always' }} {{ filterSummary(entry) }} @@ -749,6 +771,32 @@ function filterSummary(entry) { if (entry._CmmVersion) parts.push(`cmm:${entry._CmmVersion}`) return parts.join(' ') || '-' } + +// Plain-English one-liner: what this entry does, when it self-heals, who it +// applies to. Turns "eDNC | MSI | Registry | -" into intent an IT tech reads. +const ACTION_VERB = { + MSI: 'Installs', EXE: 'Installs', CMD: 'Runs', BAT: 'Runs', + PS1: 'Runs a script for', INF: 'Installs a driver for', + File: 'Copies a file for', Registry: 'Sets a registry value for', +} +function describeEntry(entry) { + let text = `${ACTION_VERB[entry.Type] || 'Applies'} ${entry.Name}` + const method = entry.DetectionMethod + if (!method || method === 'Always') text += '; runs every cycle' + else if (method === 'FileVersion') text += `; reinstalls unless version is ${entry.DetectionValue || 'set'}` + else if (method === 'Hash') text += '; re-copies if the file changed' + else if (method === 'MarkerFile') text += '; installs once' + else text += '; reinstalls if not detected' + if (entry._CmmVersion) text += `; CMM ${entry._CmmVersion} bays only` + else if (entry.TargetMachineNumbers && entry.TargetMachineNumbers.length) { + text += `; ${entry.TargetMachineNumbers.length} specific bay(s)` + } else if (entry.TargetHostnames && entry.TargetHostnames.length) { + text += '; specific hostname(s)' + } else if (entry.PCTypes && entry.PCTypes.length) { + text += `; ${entry.PCTypes.length} PC type(s)` + } + return text +} function formatDate(value) { return value ? new Date(value).toLocaleString() : '' } @@ -844,6 +892,13 @@ loadConfig() /* Modals: wider entry/preview, still responsive (global max-width + width:100%). */ .modal-entry { max-width: min(1100px, 96vw); } .toggle-all { font-weight: 400; font-size: 0.8rem; } +.entry-desc { font-size: 0.78rem; margin-top: 0.15rem; } +.help-panel { margin: 0 0 1rem 0; border: 1px solid var(--border); + border-radius: 6px; padding: 0.5rem 0.75rem; background: var(--bg-card); } +.help-panel summary { cursor: pointer; font-weight: 600; font-size: 0.9rem; } +.help-body { margin-top: 0.5rem; } +.help-body p { margin: 0 0 0.5rem 0; font-size: 0.85rem; color: var(--text-light); } +.help-body code { font-size: 0.8rem; } .modal-close { background: transparent; border: none;