Commit Graph

3105 Commits

Author SHA1 Message Date
rcourtman c57993adb9 Canonicalize settings dialog close buttons 2026-06-12 17:48:28 +01:00
rcourtman ce2cc8948b Canonicalize compact settings row actions 2026-06-12 17:40:28 +01:00
rcourtman 60f153611b Broaden shared Button settings governance 2026-06-12 17:31:46 +01:00
rcourtman bbdd590a82 Register shared Button primitive governance 2026-06-12 17:13:29 +01:00
rcourtman 6d27522ce4 Broaden shared form select governance 2026-06-12 16:50:40 +01:00
rcourtman 6e81424a59 Register shared status indicator badge governance 2026-06-12 16:35:40 +01:00
rcourtman 9346d72516 Add subtle nested runtime cue 2026-06-12 16:25:08 +01:00
rcourtman 7b251f5651 Register shared toggle governance 2026-06-12 16:15:09 +01:00
rcourtman 4ccadb0b38 Register shared form textarea governance 2026-06-12 16:03:34 +01:00
rcourtman 21c79c898c Scope Docker drawer handoff by host 2026-06-12 15:54:29 +01:00
rcourtman 6ca91d47e6 Register shared form select governance 2026-06-12 15:46:56 +01:00
rcourtman 23e100e4c0 Show nested Docker context in LXC drawers 2026-06-12 15:36:42 +01:00
rcourtman 87e19f5101 Register shared status dot governance 2026-06-12 15:25:25 +01:00
rcourtman b4a3fb868d Register shared search control governance 2026-06-12 15:07:50 +01:00
rcourtman 527514fd85 Register table card shell governance 2026-06-12 14:51:14 +01:00
rcourtman 23e65040cc Register filter button group governance 2026-06-12 14:38:43 +01:00
rcourtman 36af6620d3 Register selection card group governance 2026-06-12 14:24:56 +01:00
rcourtman 52928706d9 Register column picker template governance 2026-06-12 14:17:33 +01:00
rcourtman 40af69bdb8 Register chart visibility toggle governance 2026-06-12 14:11:59 +01:00
rcourtman c5afa2e52f Register grouped table mode control governance 2026-06-12 14:06:19 +01:00
rcourtman 77134632e2 Register platform section tabs governance 2026-06-12 14:01:39 +01:00
rcourtman 51ac42870c Canonicalize platform error states 2026-06-12 13:52:53 +01:00
rcourtman 0109361296 Register platform loading template governance 2026-06-12 13:38:05 +01:00
rcourtman f90ae48814 Register FilterBar template governance 2026-06-12 13:29:25 +01:00
rcourtman 1e634709a2 Canonicalize platform table empty states 2026-06-12 13:08:40 +01:00
rcourtman 97bb7035d9 Canonicalize inline detail table rows 2026-06-12 12:55:56 +01:00
rcourtman 43bb0184ba Canonicalize node header web interface links 2026-06-12 12:31:31 +01:00
rcourtman ddb5bd5e1a Canonicalize Proxmox web interface links 2026-06-12 12:22:20 +01:00
rcourtman 2169d94b41 Canonicalize platform row detail toggles 2026-06-12 12:11:50 +01:00
rcourtman b179b1782c Stabilize Proxmox guest Docker inventory 2026-06-12 12:10:54 +01:00
rcourtman d221640086 Retire platform table frame allowlist 2026-06-12 11:36:18 +01:00
rcourtman 6dc931aac6 Migrate host tables onto PlatformTableShell 2026-06-12 11:07:14 +01:00
rcourtman eab067171c Clarify Proxmox Docker LXC host setup 2026-06-12 10:46:38 +01:00
rcourtman 048db1c91c Enforce shared template registry for platform tables 2026-06-12 10:44:11 +01:00
rcourtman 93420753aa Enforce shared templates for web interface links
Refs #1276
2026-06-12 10:06:01 +01:00
rcourtman 841100caa7 Clarify Proxmox LXC Docker install path 2026-06-12 09:57:58 +01:00
rcourtman ff5a0b4957 memory(cache): extend the reclaimable split to standalone host agents
f62f35e24 restored the v5 used | cache | free memory split for Proxmox
nodes and guests, but standalone host agents still reported a flat
used/free pair, so the Machines page memory bar could not show the
reclaimable segment. Flagged by the Machines page v5 parity audit.

- Host agent reports cacheBytes (gopsutil Available minus Free); the
  ZFS ARC adjustment recomputes free so used + cache + free still
  covers the total.
- ApplyHostReport maps the field into models.Memory.Cache and clamps
  inconsistent or older-agent reports so used + cache never exceeds
  total.
- AgentMemoryMeta carries cache onto unified resources so the frontend
  agent payload exposes it.
- Mock generic hosts split a third of non-used pages as cache, and the
  node-linked host conversion now holds the invariant instead of
  stacking the node's cache on top of a recomputed free.
- Contracts: monitoring, unified-resources, and storage-recovery now
  document the split (also covering the f62f35e24 node/guest surface,
  which landed without contract deltas).
2026-06-11 21:47:30 +01:00
rcourtman d38b8a6d00 Preserve Docker container metadata in unified resources 2026-06-11 21:03:14 +01:00
rcourtman 0b065f2103 storage(pools): demote the Source column, fix PBS host labels, tighten rows to 32px
Three more findings from the v5-parity audit of the pools table, all
mirroring the disks-table fixes in 809e2c900:

- The Source column rendered an identical PVE badge on every row: the
  surface's only live embedding (the Proxmox page) forces
  source=proxmox-pve, so the column was pure noise. Drop it and surface
  the record's platform as a Source row in the expansion's Configuration
  card; the FilterBar source chip still covers unforced embeddings, and
  persisted sort-by-source selections fold back to priority.
- PVE-configured PBS storage rows showed raw agent/storage resource ids
  in the Host column because the backup-repository host-label chain
  skipped the owning node and fell through to parentId. Name-bearing
  candidates (parent name, PBS instance name, PVE node, storage nodes)
  now come before raw ids.
- Rows tightened from 38px to the 32px platform table standard, and the
  freed Source width redistributed across Storage/State/Type/Host/Usage.

Also drops the row model's never-rendered platformLabel/platformToneClass/
compactImpact/compactIssue/compactIssueSummary fields - the same
extracted-but-unrendered drift flagged in #1471.

Verified live in mock mode: headers Storage/State/Type/Host/Protection/
Usage/Growth, backup-vault hosts show pve1-3, expansion shows Source PVE,
rows measure 32px.
2026-06-11 19:25:43 +01:00
rcourtman 8ce0f4a872 Show context-window usage in the last-turn summary (OpenCode parity)
OpenCode shows how full the model's context is next to the composer;
Pulse showed raw token counts with no sense of scale. The done event
now carries context_limit_tokens (providers.ContextWindowTokens for
the selected route) and the drawer's last-turn summary renders input
tokens as a percentage of the window: '8,500 tokens (6% of context)',
with the full breakdown in the hover detail. Without the limit the
summary renders exactly as before. Pinned in the plain-answer corpus
scenario.
2026-06-11 17:18:42 +01:00
rcourtman e6250fe697 storage(pools): collapse duplicate Ceph rows and lift cluster health onto the mount
Every Ceph-backed storage appeared twice on the pools table: once as the
PVE storage entry (online, mounted capacity) and once as the
cluster-internal pool row synthesized from Ceph telemetry (degraded, raw
pool accounting) - same name, conflicting state and usage, no visual
relationship. Found by the v5-parity audit; v5 rendered one row with a
cluster-health badge.

consolidateCephClusterPoolRecords now collapses the synthesized pool
rows (type ceph on the cluster pseudo-node) into the PVE rows that
mount them, matched by backing-pool detail or name. The pool row's
worse health is lifted onto the survivor (state cell, status detail,
issue summary) so a degraded cluster stays visible on the table, and
unmounted pool rows are kept so standalone-monitored clusters do not
lose their only capacity row. Raw per-pool accounting stays on the
Ceph tab's cluster drawer. Registry synthesis and pool alerting are
untouched.

Also pins the zfsSummary devices field in useStoragePoolDetailModel's
test, missed by b53e1a3e0.
2026-06-11 17:14:43 +01:00
rcourtman da10d9a5dd Mark the post-tool status handoff so silent reasoning never shows a stale tool status
After a tool round, the next model turn can reason server-side for tens
of seconds without streaming a single event. The footer kept the last
tool's status counting up ('Reading inventory. (40s)') while the model
was already composing the answer — observed live on the OpenRouter
reasoning route. OpenCode always shows a thinking state here; Pulse now
emits a model_processing workflow state at the start of every post-tool
turn ('Working on the response with the gathered results.'), which
model_thinking upgrades when reasoning deltas actually arrive.

Pinned by a new interaction-quality corpus scenario.
2026-06-11 16:52:02 +01:00
rcourtman b53e1a3e0e storage(pools): carry the full ZFS pool payload to the storage surface
The unified-resource adapter flattened models.ZFSPool to four scalars
(state + R/W/C totals), so scan status and the per-device report never
reached the frontend. The storage detail's ZFS card, the usage bar's
scrub/resilver overlay and ZFS tooltip, and ZFS-device-to-disk matching
were all dead code on the unified path: a degraded pool rendered as a
one-word State cell with no way to see which device failed or that a
resilver was running. Found by the v5-parity audit of the pools table
(same pipeline disease as #1471 on the disks table).

- Add StorageMeta.ZFSPool and populate it in resourceFromStorage
  (normalized copy, not aliased).
- Serialize it in monitorStoragePlatformData so the /api/state path
  carries it alongside the existing flat scalars.
- Map it through normalizeStorageMeta and the storage record adapter
  (meta first, flat platformData fallback).
- Render the per-device report (name, vdev type, state, R/W/C errors,
  message) in the detail's ZFS Pool card - the v6 home for what v5
  showed via the health-map dots and the degraded-pool warning row.
- Pin the end-to-end path in code_standards_test and update the
  unified-resources, monitoring, storage-recovery, and
  frontend-primitives contracts.

Verified live in mock mode: degraded pool now shows the resilver scan,
pool error totals, per-device states, and the bar pulse overlay.
2026-06-11 16:48:57 +01:00
rcourtman 70a20f6ac7 Generate session titles from the first exchange (OpenCode parity)
Session titles were the truncated first prompt, so the picker filled
with 'Reply with exactly: OK' and five identical 'hows esphome' rows.
OpenCode background-generates a short title after the first exchange;
Pulse now does the equivalent: once the done event has reached the
client, the chat runtime asks the configured chat model for a <=50-char
title from the first user message and reply, then renames the session.

- Runs only on the first exchange; any customized title is left alone,
  with a re-check before persisting to lose races against a concurrent
  rename. Failure or timeout keeps the placeholder.
- Title requests pass the model-boundary sanitizer like normal turns
  and record usage as assistant_session_title.
- Synchronous after done (no user-visible latency, no goroutine racing
  service teardown), bounded at 20s for slow reasoning routes.

Verified by tests end-to-end (hook through Rename); live verification
on a real provider route pending while the dev backend sits in mock
assistant mode, which intercepts turns before the agentic path.
2026-06-11 16:21:58 +01:00
rcourtman d26f9da709 Flag pre-rc.6 SSH sensor setups and surface a re-run-setup notice
Nodes that set up SSH temperature monitoring before v6.0.0-rc.6 have an
authorized_keys entry locked to `sensors -j`, so the pulse-sensors wrapper
never runs and SMART (SATA/SAS) disk temperatures silently never arrive;
only NVMe shows via kernel hwmon (what the #1471 reporters hit). The server
already detected the legacy payload but only logged it.

- models.Temperature gains LegacySensorsFormat, set by parseSensorsJSON
  when the payload is raw sensors -j, and preserved across the host-agent
  merge; it rides the existing temperatureDetails projection on unified
  node resources.
- New PlatformOutdatedSensorSetupNotice on the Proxmox page, data-gated:
  collection succeeded AND payload was legacy AND the node has SATA/SAS
  disks without temperatures, so nodes whose disk temps arrive via a host
  agent stay clean. Copy points at re-running the node setup script, which
  replaces the locked key entry and installs the wrapper.
- monitoring.md contract extended with the legacy-format marker behavior.
2026-06-11 16:00:25 +01:00
rcourtman ab01e56db0 Make session listing O(changed files), not O(all transcript bytes)
GET /api/ai/sessions fully re-parsed every session transcript on every
call, roughly three times over (handoff refresh List, per-session action
loads, final List). On a long-lived install the endpoint took tens of
seconds, pegged the CPU, and the dev watchdog kill-looped the backend.

- SessionStore.List() now serves summaries from a per-file cache
  validated by (modTime, size); only changed files are re-read, with a
  raw-skip decode that never deep-parses message payloads.
- The cache persists as .sessions_index.json next to the session files,
  so the first list after a restart is index-speed too (18ms on a 662MB
  legacy directory vs 23s before).
- writeSession/Delete keep cache and index coherent; external edits
  self-heal via the (modTime, size) check.
- ListSessions refreshes handoff action status only for the 20 newest
  sessions that carry actions, matching what the recent-sessions UI can
  surface; the send path already refreshes the active session on load.
- ai-runtime contract updated: listing-cost clause and bounded-refresh
  clause replace the unbounded refresh promise.
2026-06-11 14:11:17 +01:00
rcourtman f9b10316b8 fix(licensing): surface unreadable persisted v5 licenses and retry failed startup exchanges
Two silent-downgrade paths in the v5→v6 migration are now visible and
self-healing:

License load/decrypt failure (was: one log line, no UI state): when
license.enc exists but cannot be read, getTenantComponents now persists
a terminal commercial_migration state with reason
persisted_license_unreadable, so the licence panel and global banner
tell the customer to re-enter their v5 key instead of leaving them to
discover missing Pro features.

Failed startup exchange (was: once per process, Community until manual
restart): exchange failures classified as pending — license-server
blips, DNS failures, rate limits — now schedule a background retry loop
with backoff (30s → 30m cap) that re-attempts the exchange until it
succeeds, hits a terminal classification, or the org's service activates
through another path. The loop stops cleanly on manual activation and
StopAllBackgroundLoops.

Registers the new files in the subsystem registry (cloud-paid
commercial-migration verification policy + shared ownership) and pins
both behaviors in the cloud-paid and api-contracts contracts.
2026-06-11 08:44:35 +01:00
rcourtman cce1b3c783 fix(licensing): decrypt v5.0.0-era license.enc sealed with raw machine-id material
v5.0.0 derived the license.enc AES key as sha256("pulse-license-" +
material) where material was the raw /etc/machine-id file content —
trailing newline included — with hostname and a fixed dev fallback as
alternates. v6 trims machine-id before hashing and dropped both
fallbacks, so licenses activated on v5.0.0 (before .license-key shipped
in v5.0.1) could never be decrypted and the December 2025 lifetime
cohort silently booted as Community.

Collect the raw v5 key materials at construction and try them verbatim
as legacy sha256 candidates during decrypt, alongside the existing
trimmed HKDF/legacy derivations. The write path is unchanged: persistent
random key + HKDF only. Pins the cloud-paid persistence contract to the
full v5.0.x compatibility-loader material set.
2026-06-11 08:36:11 +01:00
rcourtman e8455e84b7 fix(resources): pin canonical host IDs to durable identity so they survive restarts
Canonical IDs for merged-source hosts (PVE node + pulse-agent) were minted
from whichever identity keys the creating record happened to carry: the
agent record knows the machine ID, the Proxmox node record only knows
cluster+hostname. The registry rebuilds from scratch every tick, so a boot
window where the agent had not checked in yet minted a cluster-keyed ID
(agent-7a62... for delly) while steady state minted a machine-keyed one
(agent-bdd4...). Every restart re-ran the race, fragmenting the
resource_changes journal into per-boot eras (9.4k vs 6.1k rows for the
same host) and silently truncating report availability and UI timelines.

Fix, in the layer that owns identity:
- Persist identity pins (canonical_id <-> machine_id/dmi/cluster/hostname)
  in the previously schema-only resource_identities table, written by the
  store-backed registry after monitor-adapter rebuilds, diff-aware so
  steady-state ticks cost no writes.
- Complete weak incoming identities from the pins before matching and ID
  derivation, so a node-only boot window derives the same machine-keyed
  canonical ID as steady state. Derivation itself is unchanged; ephemeral
  nil-store registries behave exactly as before.
- Expand change-journal reads (Get/Count families, SQLite and memory) to
  the full era set recomputed from the pinned identity keys, healing
  historical journals at query time with no row migration. Reads keyed by
  a stale era ID resolve to the same merged timeline.

Regression tests cover both ingest orders, restart simulation via the
monitor adapter, era ID derivation, and era-merged journal reads on both
store implementations. Contracts updated: unified-resources obligation 25
(durable identity pins), monitoring obligation 10 (adapter rebuild
persistence).
2026-06-11 08:33:00 +01:00
rcourtman 3a51429a58 fix(licensing): honest installation-limit copy and v6 connectivity disclosure
Docs and frontend half of the licensing-policy client work from the v6
upgrade sweep (license findings 5 and 6); the Go classifier and grant
refresh fix land separately once the shared pre-commit guard unblocks.

A 409 MAX_INSTALLATIONS during v5 key migration rendered as 'another v6
activation handoff is still settling. Retry activation from this
instance.' Retrying can never succeed until a slot is freed server-side,
so that copy stranded users. The notice now says the key is already
active on its maximum number of v6 installations and points at
support@pulserelay.pro to free a slot, via the new
exchange_installation_limit reason and free_installation_slot action.

docs/UPGRADE_v6.md gains a 'Breaking Change: Paid Licensing Requires
Connectivity' section: v5 validated keys fully offline, v6 needs
periodic connectivity to license.pulserelay.pro, with roughly 10 days of
offline tolerance (72 hour grant plus 7 day grace) before paid features
drop to Community, plus air-gap guidance and the migrated-key
installation cap. docs/releases/V6_CHANGELOG.md gains the matching
breaking-change bullet.

Note: the two frontend files also carry two small additive hunks from
the parallel persisted-license-unreadable slice (a copy case and its
test, inert until its Go side lands); they share these files and are
included to avoid splitting them mid-flight.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-11 08:32:07 +01:00