From 1672e349e58c1e4087bafa3bd90a11aa4b37b9a1 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 12 Jul 2026 15:29:08 -0400 Subject: [PATCH] Collector auto-links measuring tools for metrology PCs; settings rail cleanup Metrology PCs (CMM, Keyence, Genspect, wax-and-trace imaging pc-types) drive an attached measuring instrument. The PC itself stays a shopfloor PC, but the collector now models the instrument: - New METROLOGY_TOOL_MAP (pctypemap.py) maps those pc-types to a MeasuringToolType (CMM, Vision System, Genspect, Form Tracer). - ComputersPlugin._sync_measuringtool_link creates the MeasuringTool asset once and a directional PC->tool "controls" relationship, tagged collector:measuringtool. Idempotent (re-push reuses, no duplicate asset) and self-archiving (a PC re-imaged to a non-metrology type deactivates the link but keeps the asset and any calibration history). Mirrors the printer-link pattern. The MeasuringToolType is created on demand if not seeded. - 4 tests: create+link, idempotent re-push, non-metrology skip, repurpose archives. Non-metrology PCs never warn about a missing controls type. Settings rail cleanup: - Collapsible groups so the 13-group rail fits without scrolling (1511px -> 488px). The group containing the current page expands; the rest collapse. CSS-drawn caret (ASCII source, no Unicode). Empty groups never render, in both the rail and the landing page. - Measuring Tools group placed with the other asset groups (right after Machines) instead of appended last; empty placeholder positions the plugin-contributed cards. - Operating Systems moved from PCs to General Reference: OS is cross-asset (PCs, machines, measuring tools, network devices all run one). Plus docs/proposals/ge-enforce-plugin.md: a planning doc for refactoring GE-Enforce/DSC into a shopdb plugin (manifest as shopdb data, payloads on SMB/HTTP/inline), grounded in the real manifest schema. Co-Authored-By: Claude Opus 4.8 --- docs/proposals/ge-enforce-plugin.md | 331 ++++++++++++++++++ frontend/src/views/settings/SettingsIndex.vue | 7 +- .../src/views/settings/SettingsLayout.vue | 91 ++++- frontend/src/views/settings/settingsNav.js | 17 +- plugins/computers/pctypemap.py | 19 + plugins/computers/plugin.py | 123 +++++++ tests/test_core/test_collector_contract.py | 103 ++++++ 7 files changed, 669 insertions(+), 22 deletions(-) create mode 100644 docs/proposals/ge-enforce-plugin.md diff --git a/docs/proposals/ge-enforce-plugin.md b/docs/proposals/ge-enforce-plugin.md new file mode 100644 index 0000000..49f5046 --- /dev/null +++ b/docs/proposals/ge-enforce-plugin.md @@ -0,0 +1,331 @@ +# Proposal: GE-Enforce as a shopdb plugin + +Status: DRAFT / planning only. Not accepted, not built. +Author: planning session 2026-07-12. + +## 1. What this is + +Today GE-Enforce is a PowerShell manifest engine that reads per-PC-type +`manifest.json` files off an SMB share (`\\tsgwp00525.wjs.geaerospace.net\ +shared\dt\shopfloor\`). Each logon, a scheduled task running as SYSTEM mounts +the share, reads the manifest for the machine's PC type, and installs or +self-heals apps, files, drivers, registry values, and scripts. A parallel +`preinstall.json` runs the same schema once at imaging. + +This proposal turns the *manifest* into shopdb data: the authoritative manifest +lives in the shopdb database, is edited through the shopdb UI (an expansion of +`/settings/pctypemapping`), and is served to clients over HTTP as JSON. The +*payloads* (MSI/EXE/PS1/config bytes) stay on SMB, on HTTP, or both, referenced +by URL/path from the manifest rows. GE-Enforce.ps1 changes from "read a file on +W:" to "GET a manifest from shopdb, then fetch each payload from wherever the +row says." + +The result: managing imaging PC types, their apps, scripts, files, registry +rules, and version gates becomes a first-class shopdb feature instead of hand- +edited JSON on a file share. + +## 2. Why it fits shopdb + +- shopdb already models the fleet (the collector ingests every PC's hostname, + pctype, installed software, versions). Making shopdb *also* own what SHOULD be + installed closes the loop: desired-state (manifest) and observed-state + (collector) live in one system and can be diffed. +- `/settings/pctypemapping` already maps `gea-shopfloor-*` PC types to + `ComputerType`. That page becomes the entry point for full imaging-PC-type + management. +- The plugin contract (per-plugin models, migrations, API prefix, settings + cards, collector hooks) is exactly the shape this needs. +- ADR-004 (per-site instances) matches: each site's shopdb owns each site's + manifest. No multi-tenant complication. + +## 3. Grounding: the real manifest schema + +Source of truth for these field names (do not invent others): + +- Schema: `pxe-images/tsgwp00525-v2/shared/dt/shopfloor/_meta/manifest-schema.json` +- Engine: `pxe-images/common/lib/Install-FromManifest.ps1` +- Dispatcher: `.../shopfloor/common/GE-Enforce.ps1` +- Architecture: `pxe/docs/ge-enforce-v2-architecture.md` + +A manifest is `{ "Version": str, "_comment": str, "Applications": [entry, ...] }`. +Only `Name` and `Type` are required per entry. + +### Per-entry fields (complete set) + +Identity / action: +- `Name` (required, unique, also the status-key `/`) +- `Type` (required): one of `MSI EXE CMD BAT PS1 INF File Registry` +- `_comment` (documentation, heavily used in practice) + +Type-specific payload references (sparse; depends on Type): +- MSI/EXE/CMD/BAT/INF: `Installer` (relative path) + `InstallArgs` +- PS1: `Script` (relative path, falls back to `Installer`) + `Args` +- File: `Source` (relative) + `Destination` (absolute on-PC path) +- Registry: `RegPath` + `RegName` + `RegValue` + `RegType` + (`RegType` in `String DWord QWord MultiString ExpandString Binary`) +- Optional `LogFile`, `WaitTimeoutSec` (EXE hang kill), `InUseCheck` + +Detection (decides whether the action fires / self-heals): +- `DetectionMethod`: one of + `Registry File FileVersion Hash MarkerFile ValueMatches pnputil Always` +- `DetectionPath`, `DetectionName`, `DetectionValue`, `DetectionPattern` +- Note: `DetectionValue` is method-dependent - SHA256 for Hash, a 4-part + version for FileVersion, a registry value for Registry, ignored for + Always/File. Same column, different meaning per method. +- No `DetectionMethod` = always installs. + +Targeting filters (all ANDed; each is multi-value): +- `PCTypes` (array; `"*"` = all; alias graph expands old<->new names) +- `PCSubTypes` / subtype via `-` values +- `TargetHostnames` (array; exact + `-like WJS-*` wildcards) +- `TargetMachineNumbers` (array; per-bay) +- `_CmmVersion` (scalar; per-entry PC-DMIS version gate, needs lib >= 2.6) + +Nested: +- `InUseCheck`: `{ Behavior, Processes: [{Name, ExePath, GracefulCloseTimeoutSec}] }` + Behavior in `Defer CloseAndReopen ForceClose ScheduleForReboot` + +Parsed-but-inert today (model them, mark inert): +- `ApplyMode` (`Nightly Immediate ImmediateReboot`), `UpdateWindow` (`HH:MM-HH:MM`) + +Preinstall-only extras (phase discriminator): +- `PreEnrollment`, `KillAfterDetection`, `PCTypesStrict`, `_pcTypesNote` + +### Load-bearing behaviors the model must preserve + +1. **Array order IS execution order.** Config-restore entries are deliberately + placed AFTER their vendor installer so a mid-cycle overwrite heals the same + cycle (eMxInfo.txt after eDNC; udc_webserver_settings after UDC). We MUST + store an explicit per-scope `sortorder`, not a set. +2. **PCTypes alias graph** is many-to-many old<->new names resolved by set + intersection, with a `PCTypesStrict` escape hatch. Not a simple FK. +3. **Polymorphic entry by Type** - sparse column set per type. Model as + single-table with nullable columns, or a typed-payload child. STI is simpler. +4. **Two manifest phases** - runtime (self-heal, per logon) and preinstall + (once at imaging) share the schema. One table with a `phase` discriminator. + +## 4. Data model (new `geenforce` plugin) + +Per-plugin Alembic chain (ADR-008). Tables (lowercase concatenated per naming +convention): + +- `manifestscopes` - one row per imaging PC type / scope. + - `scopeid` PK + - `scopename` (e.g. `gea-shopfloor-cmm`; unique) + - `computertypeid` FK -> `computertypes` (this REPLACES the thin + `pctypemap_` setting; the mapping becomes a column here) + - `phase` enum (`runtime` | `preinstall`) + - `manifestversion` (string, mirrors manifest `Version`) + - `description`, `isactive` + - `iscommon` bool (the `common/` fleet-wide scope) + +- `manifestentries` - one row per Applications[] entry. + - `entryid` PK, `scopeid` FK + - `sortorder` int (preserves array order; the ordering contract) + - `name`, `entrytype` (MSI/EXE/.../Registry), `comment` + - payload columns (nullable, per type): `installer`, `installargs`, + `scriptpath`, `scriptargs`, `sourcepath`, `destination`, + `regpath`, `regname`, `regvalue`, `regtype` + - `payloadsource` enum (`smb` | `http` | `inline`) + `payloadref` + (see section 5) + - detection columns: `detectionmethod`, `detectionpath`, `detectionname`, + `detectionvalue`, `detectionpattern` + - gates: `cmmversion`, plus child tables for the multi-value filters + - control: `logfile`, `waittimeoutsec`, `applymode`, `updatewindow` + - preinstall flags: `preenrollment`, `killafterdetection`, `pctypesstrict` + - `isactive` + +- `manifestentrypctypes`, `manifestentryhostnames`, `manifestentrymachinenumbers` + - child rows for the ANDed multi-value filters (one value per row, wildcards + stored verbatim as patterns) + +- `manifestinusechecks` + `manifestinusecheckprocesses` + - the nested InUseCheck object and its Processes[] child list + +- `pctypealiases` - the old<->new name alias graph (seed from + `Install-FromManifest.ps1:463-475`), so the server can resolve/validate + PCTypes the same way the engine does. + +The JSON the client receives is REBUILT from these rows in exact array order. +It must byte-for-byte match what the current engine expects (round-trip tested +against the live manifests). + +## 5. Payloads: SMB and/or HTTP (both supported) + +The user asked whether payloads can be SMB and/or HTTP. Yes - per entry: + +- `payloadsource = smb`: `payloadref` is the current relative path + (`apps/eDNC_6-4-5.msi`); the client resolves it against the mounted scope + root exactly as today. Zero client behavior change for these rows. This is + the default and the migration target for large binaries (MSIs are hundreds of + MB; SMB streaming beats HTTP for those). +- `payloadsource = http`: `payloadref` is a URL (absolute, or relative to a + configured payload base). The client downloads to a local temp dir, verifies + the Hash/FileVersion detection value, then runs it. Good for small + config/script payloads and for sites with no SMB share. +- `payloadsource = inline`: for small text payloads (a `.ps1`, a config file, a + registry value), the bytes live in shopdb itself and are served in-band. No + external store at all. Best for scripts and File-type config drops. + +Manifest generation emits, per entry, whatever the client needs to fetch the +bytes. The engine's existing "stage network EXE to local temp first" logic +(SYSTEM access-denied workaround) generalizes cleanly to HTTP download. + +Hashing stays the integrity mechanism regardless of transport: `DetectionValue` +carries the SHA256, the client verifies after fetch. This is the security- +critical bit - shopdb serves a manifest that says "install X with hash H"; a +tampered payload from any transport fails the hash and does not run. + +## 6. API surface (`/api/geenforce/...`) + +Admin CRUD (gated by a new `geenforce.manage` permission via the plugin's +`get_permissions()` hook): +- `GET/POST /scopes`, `GET/PUT/DELETE /scopes/` - imaging PC types +- `GET/POST /scopes//entries`, `PUT/DELETE /entries/` - manifest entries +- `PUT /scopes//entries/reorder` - the ordering contract, drag-to-reorder +- `POST /entries//payload` - upload an inline/http payload (multipart), + compute + store its SHA256 into `detectionvalue` +- `GET /scopes//preview` - the exact JSON a client would receive (for review + before publish) + +Client-facing (gated by a collector-style service token, `geenforce.fetch` +scope, reusing the PAT + `X-API-Key` machinery already built for the collector): +- `GET /manifest?pctype=&subtype=&hostname=&machinenumber=` + Returns the assembled manifest JSON for that machine. The server can pre-apply + the PCTypes/hostname/machinenumber/cmmversion filters (thin client) OR return + the full scope and let the engine filter (fat client, matches today). Start + fat: return the scope manifest unchanged so the engine logic is untouched. +- Payload fetch for `http`/`inline` rows: `GET /payload/` streaming the + bytes with the right content type. + +## 7. Frontend: expand `/settings/pctypemapping` + +The current page (`PCTypeMappingSettings.vue`, "Collector PC Types") is a read- +only-ish table of `pxetype -> ComputerType` dropdowns. It grows into the imaging- +PC-type manager: + +- **Scopes list**: add/rename/delete imaging PC types; each still carries its + `ComputerType` mapping (that column moves from a setting into `manifestscopes`). + A `phase` toggle (runtime vs preinstall). Common scope flagged. +- **Scope detail / manifest editor**: an ordered, drag-reorderable list of + entries (the ordering contract made visible). Each entry is a typed form - + the visible fields switch on `entrytype` (MSI shows Installer+InstallArgs; + PS1 shows Script+Args; File shows Source+Destination; Registry shows the Reg* + quartet). Detection block with a method dropdown that reveals only the + relevant Detection* fields. Filter chips for PCTypes/hostnames/machine numbers. + InUseCheck sub-editor. Payload source selector (smb/http/inline) with upload + for the latter two. +- **Preview + publish**: show the generated JSON; "publish" bumps + `manifestversion` and writes a history snapshot (mirrors the current + `_meta/history/-.json` backups). +- **Desired vs observed**: because the collector already reports + `installedversions`, the scope page can show, per entry, how many fleet PCs + match the expected detection value. This is the payoff of unifying the two. + +This is an ADR-010 settings card contributed by the geenforce plugin, so it only +appears when the plugin is enabled. + +## 8. Client change (minimal, staged) + +`GE-Enforce.ps1` today: mount W:, read `\manifest.json`, hand to +`Install-FromManifest`. New path: GET the manifest from shopdb, write it to the +same local location the engine reads, then run the engine unchanged. That is the +smallest possible client delta - the engine, detection logic, self-heal, and +SMB payload resolution all stay identical. Only the *source of the JSON* moves +from file to HTTP. + +Payloads: `smb` rows need no client change. `http`/`inline` rows need a small +fetch-and-verify helper (download to temp, check SHA256, then the existing +installer action runs against the local copy). The engine already stages network +EXEs to temp, so this is an extension, not a rewrite. + +Auth: the client already has SFLD credentials in +`HKLM:\SOFTWARE\GE\SFLD\Credentials`. Add a shopdb service token (a +`geenforce.fetch` PAT) provisioned the same way (Azure DSC writes it to +registry), sent as `X-API-Key`. If shopdb is unreachable, the client falls back +to the last-known-good manifest cached locally (fail-safe: never leave a PC +unmanaged because the web app is down). This mirrors today's "creds missing = +exit 0, retry next cycle" resilience. + +## 9. Cutover strategy + +The manifest is desired-state that runs as SYSTEM and installs software fleet- +wide. A bad cutover = a fleet-wide mis-install. Stage it: + +1. **Import + parity.** Write a one-shot importer that reads the current + on-share manifests (common + every `gea-shopfloor-*` + preinstall.json) into + the new tables. Then generate JSON back out and `diff` against the originals + until byte-identical for every scope. This proves the model is lossless + before anything depends on it. (This is the same discipline as the ADR-001 + data migration.) +2. **Shadow mode.** shopdb serves the manifest at a new endpoint; a canary PC + fetches from shopdb but ALSO reads the share, and logs any diff. No install + behavior changes. Run across one of each PC type for a few cycles. +3. **Read cutover, payloads still SMB.** Flip GE-Enforce to source the JSON from + shopdb (payloads stay `smb`). The blast radius is only "where the JSON comes + from"; the bytes and engine are unchanged. Keep the share manifests as the + rollback (revert the dispatcher one-liner). +4. **Payload migration (optional, per entry).** Move small scripts/configs to + `inline`/`http` opportunistically. Leave big MSIs on SMB indefinitely - SMB + is the right transport for them. +5. **Author in shopdb.** Once read-cutover is stable, new manifest edits happen + in the shopdb UI and the on-share JSON is retired (or auto-exported as a + backup for break-glass). + +Rollback at every stage is a one-line dispatcher revert, because the engine and +payload layout never stop working from the share. + +## 10. Risks / open questions + +- **The engine is the contract.** Any drift between shopdb's generated JSON and + what `Install-FromManifest.ps1` expects is a fleet-wide install bug. The + byte-identical round-trip test (step 1) is non-negotiable, and the plugin must + pin which engine lib version it targets (>= 2.6 for `_CmmVersion`). +- **PCTypes alias graph** must be seeded and kept in sync with + `Install-FromManifest.ps1:463-475`. If the engine's alias map changes, ours + must too. Consider having the engine fetch the alias map from shopdb as well, + so there is one source. +- **Availability coupling.** GE-Enforce currently depends only on SMB. Adding an + HTTP dependency on shopdb means shopdb downtime could stall enforcement - + hence the last-known-good local cache in section 8. Must be built in from day + one, not bolted on. +- **Secrets in payloads.** Some config drops (site-config, credentials) may + contain secrets. `inline` payloads live in the shopdb DB - those must respect + the existing "secrets stay in .env, not the settings table" rule. Likely keep + any secret-bearing payload on SMB with ACLs, never inline. +- **Preinstall runner** is a separate consumer (`00-PreInstall-*` at imaging, + before enrollment). It may not have a shopdb token yet at that point in the + imaging sequence. Preinstall may need to stay share-sourced longer than + runtime, or fetch a bootstrap manifest anonymously over HTTP. +- **This is a big build.** Realistically phased: (P1) model + importer + parity + test; (P2) admin API + CRUD; (P3) frontend editor on /settings/pctypemapping; + (P4) client fetch + shadow mode; (P5) read cutover; (P6) payload migration. + P1 is the gating de-risk - if the round-trip is not lossless, stop. + +## 11. Relationship to existing work + +- Replaces `plugins/computers/pctypemap.py` (the thin `pctypemap_` + settings) - the pctype -> ComputerType mapping becomes the `computertypeid` + column on `manifestscopes`. Migrate those settings in, then retire them. +- Also folds in the metrology mapping now living in `pctypemap.py` + (`METROLOGY_TOOL_MAP`). The collector already auto-creates a MeasuringTool + asset and a directional PC->tool `controls` relationship when it sees a + metrology pctype (CMM / Keyence / Genspect / wax-and-trace); the PC stays a + shopfloor PC. A metrology scope in the manifest model should carry the + attached-measuring-tool type alongside its ComputerType so imaging and + collector agree on what device the scope implies. +- Reuses the collector's token machinery (PAT + `X-API-Key` + scopes) for the + client-facing endpoints. +- Reuses `get_permissions()` (contract 0.10.0) for `geenforce.manage` / + `geenforce.fetch`. +- Pairs with the collector: desired-state (this plugin) + observed-state + (collector) enable a fleet compliance view. + +## 12. Recommendation + +Feasible and a strong architectural fit, but it is a multi-phase build with a +fleet-wide blast radius. The single most important gate is P1: import the real +manifests and prove byte-identical round-trip. Do not build the UI or touch a +client until that parity test is green. If and when we proceed, this warrants a +new ADR (ADR-012: GE-Enforce manifest ownership) capturing the desired-state +model, the SMB/HTTP/inline payload contract, and the fail-safe cache. diff --git a/frontend/src/views/settings/SettingsIndex.vue b/frontend/src/views/settings/SettingsIndex.vue index d3d9c8e..53ab336 100644 --- a/frontend/src/views/settings/SettingsIndex.vue +++ b/frontend/src/views/settings/SettingsIndex.vue @@ -4,7 +4,7 @@ Pick a section from the left, or choose one below.

-
+

{{ group.title }}