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.
-