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

12 KiB

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

# 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:

# 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:

& "$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:

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:

# 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:

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.


Part 2: EventSaver

What you have to decide

EVENTSAVER 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:

(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:

# 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

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