geenforce: let a kiosk say what it is instead of guessing

A display knows whether it is a Dashboard, a Lobby screen or the 3D print
room - the dispatcher reads C:\Enrollment\display-type.txt to choose which
page to open. It never told shopdb, so the fleet table inferred it from the
DashboardDefault fqdn mapping, which is empty unless somebody added a row
per kiosk. The column was blank for every display.

The client now reads that file and reports it, the report stores it, and
the API prefers the reported value with the old mapping left as a fallback
for hosts still on an older client. Reported by the device beats inferred
from a lookup table, the same way enforcerversion already works. A PC with
no display-type.txt reports nothing rather than something invented, and an
empty string lands as NULL.

Two guards had to learn about it. The DDL parity check read only the 0001
baseline, so a column added by a later revision looked like drift even
though its migration existed; it now runs the whole chain, which is what
'do the models match what the migrations build' means. 0002 added a whole
table rather than a column, which is why this is the first time it bit.
This commit is contained in:
cproudlock
2026-08-12 17:07:21 -04:00
parent d572c913e5
commit 84bf5d04ed
8 changed files with 120 additions and 13 deletions

View File

@@ -1106,7 +1106,9 @@ def list_reports():
'machineassetid': known.get('machineassetid'),
'toolassetnumber': known.get('toolassetnumber'),
'toolassetid': known.get('toolassetid'),
'displayrole': known.get('displayrole'),
# Reported by the device wins; the DashboardDefault mapping is a
# fallback for hosts on an older client that does not send it.
'displayrole': report.subtype or known.get('displayrole'),
'backupkind': known.get('backupkind'),
'backuplastseen': known.get('backuplastseen'),
'scopename': report.scopename,

View File

@@ -210,7 +210,8 @@ function New-ShopdbReport {
param([string]$Hostname = $env:COMPUTERNAME,
[Parameter(Mandatory)][string]$Scope,
[int]$AppliedVersion,
[Parameter(Mandatory)][hashtable]$Summary)
[Parameter(Mandatory)][hashtable]$Summary,
[string]$SubType = (Get-ShopdbSubType))
$results = @(foreach ($entry in @($Summary.Results)) {
if ($null -eq $entry) { continue }
$mapped = @{
@@ -230,6 +231,7 @@ function New-ShopdbReport {
scopename = $Scope
appliedversion = $AppliedVersion
enforcerversion = $Summary.EnforcerVersion
subtype = $SubType
counts = @{
installed = [int]$Summary.Installed
skipped = [int]$Summary.Skipped
@@ -240,6 +242,32 @@ function New-ShopdbReport {
}
}
function Get-ShopdbSubType {
<#
.SYNOPSIS
What this PC is WITHIN its scope, read off the machine itself.
.DESCRIPTION
A display knows whether it is a Dashboard, a Lobby screen or the 3D print
room: the dispatcher reads C:\Enrollment\display-type.txt to choose which
page to open. It just never told shopdb, so the fleet table had to guess
from a DashboardDefault fqdn mapping that is empty unless somebody added a
row per kiosk. Reported by the device beats inferred from a lookup table,
the same way enforcerversion already works.
Returns '' when the file is absent - every non-display PC type - so the
report simply carries no subtype rather than a made-up one.
#>
param([string]$Path = 'C:\Enrollment\display-type.txt')
try {
if (-not (Test-Path $Path)) { return '' }
$value = (Get-Content -Path $Path -TotalCount 1 -ErrorAction Stop)
return ([string]$value).Trim()
} catch {
return ''
}
}
function Get-ShopdbPayload {
<#
Fetch a payload blob by content hash over HTTPS, verify the sha256, and
@@ -449,4 +477,4 @@ function ConvertTo-ShopdbSummary {
Export-ModuleMember -Function Get-ShopdbConfig, Sync-ShopdbManifest, `
Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion, `
Get-ShopdbPayload, Resolve-ShopdbPayloads, Merge-ShopdbManifests, `
ConvertTo-ShopdbSummary
ConvertTo-ShopdbSummary, Get-ShopdbSubType

View File

@@ -0,0 +1,28 @@
"""Record the display subtype a PC reports for itself.
A kiosk knows whether it is a Dashboard, Lobby or 3DPrintRoom - the dispatcher
reads C:\\Enrollment\\display-type.txt to pick its target - but it never told
ShopDB, so the reports table had to infer it from the DashboardDefault
fqdn/ip mapping, which is blank unless someone added a row per kiosk. Reported
by the device beats inferred from a mapping table, the same way enforcerversion
already works.
Revision ID: geenforce0003subtype
Revises: geenforce0002blobs
"""
from alembic import op
import sqlalchemy as sa
revision = 'geenforce0003subtype'
down_revision = 'geenforce0002blobs'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('manifestenforcementreports',
sa.Column('subtype', sa.String(length=50), nullable=True))
def downgrade():
op.drop_column('manifestenforcementreports', 'subtype')

View File

@@ -295,6 +295,11 @@ class ManifestEnforcementReport(db.Model):
# published version to see whether this PC received the latest manifest.
appliedversion = db.Column(db.Integer, nullable=True)
enforcerversion = db.Column(db.String(20), nullable=True)
# What the PC says it IS within its scope (a display's Dashboard / Lobby /
# 3DPrintRoom). Reported by the device, which reads it from its own
# enrollment file - not inferred from a mapping table that may have no row
# for this host.
subtype = db.Column(db.String(50), nullable=True)
installedcount = db.Column(db.Integer, nullable=False, default=0)
skippedcount = db.Column(db.Integer, nullable=False, default=0)
failedcount = db.Column(db.Integer, nullable=False, default=0)

View File

@@ -263,6 +263,7 @@ def record_enforcement_report(payload):
phase=phase,
appliedversion=payload.get('appliedversion'),
enforcerversion=payload.get('enforcerversion'),
subtype=(payload.get('subtype') or None),
installedcount=installed,
skippedcount=int(counts.get('skipped', 0)),
failedcount=failed,