Files
shopdb-flask/docs/FLEET-ARCHITECTURE.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

145 lines
9.0 KiB
Markdown

<!-- docmeta
purpose: explanation
audience: operator, adopter, developer
status: current
summary: How the server and the things that run on shop-floor PCs relate, and which page to open next.
loadwith:
-->
# How the pieces fit together
ShopDB is a server. Around it are the things that run on shop-floor PCs, each
documented in its own page, and each of those pages assumes you already know how
it relates to the others. This page is that relationship.
Read this one first, then open ONE of the pages in the table below. They are
written to be read alone once you have this page's shape in your head.
## The shape
```
+-------------------------------+
| ShopDB (one per site) |
| Flask API + Vue UI + MySQL |
+-------------------------------+
^ ^ ^ |
reports | | | | serves
| | | v
+----------------+ | | | +------------------+
| asset reporter |---+ | | | kiosks, displays |
| on every PC | | | | (browser, HTTPS) |
+----------------+ | | +------------------+
| |
+----------------+ | | +------------------+
| backup |----------+ +----| EventSaver |
| collectors | config revisions | screensaver |
+----------------+ +------------------+
pulls the slide feed
+----------------+
| endpoint agent | enrols, reports health, runs queued commands
| service, SYSTEM|
+----------------+
```
Every arrow is one direction on purpose. **Nothing on the server reaches out to
a PC.** The server holds state and answers questions; the fleet asks. That is
what lets a bay behind a firewall, asleep, or on a different network segment be
merely out of date rather than broken - and it is why every one of these tools
degrades to "stale" instead of "failed".
## What each piece is for
| Piece | Direction | What it does | Page |
|---|---|---|---|
| **Asset reporter** | PC to server | Says what this PC is: hostname, serial, type, user, NICs, machine number. Creates the PC record the rest of the system hangs off. | [COLLECTOR-INTEGRATION](COLLECTOR-INTEGRATION.md) |
| **Endpoint agent** | PC to server | Enrols the PC for its own credential, reports health, and runs the commands the server has queued for it. | [COLLECTOR-AUTH](COLLECTOR-AUTH.md) |
| **Backup collectors** | PC to server | Post machine configuration (NTLARS/DNC, CMM, part marker, UDC) as revisions, so a controller can be restored and a change can be seen. | [BACKUP-RESTORE](BACKUP-RESTORE.md) |
| **EventSaver** | PC to server | Pulls the slide playlist and shows it when a bay is idle. | [EVENTSAVER](EVENTSAVER.md) |
| **Kiosks and displays** | PC to server | A browser in kiosk mode on a ShopDB route. No client software beyond the browser. | [EVENTSAVER](EVENTSAVER.md) |
| **Printer installer** | PC to server | Reads the printer map and installs the network printers a bay should have. Laid down by a manifest entry. | [PRINTER-INSTALLER](PRINTER-INSTALLER.md) |
**ShopDB does not lay software on a bay.** Getting the reporters and the
screensaver onto a PC in the first place is the job of whatever already
converges shop-floor state at your site - an imaging task, Intune, DSC, or a
configuration-enforcement tool of your own. ShopDB serves the data those tools
and those clients read; it never pushes.
Setting all of this up at a plant that does not have it yet is
[ADOPTING-AT-ANOTHER-SITE](ADOPTING-AT-ANOTHER-SITE.md).
## The three ways a PC talks to the server
**The collector API**, `POST /api/collector/*`. Used by the asset reporter, the
printer-queue reporter and the endpoint agent. A PC authenticates as ITSELF with
a per-PC credential where it has enrolled, and with a shared fleet key where it
has not. Idempotent by design: the same report twice changes nothing.
[COLLECTOR-AUTH](COLLECTOR-AUTH.md) is the one page that defines that choice;
[COLLECTOR-PAYLOADS](COLLECTOR-PAYLOADS.md) is the payload contract, and ADR-006
is the rationale.
**Scoped service tokens.** An unattended integration that is not a single PC -
a build job, a site tool, another system - authenticates with a managed token
limited to named permissions, optionally bound to specific resources. This path
does NOT take per-PC credentials: it is a scope talking to the server, not a PC.
Minting, binding and revoking one is Settings > API Tokens.
**Plain public reads.** The slide feed, the display-role lookup and the kiosk
dashboard routes need no credentials at all, because a screensaver and a wall
display have nowhere to keep one. They are read-only and carry nothing a floor
visitor should not see.
## Where the credentials live
Both ends of every credential, in one table, because a credential described from
only one end is the kind that gets provisioned twice. The collector row is
defined in full by [COLLECTOR-AUTH](COLLECTOR-AUTH.md).
| Credential | On the server | On the PC | Rotate by |
|---|---|---|---|
| Per-PC endpoint credential | One row per asset, sha256 only. Issued by enrollment, revocable per PC | `%ProgramData%\ShopDB\EndpointAgent\credential\credential.bin`, DPAPI LocalMachine, ACL'd to SYSTEM and Administrators | Revoking it and letting the PC enroll again. One PC at a time, no fleet touch |
| Shared collector key (legacy) | A managed token scoped to `collector.ingest` only. `COLLECTOR_API_KEY` in `.env` also works and is simpler for a pilot, but cannot be revoked per fleet | `HKLM:\SOFTWARE\GE\ShopDB` value `CollectorKey` | Replacing the value the fleet reads from, so every PC picks it up on its next cycle. Never put it in a manifest - manifests sync broadly |
| Scoped service token | A managed token limited to named permissions, optionally resource-bound | Wherever the integration keeps its secrets; on a PC, `HKLM:\SOFTWARE\GE\ShopDB` value `ApiToken` | Minting a bound replacement and revoking the old one |
| Server URL | n/a | `HKLM:\SOFTWARE\GE\ShopDB` value `BaseUrl` | Setting it once at install; nothing should overwrite a bay's own value |
| Server pins | The leaf certificate's SPKI SHA-256, shown on the Server Certificate settings page | `HKLM:\SOFTWARE\GE\ShopDB` value `ServerPins` (REG_MULTI_SZ) | Writing the new pin set; the agent re-reads it within about a minute |
| Application secrets | `.env`: `SECRET_KEY`, `JWT_SECRET_KEY`, `DATABASE_URL`, integration tokens | n/a | See [BACKUP-RESTORE](BACKUP-RESTORE.md) - losing these invalidates every issued token |
Two rules behind that table. A shop-floor PC is not a trusted place, so what it
holds is scoped to exactly what it does: a credential that leaks buys the reader
the ability to file ONE PC's report, not to read the asset register. And a
credential is delivered, never typed - a value that has to be entered per machine
is a value that is wrong on some machine.
## What owns what
The question that decides most arguments: **who is allowed to change this?**
- **ShopDB owns the record.** What a PC is, what it drives, what it should have.
- **Your configuration tool owns the PC's configuration.** Whatever converges
shop-floor state at your site puts back anything edited on the bay, on its own
schedule. ShopDB records; it does not enforce.
- **The PC owns its own identity.** Serial, hostname, installed software: the
server records what it is told rather than deciding. The one exception is
which NIC is the PC's address, which moved to the server because only the
server knows the site's addressing.
Where two systems both converge state - Intune DSC and a shop-floor enforcement
tool, say - decide which owns which layer before deploying both, or they will
fight in a way that is very hard to see.
[ADOPTING-AT-ANOTHER-SITE](ADOPTING-AT-ANOTHER-SITE.md) has the recommended
split.
## When something looks wrong
| Symptom | The piece to look at first |
|---|---|
| A PC is missing from ShopDB entirely | Asset reporter: never ran, or no credential and no collector key |
| A PC is there but its details are old | Asset reporter: stopped running. The dashboard's "PCs not reporting" card is this |
| A PC reports 401 after an admin action | Its credential was revoked. See [COLLECTOR-AUTH](COLLECTOR-AUTH.md) |
| A PC has the wrong software | Not ShopDB: check whatever converges configuration at your site. ShopDB records what the PC reported, it does not decide it |
| A bay shows the wrong thing on screen | EventSaver config, or the display-type dashboard default - both are per PC type |
| A machine has no recent config backup | Backup collector on the controlling PC. "Backups that have stopped" on the dashboard |
| Two PCs claim one machine number | Neither: it is a claim in progress, or a typo. The dashboard card names both |
Each row is a different program on a different schedule. Knowing which one to
open is most of the diagnosis.