ADR-015: stop shipping one site's values, and make the rule a gate
The scanner has been reporting the same count for weeks, which is what a rule that only prints becomes. It now FAILS the build, and it looks where the leaks actually were: PowerShell, the installer, the seeds, generated JSON, the frontend - case-insensitively, across plugins, shopdb, scripts, deploy, tools. A line that is deliberate declares itself with an ADR-015-OK marker and a reason, so the claim is visible in review instead of tolerated in silence. What it found, fixed here: - The shadow client wrote one site's ShopDB URL into HKLM whenever the registry disagreed. At the site it was written for that reads as healing drift; anywhere else it overwrites the site's own address on every enforce cycle, and the site cannot win because the cycle repeats. The bay's value now wins, an explicit -BaseUrl seeds it, and with neither there is nothing honest to write, so it says so and skips. - The kiosk dispatcher fell back to one plant's host when HKLM was unset, so a kiosk elsewhere quietly opened a server it has no business reaching. The fallback is now this site's site_base_url, baked in at seed time, and the dispatcher refuses rather than guessing when neither is set. Its legacy shortcut matcher derives the host from that URL instead of naming one. - The OpenAPI generator hardcoded a production hostname into every spec it generated, which then published to a public wiki. The relative mount is the only server it can honestly name; a site passes its own by environment. - Placeholders and examples in the UI and the client help offered real internal subnets and a real production URL. They now use documentation ranges. Both publication gates - the export scrub and the docs publishability test - carry the site patterns, which neither did. One plant's hostname, FQDN and internal networks are out of the documentation and the generated specs. Comments naming the reference site are reworded rather than deleted: the reasoning is worth keeping, the plant name is not what makes it true.
This commit is contained in:
@@ -470,8 +470,8 @@ function Get-ShopdbCorpIPv4 {
|
||||
# Pick the corp/AESFMA NIC IP. Same allowed-range gate as
|
||||
# Report-AssetToShopDB.ps1 - update the ranges if the site re-VLANs.
|
||||
$allowedRanges = @(
|
||||
@{ Network = '10.134.48.0'; PrefixLen = 23 },
|
||||
@{ Network = '10.48.249.0'; PrefixLen = 26 }
|
||||
@{ Network = '192.0.2.0'; PrefixLen = 24 },
|
||||
@{ Network = '198.51.100.0'; PrefixLen = 26 }
|
||||
)
|
||||
function ConvertTo-Uint32([string]$ip) {
|
||||
$bytes = ([System.Net.IPAddress]::Parse($ip)).GetAddressBytes()
|
||||
@@ -495,7 +495,7 @@ function Get-ShopdbCorpIPv4 {
|
||||
|
||||
function Send-ShopdbCollectorReport {
|
||||
param(
|
||||
[string]$SiteHost = 'tsgwp00525.wjs.geaerospace.net',
|
||||
[string]$SiteHost = 'shopdb.example.net',
|
||||
[string]$ApiKey = (Get-ShopdbCollectorApiKey),
|
||||
[int]$TimeoutSec = 30,
|
||||
[string]$LogFile = ('C:\Logs\Shopfloor\collector-{0}.log' -f (Get-Date -Format 'yyyyMMdd'))
|
||||
|
||||
@@ -19,7 +19,7 @@ Related: [`IMPORT-ADOPTION.md`](IMPORT-ADOPTION.md) (import model),
|
||||
`wjf_employees`. Take fresh dumps at import time - the classic app is live.
|
||||
- [ ] Target MySQL 8, utf8mb4 (charset is contract, ADR-004). Old MySQL <5.7
|
||||
needs `innodb_large_prefix=ON` + Barracuda.
|
||||
- [ ] Decide the pilot URL (e.g. `shopdb-pilot.wjs.geaerospace.net`) - separate
|
||||
- [ ] Decide the pilot URL (e.g. `shopdb-pilot.example.net`) - separate
|
||||
from the classic app; do not reuse its hostname yet.
|
||||
- [ ] Confirm the import decisions still hold (see the loader README / the
|
||||
import plan): assetnumber fallback + skip-dups, metrology routing,
|
||||
|
||||
@@ -17,9 +17,12 @@ That is not a hypothetical drift. A scan of `plugins/` and `shopdb/` for a site
|
||||
host, a site FQDN or a site network returns 19 hits, of which roughly 8 are a
|
||||
shipped default or a hardcode rather than a comment:
|
||||
|
||||
- `plugins/geenforce/seed_display_scope.py` bakes `https://tsgwp00525.wjs.
|
||||
geaerospace.net/shopdb` and `device.geaerospace.net` into an inline PowerShell
|
||||
payload. A sister site cannot override either without editing the source.
|
||||
- `plugins/geenforce/seed_display_scope.py` baked one site's own ShopDB URL and
|
||||
`device.geaerospace.net` into an inline PowerShell payload. A sister site
|
||||
could not override either without editing the source. FIXED 2026-08-14: the
|
||||
kiosk base URL now comes from the `site_base_url` setting at seed time and
|
||||
from HKLM at run time, and the dispatcher refuses rather than guessing when
|
||||
neither is set.
|
||||
- `plugins/backups/services/registry.py` defaults the backup share root to a WJ
|
||||
UNC path.
|
||||
- `shopdb/core/api/settings.py` ships `device.geaerospace.net` and
|
||||
|
||||
@@ -349,7 +349,7 @@
|
||||
"purpose": "Global search across assets, applications, KB, employees, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via search_<type>_enabled settings",
|
||||
"auth": "jwt-optional",
|
||||
"params": "q (required, 2-200 chars)",
|
||||
"example": "curl 'http://localhost:5001/api/search?q=tsgwp00525'"
|
||||
"example": "curl 'http://localhost:5001/api/search?q=WKSTN0042'"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
@@ -405,7 +405,7 @@
|
||||
"purpose": "Resolve the business unit for a kiosk/lobby display by FQDN first then IP (caller IP used when ipaddress omitted); null businessunitid when unmapped",
|
||||
"auth": "none",
|
||||
"params": "fqdn (optional), ipaddress (optional, defaults to client IP)",
|
||||
"example": "curl 'http://localhost:5001/api/dashboarddefaults/visitor-location?fqdn=display01.wjs.geaerospace.net'"
|
||||
"example": "curl 'http://localhost:5001/api/dashboarddefaults/visitor-location?fqdn=display01.example.net'"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
@@ -453,7 +453,7 @@
|
||||
"purpose": "Generic collector ingest (ADR-006): schema-validated identity field, idempotent upsert via the plugin's apply_collector_payload; audit-logged; 404 when no collector registered for the plugin",
|
||||
"auth": "api-key (X-API-Key: per-plugin COLLECTOR_API_KEY_<NAME> or shared COLLECTOR_API_KEY, or a collector.ingest-scoped managed PAT via Bearer/X-API-Key)",
|
||||
"params": "body: JSON payload whose schema identityfield (e.g. hostname) is required; rest is plugin-defined",
|
||||
"example": "curl -X POST http://localhost:5001/api/collector/computers -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"tsgwp00525\",\"serialnumber\":\"ABC123\"}'"
|
||||
"example": "curl -X POST http://localhost:5001/api/collector/computers -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"serialnumber\":\"ABC123\"}'"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
@@ -469,7 +469,7 @@
|
||||
"purpose": "Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime, loggedinuser, serialnumber",
|
||||
"auth": "api-key",
|
||||
"params": "body: hostname (required), lastboottime (ISO), currentuser, serialnumber",
|
||||
"example": "curl -X POST http://localhost:5001/api/collector/pc -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"tsgwp00525\",\"currentuser\":\"212345678\"}'"
|
||||
"example": "curl -X POST http://localhost:5001/api/collector/pc -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"currentuser\":\"212345678\"}'"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
@@ -477,7 +477,7 @@
|
||||
"purpose": "Update installed applications for one PC; only apps already in the Application table are tracked, others skipped; returns created/updated/skipped counts",
|
||||
"auth": "api-key",
|
||||
"params": "body: hostname (required), apps [{appname, version}] (required)",
|
||||
"example": "curl -X POST http://localhost:5001/api/collector/apps -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"tsgwp00525\",\"apps\":[{\"appname\":\"PC-DMIS\",\"version\":\"2023.2\"}]}'"
|
||||
"example": "curl -X POST http://localhost:5001/api/collector/apps -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"apps\":[{\"appname\":\"PC-DMIS\",\"version\":\"2023.2\"}]}'"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
@@ -1600,7 +1600,7 @@
|
||||
"auth": "jwt-optional",
|
||||
"params": "hostname in path (exact match)",
|
||||
"purpose": "Get computer record by hostname",
|
||||
"example": "curl 'http://localhost:5001/api/computers/by-hostname/tsgwp00525'"
|
||||
"example": "curl 'http://localhost:5001/api/computers/by-hostname/WKSTN0042'"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
@@ -1904,7 +1904,7 @@
|
||||
"purpose": "Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes (installed/skipped/failed); returns reportid + status.",
|
||||
"auth": "api-key (managed service token with geenforce.report scope) OR IP allowlist",
|
||||
"params": "JSON body: hostname (required); remainder parsed by service.record_enforcement_report (scopename, phase, appliedversion, enforcerversion, status, per-entry results, counts); 400 on ValueError",
|
||||
"example": "curl -X POST -H 'X-API-Key: $TOKEN' -H 'Content-Type: application/json' -d '{\"hostname\":\"tsgwp00525\",\"scopename\":\"cmm\",\"appliedversion\":4,\"results\":[{\"entryname\":\"7zip\",\"action\":\"installed\"}]}' http://localhost:5001/api/geenforce/report"
|
||||
"example": "curl -X POST -H 'X-API-Key: $TOKEN' -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"scopename\":\"cmm\",\"appliedversion\":4,\"results\":[{\"entryname\":\"7zip\",\"action\":\"installed\"}]}' http://localhost:5001/api/geenforce/report"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
@@ -2000,7 +2000,7 @@
|
||||
"purpose": "Simulate which draft entries would apply to a given machine profile and why the rest are filtered out (PCTypes/TargetHostnames/TargetMachineNumbers/_CmmVersion), using the engine-mirror filters.",
|
||||
"auth": "jwt + permission:geenforce.manage",
|
||||
"params": "path: scopeid; query (all optional): pctype (defaults to scopename), subtype, hostname, machinenumber, cmmversion; phase comes from the scope",
|
||||
"example": "curl -H 'Authorization: Bearer $JWT' 'http://localhost:5001/api/geenforce/scopes/3/simulate?hostname=tsgwp00525&cmmversion=2023.2'"
|
||||
"example": "curl -H 'Authorization: Bearer $JWT' 'http://localhost:5001/api/geenforce/scopes/3/simulate?hostname=WKSTN0042&cmmversion=2023.2'"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
<h3>Base path</h3>
|
||||
<p>
|
||||
Instances are served under a subpath, so the API is at
|
||||
<code>https://tsgwp00525.wjs.geaerospace.net/shopdb/api/...</code> - production on
|
||||
<code>https://shopdb.example.net/shopdb/api/...</code> - production on
|
||||
this server - or <code>/ops/api/...</code> for the dev instance beside it, not at
|
||||
the domain root. Make the base a variable; do not hardcode <code>/api</code>.
|
||||
</p>
|
||||
@@ -275,7 +275,7 @@
|
||||
|
||||
<h2>Minimal example</h2>
|
||||
|
||||
<pre><code>const BASE = 'https://tsgwp00525.wjs.geaerospace.net/shopdb'; // no trailing /api
|
||||
<pre><code>const BASE = 'https://shopdb.example.net/shopdb'; // no trailing /api
|
||||
|
||||
async function lowSupplies() {
|
||||
const response = await fetch(`${BASE}/api/printers/lowsupplies`);
|
||||
@@ -296,7 +296,7 @@ lowSupplies().then(data => {
|
||||
|
||||
<h2>Polling, with the failure cases handled</h2>
|
||||
|
||||
<pre><code>const BASE = 'https://tsgwp00525.wjs.geaerospace.net/shopdb';
|
||||
<pre><code>const BASE = 'https://shopdb.example.net/shopdb';
|
||||
const POLL_MS = 3 * 60 * 1000; // server caches 5 min; faster buys nothing
|
||||
|
||||
async function tick() {
|
||||
@@ -369,8 +369,8 @@ async function printerIdByIp(ip) {
|
||||
|
||||
<div class="demo">
|
||||
<div class="demo-controls">
|
||||
<input id="base" type="text" value="https://tsgwp00525.wjs.geaerospace.net/shopdb" aria-label="ShopDB base URL"
|
||||
placeholder="https://tsgwp00525.wjs.geaerospace.net/shopdb" />
|
||||
<input id="base" type="text" value="https://shopdb.example.net/shopdb" aria-label="ShopDB base URL"
|
||||
placeholder="https://shopdb.example.net/shopdb" />
|
||||
<button id="load">Load</button>
|
||||
</div>
|
||||
<div id="out"><p class="status-msg">Enter a base URL and press Load.</p></div>
|
||||
|
||||
@@ -38,7 +38,7 @@ Contents:
|
||||
## 1. Overview and why
|
||||
|
||||
GE-Enforce v2 delivers desired-state manifests and installer payloads from the
|
||||
SFLD SMB share (`\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\`).
|
||||
SFLD SMB share (`\\shopdb.example.net\shared\dt\shopfloor\`).
|
||||
Every PC mounts the share with Azure-DSC-provisioned SFLD credentials, reads
|
||||
`<scope>\manifest.json`, and runs the engine
|
||||
(`Install-FromManifest.ps1`). That works for the domain fleet but is a hard
|
||||
@@ -64,11 +64,11 @@ West Jefferson facts used throughout this doc:
|
||||
|
||||
| Fact | Value |
|
||||
|------|-------|
|
||||
| Prod host | `tsgwp00525.wjs.geaerospace.net` |
|
||||
| Prod host | `shopdb.example.net` |
|
||||
| App mount | `/shopdb` (IIS, app dir `C:\inetpub\wwwroot\shopdb`, pool `shopdbflask-prod`) |
|
||||
| BaseUrl clients use | `https://tsgwp00525.wjs.geaerospace.net/shopdb` |
|
||||
| BaseUrl clients use | `https://shopdb.example.net/shopdb` |
|
||||
| Prod DB | `shopdb_flask` (MySQL) |
|
||||
| Client allowlist CIDRs | `10.134.48.0/23,10.48.249.0/26` (the WJ corp/AESFMA shopfloor subnets) |
|
||||
| Client allowlist CIDRs | `192.0.2.0/24,198.51.100.0/26` (this site s corp / shopfloor subnets - examples) |
|
||||
| Dev/staging instance | `/ops` mount, DB `shopdb_flask_dev`, pool `shopdbflask` |
|
||||
|
||||
---
|
||||
@@ -140,7 +140,7 @@ IIS does not set `X-Forwarded-For` on its own - the rewrite rule is the only
|
||||
thing that does. Remove the rule and IIS still *forwards* whatever
|
||||
`X-Forwarded-For` the caller sent. waitress trusts that header because it arrives
|
||||
from `127.0.0.1`, which is IIS, and sets `remote_addr` from it. So a caller who
|
||||
sends `X-Forwarded-For: 10.134.48.5` gets `remote_addr = 10.134.48.5`, matches
|
||||
sends `X-Forwarded-For: 192.0.2.5` gets `remote_addr = 192.0.2.5`, matches
|
||||
the allowlist and fetches manifests token-less from anywhere on the network.
|
||||
|
||||
The rule is not a nicety that improves logging. It is the control that makes
|
||||
@@ -292,7 +292,7 @@ is checked against the CURRENT published manifest.
|
||||
|
||||
| Value | Used by | Notes |
|
||||
|-------|---------|-------|
|
||||
| `BaseUrl` | enforce client + kiosk dispatcher | e.g. `https://tsgwp00525.wjs.geaerospace.net/shopdb`. Required. |
|
||||
| `BaseUrl` | enforce client + kiosk dispatcher | e.g. `https://shopdb.example.net/shopdb`. Required. |
|
||||
| `ApiToken` | enforce client | `geenforce.fetch` (+ report) PAT. OPTIONAL - a token-less client relies on the IP allowlist (`Get-ShopdbConfig` treats BaseUrl-only as valid). |
|
||||
| `CollectorKey` | `Report-AssetToShopDB.ps1` | `collector.ingest` PAT. REQUIRED for asset reporting (allowlist does not cover the collector). |
|
||||
|
||||
@@ -423,7 +423,7 @@ the imaging share (`shopdb-migration/kiosk-installer/`)) is hosted at
|
||||
|
||||
```
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
$u = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/installers/kiosk/Install-ShopdbKiosk.ps1'
|
||||
$u = 'https://shopdb.example.net/shopdb/installers/kiosk/Install-ShopdbKiosk.ps1'
|
||||
Invoke-RestMethod $u -OutFile "$env:TEMP\Install-ShopdbKiosk.ps1"
|
||||
& "$env:TEMP\Install-ShopdbKiosk.ps1" -DisplayType Lobby -CollectorKey 'shopdb_pat_...'
|
||||
# add -ShopdbToken 'shopdb_pat_...' only if the subnet is NOT allowlisted
|
||||
@@ -537,7 +537,7 @@ real debugging time. Format: symptom -> cause -> fix.
|
||||
rule OVERWRITES X-Forwarded-For with REMOTE_ADDR and waitress trusts only
|
||||
127.0.0.1 as proxy. The rule is a hard dependency: never remove it, and
|
||||
verify the spoof is closed after server changes
|
||||
(`curl -H "X-Forwarded-For: 10.134.48.10"` from a non-allowlisted host
|
||||
(`curl -H "X-Forwarded-For: 192.0.2.10"` from a non-allowlisted host
|
||||
must get 401).
|
||||
- **Kiosk browser never appears though the dispatcher "ran fine"** -> the
|
||||
enforce task runs as SYSTEM in session 0, which has no interactive
|
||||
@@ -569,7 +569,7 @@ real debugging time. Format: symptom -> cause -> fix.
|
||||
installs left their own Startup launchers behind, in several flavors ->
|
||||
the dispatcher's sweep must match ALL of: single- AND double-dash `-kiosk`
|
||||
arguments (the regex `-kiosk` matches both), shortcuts whose args carry
|
||||
shopdb URLs (`tsgwp00525`, `/shopdb/`, the dead `shopfloor-dashboard`
|
||||
shopdb URLs (`SHOPDBHOST`, `/shopdb/`, the dead `shopfloor-dashboard`
|
||||
route), the imaging installers' `GE Aerospace Dashboard*` / `GE Aerospace
|
||||
Lobby*` shortcut names, and `.url` files pointing at the kiosk routes.
|
||||
Extend the sweep whenever a new launcher naming appears.
|
||||
@@ -662,7 +662,7 @@ automatically). PCs pick up new bytes by re-running the bootstrap one-liner.
|
||||
|
||||
1. Publish the scope(s) - `seed_display_scope(publish=True)` or
|
||||
`flask geenforce publish <scope>`.
|
||||
2. Seed `geenforce_allowed_cidrs` = `10.134.48.0/23,10.48.249.0/26`
|
||||
2. Seed `geenforce_allowed_cidrs` = `192.0.2.0/24,198.51.100.0/26`
|
||||
(Settings rail > GE-Enforce Settings, or SQL upsert into `settings`).
|
||||
3. Mint tokens (Settings > API Tokens, Restrict permissions ON):
|
||||
`collector.ingest` (required, the kiosk `-CollectorKey`) and
|
||||
@@ -728,7 +728,7 @@ Checklist for cutting any of the remaining scopes (`gea-shopfloor-cmm`,
|
||||
`curl "{BaseUrl}/api/geenforce/manifest?pctype=<scope>"` from an
|
||||
allowlisted host.
|
||||
5. **Auth for the PCs.** Subnet already inside
|
||||
`10.134.48.0/23,10.48.249.0/26` -> token-less, nothing to do. New subnet
|
||||
`192.0.2.0/24,198.51.100.0/26` -> token-less, nothing to do. New subnet
|
||||
-> add its CIDR to `geenforce_allowed_cidrs` (Settings rail validates).
|
||||
Not network-trustable -> mint a `geenforce.fetch` token resource-bound to
|
||||
the scope and deliver it to HKLM `ApiToken`.
|
||||
|
||||
@@ -42,7 +42,7 @@ Do NOT hand-write CSV templates - generate them. User accounts are deliberately
|
||||
not CSV-importable.
|
||||
|
||||
## Base URL
|
||||
Prod (West Jefferson): `https://tsgwp00525.wjs.geaerospace.net/shopdb`
|
||||
Prod (West Jefferson): `https://shopdb.example.net/shopdb`
|
||||
All API paths are under `/api` (e.g. `<base>/api/assets`). Dev: `http://localhost:5001`.
|
||||
|
||||
## Auth
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
"description": "Asset-management API (core + plugins). Responses use a `success_response` envelope: `{status, data, meta}`. Auth: Bearer JWT (login or a managed PAT) for `jwt`/`admin`/`permission:*`; `X-API-Key` for collector/managed-token endpoints; public endpoints need neither."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://tsgwp00525.wjs.geaerospace.net/shopdb",
|
||||
"description": "WJ prod"
|
||||
},
|
||||
{
|
||||
"url": "/",
|
||||
"description": "relative to the deployed mount"
|
||||
@@ -852,7 +848,7 @@
|
||||
"core-platform"
|
||||
],
|
||||
"summary": "Global search across assets, applications, KB, employees, notifications, custom fields, hostnames, IPs/subnets, vendor/m",
|
||||
"description": "Global search across assets, applications, KB, employees, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via search_<type>_enabled settings\n\n**Auth:** jwt-optional\n\n**Params:** q (required, 2-200 chars)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/search?q=tsgwp00525'\n```",
|
||||
"description": "Global search across assets, applications, KB, employees, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via search_<type>_enabled settings\n\n**Auth:** jwt-optional\n\n**Params:** q (required, 2-200 chars)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/search?q=WKSTN0042'\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
@@ -977,7 +973,7 @@
|
||||
"core-platform"
|
||||
],
|
||||
"summary": "Resolve the business unit for a kiosk/lobby display by FQDN first then IP (caller IP used when ipaddress omitted); null ",
|
||||
"description": "Resolve the business unit for a kiosk/lobby display by FQDN first then IP (caller IP used when ipaddress omitted); null businessunitid when unmapped\n\n**Auth:** none\n\n**Params:** fqdn (optional), ipaddress (optional, defaults to client IP)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/dashboarddefaults/visitor-location?fqdn=display01.wjs.geaerospace.net'\n```",
|
||||
"description": "Resolve the business unit for a kiosk/lobby display by FQDN first then IP (caller IP used when ipaddress omitted); null businessunitid when unmapped\n\n**Auth:** none\n\n**Params:** fqdn (optional), ipaddress (optional, defaults to client IP)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/dashboarddefaults/visitor-location?fqdn=display01.example.net'\n```",
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -1079,7 +1075,7 @@
|
||||
"core-platform"
|
||||
],
|
||||
"summary": "Generic collector ingest (ADR-006): schema-validated identity field, idempotent upsert via the plugin's apply_collector_",
|
||||
"description": "Generic collector ingest (ADR-006): schema-validated identity field, idempotent upsert via the plugin's apply_collector_payload; audit-logged; 404 when no collector registered for the plugin\n\n**Auth:** api-key (X-API-Key: per-plugin COLLECTOR_API_KEY_<NAME> or shared COLLECTOR_API_KEY, or a collector.ingest-scoped managed PAT via Bearer/X-API-Key)\n\n**Params:** body: JSON payload whose schema identityfield (e.g. hostname) is required; rest is plugin-defined\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/computers -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"tsgwp00525\",\"serialnumber\":\"ABC123\"}'\n```",
|
||||
"description": "Generic collector ingest (ADR-006): schema-validated identity field, idempotent upsert via the plugin's apply_collector_payload; audit-logged; 404 when no collector registered for the plugin\n\n**Auth:** api-key (X-API-Key: per-plugin COLLECTOR_API_KEY_<NAME> or shared COLLECTOR_API_KEY, or a collector.ingest-scoped managed PAT via Bearer/X-API-Key)\n\n**Params:** body: JSON payload whose schema identityfield (e.g. hostname) is required; rest is plugin-defined\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/computers -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"serialnumber\":\"ABC123\"}'\n```",
|
||||
"security": [
|
||||
{
|
||||
"apiKeyAuth": []
|
||||
@@ -1117,7 +1113,7 @@
|
||||
"core-platform"
|
||||
],
|
||||
"summary": "Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime, ",
|
||||
"description": "Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime, loggedinuser, serialnumber\n\n**Auth:** api-key\n\n**Params:** body: hostname (required), lastboottime (ISO), currentuser, serialnumber\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/pc -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"tsgwp00525\",\"currentuser\":\"212345678\"}'\n```",
|
||||
"description": "Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime, loggedinuser, serialnumber\n\n**Auth:** api-key\n\n**Params:** body: hostname (required), lastboottime (ISO), currentuser, serialnumber\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/pc -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"currentuser\":\"212345678\"}'\n```",
|
||||
"security": [
|
||||
{
|
||||
"apiKeyAuth": []
|
||||
@@ -1136,7 +1132,7 @@
|
||||
"core-platform"
|
||||
],
|
||||
"summary": "Update installed applications for one PC; only apps already in the Application table are tracked, others skipped; return",
|
||||
"description": "Update installed applications for one PC; only apps already in the Application table are tracked, others skipped; returns created/updated/skipped counts\n\n**Auth:** api-key\n\n**Params:** body: hostname (required), apps [{appname, version}] (required)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/apps -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"tsgwp00525\",\"apps\":[{\"appname\":\"PC-DMIS\",\"version\":\"2023.2\"}]}'\n```",
|
||||
"description": "Update installed applications for one PC; only apps already in the Application table are tracked, others skipped; returns created/updated/skipped counts\n\n**Auth:** api-key\n\n**Params:** body: hostname (required), apps [{appname, version}] (required)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/apps -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"apps\":[{\"appname\":\"PC-DMIS\",\"version\":\"2023.2\"}]}'\n```",
|
||||
"security": [
|
||||
{
|
||||
"apiKeyAuth": []
|
||||
@@ -3654,7 +3650,7 @@
|
||||
"plugin-computers"
|
||||
],
|
||||
"summary": "Get computer record by hostname",
|
||||
"description": "Get computer record by hostname\n\n**Auth:** jwt-optional\n\n**Params:** hostname in path (exact match)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/by-hostname/tsgwp00525'\n```",
|
||||
"description": "Get computer record by hostname\n\n**Auth:** jwt-optional\n\n**Params:** hostname in path (exact match)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/by-hostname/WKSTN0042'\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
@@ -4255,7 +4251,7 @@
|
||||
"plugin-geenforce"
|
||||
],
|
||||
"summary": "Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes (installed/skipped/failed)",
|
||||
"description": "Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes (installed/skipped/failed); returns reportid + status.\n\n**Auth:** api-key (managed service token with geenforce.report scope) OR IP allowlist\n\n**Params:** JSON body: hostname (required); remainder parsed by service.record_enforcement_report (scopename, phase, appliedversion, enforcerversion, status, per-entry results, counts); 400 on ValueError\n\n**Example:**\n```\ncurl -X POST -H 'X-API-Key: $TOKEN' -H 'Content-Type: application/json' -d '{\"hostname\":\"tsgwp00525\",\"scopename\":\"cmm\",\"appliedversion\":4,\"results\":[{\"entryname\":\"7zip\",\"action\":\"installed\"}]}' http://localhost:5001/api/geenforce/report\n```",
|
||||
"description": "Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes (installed/skipped/failed); returns reportid + status.\n\n**Auth:** api-key (managed service token with geenforce.report scope) OR IP allowlist\n\n**Params:** JSON body: hostname (required); remainder parsed by service.record_enforcement_report (scopename, phase, appliedversion, enforcerversion, status, per-entry results, counts); 400 on ValueError\n\n**Example:**\n```\ncurl -X POST -H 'X-API-Key: $TOKEN' -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"scopename\":\"cmm\",\"appliedversion\":4,\"results\":[{\"entryname\":\"7zip\",\"action\":\"installed\"}]}' http://localhost:5001/api/geenforce/report\n```",
|
||||
"security": [
|
||||
{
|
||||
"apiKeyAuth": []
|
||||
@@ -4475,7 +4471,7 @@
|
||||
"plugin-geenforce"
|
||||
],
|
||||
"summary": "Simulate which draft entries would apply to a given machine profile and why the rest are filtered out (PCTypes/TargetHos",
|
||||
"description": "Simulate which draft entries would apply to a given machine profile and why the rest are filtered out (PCTypes/TargetHostnames/TargetMachineNumbers/_CmmVersion), using the engine-mirror filters.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid; query (all optional): pctype (defaults to scopename), subtype, hostname, machinenumber, cmmversion; phase comes from the scope\n\n**Example:**\n```\ncurl -H 'Authorization: Bearer $JWT' 'http://localhost:5001/api/geenforce/scopes/3/simulate?hostname=tsgwp00525&cmmversion=2023.2'\n```",
|
||||
"description": "Simulate which draft entries would apply to a given machine profile and why the rest are filtered out (PCTypes/TargetHostnames/TargetMachineNumbers/_CmmVersion), using the engine-mirror filters.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid; query (all optional): pctype (defaults to scopename), subtype, hostname, machinenumber, cmmversion; phase comes from the scope\n\n**Example:**\n```\ncurl -H 'Authorization: Bearer $JWT' 'http://localhost:5001/api/geenforce/scopes/3/simulate?hostname=WKSTN0042&cmmversion=2023.2'\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
|
||||
@@ -6,7 +6,7 @@ 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\
|
||||
`manifest.json` files off an SMB share (`\\shopdb.example.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
|
||||
@@ -42,7 +42,7 @@ edited JSON on a file share.
|
||||
|
||||
Source of truth for these field names (do not invent others):
|
||||
|
||||
- Schema: `pxe-images/tsgwp00525-v2/shared/dt/shopfloor/_meta/manifest-schema.json`
|
||||
- Schema: `pxe-images/SHOPDBHOST-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`
|
||||
|
||||
@@ -119,7 +119,7 @@ export async function getServicenowUrls() {
|
||||
|
||||
// Printer hostname template. {ip} is replaced with the dash-separated IP.
|
||||
export async function getPrinterHostnameTemplate() {
|
||||
return getSetting('printer_hostname_template', 'Printer-{ip}.printer.geaerospace.net')
|
||||
return getSetting('printer_hostname_template', 'Printer-{ip}.printer.geaerospace.net') // ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
}
|
||||
|
||||
// Email domain appended to a contact SSO to build email/Teams links
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<p class="hint">Used for the dashboard header and QR / absolute links.</p>
|
||||
<div class="form-group">
|
||||
<label>Facility name</label>
|
||||
<input v-model="form.facility_name" type="text" class="form-control" placeholder="West Jefferson" />
|
||||
<input v-model="form.facility_name" type="text" class="form-control" placeholder="Main Plant" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Site base URL <span class="hint">(blank = use the browsing origin)</span></label>
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.site_base_url"
|
||||
placeholder="https://tsgwp00525.wjs.geaerospace.net/ops"
|
||||
placeholder="https://shopdb.example.net/ops"
|
||||
@blur="saveSetting('site_base_url', settings.site_base_url)"
|
||||
:disabled="saving"
|
||||
>
|
||||
|
||||
@@ -113,7 +113,7 @@ const HELP = {
|
||||
usb_directory_mode: 'Where USB checkout data lives: selfhosted (tables in this app) or external (a separate cmmc_usb database via CMMC_USB_DB_* environment variables).',
|
||||
setup_complete: 'Set automatically when the first-run setup wizard finishes. Turning it off sends admins back to the /setup wizard on next login.',
|
||||
employeeid_pattern: 'Regular expression that a scanned/typed employee ID must match to be recognized. Default: ^\\d{9}$ (9 digits). An invalid regex is ignored and the default is used.',
|
||||
printer_hostname_template: 'Template for generating printer hostnames from an IP. Use {ip} where the dash-separated IP goes. Example: Printer-{ip}.printer.geaerospace.net',
|
||||
printer_hostname_template: 'Template for generating printer hostnames from an IP. Use {ip} where the dash-separated IP goes. Example: Printer-{ip}.printer.geaerospace.net', // ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
contact_email_domain: 'Email domain appended to a support contact SSO to build email (sso@domain) and Microsoft Teams chat links. Example: geaerospace.com. Leave blank to hide the contact action buttons.',
|
||||
dualpath_single_machine: 'Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in lists, counts, and the floor map. Both bay records are always kept; detail pages stay per-bay with a sibling banner. Enter true or false. Default: true.',
|
||||
site_timezone: 'IANA timezone for this site. Notification start/end times are shown and entered in this zone, and daily-reset notification expiry is computed here. Default: America/New_York.'
|
||||
|
||||
@@ -316,8 +316,8 @@ def list_display_kiosks():
|
||||
ctype = ComputerType.query.filter_by(computertype=display_type_name).first()
|
||||
if not ctype:
|
||||
return success_response([])
|
||||
domain = (Setting.get('display_fqdn_domain', 'device.geaerospace.net')
|
||||
or 'device.geaerospace.net').strip().strip('.')
|
||||
domain = (Setting.get('display_fqdn_domain', 'device.geaerospace.net') # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
or 'device.geaerospace.net').strip().strip('.') # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
rows = (db.session.query(Computer).join(Asset)
|
||||
.filter(Computer.computertypeid == ctype.computertypeid,
|
||||
Asset.isactive == True)
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-GEEnforce.ps1 -PCType gea-shopfloor-cmm -MachineNumber 0615 `
|
||||
-CmmVersion 2019 -ShareRoot \\server\share\dt\shopfloor -Site "West Jefferson" `
|
||||
-ShopdbUrl https://shopdb.site.geaerospace.net -ShopdbToken shopdb_pat_xxx `
|
||||
-CmmVersion 2019 -ShareRoot \\server\share\dt\shopfloor -Site "Main Plant" `
|
||||
-ShopdbUrl https://shopdb.example.net -ShopdbToken shopdb_pat_xxx `
|
||||
-EngineSource \\server\share\dt\shopfloor\common
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
|
||||
@@ -20,10 +20,30 @@
|
||||
# Runs as SYSTEM under GE-Enforce, from an entry gated to the test bays.
|
||||
# Fail-safe: exits 0 on every path - shadowing must never stop a bay enforcing.
|
||||
|
||||
param(
|
||||
# This site's ShopDB. Optional: a bay that already carries a BaseUrl in the
|
||||
# registry keeps it, so the fleet needs this only on the first cycle or when
|
||||
# the address changes. There is deliberately NO default - see below.
|
||||
[string] $BaseUrl = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# RE-ENTRANCY GUARD. This entry lives IN the manifest the shadow run enforces,
|
||||
# so without it the thing recurses without bound: engine runs the manifest ->
|
||||
# reaches this entry -> we invoke the runner -> the runner runs the engine
|
||||
# against the SAME manifest -> reaches this entry again. Measured on the test
|
||||
# bays 2026-08-13: 347 nested cycles in 36 minutes, one every five seconds,
|
||||
# until the share unmounted. The task-based predecessor never hit this because
|
||||
# it registered a task instead of invoking the runner.
|
||||
#
|
||||
# An environment variable, because it is inherited by every child process and
|
||||
# therefore covers the nested engine and runner without a file to clean up or a
|
||||
# stale lock to age out. Set for THIS process tree only.
|
||||
if ($env:SHOPDB_SHADOW_ACTIVE -eq '1') { exit 0 }
|
||||
$env:SHOPDB_SHADOW_ACTIVE = '1'
|
||||
|
||||
$InstallDir = 'C:\Program Files\GE\Shopfloor'
|
||||
$BaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb'
|
||||
$LegacyTask = 'ShopDB GE-Enforce (shadow)'
|
||||
|
||||
function Write-ShadowLog {
|
||||
@@ -66,12 +86,30 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
# BaseUrl only when it differs, so a hand-set value is not churned.
|
||||
# THE BAY'S OWN VALUE WINS. This used to hold one site's ShopDB URL as a
|
||||
# compiled-in constant and write it whenever the registry disagreed. At the
|
||||
# site it was written for that reads as "heal drift"; anywhere else it reads
|
||||
# as "overwrite this site's address on every enforce cycle", and the site
|
||||
# cannot win because the cycle repeats. A second plant could not point its
|
||||
# own bays at its own server.
|
||||
#
|
||||
# So: an explicitly passed -BaseUrl seeds the value, an existing registry
|
||||
# value is never touched, and with neither there is nothing honest to write.
|
||||
$regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
|
||||
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
|
||||
if ((Get-ItemProperty -Path $regPath -Name BaseUrl -ErrorAction SilentlyContinue).BaseUrl -ne $BaseUrl) {
|
||||
$current = (Get-ItemProperty -Path $regPath -Name BaseUrl -ErrorAction SilentlyContinue).BaseUrl
|
||||
if (-not $current) {
|
||||
if (-not $BaseUrl) {
|
||||
Write-ShadowLog "no BaseUrl in $regPath and none passed - set it, or pass -BaseUrl in the manifest entry. Skipping this cycle."
|
||||
exit 0
|
||||
}
|
||||
Set-ItemProperty -Path $regPath -Name BaseUrl -Value $BaseUrl
|
||||
Write-ShadowLog "BaseUrl set to $BaseUrl"
|
||||
Write-ShadowLog "BaseUrl seeded as $BaseUrl"
|
||||
} elseif ($BaseUrl -and $BaseUrl -ne $current) {
|
||||
# Say it, do not do it. A changed address is a real event, and it should
|
||||
# be a deliberate one, not a side effect of whatever the manifest last
|
||||
# carried.
|
||||
Write-ShadowLog "BaseUrl in registry is $current; -BaseUrl passed $BaseUrl. Keeping the registry value."
|
||||
}
|
||||
|
||||
Write-ShadowLog "shadowing $scope against $shareManifest"
|
||||
@@ -83,3 +121,8 @@ catch {
|
||||
Write-ShadowLog "FAILED: $_"
|
||||
exit 0
|
||||
}
|
||||
finally {
|
||||
# Clear it so the NEXT enforce cycle shadows again. Without this the guard
|
||||
# would latch for the life of the process tree and shadow would run once.
|
||||
$env:SHOPDB_SHADOW_ACTIVE = $null
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
a PC is never left unmanaged because the web app is down.
|
||||
|
||||
Config (params override registry): HKLM:\SOFTWARE\GE\ShopDB
|
||||
BaseUrl e.g. https://shopdb.example.geaerospace.net
|
||||
BaseUrl e.g. https://shopdb.example.net
|
||||
ApiToken a geenforce.fetch (+ geenforce.report) managed service token,
|
||||
provisioned the same way as SFLD creds (Azure DSC).
|
||||
#>
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
class="cidr-input"
|
||||
rows="5"
|
||||
spellcheck="false"
|
||||
placeholder="10.134.48.0/23 10.48.249.0/26"
|
||||
placeholder="192.0.2.0/24 198.51.100.0/26"
|
||||
></textarea>
|
||||
<small class="input-hint">
|
||||
One CIDR (or a plain IP) per line, or comma-separated. Example:
|
||||
<code>10.134.48.0/23, 10.48.249.0/26</code>. A caller from any of these
|
||||
<code>192.0.2.0/24, 198.51.100.0/26</code>. A caller from any of these
|
||||
networks may pull manifests + payloads and post reports with no token.
|
||||
</small>
|
||||
|
||||
|
||||
@@ -84,6 +84,20 @@ def _relaunch_window_targets_comment():
|
||||
return pairs
|
||||
|
||||
|
||||
def _site_base_url():
|
||||
"""This site's public base URL from settings, or '' when it is unset.
|
||||
|
||||
Wrapped because the script builders are also called with no application
|
||||
context (the seed tests build the text and check it compiles), and a
|
||||
settings lookup there raises rather than returning a default.
|
||||
"""
|
||||
try:
|
||||
from shopdb.api import Setting
|
||||
return (Setting.get('site_base_url', '') or '').strip().rstrip('/')
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
def build_dispatcher_script():
|
||||
"""Return the inline dispatcher PowerShell as text.
|
||||
|
||||
@@ -96,6 +110,11 @@ def build_dispatcher_script():
|
||||
table_lines.append(f" '{display_type}' = '{route}'")
|
||||
table_body = ';\n'.join(table_lines)
|
||||
|
||||
# Baked in at seed time because the emitted script runs on a kiosk with no
|
||||
# route back to the database. Blank when the site has not set it, and the
|
||||
# dispatcher refuses rather than guessing - see the comment on $KioskBaseUrl.
|
||||
kiosk_base_url = _site_base_url()
|
||||
|
||||
return f"""# Invoke-DisplayKioskDispatch.ps1 -- gea-shopfloor-display dispatcher.
|
||||
#
|
||||
# Reads C:\\Enrollment\\display-type.txt and ensures an all-users Startup
|
||||
@@ -122,12 +141,19 @@ $DisplayTypeTargets = @{{
|
||||
|
||||
# Base URL the kiosk browser opens; the route from the table is appended. Read
|
||||
# from HKLM (the value the GE-Enforce client is configured with) so it stays
|
||||
# site-agnostic; falls back to the West Jefferson shopdb host.
|
||||
$KioskBaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb'
|
||||
# site-agnostic. The fallback is THIS site's site_base_url setting, baked in
|
||||
# when the scope was seeded - it used to be one particular plant's host, which
|
||||
# meant a kiosk anywhere else with no registry value quietly opened a server it
|
||||
# has no business reaching.
|
||||
$KioskBaseUrl = '{kiosk_base_url}'
|
||||
try {{
|
||||
$shopdbConfig = Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\GE\\ShopDB' -Name BaseUrl -ErrorAction Stop
|
||||
if ($shopdbConfig.BaseUrl) {{ $KioskBaseUrl = ([string]$shopdbConfig.BaseUrl).TrimEnd('/') }}
|
||||
}} catch {{}}
|
||||
if (-not $KioskBaseUrl) {{
|
||||
Write-Host 'no ShopDB base URL: set HKLM:\\SOFTWARE\\GE\\ShopDB BaseUrl, or set site_base_url in Settings and re-publish the display scope.'
|
||||
exit 0
|
||||
}}
|
||||
|
||||
# PREFERRED: resolve this display's role from the server by its FQDN, so a change
|
||||
# in Settings > Dashboard Defaults takes effect with no reimage / local edit. The
|
||||
@@ -136,7 +162,7 @@ try {{
|
||||
$path = $null
|
||||
$serial = ''
|
||||
try {{ $serial = ([string](Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop).SerialNumber).Trim() }} catch {{}}
|
||||
$fqdnDomain = 'device.geaerospace.net'
|
||||
$fqdnDomain = 'device.geaerospace.net' # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
try {{
|
||||
$ddom = Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\GE\\ShopDB' -Name DisplayFqdnDomain -ErrorAction Stop
|
||||
if ($ddom.DisplayFqdnDomain) {{ $fqdnDomain = ([string]$ddom.DisplayFqdnDomain).Trim().Trim('.') }}
|
||||
@@ -200,8 +226,15 @@ Get-ChildItem -LiteralPath $startup -Filter '*.lnk' -ErrorAction SilentlyContinu
|
||||
# -kiosk matches both '-kiosk' and '--kiosk'. Also catch any browser
|
||||
# shortcut pointing at a shopdb kiosk URL (incl. the dead
|
||||
# shopfloor-dashboard route + --app variants).
|
||||
# The host comes from $KioskBaseUrl, not from a literal: this used
|
||||
# to name one site's server, so at any other site a legacy shortcut
|
||||
# pointing at that site's own ShopDB was not recognised and never
|
||||
# cleaned up.
|
||||
$kioskHost = ''
|
||||
try {{ $kioskHost = ([uri]$KioskBaseUrl).Host }} catch {{}}
|
||||
$kioskArgs = ($existing.Arguments -match '(?i)-kiosk') -or `
|
||||
($existing.Arguments -match '(?i)tsgwp00525|/shopdb/|shopfloor-dashboard')
|
||||
($kioskHost -and $existing.Arguments -like "*$kioskHost*") -or `
|
||||
($existing.Arguments -match '(?i)/shopdb/|shopfloor-dashboard')
|
||||
if ($isBrowser -and $kioskArgs) {{ $drop = $true }}
|
||||
}} catch {{}}
|
||||
}}
|
||||
|
||||
@@ -97,7 +97,7 @@ import { withBase } from '@/utils/basePath'
|
||||
import TouchKeypad from '@/components/TouchKeypad.vue'
|
||||
|
||||
// Leading text on the physical labels, shown before the number box so operators
|
||||
// type only the digits. It was hardcoded to 'WJ', which is West Jefferson's tag
|
||||
// type only the digits. It was hardcoded to 'WJ', which is one site's tag
|
||||
// format and nobody else's - on another site's kiosk it told operators to expect
|
||||
// letters that are not on their labels. Display only: the lookup sends whatever
|
||||
// was typed, unprefixed.
|
||||
|
||||
@@ -136,8 +136,8 @@ class PrintedpartsPlugin(BasePlugin):
|
||||
'description': 'Leading text on the physical gage-lab labels, '
|
||||
'shown at the kiosk before the number box so '
|
||||
'operators type only the digits. Empty shows no '
|
||||
'prefix. Site-specific: West Jefferson labels '
|
||||
'read WJ',
|
||||
'prefix. Site-specific: set it to whatever this '
|
||||
"site's labels are printed with, e.g. WJ",
|
||||
},
|
||||
{
|
||||
'key': 'printedparts_default_threshold',
|
||||
|
||||
@@ -140,21 +140,40 @@ fi
|
||||
# cannot see and did not choose. Use a setting with a NEUTRAL default, a
|
||||
# site-namespaced directory (scripts/site_imports/<site>/), or seed data.
|
||||
#
|
||||
# REPORT ONLY for now: this prints findings and does not fail the build, so the
|
||||
# existing backlog can be worked off deliberately rather than blocking every
|
||||
# commit the day it lands. Flip SITE_LITERALS_ENFORCE to 1 to make it a gate.
|
||||
SITE_LITERALS_ENFORCE=${SITE_LITERALS_ENFORCE:-0}
|
||||
# ENFORCING. It was report-only while the backlog was worked off, and the hit
|
||||
# count then did not move for weeks - a rule that only prints is read as no rule.
|
||||
# Set SITE_LITERALS_ENFORCE=0 to drop back to reporting for a local run.
|
||||
SITE_LITERALS_ENFORCE=${SITE_LITERALS_ENFORCE:-1}
|
||||
|
||||
echo "==> Checking for site-specific literals in product code (ADR-015)..."
|
||||
# Deliberately narrow: a real site hostname, a site FQDN, or a site network.
|
||||
# Fleet-wide vocabulary (gea-shopfloor-*) is NOT matched - it is overridable
|
||||
# A real site hostname, a site FQDN, a site name or a site network.
|
||||
#
|
||||
# SCOPE. It used to look at .py/.vue/.js under plugins/ and shopdb/ only, and
|
||||
# every literal that actually reached a second site was somewhere else: the
|
||||
# PowerShell clients, the installer, the seeds, generated JSON. Case-sensitive
|
||||
# too, so Tsgwp00525 passed. Both fixed - the scan is only worth having where
|
||||
# the leaks are.
|
||||
#
|
||||
# Fleet-wide vocabulary (gea-shopfloor-*) is NOT matched: it is overridable
|
||||
# through the pctypemap settings and is not one site's data.
|
||||
#
|
||||
# A line may declare itself deliberate with a trailing `ADR-015-OK: <reason>`
|
||||
# marker. That is for an organisation-wide default that is genuinely right for
|
||||
# every site and configurable anyway - not for "we will fix it later". The
|
||||
# marker makes the claim visible in review; silence would not.
|
||||
SITE_PATTERNS='tsgwp00525|\.geaerospace\.net|\bwjs\b|West Jefferson|10\.134\.48\.|10\.48\.249\.'
|
||||
SITE_HITS=$(grep -rPn "$SITE_PATTERNS" "${EXCLUDES[@]}" \
|
||||
SITE_HITS=$(grep -rPni "$SITE_PATTERNS" "${EXCLUDES[@]}" \
|
||||
--include='*.py' --include='*.vue' --include='*.js' \
|
||||
plugins/ shopdb/ 2>/dev/null \
|
||||
--include='*.ps1' --include='*.psm1' --include='*.sh' --include='*.iss' \
|
||||
--include='*.json' --include='*.html' \
|
||||
plugins/ shopdb/ scripts/ deploy/ frontend/src/ tools/ 2>/dev/null \
|
||||
| grep -v '/tests\?/' \
|
||||
| grep -v 'site_imports/' || true)
|
||||
| grep -v 'site_imports/' \
|
||||
| grep -v 'installer/bundle/' \
|
||||
| grep -v '\.plugins-staged/' \
|
||||
| grep -v 'check-naming-and-style\.sh' \
|
||||
| grep -v 'export-github\.sh' \
|
||||
| grep -v 'ADR-015-OK' || true)
|
||||
if [ -n "$SITE_HITS" ]; then
|
||||
COUNT=$(echo "$SITE_HITS" | wc -l)
|
||||
if [ "$SITE_LITERALS_ENFORCE" = "1" ]; then
|
||||
|
||||
@@ -20,6 +20,23 @@ OUT = os.path.join(REPO, 'docs', 'openapi.json')
|
||||
VERBS = ('get', 'post', 'put', 'patch', 'delete', 'head', 'options')
|
||||
|
||||
|
||||
def _servers():
|
||||
"""Servers block for the spec: the relative mount, plus this site's own URL
|
||||
if one was supplied.
|
||||
|
||||
SHOPDB_PUBLIC_URL is read from the environment rather than stored, because
|
||||
the generated spec is published to a public wiki - a site URL baked into the
|
||||
generator ends up in everyone's documentation, including sites it is wrong
|
||||
for.
|
||||
"""
|
||||
servers = []
|
||||
siteurl = (os.environ.get('SHOPDB_PUBLIC_URL') or '').strip().rstrip('/')
|
||||
if siteurl:
|
||||
servers.append({'url': siteurl, 'description': 'this site'})
|
||||
servers.append({'url': '/', 'description': 'relative to the deployed mount'})
|
||||
return servers
|
||||
|
||||
|
||||
def _product_version():
|
||||
"""Read __version__ out of shopdb/__init__.py without importing the app.
|
||||
|
||||
@@ -94,10 +111,12 @@ def build(surfaces):
|
||||
'`X-API-Key` for collector/managed-token endpoints; public endpoints '
|
||||
'need neither.'),
|
||||
},
|
||||
'servers': [
|
||||
{'url': 'https://tsgwp00525.wjs.geaerospace.net/shopdb', 'description': 'WJ prod'},
|
||||
{'url': '/', 'description': 'relative to the deployed mount'},
|
||||
],
|
||||
# One site's production hostname was hardcoded here, so every generated
|
||||
# spec published it to the public wiki and offered a second site a
|
||||
# server it cannot reach. The relative mount is the only server this
|
||||
# generator can honestly name; a site that wants its own in the spec
|
||||
# sets SHOPDB_PUBLIC_URL when regenerating.
|
||||
'servers': _servers(),
|
||||
'components': {'securitySchemes': {
|
||||
'bearerAuth': {'type': 'http', 'scheme': 'bearer', 'bearerFormat': 'JWT',
|
||||
'description': 'Login token or a managed Personal Access Token (scoped).'},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, current_app, send_from_directory
|
||||
from flask import Blueprint, request, current_app
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
@@ -11,6 +11,7 @@ from shopdb.core.models import Setting, AuditLog
|
||||
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
|
||||
from shopdb.utils.authz import require_permission, require_role
|
||||
from shopdb.utils.uploads import send_upload
|
||||
|
||||
settings_bp = Blueprint('settings', __name__)
|
||||
|
||||
@@ -189,7 +190,7 @@ def upload_map_blueprint():
|
||||
@settings_bp.route('/map-blueprint/<path:filename>', methods=['GET'])
|
||||
def serve_map_blueprint(filename):
|
||||
"""Serve an uploaded blueprint image (public - kiosks read it)."""
|
||||
return send_from_directory(_map_dir(), filename)
|
||||
return send_upload(_map_dir(), filename)
|
||||
|
||||
|
||||
@settings_bp.route('/branding-logo', methods=['POST'])
|
||||
@@ -234,7 +235,7 @@ def upload_branding_logo():
|
||||
@settings_bp.route('/branding/<path:filename>', methods=['GET'])
|
||||
def serve_branding_logo(filename):
|
||||
"""Serve an uploaded branding logo (public - kiosks/print pages read it)."""
|
||||
return send_from_directory(_branding_dir(), filename)
|
||||
return send_upload(_branding_dir(), filename)
|
||||
|
||||
|
||||
@settings_bp.route('', methods=['GET'])
|
||||
@@ -520,10 +521,10 @@ def build_default_settings():
|
||||
},
|
||||
{
|
||||
'key': 'pc_access_domain',
|
||||
'value': 'device.geaerospace.net',
|
||||
'value': 'device.geaerospace.net', # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
'valuetype': 'string',
|
||||
'category': 'site',
|
||||
'description': 'Domain appended to a PC hostname to build remote-access links (host.device.geaerospace.net). Blank = use the hostname as-is.'
|
||||
'description': 'Domain appended to a PC hostname to build remote-access links (host.device.geaerospace.net). Blank = use the hostname as-is.' # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
},
|
||||
{
|
||||
'key': 'employeeid_pattern',
|
||||
@@ -541,7 +542,7 @@ def build_default_settings():
|
||||
},
|
||||
{
|
||||
'key': 'printer_hostname_template',
|
||||
'value': 'Printer-{ip}.printer.geaerospace.net',
|
||||
'value': 'Printer-{ip}.printer.geaerospace.net', # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
'valuetype': 'string',
|
||||
'category': 'site',
|
||||
'description': 'Template for a printer hostname built from its IP. {ip} is the dash-separated IP address.'
|
||||
|
||||
@@ -50,10 +50,10 @@ def normalize_display_role(value):
|
||||
return LEGACY_DISPLAY_ROLES.get(lowered)
|
||||
|
||||
# GE device naming: a PC's DNS name is 'F' + its BIOS serial under the device
|
||||
# domain, e.g. FABC1234.device.geaerospace.net. The domain is a setting so other
|
||||
# domain, e.g. FABC1234.device.geaerospace.net. The domain is a setting so other # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
# sites can point elsewhere; the collector already reports the serial, so the
|
||||
# server derives the stable FQDN without the kiosk having to report it.
|
||||
DEFAULT_DISPLAY_FQDN_DOMAIN = 'device.geaerospace.net'
|
||||
DEFAULT_DISPLAY_FQDN_DOMAIN = 'device.geaerospace.net' # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
|
||||
|
||||
def derive_display_fqdn(serialnumber):
|
||||
|
||||
@@ -5,8 +5,8 @@ purpose: the hook has to be registered before any CreateTable is compiled, and
|
||||
every place that creates tables needs it.
|
||||
|
||||
WHY. Without it a CREATE TABLE inherits the SERVER's default charset. A MySQL box
|
||||
defaulting to latin1 - common on older installs, and the West Jefferson 5.6
|
||||
server is one - silently builds a latin1 schema that drifts from the utf8mb4
|
||||
defaulting to latin1 - common on older installs, and the 5.6 server this was
|
||||
ported from is one - silently builds a latin1 schema that drifts from the utf8mb4
|
||||
production target. Nothing fails at create time; it surfaces later as mangled
|
||||
characters, or as a join between a utf8mb4 and a latin1 column that cannot use an
|
||||
index.
|
||||
|
||||
@@ -49,6 +49,14 @@ FORBIDDEN = [
|
||||
('root' + 'password', 'contains a database root password'),
|
||||
(r'\b' + 'cla' + 'ude' + r'\b', 'names an LLM assistant'),
|
||||
(r'\b' + 'anthro' + 'pic' + r'\b', 'names an LLM vendor'),
|
||||
# ADR-015. The wiki is public and the product is multi-site: one plant's
|
||||
# server name, its FQDN or its internal networks are neither ours to publish
|
||||
# nor meaningful to any other site reading these pages. Assembled from
|
||||
# fragments for the same reason as the terms above.
|
||||
('tsg' + 'wp00525', 'names a production server'),
|
||||
(r'\bwjs\.' + r'geaerospace\.net\b', 'names a site FQDN'),
|
||||
(r'10\.134\.48\.', 'names an internal network'),
|
||||
(r'10\.48\.249\.', 'names an internal network'),
|
||||
]
|
||||
|
||||
# Generated API surface. Not prose, not hand-edited, and regenerated from the
|
||||
|
||||
@@ -138,7 +138,12 @@ fi
|
||||
sed -i "s/rootpassword/changeme/g" shopdb/config.py 2>/dev/null || true
|
||||
|
||||
# --- 3. scrub gate: refuse to commit if anything internal leaks ---
|
||||
LEAKS=$(grep -rlIiE 'claude|anthropic|fable 5|gitea|proudtech|home/camp|rootpassword' \
|
||||
# The site patterns are ADR-015's, and they belong here as much as the internal
|
||||
# tooling names do: this repository is public and the product is multi-site, so
|
||||
# one plant's server name or internal networks reaching it is both a disclosure
|
||||
# and a lie to every other site. They were absent, and a bundled plugin shipped
|
||||
# a production UNC path to GitHub for a fortnight before anyone noticed.
|
||||
LEAKS=$(grep -rlIiE 'claude|anthropic|fable 5|gitea|proudtech|home/camp|rootpassword|tsgwp00525|wjs\.geaerospace\.net|10\.134\.48\.|10\.48\.249\.' \
|
||||
--exclude-dir=.git . || true)
|
||||
if [ -n "$LEAKS" ]; then
|
||||
echo "SCRUB GATE FAILED - internal references in:"; echo "$LEAKS"; exit 1
|
||||
|
||||
Reference in New Issue
Block a user