Commit Graph

53 Commits

Author SHA1 Message Date
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
benjamin.baker
7905917cd4 Green Up: scope panel-group check to panel-backed tabs
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
Corporate Tools is the first PCDetail tab that mounts no PluginAssetPanels: it is permission-gated and renders a fixed action list, so no panel can land in it. The vocabulary check asserted every tabs.push key appears in RENDERED_GROUPS, which cd70556 broke by adding that tab.

Adding 'corptools' to the vocabulary would have made it a legal panel group with no panel host - fetched, held, rendered nowhere - the silent-drop failure this file exists to catch, and it would also have failed the server-vocabulary check against ASSET_PANEL_GROUPS. So the invariant is narrowed to panel-backed tabs instead, with a mirror check that any tab mounting a PluginAssetPanels must have its group rendered.
2026-09-06 05:38:44 -04:00
benjamin.baker
82d9dbae8a Fix CI: untrack docs prose, correct two stale frontend specs
wilm has been red for eight consecutive runs across backend, gates and
frontend. Three failures, two unrelated causes, neither introduced by the
endpoint agent work.

docs/ prose (backend + gates):
The docs gates detect a published checkout by the ABSENCE of docs/*.md,
because docs/ becomes the wiki on that side. DEV-BUILD-TEST.md and
PLUGIN-HOOKS.md were committed in unrelated changes, which made CI look
like an internal checkout carrying almost no prose. test_there_are_docs_
to_check then failed on `assert 3 > 20`, test_the_map_itself_is_current
failed on a generated PROJECT-MAP.md that is correctly untracked, and
check-gates-ran.sh failed on the resulting skip - a skipped gate reports
green and checks nothing, so it refuses to accept one.

The existing .gitignore comment predicted this exactly and was defeated by
naming files individually. Untracked both (kept on disk) and replaced the
per-file list with docs/*.md so the next authored doc cannot reintroduce
it. Scoped to the top level: docs/notes/ prose and docs/openapi.json stay
tracked, and the gates only glob docs/*.md.

Frontend specs:
getEntraDeviceUrls() gained lapsUrl and loaded, but two siteSettings specs
still asserted the old two-key shape with toEqual, which is exact-match.
The implementation is correct - loaded is what lets a caller distinguish a
failed fetch from an unconfigured site, since both read as empty strings -
so the specs were brought up to the current shape rather than the return
value narrowed.

pinnedPanels.spec.js sliced 420 characters from getEntraDeviceUrls() and
asserted the finally block appeared inside it. Adding the retry logic
pushed finally past that offset, so a correct implementation reported as
"the finally is missing". Rebounded the slice to the function's own extent,
matching the structural approach already used elsewhere in this file for
the same reason. Verified by mutation: renaming the flag in the source
fails the test, which also caught that the spec reads
plugins/computers/frontend/views/PCDetail.vue and not the .plugins-staged
build copy.
2026-09-05 17:50:02 -04:00
benjamin.baker
ba38e4d39d PC Detail: flow Details cards, drop the IP Configuration card
Details tab card layout
-----------------------
The Details tab sat on .content-grid, a two-column CSS grid. Grid packs
items into implicit rows, so every row is as tall as its tallest card and
align-items:start leaves the shorter one with dead space beneath it:
Warranty is short, Endpoint Status is tall, and Policy had to wait for the
taller of the two rather than sliding up. That read as a staggered layout.

Switched that one tab to multicol via a .details-flow modifier, which has no
rows at all - each card starts where the previous one ended, so the holes
close while cards still size to their content.

Multicol is deliberately NOT applied to .content-grid globally. The note by
that rule in style.css records why it was abandoned there: a column-span:all
child (.pap-span, used by table panels) overlaps the shorter column when the
two are unequal. That cannot happen on this tab - every panel in the details
group is a keyvalue or list render, and spansFullWidth() only returns true
for table (or a tabs panel holding one), so nothing in the subtree spans.
Monitors and Service/Process DO carry table panels and stay on grid.

The modifier is scoped to PCDetail, so the nine other detail views on
.content-grid are untouched.

Chosen over JS masonry on purpose. Masonry packs perfectly but has to
measure every card and re-measure on resize, on panel collapse and on every
async panel load - and these panels fetch after paint, so it would mean
continuous forced reflow on a page that already polls. This is pure CSS:
no measurement, no observer, no runtime cost. The tradeoff accepted is that
column bottoms can end at slightly different heights.

Cards carry break-inside: avoid so none fragments across the column break,
and the 25px spacing returns as a margin because multicol has no gap between
stacked items the way grid does.

IP Configuration card
---------------------
Removed: the data has no purpose on the page anymore. Taken out at the root
rather than hidden - the panel declaration, the /asset/<id>/ipconfig route
and the _IPCONFIG_COLUMNS constant all go, since a route with no consumer is
dead surface that still answers requests.

test_the_two_cards_do_not_duplicate_columns_beyond_the_address existed only
to police the history/config column split; with one card gone it guards
nothing, so it and its import go with it. The panel-count assertion in
test_endpoints_asset_panels_are_computer_scoped moves 15 -> 14, which is the
test doing its job and catching the removal.

IP History is unchanged and still carries the address list.

Not addressed here: three tzset() tests in test_endpoints_plugin.py fail on
Windows because time.tzset() is Unix-only. Verified pre-existing against
unmodified code; they pass in the Linux container.
2026-09-05 15:27:50 -04:00
benjamin.baker
86985efcf2 Fix user-context collection, split agent log destinations, widen upload cap
Mapped drives stopped reporting because the user-context path failed silently
in three separate places, and none of them left a trace to read back.

- Probe: launch via -EncodedCommand instead of staging a temp .ps1 and running
  powershell.exe -File. A missing or quarantined script file fails BEFORE any
  statement runs, so the script's own try/catch never sees it and the only
  symptom is exit -196608.
- Spool: pin to the ShopFloor profile. Resolving it through WTSQueryUserToken
  is privileged, endpoint protection blocks it, and on failure the tray
  (writer) and service (reader) silently disagree about the path.
- Spool: consume after a successful parse, so a stopped tray cannot keep
  re-reporting stale mapped drives as current. Unparseable files are left in
  place for the next cycle.
- Logging: user-session components cannot write to Program Files or to the
  SYSTEM-owned ProgramData log directory, and the file sink degrades to a
  silent no-op when it cannot create its directory. Tray and probe now log to
  C:\Users\ShopFloor\ShopDB\Logs, installs transcript to ProgramData, and the
  sink records a SinkProblem instead of swallowing UnauthorizedAccessException.
- Installer: provision the operator spool/log directories with ShopFloor:Modify
  before the tray first runs, and stop the transcript via trap so a throw
  cannot orphan exactly the failed install worth reading.
- Upload cap 64 -> 128 MB: the self-contained tray publishes at ~69 MB and was
  rejected on SIZE, but the size check returns before the PE is parsed, so the
  operator only ever saw a version-detection failure.

AGENT-CONTRACT.md gains a normative section defining the three security
contexts, their log destinations, the fixed spool path and consume-on-read.

Agent 1.8.0 -> 1.8.1, tray 1.1.0 -> 1.1.1 so the deployed build is
identifiable from a log line.
2026-09-05 14:20:07 -04:00
benjamin.baker
7f84e8ca1e Admit serviceprocess server-side and give Operator Reports its own tab
asset_panels() rewrites any group missing from ASSET_PANEL_GROUPS to 'details' before the client sees it, and Phase 3 never added 'serviceprocess' there. Both panels were relabelled on the wire, so the tab could not appear no matter how consistent the frontend was. That is why the tab was still missing after the schema fix - a second, unrelated cause wearing the same symptom.

Operator Reports moves out of System Monitors into a tab of its own: that tab is machine-collected health, this is a person reporting something, and the thumbnail grid dwarfed the fact cards it sat between. Full width, since it is alone in the tab.

panelGroups.spec.js claimed to cover four files and checked two; it now reads the server tuple as well. The plugin test's hand-copied set was a fifth copy and now derives from the server tuple instead.
2026-09-05 07:37:01 -04:00
benjamin.baker
f77a288bdc Show empty-state text on empty table panels
A table panel with no rows rendered its column headers over an empty body. On a live PC Details page that made Network Interfaces and Listening Ports look like cards that had failed to load, when the real answer was that the agent had not reported any yet. List panels already honoured panel.empty; table panels did not.

Also records the simulation-suite requirement uncovered when rolling containers: the pytest suite builds its schema from the models, so an unapplied migration is invisible to it while every query against the affected table 500s in the running image.
2026-09-05 07:22:58 -04:00
benjamin.baker
e00bb44d6b merge storage and disk health into one card with two sections
Volume capacity and physical drive health were two adjacent cards, and the operator reading a full disk is usually the operator reading a dying disk in the same minute. They are now one Storage card with two sections. Visual grouping only - the two tables share no key, storage being per volume and health per physical device, and one drive can back several volumes or none the operator sees. Nothing is reconciled across the sections; this is not the mapped-drive merge where two sources described the same drive and the worse status won. The tabs render type was the only sections-in-one-card mechanism and emitted label/value fields with no table path, so it now takes table sections too. That is the renderer every plugin uses, so both behaviour changes in it are mutation-tested. Two things only surfaced on contact: the empty-section filter had to grow a table branch, because a table section declares columns up front and dropping it when it has no rows takes its tab with it and reads as no disks rather than nothing reported; and full-width span was keyed off the declared render type, but a tabs panel holding a six-column table needs the same span and that is only knowable from fetched data. The diskhealth route stays. It no longer backs a panel, but it is a stable read for callers outside the renderer and removing it for a UI-only change would break them.
2026-09-05 07:05:30 -04:00
benjamin.baker
af42bf5abd split monitors tab and surface boot type and nic link state
Tracked Services and Tracked Processes were sharing a tab with system health cards, so what is running on the box and how the box itself is doing read as one list. They now sit in a Service/Process Monitors tab and the old tab is System Monitors. The group vocabulary is duplicated across the renderer, the tab map and the panel declarations with nothing checking they agree, and a group named in one but not the others is dropped in silence - that has eaten cards twice. Tests now pin the vocabulary on both sides. Boot type and NIC link state were collected in Phase 2 but shown nowhere. Boot type only renders when fast startup applies, because uptime measured from a resume understates how long the kernel has been up and that is the case worth naming. Link state leaves a disconnected NIC unknown rather than half duplex: live hardware returns empty speed and duplex when unplugged, and half duplex is the cable fault signature this column exists to catch.
2026-09-05 06:58:43 -04:00
benjamin.baker
9ac711ce2d Surface the endpoint inventory, and let an event show its message
The hardware, OS and storage inventory had columns and data but nowhere to
be read: the only per-PC card was the status keyvalue, so a machine's
processor, firmware, disks and listening ports were reachable only by
querying the database.

Adds four Monitors cards - Hardware, Operating system, Storage, Listening
ports - split by the question each answers rather than by which JSON blob
they came from. OS and BIOS share one card deliberately; apart they are
three short rows each. A card yields no fields when its columns are null,
so an endpoint on an older agent hides them rather than showing a grid of
blanks.

User context is declared last on the page. The probe feeding it is not yet
producing output, so it reads empty for now, and being last means an empty
card sits below the populated ones instead of pushing them down.

The Activity feed showed a title and an event id, but the title is the
catalog name for that id - the same text on every occurrence - so two
different faults read identically. Rows now carry the provider name, and
the stored message body expands in place when the severity badge is
clicked. Commands expand to their result message, which is the reason a
failed one failed.

The badge doubles as the expander rather than gaining a control beside it,
and it keeps its severity label when open: swapping that for "Hide" would
cost the at-a-glance severity scan down the feed. Being a button rather
than a span, it is also now keyboard reachable.

map.body is additive in the shared renderer - panels that do not declare
it render exactly as before.
2026-09-04 19:14:16 -04:00
ff9ae693a6 Remediate the 2026-09-03 feature-test findings 2026-09-03 17:14:53 -04:00
bb733f1332 Merge the wilm work, act on the five-agent review, verify the launch-path policy on Windows 2026-09-02 16:15:53 -04:00
f087a7a5b0 Rename Metrics to Monitors and move it second 2026-08-31 15:21:48 -04:00
b21503e250 Fix collapse, group skew, empty tables and the tab strip scrollbar 2026-08-31 15:01:24 -04:00
2fce339735 Drop the Actions tab and give wide panels the full width 2026-08-31 14:41:20 -04:00
0d49341a2a Tab the PC detail page and add panel groups to the ADR-010 contract 2026-08-31 14:25:53 -04:00
43fed63277 Site branch tooling, branch-aware publication, dashboard fixes
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.
2026-08-30 11:19:00 -04:00
870e068c44 Rebuild the dashboard around what needs a person
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.
2026-08-29 21:35:03 -04:00
330fd31524 Collapse the relationships card by default, with an honest count
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.
2026-08-29 19:52:20 -04:00
dd7dd435c0 Put the dual-bay sibling in the triage band, and fix the gage counterpart
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.
2026-08-29 19:38:07 -04:00
8666e0271c Reciprocal controls links, dead chip links, and a resumable legacy import
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.
2026-08-29 19:09:06 -04:00
6beb2afc09 Service state colours, activity badge colours, readable named colours 2026-08-29 17:00:34 -04:00
b26bbd0456 USB external import, and filter chips on health-dot cards 2026-08-29 13:02:45 -04:00
09d06399d3 Fleet console gap closures: critical tile, filters, policy card, action placement 2026-08-28 23:00:37 -04:00
7e779a6829 Fleet console: triage band, activity feed, live state first 2026-08-28 22:35:16 -04:00
benjamin.baker
af133372b4 Gate endpoint UI refresh on fresh telemetry and land endpoint agent
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.
2026-08-28 15:07:20 -04:00
benjamin.baker
01734e5449 harden CSV import reconciliation and preserve saved model selections
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.
2026-08-28 09:29:09 -04:00
04107ff8ab Search spelling suggestions across every list 2026-08-21 11:15:06 -04:00
fdf3c51cb0 Hover mini-map marker in box coordinates 2026-08-21 10:20:38 -04:00
d26355cebb Hover mini-map marker, knowledge base search, and the label generator 2026-08-21 09:51:12 -04:00
d267813999 Printer assignment end to end, and a container that keeps a site's files
Assign printers to a MACHINE and let whichever PC controls it install them, with
the observed half kept strictly apart from the assigned half: a collector report
can never write an assignment row. Adoption is explicit. Adds a singular
relationship type so defaultprinter can mean exactly one, client scripts that
converge a bay's queues and correct a drifted one in place, and a wave gate so a
fleet-wide driver rollout cannot stampede its own share.

On the container side: the compose stack never persisted the instance directory,
so a rebuild discarded plugins.json and every upload. It is a volume now, the
image ships instance/ owned by the app user so the mountpoint is writable, and
the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended
since 8.0 went end of life. The image also could not build at all: the frontend
stage never copied the plugin staging script it runs.

Adds a topology migration guide covering IIS to Docker and back.
2026-08-20 07:46:35 -04:00
09e9d5f047 Carry the level everywhere a position is drawn, and gate it per occurrence 2026-08-18 09:37:29 -04:00
3af77bdb5b Make the map editor accept a click, and unclutter its panel 2026-08-17 15:21:34 -04:00
4c3d5746e8 Fix the levels viewer: markers were drawn on whichever plan was showing 2026-08-17 15:01:09 -04:00
fbd99041b0 Buildings and levels for the floor map, and make every identifier searchable 2026-08-17 13:03:54 -04:00
d32513727b Tech Tools section with a code generator, toner report that names the part, toner forecasting, warranty proof of cover, application uploads 2026-08-12 11:51:38 -04:00
568dfbb91c Dashboard corrections 2026-08-11 16:27:14 -04:00
721828b5cb Card refinements 2026-08-11 15:58:15 -04:00
793dfab5c8 Printer card chips 2026-08-11 15:46:21 -04:00
34cb9a5e3e Dashboard polish 2026-08-11 15:28:29 -04:00
9ae9879b90 Dashboard fixes: staleness, toner, layout 2026-08-11 14:45:29 -04:00
0ed8be75d9 Dashboard cards, backup last-seen 2026-08-11 14:01:57 -04:00
3ebc1f353a tabbed DNC card, machine-numbered downloads, themed backup history 2026-08-07 15:22:59 -04:00
07d390789a Modal overlay-close removed from data-entry; console menu grouped 2026-08-05 13:49:34 -04:00
ff61a4c5e9 nav resilience after pool restart, thinner marker rings, drop dead marker popup 2026-07-31 10:17:28 -04:00
d84f7e3911 map marker rings keyed on surface, legend parity 2026-07-31 09:29:49 -04:00
59578e1c5b map marker legibility, PDF export blueprint fix, asset list type search 2026-07-31 09:06:22 -04:00
a595eaffd5 map: fix subtype filter dropping every measuring tool 2026-07-31 07:59:20 -04:00
fd2ab0ab1c ADR-013 Phase 3 complete: asset-panels + asset-presentation + map-overlays renderers 2026-07-18 23:10:27 -04:00
c22d3a7c23 ADR-013 Phase 3: asset-panels renderer + full detail-page migration 2026-07-18 22:52:06 -04:00