A restore could load a database dump whose schema was older than the running
code. The manifest stamped schemarev from the code image on disk, so the
compatibility check compared the code against itself and always passed. A set
captured from a database at 7d37_import_batches_and_journal restored under
code at 7d41_drop_guest_tools_secrets, leaving the data behind the code: every
query on a table the newer migrations touched failed with 1054 Unknown column,
which surfaced as empty configuration-policy dropdowns and a failure to load
data on edit PC.
The authoritative signal is the alembic_version of the database that was
dumped, so manifests now carry datarev alongside schemarev, backup.sh reads it
from the database and warns when it has drifted from the image, and the gate
asks whether the running code knows that revision. Data behind the code is
allowed because it can be rolled forward; data ahead of the code is refused
because it cannot.
Rolling it forward has to happen somewhere with an app context. The backup
sidecar is mysql plus restic plus alembic only and has no Flask, so restore.sh
loads the dump and parks the run in an upgrading phase behind an
.upgrade-requested sentinel rather than declaring a success it cannot verify.
The api container owns the Flask CLI and already runs these exact idempotent
commands on every deploy, so a finalize endpoint there runs db upgrade plus
plugin upgrade-all and stamps the terminal phase. The UI drives it on seeing
the upgrading phase, so recovery needs no operator intervention.
The live simulation runs one codebase against itself, so its dump is always at
the same revision as the code restoring it and it can never reproduce this
drift. The gate therefore gets a standalone tester that builds throwaway
migration chains and dumps offline, invoked as a simulation preflight. It
caught a real bug in the legacy dump-parse fallback, where the revision was
matched loosely enough to return the alembic_version table name instead of the
value it holds.
The watcher scripts now write live status and append-only run history onto the shared volume; the api exposes them via /shopdbbackups/status and /history. The restore modal shows an indeterminate spinner with phase labels, and two cards list backup and restore run records. Timestamps render in UTC to match the Zulu-stamped set ids, the history directory is excluded from the set list, and a stale-status guard stops a previous run's done phase from collapsing the spinner on the next restore.
The api queued .restore-requested but nothing consumed it, so restores silently did nothing. The watcher now services the restore sentinel by running restore.sh, and its instance_data mount is read-write so restore can lay files back down (the manual one-shot stays read-only). Backup stamps use a colon-free UTC form that Date could not parse, so the UI showed raw strings; normalize them before formatting.
Backups are per-site and not part of the base stack, so they ship as a separate compose overlay with an isolated restic REST server the app can only reach through the backup utility (no shell or db creds). The sidecar image trusts the corporate proxy root CA at build time so its Oracle yum, github, and PyPI fetches succeed behind TLS interception; shell scripts are pinned to LF via .gitattributes so the container entrypoints do not die on a CRLF shebang.
Ben Baker's installed-software collection, software delivery (run_script and
update_app), agentless adoption tool and command-queue hardening, with his
authorship preserved.
Reviewed before promotion. Fixed: the endpoints plugin tests were never collected
by pytest, the SBOM snapshot table's key would exhaust on MySQL, ingest did not
truncate two columns strict MySQL would reject, and update_app's version branch
pinned a digest for a file that route never serves. Agent packaging moved off
require_role('admin') onto a new endpoints.agentpackage permission.
Contract 0.27.0. Full suite 2231, frontend 314.
The dashboard panel now matches the sidebar's own vocabulary. The export refuses
--dist anywhere but main, because the dist directories are what the prod update
script mirrors onto the server and they are not branch-qualified.
Publication now mirrors the branch and writes one bundle per branch. Dashboard:
guests see what they can reach rather than a blank page, and the estate total
lists every type it counts.
The board was one card per source in a fixed grid: every source got the same
small box whatever it had to say (warranties showed 5 of 50), and a box per
source flattened priority, while the largest type on the page belonged to counts
that need nobody.
Now a severity rollup, one worst-first queue of everything needing a person, and
a row of context beneath. The queue is full width, capped per source so a noisy
check cannot bury a rare one, and names what did not fit with a link to it. Chips
filter the queue by check as well as by severity; both narrow what is already
loaded, so the counts and rows cannot disagree.
Dropped: an 'All assets' tile shown beside its own four addends, a printer strip
carrying the same count a third time, three cards whose job was to display 0, and
a Recent Devices table listing five machines all 'In Use'.
Contract 0.26.0 adds optional 'shortname' on dashboard cards - a title is written
as a heading and ran wider than the hostname it qualified as a row badge.
Also: the supplies card reported 'Low Supplies 0' when Zabbix was unreachable,
which reads as reassurance rather than the truth that nothing could see the
cartridges. It now distinguishes unreachable (says so) from unconfigured (stays
quiet - the site declined the feature) from a real zero.
Full suite 2182 passed, frontend 314 passed.
The triage band answers the common case above the fold, so the full edge list is
reference rather than the first thing to read. Same caret and keyboard handling
as the collapsible panels on the same page.
The card stays: it is the only surface with Add and delete, and it keeps each
edge's exact type where the band deduplicates by asset - on a printer, four rows
collapse to two chips and which machine DEFAULTS to it is lost.
The header count is of RENDERED items, not raw edges: a symmetric Dualpath link
is stored in both directions, so counting edges read 3 above a card showing 2.
Frontend 280 passed.
The machine page opened with a dual-bay banner directly above a hero band saying
'Controlled by'. Both answer 'what am I looking at', so the sibling is now a chip
in the band. The band takes a generic extraitems prop rather than learning what
dualpath is - the pairing is resolved by the machines plugin, not from the
relationships endpoint.
MeasuringToolDetail filtered for a machine counterpart, but the import creates a
controls edge PC -> tool, so a gage's counterpart is a computer. It rendered
nothing on every metrology station that has a PC.
Verified on screen. Full suite 2178 passed, frontend 280 passed.
The machine page now shows the PC that controls it. The endpoint was correct all
along and returned both directions; AssetTriageBand discarded every peer whose
type was not 'machine' and was mounted only on the PC page. The counterpart type,
label and relationship types are now props.
Fixed while adopting it more widely: every band chip linked to /assets/<id>, and
there is no such route. The resolver that AssetRelationships.vue kept privately
is now frontend/src/utils/assetRoute.js, used by both - and its type lookup is
case-insensitive, because seed data capitalises the type names while this site's
database does not.
The legacy import is now safe to resume. The 409-resolve asked for
per_page=10000 against a server that clamps to 100, so it silently searched the
first 100 rows and dropped links past that on every re-run. The crosswalk is now
bound to the database it describes, so one carried across a rebuild is refused
rather than attaching rows to the wrong assets. Source database names are
resolved from the environment, so the loader can be aimed without editing it.
Full suite 2178 passed, frontend 280 passed.
Clears every shipped finding the SBOM scan reported: pyjwt 2.13.0, idna 3.19,
axios 1.20.0, postcss 8.5.26, dompurify 3.4.14, nanoid 3.3.18, follow-redirects
1.16.0, form-data 4.0.6. All in-major bumps inside ranges the manifests already
allowed, so this is a lockfile refresh rather than a migration.
The axios and dompurify floors were raised to match, since the old ones still
nominally admitted the vulnerable range.
The PyJWT allowlist entry was removed rather than kept - it stopped matching once
the upgrade landed.
Verified: full suite 2161 passed, frontend 273 passed, build clean, npm audit
--omit=dev reports 0 vulnerabilities.
Applied on top of af13337 so nothing already on wilm is rewritten.
AUTHORIZATION. The six new policy GETs were @jwt_required(optional=True) with
no permission at all, so an anonymous caller could list configuration and
business policies - gated only by a feature flag that 404s while off. They now
require computers.view. The six policy write routes stacked @jwt_required()
above @require_permission, the same pattern removed from nine endpoints routes
in this very push: the outer decorator answers an anonymous request 401 before
require_permission can consult the guest role, so a grant to guests silently
does nothing. Removed, and the now-unused import with them.
tests/test_core/test_public_read_surface.py failed at af13337 with seven ungated
GETs. It passes now. The seventh, endpoints.claim_next_command, is genuinely
agent-facing - authenticated in the handler by the endpoints collector key,
because a bay has no user session - so it joins PUBLIC_ENDPOINTS with its reason
written down, exactly as report_command_result already does in test_authz.py.
Note which half is public: claiming a queued command is the agent's business,
while ENQUEUEING one is the real authority and stays gated on endpoints.command.
INSTALLER. -BaseUrl and -CollectorKey are now mandatory with no defaults, and
https is enforced unless -AllowInsecureHttp is passed for a local test box. The
defaults were a real hostname (ADR-015) and a live collector key: a key in
source is a key in git history, and that key is also the command channel's kill
switch. Removing it here does not undo the disclosure - it must be rotated on
the server, and purged from af13337 - but it stops the next clone carrying it.
IMPORT. A committed import wrote thousands of rows and left no audit trail; it
now writes one AuditLog row inside the same transaction, so it cannot survive a
rollback or go missing from a commit. The computer-type dropdown asked for
per_page: 200, which the API ignores in favour of perpage and clamps to 100
anyway - it uses a new computersApi.types.listAll(), the pattern already applied
to the other selectors. And the comment claiming the template example rows are
"emitted as commented rows the engine skips" was the opposite of the truth: they
import as live data, the emitted instructions say so, and a test depends on it.
Not fixed here, deliberately: rotating the leaked key, purging it from history,
and the endpoint agent's own hardening (per-host credentials, a protected
process denylist so stop cannot kill security tooling, signing before the
PowerShell runner is enabled, registry ACLs, and the 25-second long poll that
will pin a worker thread per bay).
A command reaching 'succeeded' only proves the agent ran the action, not that it posted a new telemetry snapshot. Reloading panels on command success alone re-rendered stale service/process rows, so a stopped process could still show running right after Refresh Status.
Add a lightweight telemetrymeta probe exposing lastreportedat and gate both the header Refresh Status action and inline row Start/Stop on that stamp advancing past a pre-action baseline. Polling is bounded so an offline or idle agent never hangs the button, and messaging distinguishes a confirmed refresh from a queued one.
Bring the .NET endpoint agent source (service, tray, installer) into the repo so Cameron can merge and test, with an agent/.gitignore keeping bin/obj/publish out. Includes the DncMain launch-path fix, tray single-file host env scrub, and best-effort file logger.
Default fuzzy reconcile threshold to 0 and refuse merges when digit runs differ so numbered labels like Bay 311 never bind to Bay 310. Keep edit forms from clearing saved model fields when category filtering hides untyped rows. Includes in-progress computers policy and endpoints command feature work.
Server-side RMM telemetry plugin: current-snapshot endpointstates plus append-only curated endpointevents, resolved to existing Computer assets by hostname. Adds ADR-010 asset panels (status, services, recent events), dashboard quiet and stability cards, and a minimal Fleet list view that links into existing PC detail pages. Registers the activity sidebar icon and a dev-only sample seeder. Ingest arrives on the generic collector route; the future C# agent design record lives in docs/DESIGN-DECISIONS-agent.md.
The backend clamps perpage to MAX_PAGE_SIZE (100), so location selectors that requested a large page received only the first page and still got a 200. Late-alphabet locations (past ~C5W) simply went missing from every module's dropdown, and no error surfaced.
Add locationsApi.listAll, which pages through the full set via fetchAllPages, matching the established machines/printers pattern, and switch every location selector consumer to it.
Adds a concise native-CSV import guide covering exact headers, required columns, foreign-key-by-name resolution, import order, and the do-not-edit rule for Azure/Thor exports. Also clarifies the Import Site Assets Azure card that Bulk operations lives in the top command bar of the group Members page, matching the Entra UI.
PC records now carry Entra/Azure device identity so imported machines keep a durable link back to their directory object instead of matching on hostname alone. Adds azuredeviceid, azureobjectid, and azuredevicecategory to the Computer model with matching migrations, surfaces them read-only in PCDetail as an Entra Device Info card, and lets PCForm edit the device id directly. The computers plugin registers a machineassetnumber import hook that reconciles PC-to-machine links during bulk import and warns on unresolved or conflicting relationships.
Adds importsources adapters that normalize Azure device-group and Thor PC inventory exports into canonical ShopDB CSVs via hostname alignment, exposes register_import_hook and RowWarning on the shopdb.api contract surface (contract 0.21.0), and refreshes the Import Site Assets page with clearer source instructions. Includes reconcile_locations maintenance script and coverage for the new import paths.