Files
shopdb-flask/docs/ADOPTING-AT-ANOTHER-SITE.md
cproudlock 534f179ee6 Make main the lean product a site receives
main was 16 days and 259 commits behind the line the product is actually built
on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It
was neither current nor curated - just stale. This brings it forward and, at the
same time, makes it mean something: main is what a receiving site installs, and
nothing else.

WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides,
knowledgebase, machines, network, notifications, printers, slides, tools,
warranty - plus applications, which is manifest-less core and always ships. The
only manifest dependency inside that set is endpoints on computers, and both are
in it, so the closure is self-contained.

WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb
stay on the site line. So do the Windows installer tree, the VM rehearsal
helpers, the agent tooling config, one site's data loader, the internal audit
and findings records, and the planner spreadsheet. 1,135 tracked files against
1,438.

A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped:

deploy/site-profile-universal.json still named all seventeen plugins. It is the
file docs/DEPLOY.md tells a site to provision from, so a fresh install would
have failed at `flask plugin apply-profile` with PluginError - the first thing a
receiving site does, broken by the branch meant to serve them. It now names the
twelve that exist, and its comment says the two must agree.

agent/ was never refreshed. Whoever populated this branch copied shopdb/,
plugins/, frontend/ and tests/ from the site line and left the agent tree at its
September state: 22 C# and PowerShell sources missing, including AgentCredentials,
EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the
site line had deleted still present. The old tree is internally consistent, so it
compiles, and nothing in CI builds it - every gate was green over a server that
speaks enrollment and per-PC credentials to a bundled agent source that cannot.
Resynced whole.

DECISIONS THIS ENCODES, rather than defers:

- The plugin-authoring lab builds printedparts, whose manifest depends on
  employees, so it could not stay. The lab remains on the site line and main
  ships no build-along tutorial. That is a real loss for an adopting developer
  and is recorded here rather than discovered later.
- Recognition, Training and Recertification notifications are no longer seeded.
  Each splits one notification into a card per employee and renders a photo,
  which needs a plugin this branch does not bundle. Seeding them would offer an
  author a type that cannot resolve anybody. A site that installs employees gets
  them from that plugin's own seed.
- Windows/IIS is documented as the legacy path throughout, and the README leads
  with Docker, because that is what a receiving site gets.

The API description is regenerated for THIS branch - 347 paths and 483
operations, down from 415 and 573 - so the published contract describes what is
here rather than what the site line serves. docs/CONFIG.md lost the two
measuring-tool label keys the code no longer declares, which two lanes had
decided opposite ways.

Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run
rather than skip, and every index line count is derived from the file it names.
2026-09-19 11:24:40 -04:00

292 lines
12 KiB
Markdown

<!-- docmeta
purpose: howto
audience: adopter
status: current
summary: Point the asset reporter and EventSaver at your own ShopDB, by Intune or by hand.
loadwith:
-->
# Adopting the fleet tools at another site
Two things 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.
Neither is site-specific by design. What is site-specific is the URL, the
credential, and which PCs it runs on - and all three are inputs, not code.
**Getting them onto a PC is your site's job, not ShopDB's.** ShopDB serves the
data; something else lays the files down and keeps them there. This page covers
the two delivery paths that do not assume a tool you may not have: Microsoft
Intune, and doing it by hand. If you already run a configuration-enforcement
tool on the floor, the end state below is what it has to produce.
Read [FLEET-ARCHITECTURE](FLEET-ARCHITECTURE.md) first if you have not: it is one
page and it is what makes the rest of this make sense.
## Where these files come from
| Artifact | Where it is |
|---|---|
| `Report-AssetToShopDB.ps1` | `plugins/computers/client/`, so it versions with the collector contract it implements. It names no site: the server comes from `HKLM:\SOFTWARE\GE\ShopDB` `BaseUrl` or `-ApiUrl` |
| `Report-PrintersToShopDB.ps1` | `plugins/printers/client/`. Optional - it records what print queues a bay actually has |
| EventSaver (`EventSaver.cs`, `EventSaver.ini`) | `plugins/slides/client/`. One C# file that builds with the in-box .NET Framework compiler, so a site can rebuild it rather than trust a binary. The compiled `EventSaver.scr` is a release asset, not a file in the repository. Neither file names a site |
---
## Part 1: the asset reporter
### What it does
Runs as SYSTEM and POSTs what the PC is - hostname, BIOS serial, PC type,
logged-in user, make and model, OS, last boot, every routable NIC, the machine
number where one is configured - to
`POST https://<your-shopdb>/api/collector/computers`. The payload contract is
[COLLECTOR-PAYLOADS](COLLECTOR-PAYLOADS.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`, or `HKLM:\SOFTWARE\GE\ShopDB` value `BaseUrl`. A script downloaded from Settings has your URL already in it |
| A credential | `HKLM:\SOFTWARE\GE\ShopDB` value `CollectorKey` for the shared key, or the PC's own credential once it has enrolled. Never stamped into the script. See [COLLECTOR-AUTH](COLLECTOR-AUTH.md) |
| Your routable ranges | A **server** setting, `collector_corporate_ranges`. The bay reports every NIC and takes no view; the server picks which one is the PC's address |
The reporter still accepts `-AllowedRanges` and reads a `CollectorRanges`
registry value, and **ignores both**, logging a note when either is set. Choosing
the NIC on the bay was wrong - a bay cannot know a site's addressing - so it
moved to the server. The parameters remain only so an existing scheduled task
does not fail on an unknown argument. Set the server setting.
**Shortcut: download it pre-configured.** Settings > Computers > Asset reporter
generates the script with your server's URL already in the parameter defaults,
and shows its SHA-256. The credential is deliberately not included.
### The credential to start with
Mint a managed personal access token scoped to `collector.ingest` and nothing
else, so a token recovered off a shop-floor PC cannot read your asset register.
Plan to move off it onto per-PC credentials.
[COLLECTOR-AUTH](COLLECTOR-AUTH.md) is the whole story and the only page that
describes it.
### 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
credential 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)
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'
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. Restrict
the registry key's ACL to SYSTEM and Administrators so an interactive user on the
bay cannot read it.
**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 *credential* 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'
}
# ...and a Script resource whose SetScript registers the scheduled task
# below (option B has the three lines). Pass it no -ApiUrl: BaseUrl
# above is where the script reads the server from, and one source beats
# two that can disagree.
}
}
```
Do not add a `CollectorRanges` resource. That value is read and ignored; the
ranges are a server setting now.
#### Option B: 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, and what to do when it fails
Open Computers in the web UI and find the hostname: it should be there with its
serial, type and a last-reported timestamp within one schedule tick. The same
answer over the API needs a logged-in session, because the read side is
permissioned (`computers.view`) even though the ingest side is not:
```powershell
Invoke-RestMethod -Uri 'https://shopdb.example.net/api/computers/by-hostname/<HOSTNAME>' `
-Headers @{ Authorization = "Bearer $jwt" }
```
The collector key does NOT work here - it grants `collector.ingest` and nothing
else, which is the point of it. The script also logs to `C:\Logs\Shopfloor`.
Every failure mode and its fix is the table in
[COLLECTOR-CLIENT-SETUP](COLLECTOR-CLIENT-SETUP.md).
---
## Part 2: EventSaver
### What you have to decide
[EVENTSAVER](EVENTSAVER.md) is what EventSaver is, how it picks slides and how to
diagnose it. Read it first; adopting it adds only three decisions:
1. **Your feed URL**, in `EventSaver.ini`:
`url=https://<your-shopdb>/api/slides/feed?surface=shopfloor`.
2. **Ship the ini with the binary.** There is no compiled-in fallback, so a
missing ini is a blank screen. Check any `.scr` you were handed rather than
built: an early build still has another site's file server baked in, which is
exactly why the fallback was removed (ADR-015).
3. **Which PCs get it.** A screensaver over a live display is a defect. Exclude
kiosks, lobby displays and metrology bays.
If your configuration tool enforces the ini by hash, your own hand edits on a
bay revert - that is the feature working. Edit the copy your tool deploys from,
and update the hash it detects on:
```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.
Enable it per user with a configuration profile (Settings catalog > Screen
saver) or a user-context script setting the three values under option B. With
Machine Configuration/DSC, the two files are `File` resources - but DSC runs as
SYSTEM, so the 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: 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'
```
Those three values are the whole "enable" step in every option above: a
screensaver is a per-user setting, so whatever deploys the binary still has to
reach the user hive.
### Verifying
Run `EventSaver.scr /c` on the PC - it reports which source it resolved and
where it is caching - then open that feed URL in a browser on the same PC. The
symptom table in [EVENTSAVER](EVENTSAVER.md) reads the answers.
---
## Which path to choose
| You have | Use |
|---|---|
| Intune across the estate | Option A. A remediation for the reporter, a Win32 app for EventSaver. |
| A configuration-enforcement tool on the floor | Its own idiom, producing the end state option B describes: the script on disk, the credential in the registry, a task as SYSTEM. |
| Neither, or a pilot of a few bays | Option B, then move to A once it earns its place. |
These are not exclusive, and on a real floor they usually are not. Where two
tools both converge the same PC, decide which owns which layer before deploying
both: the deciding question is which one you would look at first to answer "why
is this bay wrong".