A site adopting ShopDB had to be handed two files and told what to edit in them. Both are now the product's, and one of them the server writes for you. GET /api/computers/client-script (admin) returns Report-AssetToShopDB.ps1 with this site's values already in it: site_base_url becomes the -ApiUrl default and the new computers_routableranges setting becomes -AllowedRanges. Only the PARAMETER DEFAULTS are substituted - the copy in plugins/computers/client/ stays runnable, so there is no second version to drift from the first - and everything stamped stays overridable by argument or registry, because a bay may need to differ from its site. Settings > Computers > Asset reporter edits the ranges, downloads the script and shows its SHA-256. The collector key is deliberately not stamped in, and a test fails if it ever is. That file lands on every shop-floor PC, and a token spread across hundreds of bays cannot be rotated quietly; it stays in the registry, provisioned per ADOPTING-AT-ANOTHER-SITE.md. The routable ranges are the last thing that was hardcoded in that script. They are now a setting, so West Jefferson's two CIDRs move out of source code and into that site's own configuration - which is what ADR-015 asks for - and a site that sets nothing still works, because the script falls back to the NIC carrying the default route. EventSaver joins it in plugins/slides/client/, source only: EventSaver.cs and EventSaver.ini, no compiled .scr - a binary is a release asset, like the installer exe. The share path that was compiled into Config.Folder is gone. It used to be the fallback when the ini was missing, which silently pointed a new site at the reference site's file server; it is now empty, and failing visibly beats displaying another site's slides. Verified by compiling the edited source in the Windows VM with the in-box csc.exe: 15,872 bytes, exit 0. Also: the DSC example in the adoption guide gains a CollectorRanges resource and stops passing -ApiUrl to a script that already reads BaseUrl from the registry the same example writes, and the guide points at the generated download instead of hand-editing a URL. The contract test caught the endpoint importing shopdb directly for the version string, which ADR-002 forbids a plugin from doing. The product and contract versions are in app.config now, which a plugin reads through current_app. Adds docs/proposals/printer-assignment.md: assign printers to a PC in ShopDB and let the bay install them, with what the fleet data says about drivers - HP and Xerox cover 41 of 44 printers with universal drivers, there are no Brother printers at all despite 208 files of Brother inkjet drivers in the installer, and printerdrivers holds one row pointing at a per-model folder instead of a universal driver.
19 KiB
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
-ApiUrlparameter 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.
Where these files come from
The commands below copy files. This is where each one is obtained, because they do not all ship from the same place and two of them are not in this repository at all yet.
| Artifact | Where it is today |
|---|---|
GE-Enforce client (Install-GEEnforce.ps1, Invoke-ShopdbEnforce.ps1, ShopdbEnforceClient.psm1) |
This repository, plugins/geenforce/client/. Present on any installed server under the install directory. |
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 (which Install-GEEnforce.ps1 writes) or -ApiUrl, and the NIC it reports is the one carrying the default route unless the site names its ranges via -AllowedRanges or the CollectorRanges registry value. |
EventSaver (EventSaver.cs, EventSaver.ini) |
plugins/slides/client/. The source is a single 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. See EVENTSAVER.md. |
A site that would rather not run a binary it cannot rebuild should take EventSaver's source and compile it locally - the build needs no SDK and is one command.
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://<your-shopdb>/api/collector/computers
X-API-Key: <collector token>
Content-Type: application/json
The payload contract is COLLECTOR-INTEGRATION.md and
ADR-006. 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 |
| API key | -ApiKey, or HKLM:\SOFTWARE\GE\ShopDB value CollectorKey. Never stamped into the script |
| Routable ranges | Optional. -AllowedRanges '10.20.0.0/23,10.21.4.0/26', the CollectorRanges registry value, or the computers_routableranges setting. Unset reports the NIC carrying the default route, which is right at most sites |
Shortcut: download it pre-configured. Settings > Computers > Asset reporter generates this script with your server's URL and ranges already in the parameter defaults, and shows its SHA-256. The key is deliberately not included.
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.
# 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:
# 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 key 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'
}
# Optional. Only for a site whose bays carry both a controller NIC and a
# corporate one AND whose default route is not the corporate NIC. Leave
# this resource out otherwise - the script picks the default-route NIC.
Registry CollectorRanges
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\GE\ShopDB'
ValueName = 'CollectorRanges'
ValueData = '10.20.0.0/23,10.21.4.0/26'
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 = {
# No -ApiUrl: BaseUrl above is where the script reads it from,
# and one source beats two that can disagree.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\ShopDB\Report-AssetToShopDB.ps1"'
$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.
{
"_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:
{
"_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:
# 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
Invoke-RestMethod -Uri 'https://shopdb.example.net/api/computers/by-hostname/<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:
# 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:
(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.
The screensaver itself is per-user, so set it with a configuration profile (Settings catalog > Screen saver) or a user-context script:
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.
{
"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": "<sha256 of your staged .scr>",
"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": "<sha256 of YOUR .ini, pointing at YOUR shopdb>",
"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
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.
Part 3: bootstrapping GE-Enforce itself
The two parts above assume the tools are already on the PC. GE-Enforce is what puts them there and keeps them there - so the question is how GE-Enforce gets onto a bay in the first place. Two paths are supported, and they are the two that actually occur: imaging time, and a management plane such as Intune.
Whichever you use, the end state is the same three things:
- The client files in
C:\Program Files\GE\Shopfloor. HKLM:\SOFTWARE\GE\ShopDBcarryingBaseUrl, andApiTokenwhere the scope is served over HTTPS.- A scheduled task running the enforce cycle as SYSTEM.
At imaging time (the usual path)
The shop-floor imaging pipeline registers the enforce task as its last step, once the PC type is known - the type is what decides which manifest scope the bay enforces. Self-contained types are skipped deliberately: a display kiosk gets its configuration at imaging time and no share, so registering an enforce task on one would give it a cycle with nothing to do.
If you are building your own imaging pipeline, the equivalent step is: copy the
engine into a runtime directory, write the PC type where the client can read it,
and register the task. That is what Install-GEEnforce.ps1 does in one call.
Through Intune
Install-GEEnforce.ps1 is parameterised for exactly this, and this is how the
display cohort is deployed today - those PCs are Entra-joined, have no file
share, and fetch their manifest over HTTPS:
.\Install-GEEnforce.ps1 `
-PCType 'gea-shopfloor-common' `
-ShopdbUrl 'https://shopdb.example.net' `
-ShopdbToken 'shopdb_pat_REPLACE_WITH_YOUR_TOKEN'
Package it as a Win32 app with the client files. Detection rule: the scheduled
task exists and HKLM:\SOFTWARE\GE\ShopDB\BaseUrl matches your server, so a
PC that was imaged for another site is repaired rather than skipped.
The token is a managed service token scoped to the GE-Enforce fetch scope, not an admin credential. Scope it that way and a token read off a bay buys the reader a manifest they could have read anyway.
With DSC (Machine Configuration)
There is no shipped DSC configuration, but nothing about the end state resists
one - it is two registry values, a set of files and a scheduled task, which is
ordinary DSC territory. Express it the same way as the reporter example above:
Registry resources for BaseUrl and ApiToken, File resources for the
client, and a Script resource that registers the task.
One caution. DSC converges state on a schedule and GE-Enforce is itself a convergence loop, so running both against the same PC means two things fighting to own the same configuration. Use DSC to install and configure the client, and let GE-Enforce own everything downstream of that. Deciding which tool owns what is the whole job; splitting it by layer is what keeps it answerable.
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".