diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e10b4..04a97eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,14 @@ ADR-007 and ADR-002. returning nothing and leaving the caller to guess. - The display client module updates itself through the manifest, so a client change no longer means hands on every kiosk. +- `docs/ADOPTING-AT-ANOTHER-SITE.md`: how another site repoints the asset + reporter and the EventSaver screensaver at its own ShopDB, with worked + examples for Intune (including Machine Configuration/DSC), a GE-Enforce + manifest entry, and manual installation. Both tools were already built to be + repointed - the URL, the key and the targeting are inputs - but nothing said + so, and the two things that catch people out are written down: EventSaver's + compiled-in fallback path is not neutral, and a config enforced by hash + reverts a hand edit by design. - `docs/PROJECT-MAP.md`, generated by `scripts/gen_project_map.py`: versions, the plugin inventory, every Alembic chain head, the ADR index and the size of the codebase, derived from the code. The hand-written equivalents in CLAUDE.md diff --git a/docs/ADOPTING-AT-ANOTHER-SITE.md b/docs/ADOPTING-AT-ANOTHER-SITE.md new file mode 100644 index 0000000..e4e98e3 --- /dev/null +++ b/docs/ADOPTING-AT-ANOTHER-SITE.md @@ -0,0 +1,366 @@ +# Adopting the fleet tools at another site + +Two tools run on shop-floor PCs and talk to a ShopDB instance: the **asset +reporter**, which posts what a PC is to the collector API, and **EventSaver**, +a screensaver that shows a slideshow served by ShopDB. Both are designed to be +repointed at another site's server. This page is how. + +Neither tool is site-specific by design. What is site-specific is the URL, the +API key, and which PCs it runs on - and all three are inputs, not code. + +> **Prerequisite.** The reporter's `-ApiUrl` parameter still carries a default +> pointing at the reference site's server. Adopt it with an explicit `-ApiUrl` +> (every example below does) until that default is removed. See +> [ADR-015](adr/ADR-015-site-specific-configuration.md). + +--- + +## Part 1: the asset reporter + +### What it does + +Runs as SYSTEM, collects hostname, BIOS serial, PC type, logged-in user, IP +addresses and the machine number where one is configured, and POSTs them to: + +``` +POST https:///api/collector/computers +X-API-Key: +Content-Type: application/json +``` + +The payload contract is [COLLECTOR-INTEGRATION.md](COLLECTOR-INTEGRATION.md) and +[ADR-006](adr/ADR-006-collector-contract.md). A field your site does not collect +is simply absent; the server upserts on `hostname` and leaves the rest alone. + +### What you must provide + +| Input | Where it comes from | +|---|---| +| Server URL | `-ApiUrl https:///api/collector/computers` | +| API key | `-ApiKey`, or `HKLM:\SOFTWARE\GE\ShopDB` value `CollectorKey` | + +### Minting the key on your ShopDB + +Create a **managed personal access token scoped to `collector.ingest` and +nothing else**, and use it as the `X-API-Key` value. That scope authorises the +collector ingest API and no normal route, so a token recovered off a shop-floor +PC cannot read your asset register. A shared `COLLECTOR_API_KEY` environment +variable also works and is simpler for a pilot, but it cannot be rotated per +fleet or revoked individually. + +### Deploying it + +The three paths below are alternatives - pick one. All of them end with the same +two facts on the PC: the script exists somewhere it can run from, and the key is +in the registry. + +#### Option A: Microsoft Intune + +**A1. The key, as a platform script.** Devices > Scripts and remediations > +Platform scripts > Add > Windows 10 and later. Run as SYSTEM, do not run in the +64-bit context only. + +```powershell +# Set-ShopdbCollectorKey.ps1 (Intune platform script, runs once per device) +$RegPath = 'HKLM:\SOFTWARE\GE\ShopDB' +$Key = 'shopdb_pat_REPLACE_WITH_YOUR_TOKEN' +if (-not (Test-Path $RegPath)) { New-Item -Path $RegPath -Force | Out-Null } +$current = (Get-ItemProperty -Path $RegPath -Name CollectorKey -ErrorAction SilentlyContinue).CollectorKey +if ($current -ne $Key) { Set-ItemProperty -Path $RegPath -Name CollectorKey -Value $Key } +exit 0 +``` + +Putting a token in an Intune script body means anyone who can read the Intune +configuration can read the token. That is an argument for the `collector.ingest` +scope, not against Intune: the blast radius of that token is one API. + +**A2. The reporter, as a remediation on a schedule.** Devices > Scripts and +remediations > Remediations. A remediation is the natural fit because it re-runs +on a schedule, which is exactly what reporting is. + +*Detection script* - forces the remediation to run every cycle: + +```powershell +# Always "not compliant": reporting is not a state to converge on, it is an +# event that should happen on every schedule tick. +exit 1 +``` + +*Remediation script* - the reporter itself, with your URL: + +```powershell +& "$PSScriptRoot\Report-AssetToShopDB.ps1" ` + -ApiUrl 'https://shopdb.example.net/api/collector/computers' +exit 0 +``` + +Assign it with a daily schedule. Run as SYSTEM, 64-bit. + +**A3. Intune with DSC (Machine Configuration).** If your estate is already +governed by Azure Machine Configuration, express the *key* as configuration and +leave the *reporting* to a scheduled task, because a report is an event and DSC +converges state: + +```powershell +Configuration ShopdbCollector +{ + Import-DscResource -ModuleName PSDesiredStateConfiguration + + Node localhost + { + Registry CollectorKey + { + Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\GE\ShopDB' + ValueName = 'CollectorKey' + ValueData = 'shopdb_pat_REPLACE_WITH_YOUR_TOKEN' + ValueType = 'String' + Ensure = 'Present' + } + + Registry CollectorUrl + { + Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\GE\ShopDB' + ValueName = 'BaseUrl' + ValueData = 'https://shopdb.example.net' + ValueType = 'String' + Ensure = 'Present' + } + + Script ReportingTask + { + GetScript = { @{ Result = (Get-ScheduledTask -TaskName 'ShopDB asset report' -ErrorAction SilentlyContinue) } } + TestScript = { [bool](Get-ScheduledTask -TaskName 'ShopDB asset report' -ErrorAction SilentlyContinue) } + SetScript = { + $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` + -Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\ShopDB\Report-AssetToShopDB.ps1" -ApiUrl "https://shopdb.example.net/api/collector/computers"' + $trigger = New-ScheduledTaskTrigger -Daily -At 7am + Register-ScheduledTask -TaskName 'ShopDB asset report' -Action $action ` + -Trigger $trigger -User 'SYSTEM' -RunLevel Highest -Force + } + } + } +} +``` + +#### Option B: GE-Enforce + +If you already run the GE-Enforce client, this is one manifest entry, and it is +how the reference site does it. `Type: PS1` with `DetectionMethod: Always` runs +the script from the share every cycle with no local copy and no hash to bump +when the script changes. + +```json +{ + "_comment": "Reports host, BIOS serial, pc-type, logged-in user, IPs and machine number to ShopDB every cycle, as SYSTEM, straight off the share.", + "Name": "Report asset to ShopDB", + "Type": "PS1", + "Script": "apps/Report-AssetToShopDB.ps1", + "Args": "-ApiUrl https://shopdb.example.net/api/collector/computers", + "DetectionMethod": "Always" +} +``` + +Provision the key with a second entry, ordered **before** it, so the key is +present the same cycle the first report fires: + +```json +{ + "_comment": "Writes the collector key from an ACL'd share file. The token is NOT in this manifest - manifests sync broadly. Rotate by replacing configs/collector-key.txt on the share; every PC picks it up next cycle.", + "Name": "Provision the ShopDB collector key", + "Type": "PS1", + "Script": "scripts/Set-CollectorKey.ps1", + "DetectionMethod": "Always" +} +``` + +Keep the token in `configs/collector-key.txt` on your share, locked to SYSTEM +and administrators - not in the manifest JSON. + +#### Option C: manual + +For a pilot, a single bay, or an estate with no management plane: + +```powershell +# As administrator, once per PC. +New-Item -Path 'C:\ProgramData\ShopDB' -ItemType Directory -Force | Out-Null +Copy-Item .\Report-AssetToShopDB.ps1 'C:\ProgramData\ShopDB\' + +New-Item -Path 'HKLM:\SOFTWARE\GE\ShopDB' -Force | Out-Null +Set-ItemProperty -Path 'HKLM:\SOFTWARE\GE\ShopDB' -Name CollectorKey ` + -Value 'shopdb_pat_REPLACE_WITH_YOUR_TOKEN' + +$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument @' +-NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\ShopDB\Report-AssetToShopDB.ps1" -ApiUrl "https://shopdb.example.net/api/collector/computers" +'@ +$trigger = New-ScheduledTaskTrigger -Daily -At 7am +Register-ScheduledTask -TaskName 'ShopDB asset report' -Action $action -Trigger $trigger ` + -User 'SYSTEM' -RunLevel Highest -Force + +Start-ScheduledTask -TaskName 'ShopDB asset report' +``` + +Lock down `C:\ProgramData\ShopDB` if you put it there: a script a user can edit, +run by SYSTEM on a schedule, is a local privilege escalation. Break inheritance +and grant Administrators and SYSTEM only. + +### Verifying + +```powershell +Invoke-RestMethod -Uri 'https://shopdb.example.net/api/computers/by-hostname/' +``` + +The PC should exist with its serial, type and last-reported timestamp. The +script also logs to `C:\Logs\Shopfloor`. + +--- + +## Part 2: EventSaver + +### What it does + +A Windows screensaver (`EventSaver.scr`) that shows a slideshow. It reads +`EventSaver.ini` from the directory it lives in, **on every launch**, so +retargeting it needs no recompile: + +```ini +# HTTP mode (recommended): pull slides from ShopDB, cache locally. No share. +url=https://shopdb.example.net/api/slides/feed?surface=shopfloor + +# Folder mode (fallback): used only when url is blank. SMB or local path. +# folder=\\fileserver\shopfloor\tv + +interval=10 # seconds per image, when a slide carries no time of its own +shuffle=0 # 1 = random, 0 = ordered +fadems=600 # crossfade length, reserved +``` + +HTTP mode is the one to adopt: slides are managed in ShopDB (Slides), and the +screensaver caches them locally, so a PC that cannot reach the server keeps +showing the last set instead of going black. + +### The two things that catch people out + +**The compiled-in fallback is not yours.** If `EventSaver.ini` is missing or +both `url` and `folder` are blank, the binary falls back to a folder path +compiled into `EventSaver.cs`, which is the reference site's file server. Ship +the ini. A missing ini is not a neutral default. + +**If you enforce the ini by hash, your own edits revert.** The reference +manifest pins `EventSaver.ini` with `DetectionMethod: Hash`, which is what makes +a retune propagate to the fleet automatically - and equally means a hand edit on +one PC is undone next cycle. That is the feature working. Edit the copy on +**your** share and update `DetectionValue` to its new SHA-256: + +```powershell +(Get-FileHash .\configs\EventSaver.ini -Algorithm SHA256).Hash +``` + +### Deploying it + +#### Option A: Intune + +Package the three files as a Win32 app (`EventSaver.scr`, `EventSaver.ini`, and +the enable script) with this install command: + +```powershell +# install.ps1 for the Win32 app +Copy-Item .\EventSaver.scr 'C:\Windows\System32\EventSaver.scr' -Force +Copy-Item .\EventSaver.ini 'C:\Windows\System32\EventSaver.ini' -Force +exit 0 +``` + +Detection rule: file `C:\Windows\System32\EventSaver.scr` exists **and** matches +the expected SHA-256, so a changed binary reinstalls. + +The screensaver itself is per-user, so set it with a configuration profile +(Settings catalog > Screen saver) or a user-context script: + +```powershell +Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name SCRNSAVE.EXE -Value 'C:\Windows\System32\EventSaver.scr' +Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '1' +Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '480' +``` + +With Machine Configuration/DSC, express the two files with `File` resources and +the screensaver with a `Registry` resource in the user hive - noting that DSC +runs as SYSTEM, so a per-user setting needs the logged-on user's hive or a +default-user template, which is usually more trouble than a login script. + +#### Option B: GE-Enforce + +Three entries, which is exactly how the reference fleet does it. Note the +per-PC-type targeting: a kiosk showing a live dashboard must **not** get a +screensaver over the top of it. + +```json +{ + "Name": "EventSaver screensaver (binary)", + "Type": "File", + "Source": "apps/EventSaver.scr", + "Destination": "C:\\Windows\\System32\\EventSaver.scr", + "DetectionMethod": "Hash", + "DetectionPath": "C:\\Windows\\System32\\EventSaver.scr", + "DetectionValue": "", + "PCTypes": ["gea-shopfloor-common"] +}, +{ + "Name": "EventSaver screensaver (config)", + "Type": "File", + "Source": "configs/EventSaver.ini", + "Destination": "C:\\Windows\\System32\\EventSaver.ini", + "DetectionMethod": "Hash", + "DetectionPath": "C:\\Windows\\System32\\EventSaver.ini", + "DetectionValue": "", + "PCTypes": ["gea-shopfloor-common"] +}, +{ + "Name": "EventSaver enable (per-user screensaver)", + "Type": "PS1", + "Script": "scripts/Set-EventSaverScreensaver.ps1", + "Args": "-TimeoutSeconds 480", + "DetectionMethod": "Always", + "PCTypes": ["gea-shopfloor-common"] +} +``` + +#### Option C: manual + +```powershell +Copy-Item .\EventSaver.scr 'C:\Windows\System32\' -Force +Copy-Item .\EventSaver.ini 'C:\Windows\System32\' -Force # with YOUR url= + +# Per user, in the user's own session: +Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name SCRNSAVE.EXE -Value 'C:\Windows\System32\EventSaver.scr' +Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '1' +Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '480' +``` + +Run `EventSaver.scr /c` to see which source it resolved and where it is caching. + +### Verifying + +Open the feed in a browser from the PC itself: + +``` +https://shopdb.example.net/api/slides/feed?surface=shopfloor +``` + +It is a public endpoint - the screensaver has no credentials - and returns the +playlist with per-slide durations. An empty `slides` array means slides have not +been uploaded for that surface, not that the PC is misconfigured. + +--- + +## Which path to choose + +| You have | Use | +|---|---| +| Intune, no GE-Enforce | Option A. A remediation for the reporter, a Win32 app for EventSaver. | +| GE-Enforce | Option B. One manifest entry each, and the fleet converges on its own cycle. | +| Neither, or a pilot of a few bays | Option C, then move to A or B once it earns its place. | + +GE-Enforce and Intune are not exclusive. The reference site uses Intune for +enrolment and imaging and GE-Enforce for shop-floor configuration state, because +Intune is not on the shop floor network path in the same way. If you have both, +the deciding question is which one you would look at first to answer "why is +this bay wrong". diff --git a/docs/llms.txt b/docs/llms.txt index 8dea40b..8746962 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -12,6 +12,13 @@ specialists, and they ask assistants for help. Two documents are authoritative: - `docs/OPERATE-WINDOWS.md` - restart, logs, backups, upgrades, troubleshooting. Both ship in `docs/` inside the install directory on every installed server. +Another site adopting the shop-floor tools - the asset reporter that feeds the +collector API, and the EventSaver screensaver - should read +`docs/ADOPTING-AT-ANOTHER-SITE.md`. It has worked deployment examples for Intune +(including Machine Configuration/DSC), GE-Enforce and manual installation. +Neither tool is site-specific: the server URL, the API key and the targeting are +inputs, not code. + Do NOT walk someone through `docs/INSTALL-WINDOWS-IIS.md` or `docs/DEPLOY-WINDOWS-IIS.md` for a new site. Those are the MANUAL procedure, kept only for hand-built servers that predate the installer; following them produces a