From ba38e4d39d4e97fe0cec3d910404634013c689a9 Mon Sep 17 00:00:00 2001 From: "benjamin.baker" Date: Sat, 5 Sep 2026 15:26:11 -0400 Subject: [PATCH] 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//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. --- .github/copilot-instructions.md | 2 + frontend/src/components/pluginAssetPanels.js | 9 + frontend/src/utils/siteSettings.js | 14 +- plugins/computers/frontend/views/PCDetail.vue | 307 ++++++++++++++---- plugins/endpoints/api/routes.py | 53 +-- plugins/endpoints/plugin.py | 24 -- shopdb/core/api/settings.py | 15 + .../test_plugins/test_endpoints_ip_history.py | 10 - tests/test_plugins/test_endpoints_plugin.py | 2 +- 9 files changed, 298 insertions(+), 138 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3a4394f5..2631a058 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,6 +7,8 @@ by CI (`.github/workflows/ci.yml`) - suggestions that break them fail the build. ## General Guidelines - Finish the in-flight task before starting new workstreams. - When asked to "take notes" on a future idea, capture it in the notes file only - do not implement it, and do not expand on it at length in replies. +- Use a temp file for multi-line git commit messages instead of heredoc syntax: `git commit -F `. +- Avoid using `powershell.exe -File` for launching scripts, as it fails if the script file is missing or quarantined. ## Naming (LOCKED - the hook rejects violations) diff --git a/frontend/src/components/pluginAssetPanels.js b/frontend/src/components/pluginAssetPanels.js index 08018003..807a2303 100644 --- a/frontend/src/components/pluginAssetPanels.js +++ b/frontend/src/components/pluginAssetPanels.js @@ -19,6 +19,15 @@ export function formatValue(value, spec = {}) { const raw = String(value) return new Date(raw + (raw.length === 10 ? 'T00:00:00' : '')).toLocaleDateString() } + // Timestamps arrive as UTC ISO ("2026-09-04T16:13:29Z") because the wire + // format is UTC by contract. Render in the viewer's local zone: an operator + // comparing "last reported" against the clock on the wall cannot do the + // offset in their head, and the Z suffix invites reading it as local. + if (spec.format === 'datetime') { + const parsed = new Date(String(value)) + if (isNaN(parsed)) return value + return parsed.toLocaleString() + } if (typeof value === 'boolean') return value ? 'Yes' : 'No' return value } diff --git a/frontend/src/utils/siteSettings.js b/frontend/src/utils/siteSettings.js index 6be729ef..5dc9324c 100644 --- a/frontend/src/utils/siteSettings.js +++ b/frontend/src/utils/siteSettings.js @@ -8,6 +8,14 @@ let settingsCache = null let inFlight = null let retryAfter = 0 +// Whether the last resolved load actually produced settings. A caller cannot +// tell that from the value alone: a failed load and a site that simply has not +// configured a key both read as empty, and those want opposite handling - +// hide the feature vs try again. +export function settingsLoadFailed() { + return settingsCache === null +} + // A failed load is cached briefly, not for the page session. A transient blip // used to blank every settings-driven link until the operator reloaded, and // they have no reason to know that would fix it. A short window still bounds a @@ -151,10 +159,14 @@ export async function getServicenowUrls() { // setting instead of waiting for a release. // // Empty is the default and means "no link" - the id still renders as text. +// +// `loaded` distinguishes a real answer from a failed fetch. Both return empty +// strings, but only one of them means "this site has no portal links". export async function getEntraDeviceUrls() { const entraUrl = await getSetting('entra_device_url', '') const intuneUrl = await getSetting('intune_device_url', '') - return { entraUrl, intuneUrl } + const lapsUrl = await getSetting('laps_device_url', '') + return { entraUrl, intuneUrl, lapsUrl, loaded: !settingsLoadFailed() } } // Printer hostname template. {ip} is replaced with the dash-separated IP. diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue index 52ff46a6..e8c5bce1 100644 --- a/plugins/computers/frontend/views/PCDetail.vue +++ b/plugins/computers/frontend/views/PCDetail.vue @@ -39,6 +39,7 @@ long for a narrow column and it wrapped into a tall ragged stack. Full width in the normal flow costs one short row. -->
+

{{ computer.assetnumber }}

@@ -70,6 +71,27 @@
+ +
+
+ Status + + {{ liveness.label }} + +
+
+ +
+
+
+ + - @@ -181,7 +212,7 @@ />
-
+
- -
-

Status

-
-
- Logged In User - {{ computer.computer?.loggedinuser || '-' }} -
-
- Last Reported - {{ computer.computer?.lastreporteddate ? formatDate(computer.computer.lastreporteddate) : 'Never' }} -
-
- Last Boot - {{ computer.computer?.lastboottime ? formatDate(computer.computer.lastboottime) : '-' }} -
-
-
+