Commit Graph

496 Commits

Author SHA1 Message Date
Proudlock
9f78d0bfc7 Merge origin/wilm into wj
Brings in agent upgrade and log collection features:
- One-click agent self-upgrade from endpoint detail page
- Auto-register staged agent builds as pushable applications
- Log collection and script output capture
- Endpoint log collections plugin integration

Resolved conflict in ShopDbEndpointAgent.csproj by taking wilm version 1.16.0
2026-09-10 09:24:08 -04:00
benjamin.baker
8be1c2379f Add one-click agent self-upgrade from the endpoint detail page
An operator could see that an endpoint was behind the published build but
had no way to act on it. update_app resolves against Application rows, and
the agent lives in the version store, so there was nothing to push.

Bridge the two: the staged build is packaged and registered as a reserved
Application row, and a managed PowerShell script is seeded to install it.
The endpoint downloads that package over the existing collector-authenticated
app-package route, so no parallel delivery path is introduced.

The new route prepares the delivery and then DELEGATES to
enqueue_endpoint_command as a digest-pinned run_script. It deliberately does
not implement its own enqueue: a second path would duplicate, and eventually
drift from, the endpoint lock, the Configuration Policy gate, stale-command
reaping and the at-most-one-in-flight rule. A fleet-wide agent swap is the
last action that should skip a quarantine check. Tests pin that delegation
rather than only the happy path.

Because it lands as run_script, the caller needs endpoints.runscript on top
of endpoints.agentupdate. That is intended: this delivers arbitrary SYSTEM
PowerShell by the same mechanism as any other script push. The audit event
is written only on an actual 201, so a refused push does not record an
upgrade that never happened.

Replacing the agent is self-modifying the control plane - the process
receiving the command is the one being swapped. The script therefore
re-launches itself detached and exits immediately, because the agent runs
run_script as a direct child and holds its stdout/stderr pipes while
waiting on it. Expect the command to report queued rather than success:
the process that would report completion is stopped mid-run. Judge the
result by the version the endpoint reports afterward.

Tray relaunch does not use the installer's `explorer.exe <path>` trick.
That works in install-shopdbagent.ps1 only because the installer runs as
the user - note its own `-ne "SYSTEM"` guard. This script always runs as
SYSTEM in session 0, where the hand-off reaches no shell and exits 0, so
Start-Process reported success for a relaunch that never happened. It also
cannot descend into the session with a token-duplicating launch:
UserContextSpool documents that WTSQueryUserToken is blocked by endpoint
protection on these hosts. Instead a one-shot scheduled task targets the
pinned ShopFloor account, matching how the user-context spool already
resolves that user, and the tray must run as that account or the
mapped-drive probe reports the wrong (empty) drive table. The relaunch is
verified by polling for the process rather than assumed, and stays
best-effort: the Run key still starts the tray at next logon, and a late
tray is not worth rolling back a good agent upgrade.

panel_status now carries agentbehind/agentversion/agentcurrent so the UI
gates the action on a flag rather than parsing "(behind X)" out of a label.
The button is confirmed by typing UPDATE, naming both versions and the
service interruption, matching how run_script and system_restart are gated.

Verified on live endpoints: upgraded 1.13.0.0 and 1.15.0.0 machines to
1.16.0.0. run_script delivery, digest fetch and system context all predate
1.13.0.0, so no minimum agent version is declared for this path.
2026-09-09 22:56:21 -04:00
benjamin.baker
d32293ccb2 Auto-register staged agent builds as a pushable application
Gate collect_log on a minimum agent version and bridge the agent version
store to the applications table so a staged build can actually be pushed.

update_app resolves an install source from the applications table alone; it
cannot see agentstore. A build could therefore be staged and current while no
application row existed at all, which is what the fleet was in: the agent was
unpushable. Staging is now the single source of truth - it packages the
current build and upserts the row in the same request, so the row cannot
describe a build that is no longer staged.

PARTIAL BUILD ADVISORY
This is steps 1-4 of a longer rollout and is deliberately incomplete:
  - There is no push trigger yet. The registered package is resolvable and
	verifiable, but nothing enqueues an upgrade; the "behind current build"
	indicator is still display-only.
  - The upgrade template script, its script row, and the enqueue route
	(endpoints.agentupdate) are not written.
  - Blocking constraint for the next slice: run_script executes
	powershell.exe -File <script> with NO argument channel, so a delivered
	upgrade script cannot be passed BaseUrl/CollectorKey and must either
	source them itself or run_script must be extended first.
Nothing here touches a live endpoint. Staging only changes what a future
push would contain.

Details:
  - Package members carry a fixed timestamp. zipfile stamps the current mtime
	by default, so identical input produced a different archive digest on
	every call. update_app pins a download by SHA-256, so a drifting hash
	would rewrite installersha256 out from under an endpoint mid-download and
	fail its verification.
  - All three routes that can change 'current' re-register (upload,
	adopt-legacy, rollback). Covering only upload would leave the row pinned
	to the build an operator just rolled away from, and the push would ship
	it anyway - silent drift.
  - Registration cannot fail an upload. The binary is staged and audited by
	the time it runs, so a packaging problem is reported in the response
	payload rather than raised; returning an error would tell the operator a
	successful upload was rejected.
  - The package is written to a temp name and os.replace'd, so an interrupted
	write cannot leave a truncated archive at the path the row advertises.
  - The row is isinstallable but hidden: machine-managed, and it must not
	appear in the software catalogue beside real licensed applications.
  - Added application_package_location() to the shopdb.api contract surface
	(lazy, matching collector_authorized) returning the package directory and
	URL prefix together. agentregistry reached into shopdb.core internals,
	violating the plugin import contract; sourcing both from one place also
	stops the write location and installpath from drifting apart.

collect_log is refused at enqueue below its minimum agent version, with
numeric comparison - string ordering misplaces 1.9.0.0 against 1.16.0. The
refusal is mirrored in the UI so a command slot is not spent on a call that
is guaranteed to fail.

Test isolation: create_app('testing') resolves instance_path to the real repo
instance/ directory, so agent upload tests write into the developer's actual
store. The new tests redirect both the store and package output into tmp_path
and include a guard test that fails if that isolation regresses. NOTE: the
pre-existing test_endpoints_agentupload.py still has this leak and deposits
1.5.0.0 and 1.6.3 into instance/siteagent/ on a full run - not fixed here.

Suite: 2834 passed, 11 skipped. The two failures (test_docs_publishable, and
a symlink scan needing Windows privileges) were confirmed pre-existing by
stashing this work and reproducing them.
2026-09-09 21:40:51 -04:00
benjamin.baker
eee06743a6 Register endpointlogcollections in PLUGIN_TABLE_OWNERS
The new table was declared as a model and created by migration 7d51, but never added to the explicit ownership map. test_owned_tables_match_declared_models caught the drift.

Not cosmetic bookkeeping: the per-plugin migration uses this map to decide which tables it owns, so an unlisted table is invisible to plugin-scoped baseline and prune. The prune path would have treated endpointlogcollections as unowned - collected logs surviving or vanishing on a schema operation that should have been deterministic about them.
2026-09-09 19:35:21 -04:00
benjamin.baker
b49fb60c0c Add log collection, script output capture, and timestamp fixes
Log Collector: a new collect_log action reads a plaintext log file from a
known path and returns its tail for viewing and download. Rides the existing
endpoints.updateapp grant rather than inventing one, since collecting a log is
the same trust level as pushing an application. Collected logs land in their
own table with per-asset retention of 20 instead of overloading the command
audit row, and the agent refuses UNC, traversal, wildcards, directories and
non-text content independently of the server check.

collect_log is POLICY_EXEMPT. Every existing Configuration Policy predates the
action and so cannot name it, and the intersection in ALLOWED_ACTIONS_FOR drops
anything unnamed - so every policied endpoint refused log collection outright,
including for admins, who saw a permission-shaped error for a grant they held.
Exemption from policy is not exemption from permission: the elevated grant
still applies, and a regression test asserts both halves so a future edit
cannot quietly loosen the grant while "fixing" the policy gate.

Script output capture: successful runs can return output, gated per script by
captureoutput. Failures always returned it; success never did, which left a
green badge as the only evidence a run produced.

Timestamps: Activity and Operator Reports rendered date-only because the shared
formatter's 'date' branch drops the clock time. Run Scripts additionally
labels its timestamp, since a bare time meant "completed" on a terminal row but
"queued" on an in-flight one - indistinguishable, and the wrong reading.

Agent 1.16.0. Minor: collect_log is a new action name, so an older agent asked
for it fails closed on the unknown-action default.
2026-09-09 19:23:43 -04:00
Proudlock
05f2c46c74 Bump tray to 1.6.8 for coordinated release with agent 1.14.2 2026-09-09 18:42:20 -04:00
Proudlock
e8b45bf15d Merge wilm fleet alert scope fixes into wj
Brings in wilm commits:
- Make alertscope actually target fleet alerts
- Fix fleet alert server surface inherited from wj

Key changes merged:
- Added _alert_targets_endpoint() and _endpoint_scope_facts() for proper
  scope filtering (hostname, assettype, computertype, location, businessunit)
- Alertscope now properly filters which endpoints receive fleet alerts
- Added comprehensive test coverage in test_endpoints_fleet_alerts.py
- Preserved wj's operator report email enhancements (FQDN, machine relationship)

Resolved conflicts:
- plugins/endpoints/api/routes.py: merged scope filtering with operator report changes
- agent csproj: bumped to 1.14.2 combining both branches' changes
2026-09-09 18:42:09 -04:00
benjamin.baker
494b837bae Make alertscope actually target fleet alerts
alertscope was stored, serialised and editable in the authoring form but
never read when alerts were handed to an endpoint, so every fleet alert
went to every machine regardless of what was entered. The control implied
a capability nothing enforced.

get_fleet_alerts now resolves the polling endpoint's facts and filters
each alert through its scope. Supported keys: hostname, assettype,
computertype, location, businessunit. A key may hold one value or a list
(any match), matching is case-insensitive since these are operator-typed
names, and multiple keys narrow together.

Unrecognised keys are ignored rather than treated as match-nothing. A
typo like {"hostnam": "BAY-01"} would otherwise mute the alert fleet-wide
and look identical to a delivery failure; sending wider than intended is
the safe direction for something whose job is to warn people. A garbled
non-dict scope falls back to the whole fleet for the same reason. The
real fix for typos belongs at authoring time, not delivery.

BusinessUnit is not exported from shopdb.api, so its name is resolved by
table rather than importing the model and breaking the plugin import
contract.

Ten tests cover the narrowing cases and the fail-open behaviour.
2026-09-09 15:40:40 -04:00
Proudlock
2c347180e8 Bump tray to 1.6.7 for coordinated release with agent 1.14.1 2026-09-09 14:35:57 -04:00
Proudlock
a9a785fd50 Merge wilm into wj
Brings in wilm commits:
- Render script surfaces correctly in dark mode
- Stop scripts reporting success after doing nothing
- Correct two gate commands that pass without running
- Gate the script library on the grant that pushes scripts
- Let the policy list edit the rows the API accepts
- Cap plugin revision ids at the 32 chars MySQL stores
- Split shipped monitor bundle into per-monitor policies

Resolved conflict in agent ShopDbEndpointAgent.csproj by merging:
- wilm 1.14.0 script verdict and output capture features
- wj 1.13.1-1.13.5 tray coordination releases
- Bumped to 1.14.1 for coordinated agent+tray release
2026-09-09 14:35:41 -04:00
benjamin.baker
e0a7a51d46 Fix fleet alert server surface inherited from wj
The wj merge brought in a half-built fleet alert feature: server-side UI
behaviour still being stood up over a delivery proof of concept. It left
five red tests. This clears them and fixes a real hole underneath.

The cross-machine guard never ran. acknowledge_fleet_alert read .assetid
off _resolve_asset_for_hostname, which returns the id itself, so the
check raised AttributeError on every call and answered 500. That guard is
what stops one endpoint acknowledging another's alert, and the collector
key is shared across the fleet, so until now any bay could clear an alert
on another machine's behalf. Compare the id directly.

Gate both alert routes the way the rest of the agent channel is gated.
They are collector-key authenticated, not JWT+role, because a shop floor
bay has no user session - the same shape as claim_next_command and
upload_operator_report. Declared in the authz allowlists with the reason,
rather than inventing a permission no agent could hold.

Settle the delivery contract ahead of the durable agent work. The poll
response is authoritative for a host: every alert that should be live on
it right now. An alert absent from the feed is the agent's instruction to
retire the pending toast, which is how a server-side close or cancel
reaches a machine already displaying one. Acknowledgement is terminal per
product decision, so a close after an ack is a no-op and the agent can
retry a queued ack safely. A toast that merely timed out is not an
acknowledgement.

Stop reaching past the plugin contract surface for the mailer. One import
was a plain redirect; the other pulled a private helper to pre-check
recipients, duplicating what send_alert already does. It also returned
early when no email recipients existed, which silently suppressed the
independent webhook leg - misconfigured SMTP was dropping Teams alerts.

Register NotificationAcknowledgement in get_models() and give the
notifications plugin ownership of notificationacknowledgements, then move
the expected head to notifications0006fleetalerts.

Add the coverage whose absence let the .assetid bug ship. Thirteen tests
over gating, authoritative-feed withdrawal, terminal and idempotent
acknowledgement, per-asset scoping, and the cross-machine guard. Five of
them fail against the pre-fix handler.

Remaining suite failures are pre-existing and local: the symlink scan
needs elevation, and the docs check wants files absent from this checkout.
2026-09-09 13:20:54 -04:00
benjamin.baker
c635457e7f Merge wj: fleet alert notifications and operator report emails
Brings in the fleet alert notification concept, the operator report email
alerts, and the tray work that goes with them (tray 1.6.5).

Two files needed resolution:

- ShopDbEndpointAgent.csproj: wj carried 1.13.1 through 1.13.4, all
  tray-coordination bumps with no agent code behind them. Ours carried
  1.14.0, which is real code. Kept every changelog entry and took 1.14.0
  as the version, since it already supersedes the 1.13.x line.
- plugins/endpoints/api/routes.py auto-merged; verified the errormode
  descriptor survived intact.

KNOWN RED, INHERITED NOT CAUSED: five backend tests fail on this merge -
authz on endpoints.acknowledge_fleet_alert, the public GET surface scan,
two plugin-contract import checks, and the notifications plugin
migration/model agreement. All five were confirmed failing on a clean
worktree of origin/wj before merging, so they arrive with the branch
rather than from the merge. They reflect the fleet alert work being
half-landed: the push path was a delivery proof of concept and the
server-side surface was still being stood up.

These are not fixed here on purpose. The next change replaces that
delivery path with the endpoint agent, so the notification persists on
the endpoint until the operator acknowledges it or the server closes it,
and the affected surfaces get their gating and migration settled as part
of that work rather than being patched twice.
2026-09-09 12:44:38 -04:00
benjamin.baker
561988ac8f Render the script surfaces correctly in dark mode
The explainer panel used var(--surface, #fff) and PCDetail's Run Scripts
and Corporate Tools styles used --text-primary, --text-secondary and
--bg-secondary. None of those four tokens exist in this app's theme, so
var() fell through: the explainer rendered a solid white block over the
dark background and the surrounding copy lost its intended contrast.

The real tokens are --bg-card, --text and --text-light. A fallback
colour is what made this hard to catch - it looks correct in light mode
and only misrenders on the theme the shop floor actually runs, so it
would not have shown up in authoring or in review.

Both files now carry a test asserting their styles reference only
defined tokens and contain no var(--x, #colour) fallbacks. That test
immediately caught a further dead --warning fallback in the banner.

The two long per-field explanations in the Add Script form are now
collapsed behind "How this works". The modal already scrolled, and
three paragraphs of standing prose between the inputs pushed Save off
screen, so the copy was being skimmed past rather than read.
2026-09-09 11:54:05 -04:00
benjamin.baker
d6163f54a4 Stop scripts reporting success after doing nothing
A run_script command was judged solely on PowerShell's exit code, and
PowerShell treats most errors as non-terminating: it writes the error,
runs the next line, and still exits zero. The account-provisioning
script reported succeeded having provisioned nothing, and because no
output was captured there was no evidence to contradict the verdict.

Scripts now carry an errormode. Strict prepends $ErrorActionPreference
= 'Stop' so the first error ends the run and the exit code means what
it says. It is opt-in per script rather than global because bodies
authored against the permissive semantics rely on them - tolerating a
failing cleanup, probing with a cmdlet that errors when a thing is
absent, looping past a bad item - and would start aborting midway,
leaving endpoints half-configured. The default is unchanged, so every
existing script keeps running exactly as before.

The prologue is applied after digest verification, so the SHA-256 still
validates the bytes the server delivered.

stdout/stderr is captured on both paths: redirected streams for SYSTEM,
a transcript file under the ShopFloor profile for user-session runs,
since CreateProcessAsUser cannot inherit the service's pipes across the
session boundary. Output is always logged locally and returned to the
server only on failure, so routine output stays on the machine that
produced it instead of filling command history.

Also adds the optional per-script working directory, which the same
surfaces needed, and a collapsible explainer on the scripts page - the
dropdown is not self-explanatory, and an operator who does not know
about non-terminating errors has no reason to ever leave the default.

Agent 1.14.0. An older agent ignoring errormode keeps today's behaviour.
2026-09-09 11:36:42 -04:00
Proudlock
2090c0d4ef Use full FQDN for device name and fix machine URL
- Get pc_access_domain from settings to build full FQDN (hostname.device.geaerospace.net)
- Display FQDN in email subject and body instead of short hostname
- Fix machine_url to use actual request host instead of localhost fallback
- Remove unnecessary try-except around machine URL construction
2026-09-09 10:03:26 -04:00
Proudlock
18e21a87d0 Simplify operator report email body
- Device: shows only hostname (not name)
- Removed Asset Number line
- Machine#: shows only machine number as clickable link (if relationship exists)
- Keep Reason, Reported, Details, and Screenshot
2026-09-09 09:59:43 -04:00
Proudlock
51063d5d5e Add machine relationship to operator report email alerts
- Query AssetRelationship to find if PC has a machine relationship
- Subject line now shows: "Operator Issue Report: HOSTNAME (Machine #6906)"
- Email body includes machine name and number as clickable link
- Machine info only appears if relationship exists
2026-09-09 09:40:14 -04:00
Proudlock
b1deeda285 Fix operator report email to link to frontend page and include asset number
- Change device URL from API endpoint (/api/assets/{id}) to frontend page (/shopdb/pcs/{id})
- Add asset number to email subject line: "Operator Issue Report: {name} ({number})"
- Bump agent to 1.13.5 and tray to 1.6.6 for coordinated release
2026-09-09 09:34:59 -04:00
Proudlock
f736089698 Make asset number clickable link to device page in email 2026-09-09 09:30:33 -04:00
Proudlock
0ede46de68 Fix AttributeError in operator report email alerts
- Change asset.computername to asset.name (correct attribute)
- Change asset.assettag to asset.assetnumber (correct attribute)
- Update email label from 'Asset Tag' to 'Asset Number'
2026-09-09 09:26:10 -04:00
Proudlock
25b04e79d6 Add verbose logging to debug operator report email alerts
- Log when function is called with report details
- Log SMTP config and recipients being used
- Log before and after email send attempt
- Change warning to error with full traceback for failures
2026-09-09 09:20:04 -04:00
Proudlock
31c616abef Fix critical XSS vulnerabilities in operator report email alerts
SECURITY FIXES:
- Escape all user-controlled values (hostname, reason, details) to prevent XSS
- Fix endpoint name from 'assets.detail' to 'assets.get_asset'
- Fix fallback URL to be absolute instead of relative path
- Remove render_email() to avoid subject duplication in email body
- Check recipients exist before expensive HTML generation
- Build email template directly with proper escaping throughout
2026-09-09 09:10:43 -04:00
Proudlock
6e497a4751 Fix operator report email alerts with proper template and logging
- Use render_email() to wrap HTML in proper email template
- Add error handling and logging for URL generation
- Add log statement before sending to aid debugging
- Import render_email at module level
2026-09-09 09:02:43 -04:00
Proudlock
f5d1b12e3f Add email alerts for operator issue reports
- Send alert to default alert recipients when operator submits issue report
- Email includes device link, hostname, asset tag, reason, details, and timestamp
- Screenshot link included when available
- Uses existing send_alert() mailer infrastructure
2026-09-09 08:18:44 -04:00
benjamin.baker
f02e344101 Correct two gate commands that pass without running
npx vitest run bypasses the pretest hook that stages plugin frontends, so
plugin specs were never collected - it reported 28 spec files where npm test
reports 30. A plugin frontend spec could pass CI unread. Bare bash resolves to
WSL on Windows dev boxes and fails to find /bin/bash, which redirected to a log
reads as a clean style run with no findings.

Both are the same failure shape: a command that reports success without having
done the work.

Also records that a multi-line here-string passed to git commit -m silently
no-ops, reporting success without creating the commit.
2026-09-09 06:37:26 -04:00
benjamin.baker
fd18099199 Gate the script library on the grant that pushes scripts
The script library was gated on applications.*, so anyone holding the
software-delivery grant could rewrite the bytes of a script that a separately
trusted operator later pushed to a bay. The push was gated on
endpoints.runscript; the CONTENT of what got pushed was not. An installer is a
vendor artefact, but a script here is arbitrary PowerShell this site wrote that
the agent runs as SYSTEM, so the write and the push must require the same
trust.

All six script routes now require endpoints.runscript, including the body
download - reading the bytes is not a lesser act than listing them. Core names
the permission as a string rather than importing it from the endpoints plugin
so the module stays importable with the plugin absent.

This tightens access and there is deliberately no backfill: the operator scope
reaches applications.create/edit, so operator-scope holders lose the library.
Granting them endpoints.runscript on upgrade would undo the change and hand
every operator remote SYSTEM code execution, since the same grant gates the
push. A test pins that no prebuilt scope confers it.

Adds the System Scripts settings page and card. The route requires the
permission rather than admin, because the grant is meant for non-admin
operators and the sibling admin gate would lock them out of the library the
grant exists for. The settings icon map gains terminal and key; the agent-key
card had been asking for key since it shipped and silently drawing a puzzle
piece.

The library had no test coverage before this.
2026-09-09 06:37:02 -04:00
Proudlock
deb2bbe9df Fix resource leaks and disposal issues in fleet alert tray
- Dispose GraphicsPath after creating Region to prevent GDI handle leak
- Track and dispose all Font objects to prevent GDI+ resource leak
- Join worker thread before disposal to ensure client not used after disposal
- Fix Screen.PrimaryScreen null dereference with fallback rectangle
- Propagate cancellation token to FetchAlertsAsync to stop orphaned requests
- Bump tray to 1.6.5, agent to 1.13.4
2026-09-08 18:22:24 -04:00
Proudlock
57f7225195 Fix UI thread deadlock in fleet alert acknowledgment
- Acknowledgment now marks locally first, then posts to server in background
- Status check timer runs async properly without blocking UI thread
- Both changes use Task.Run to avoid .GetAwaiter().GetResult() deadlocks
- Bump tray to 1.6.4, agent to 1.13.3
2026-09-08 18:16:27 -04:00
Proudlock
a504cbf1f6 Add auto-close when notification becomes inactive
- Fleet alert forms now check every 30 seconds if notification is still active
- Automatically close if notification has ended or been deactivated
- Bump tray to 1.6.3, agent to 1.13.2
2026-09-08 18:03:42 -04:00
Proudlock
e2cfd856cf Disable auto-dismiss timer for fleet alerts
- Remove countdown timer startup
- Change footer text to 'Manual acknowledgment required'
- Bump tray version to 1.6.2
2026-09-08 18:00:03 -04:00
Proudlock
cc42f51d87 Fix crash when clicking acknowledge button on fleet alert
- Stop countdown timer before closing form
- Add FormClosing event handler to ensure cleanup
- Bump tray version to 1.6.1
2026-09-08 17:58:23 -04:00
Proudlock
14a8501569 Bump agent version to 1.13.1 for coordinated release with tray 1.6.0 2026-09-08 17:50:24 -04:00
Proudlock
a07e0e5357 Bump tray version to 1.6.0 for fleet alert styling update 2026-09-08 17:42:49 -04:00
Proudlock
8f2c2299b8 Update fleet alert form styling to match mockup
- Borderless window with rounded corners
- Improved fonts and colors (Segoe UI, green acknowledge button)
- Countdown timer in footer
- Better spacing and visual hierarchy
- Matches fleet-alert-mockup-demo.html design
2026-09-08 17:39:23 -04:00
benjamin.baker
67e701b538 Let the policy list edit the rows the API accepts
The shipped Configuration Policies rendered with working-looking Edit and
Delete buttons that did nothing. The API had already been widened to accept
shipped rows, but the buttons still carried :disabled on source !== local, so
the write was never sent and the change looked like it had never rolled.

Gate both buttons on the set the API actually accepts. The shipped badge stays:
it is provenance, not a lock, and it has to survive an edit so a site can still
tell a product-supplied row from one of its own.

Business Policies keep the local-only rule. Their API guard is unchanged and
no shipped Site Policy exists now, so the UI already matches.

Cover it with a spec, because the failure mode is a disabled attribute silently
disagreeing with a server guard - nothing errors, the button just does nothing.
2026-09-08 17:16:34 -04:00
Proudlock
6340648ad9 Fix notifications fleet alert migration for MySQL and add UI fields
- Change alertscope from JSONB to TEXT for MySQL compatibility
- Add Alert Fleet checkbox, scope, and timeout fields to notification form
- Fields appear conditionally when Alert Fleet is checked
2026-09-08 16:57:00 -04:00
benjamin.baker
0b5546bcef Cap plugin revision ids at the 32 chars MySQL stores
Rolling the containers failed on strict MySQL: the endpoints chain could not
stamp its new head with "Data too long for column version_num". Plugin version
tables declare version_num VARCHAR(32) and the new revision id was 33 chars.

Nothing in the suite caught it. The tests run on SQLite, which does not enforce
declared column widths, so an over-long id passes locally and fails only on a
real deploy - after the migration body has run and only the stamp fails. The
rollback was clean here, but the failure lands at the worst possible point.

Shorten the id and assert the limit in the test that already parses every
plugin revision, so the next over-long id fails on a laptop instead of during
a roll. Core is deliberately not covered: its alembic_version is VARCHAR(128),
and it already carries ids longer than 32 that are correct.
2026-09-08 16:47:56 -04:00
Proudlock
381cc6c845 Add fleet alert notifications for shop floor PCs
Fleet alerts push server notifications to endpoint PCs via the command pipe.
Operators acknowledge alerts via toast UI in the tray. Backend tracks
acknowledgements per asset to prevent re-showing dismissed alerts.

Backend: alertfleet/alertscope/alerttimeout columns, GET/POST /api/endpoints/alerts,
NotificationAcknowledgement model with unique constraint, hostname validation.

Frontend: Alert Fleet checkbox in notification form.

Agent: FleetAlertClient polls alerts, FleetAlertStore tracks acknowledged IDs,
FleetAlertForm displays toast (no auto-dismiss), FleetAlertWorker 60s polling.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-09-08 16:19:14 -04:00
benjamin.baker
8f115ca330 Split shipped monitor bundle into per-monitor policies
The product shipped one Configuration Policy carrying every shopfloor app
monitor, wrapped in a shipped Site Policy that applied it to every endpoint.
That bundled two decisions a site cannot separate: which monitors exist, and
which endpoints get them. A site that wanted eDNC on one PC type and MTConnect
on another had to delete the shipped policy and rebuild both halves.

Ship one Configuration Policy per monitor with no PC type attribution, and stop
shipping a Site Policy at all. Composition is a site decision; the product now
supplies parts, not an assembled opinion.

Shipped Configuration Policies become editable and deletable. Provenance stays
a source tag rather than a lock, which is safe because seeding is insert-if-
absent and no reconciling seeder exists, so a site edit is never stomped. The
stale comments claiming shipped rows were read-only and reconciled described
behavior the code never had.
2026-09-08 15:22:10 -04:00
Proudlock
86256e7041 Add Darkwing security scan results and remediation artifacts
Security scan completed on 2026-09-08, documenting findings and proposed fixes for attack surface review.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-09-08 11:35:40 -04:00
benjamin.baker
68ddf87d3b Gate operator reports behind their own grant
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled
Operator report screenshots are an uncontrolled capture of whatever was
on the operator's screen when they filed the report, so they can carry
part numbers, customer names, or other shopfloor content. The rest of
endpoints.view is uniform machine-collected telemetry. Reading a PC's
CPU load does not imply a right to read someone's screen, so the reports
and their images move to endpoints.operatorreports.

The image route is the real control point, not the panel: the panel only
decides whether a tab is drawn, while the bytes stay reachable by URL to
anyone who can guess an id. Both GET routes now require the new grant.

Migration 7d47 backfills the grant to everyone holding endpoints.view so
nobody loses a panel they were already using; tightening from there is a
per-account decision in the user editor. The grant is in the scope
denylist, so it is a per-user direct grant and Reporter cannot hold it.

Also adds test_user_editor_checkboxes_match_the_server_action_list. The
SCOPE_ENDPOINT_ACTIONS comment claimed a sync guard kept the Vue
checkbox list aligned with the Python tuple, but no such test existed.
The lists had already drifted: endpoints.operatorreports reached a
running container with no way to grant it through the UI. The guard
catches drift in both directions - a name only in Python ships the
feature dark, and a name only in Vue is worse, since the admin ticks a
box and believes access was granted while the server refuses to store it.
2026-09-08 07:36:19 -04:00
benjamin.baker
2cf46da55e Merge remote-tracking branch 'origin/wj' into wilm
# Conflicts:
#	scripts/check-naming-and-style.sh
2026-09-07 15:13:33 -04:00
benjamin.baker
0134eaa219 Hard delete applications, nest installer packages, detect PE versions
Soft delete left orphaned executables on disk and let a deleted name collide
with a fresh import, so delete is now a hard delete that removes the
associated files. The destructive paths (delete, revert-to-record) require a
typed OK because neither can be undone.

Installers keep their filename at import and nest per application/version,
which stops repeat imports doubling up. Version now comes from the same PE
detection the endpoints agent uses; when no version can be read the caller is
asked for one rather than silently storing a blank.

Applications also surface Version and a monitorable toggle, with launch path
and process name required only while monitorable is ticked. Fleet shows the
PC alias, matching PC Details.
2026-09-07 15:04:21 -04:00
benjamin.baker
79545db96e Add a Delete action to the applications list and withdraw the app policy
The Applications page had no delete control even though the endpoint and
the API client both already existed, so the gap was UI-only.

The button is gated on applications.delete via hasPermission rather than
on the admin role, mirroring the backend guard: admins bypass it anyway,
and a non-admin role granted the permission should get the button instead
of a control that would only 403.

Deleting also withdraws the generated install policy. Delete here is a
soft delete, and the policy is a separate row that did not follow, so a
deactivated application kept installing itself across the fleet with
nothing on screen to explain why. A customised policy is still left
alone, on the same ownership terms as unticking the box.
2026-09-07 13:36:06 -04:00
benjamin.baker
aa9dafc5ca Add application-driven policy generation with an acknowledgement gate
Ticking "Required on all PCs" now generates a configuration policy that
installs the application on every non-server endpoint it matches, rather
than only flagging a software-compliance report row.

Because that save is a fleet-wide deployment, it requires explicit
acknowledgement. The gate is enforced in the API, not only in the form:
a confirmation dialog that can be sidestepped by POSTing directly is
decoration, not a safeguard. It fires only on the transition INTO the
required state, since re-saving an already-required application deploys
nothing new.

Generated policies carry a new 'autoapp' source. They stay editable, and
hand-editing one transfers ownership to 'local' so it survives a later
uncheck; 'shipped' remains read-only. Existing required applications are
not migrated retroactively - policies are only created on save.

Also adds the monitorable fields that the shipped-policy breakup will
build on, and repairs a mangled non-ASCII byte in copilot-instructions.md
that was failing the style gate.
2026-09-07 13:27:59 -04:00
benjamin.baker
f73df42110 Show the PC alias as "Alias" in Fleet and PC Details
The asset name field presented as "Name" in PC Details but as "Alias" in Edit
PC, and was absent from Fleet entirely. Three surfaces, two words, one field.

Adds an Alias column to the Fleet table (positioned after Asset Tag to mirror
PC Details' ordering) and renames the PC Details label to "Alias". Edit PC
already used "Alias", so it is unchanged.

Label and presentation only: the field stays `name` on the asset and the API
payload is untouched, so no backend contract moves.
2026-09-07 13:03:31 -04:00
benjamin.baker
31dc9d49e1 Add an All PCs policy scope that excludes servers
The policy builder only offered "Matches everything" (the `true` scope node),
which also targets servers. Authoring an estate-wide policy almost always means
"every PC an operator sits at", and a reboot or script push against a server is
a materially different event.

Adds an `allpcs` scope node that matches every endpoint except those whose
computer type is server-designated. Server designation reads the computer type's
ICON slug, which is nominally a UI hint and is now load-bearing; it was chosen
over matching the type NAME (a site can rename "Server") and over a new isserver
column (needs a migration). It is isolated behind SERVER_ICON so promoting it to
a real flag later is a one-place change.

The `true` node is retained and still matches servers, so existing policies keep
their exact meaning rather than silently re-scoping. It stays visible in the
dropdown only when editing a policy that already uses it; new policies default
to `allpcs`.

Covered by tests/test_plugins/test_computers_policy_allpcs.py, verified against
a planted canary so the server-exclusion assertions are known to bite.
2026-09-07 13:03:07 -04:00
benjamin.baker
3626d7dd2e Add Report a Bug link to the sidebar
Sits in the sidebar footer below Settings and above the Light/Dark toggle, visible to everyone including guests, and opens the team's SharePoint bug list in a new tab with rel=noopener.

The URL is a development placeholder for the wilm pre-production window, marked as such in a comment next to the constant so it is not mistaken for a shipped integration. Styling matches .theme-toggle exactly and uses fixed white alphas rather than theme tokens, because the sidebar is dark in both themes - a var(--text-*) here would resolve to a dark colour on a dark sidebar in light mode and vanish, which is the same trap the lock modal hit earlier.
2026-09-07 12:48:15 -04:00
benjamin.baker
5de6a2b170 Seed plugin permissions and relink scopes on plugin upgrade
on_install/on_enable fire only on a state transition, so a permission declared by a later plugin version never reached an already-installed site through them. Until someone ran flask seed permissions by hand the name did not exist, and any route guarding on it refused every caller. Settings already had sync_all_plugin_settings() on the upgrade path for exactly this reason; permissions now have the counterpart.

The scope relink is the half that actually bites. Creating the Permission row only makes the name exist - the prebuilt scopes derive their grants from the catalog, so a permission seeded without a following Role.seedscopes() belongs to nobody but admin. The plugin installs clean and its UI is invisible to every non-admin, which reads as a broken plugin rather than a seeding gap. Ordering matches flask seed permissions: seedscopes only links rows that already exist, so it runs last. Additive, so a site's deliberate role edits survive the deploy, and best-effort per plugin so one bad plugin cannot abort a deploy.

Test deletes an existing plugin permission, runs upgrade-all, and asserts both that the row returns and that it is granted to the technician scope. Verified it fails without the fix rather than passing vacuously.
2026-09-07 12:43:39 -04:00
benjamin.baker
90dc6eb832 Record Windows tooling rules in copilot instructions
Three rules earned by losing time to them this session. Long-running commands (pytest suites, npm builds, container rolls) must be redirected to a log under devnotes/ and launched non-blocking, never polled or tailed - the user watches the file. Warn before modifying files when a test run or build may be in progress, so discussion can continue without altering files underneath a running suite. Invoke shell scripts with the explicit Git Bash path, because bare bash in PowerShell resolves to WSL, which has no /bin/bash here and fails in a way that looks like the script itself is broken.
2026-09-07 12:40:18 -04:00