Kiosks report their display type; whole-chain DDL parity

This commit is contained in:
2026-08-12 17:07:21 -04:00
parent 50578f33bd
commit d6b433e5f4
3 changed files with 82 additions and 11 deletions

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

@@ -22,9 +22,9 @@ from alembic.migration import MigrationContext
from alembic.operations import Operations
_BASELINE = (Path(__file__).resolve().parent.parent.parent / 'plugins' /
'geenforce' / 'migrations' / 'versions' /
'0001_geenforce_baseline.py')
_VERSIONS = (Path(__file__).resolve().parent.parent.parent / 'plugins' /
'geenforce' / 'migrations' / 'versions')
_BASELINE = _VERSIONS / '0001_geenforce_baseline.py'
def _geenforce_models():
@@ -53,15 +53,23 @@ def _model_schema(db, models):
def _migration_schema():
"""Run the baseline migration's upgrade() on a fresh engine; return columns.
"""Run the WHOLE chain's upgrade() on a fresh engine; return columns.
The whole chain, not just the baseline. Reading only 0001 meant a column
added by a later revision read as model/migration drift even though the
migration for it existed - the check is "do the models match what the
migrations build", and that is the chain, not its first file.
Returns {tablename: {columnname: reflected-column-dict}}. Reflection happens
while the connection is open, so we materialize the dicts before it closes.
"""
spec = importlib.util.spec_from_file_location('geenforce_baseline',
str(_BASELINE))
migration = importlib.util.module_from_spec(spec)
spec.loader.exec_module(migration)
migrations = []
for path in sorted(_VERSIONS.glob('[0-9]*.py')):
spec = importlib.util.spec_from_file_location(
'geenforce_migration_' + path.stem, str(path))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
migrations.append(module)
engine = create_engine('sqlite://')
schema = {}
@@ -69,7 +77,8 @@ def _migration_schema():
operations = Operations(MigrationContext.configure(connection))
operations._install_proxy()
try:
migration.upgrade()
for migration in migrations:
migration.upgrade()
finally:
operations._remove_proxy()
connection.commit()

View File

@@ -193,3 +193,37 @@ def test_backup_state_prefers_the_newest_confirmed_revision(db):
assert facts['wjpc01']['backupkind'] == 'udc'
assert facts['wjpc01']['backuplastseen'] is not None
# -- reported display subtype -------------------------------------------------
def test_report_stores_the_subtype_the_device_reports(db):
"""A kiosk knows whether it is a Dashboard or a Lobby; it now says so.
Previously the fleet table inferred this from the DashboardDefault fqdn
mapping, which is empty unless somebody added a row per kiosk, so the
column was blank for every display.
"""
from plugins.geenforce.service import record_enforcement_report
record_enforcement_report({'hostname': 'WJKIOSK01', 'scopename': 'gea-shopfloor-display',
'subtype': 'Lobby', 'counts': {}})
db.session.flush()
stored = ManifestEnforcementReport.query.filter_by(
hostname='WJKIOSK01', iscurrent=True).one()
assert stored.subtype == 'Lobby'
def test_report_without_a_subtype_stores_none(db):
"""Every non-display PC type has no display-type.txt, so it reports no
subtype - that must land as NULL, not as an empty string."""
from plugins.geenforce.service import record_enforcement_report
record_enforcement_report({'hostname': 'WJPC77', 'scopename': 'common',
'subtype': '', 'counts': {}})
db.session.flush()
stored = ManifestEnforcementReport.query.filter_by(
hostname='WJPC77', iscurrent=True).one()
assert stored.subtype is None