The alert-links spec pinned Open related links into the retired
standalone routes, which were removed together with those routes'
cross-link affordances; its stub timestamps had also aged out of the
history window. The stubbed TrueNAS alert now uses fresh timestamps, the
Resource action is scoped to its own row (live mock alerts share the
page), and the contract asserts the surviving canonical handoff: the
resource incidents panel scoped to the alerting resource.
The disk-history contract lived on the retired /storage route with REST
stubs the platform pages never read. It now drives the TrueNAS storage
section against the mock dataset: the SMART-failing sdc disk opens the
resource drawer, and the History tab must request the disk series from
the metrics store under a canonical key (serial when available, the
disk:<node>:<device> composite otherwise - the store serves both, which
is what protects serial-less disks).
GrantClaims and Claims gain a MaxUsers seat limit mirroring MaxGuests:
the grant JWT claim is copied at the activation bridge and surfaced
through EffectiveLimits()["max_users"], which the already-shipped
user-limit enforcement (MaxUsersLimitFromLicense) reads unchanged.
Inert by default: no plan sets max_users yet, so absent claim = 0 =
unlimited for every existing license and grant. The self-hosted
commercial volume scrub strips only max_monitored_systems and
max_guests; tests now pin that max_users survives it from both the
named field and explicit limits, plus the end-to-end proof that a grant
carrying max_users=3 enforces 3 and a grant without it stays unlimited.
Contract shape recorded in cloud-paid Extension Point 26; the grant
wire contract list gains max_users against the relay-server reference
(pulse-pro 95a18bd).
The thresholds surface replaced the neutral infrastructure/systems/
containers groupings with per-platform scopes, so the TrueNAS routing
contract inverted by design: TrueNAS systems, pools, datasets, and disks
now live under their own scope button. The spec asserts that scope's
sections against the mock dataset instead of the retired neutral pages.
The TrueNAS storage-links spec asserted cross-links from the retired
/infrastructure card into the retired /workloads, /storage, and /recovery
routes, and the VMware source-filter spec asserted the retired
/infrastructure source dropdown; platform-first navigation replaced both
concepts with the per-platform pages and their section navigation, which
the platform-pages shell spec covers.
The PBS active-task and job-health evidence drawer content this spec
pins still exists in ResourceDetailDrawerOverviewTab, but the
platform-first rework left it unreachable: proxmoxPageModel routes PBS
resources only into the Backups summary servers table with no expansion,
no UnifiedResourceTable lists the resource, and neither the workloads
search nor the command palette can reach it. That is a dropped product
surface, not spec rot, so the spec stays as the contract and skips via
fixme until the drill-in returns.
Platform pages render websocket state, so the offline-node guard's REST
stubs never reached the surface it was checking; it also targeted the
retired /infrastructure route. The mock scenario already forces pve5
(Disaster Recovery B) offline deterministically, which is the exact
state the guard exists for: the spec now asserts on /proxmox that the
offline node stays listed with dashed live metrics while keeping its
web-interface link and detail expansion.
TierBusiness: Pro's feature set, 365-day history retention, uncapped
core monitoring like every self-hosted tier, Business display name, and
a slot in the min-tier ordering. Differentiation is by max_users limit,
retention, and support rather than features (multi-user is already a Pro
capability via RBAC; see specs/pricing-segmentation.md binder findings).
Dormant until the license server issues business plan versions; no
checkout, pricing, or UI surface references it yet. Contract shape
recorded as cloud-paid Extension Point 26 and pinned by
TestBusinessTierContractShape.
1c38d7993 replaced 17-recovery-layout.spec.ts with
17-proxmox-backups-layout.spec.ts but the storage-recovery registry
entry still named the old file, which broke the registry audit for
every subsequent governance-audited commit.
The upgrade-return spec asserted the pre-redesign panel: the plan
comparison summary paragraph, Compare plans link, activation summary
copy, and purchase-state URLs that survive verbatim. The current panel
presents plan cards with View plans links, the activation summary hands
off to Patrol mode selection, purchase-state params are consumed into
notices and normalized out of the URL, and a failed activation lands
directly on the recovery deep-link with the disclosure open. Six of the
seven flows are green against that reality; the unavailable-handoff flow
is marked fixme because it exposes a real defect (rel=noopener defeats
the original-tab reopen and the popup fallback strands users on /),
tracked for a governed product fix together with the still-legacy
checkout redirect paths.
Completes the approved trial flow end to end. The License panel's plan
comparison gains a 'Start 14-day free Pro trial' action (shown only when
unlicensed, trial-eligible, and never-trialed) that rides the existing
purchase-start handoff with trial=1; the handler already forwards
non-reserved params. Pulse Account portal reads the trial arrival flag,
relabels the pro monthly checkout button, shows the card-required note,
and sends trial:true to the checkout session endpoint (server support
deployed in pulse-pro 4c3a28c). Organic portal and public pricing
arrivals see no trial anywhere, per the no-upsell doctrine; retired
local trial-acquisition routes remain 404. Portal dist rebuilt.
The billing plan panel no longer renders a monitored-system capacity
review block; capacity truth surfaces at the point of adding systems
through the connection dialogs' impact previews, which the consolidated
workspace specs pin. The spec keeps its surviving guard: self-hosted
billing shows no upgrade pressure while monitored-system usage is
unavailable.
Specs 54 and 55 never authenticated; they only ever passed against an
already-authenticated dev session, so on the e2e stack every test landed
on the login screen before reaching the billing panel. Entry helpers now
establish a session first. Both specs still carry assertions against the
pre-redesign billing copy (upgrade arrival, capacity review wording);
that rework is tracked separately - this lands the auth prerequisite that
any version of those assertions needs.
The recovery layout spec pinned the retired standalone /recovery surface
down to its testids and the /api/recovery/series endpoint, none of which
exist anymore; backup activity lives in the Proxmox Backups section now.
The replacement guards the current surface against the same regression
class: picking an activity day narrows the backups table in place, a
year-long range renders its 365 bars without pushing the page into
horizontal overflow, and the PBS servers table keeps its trailing column
inside its wrapper on the default desktop column set.
The license-activation journey (env-gated, skipped without activation
credentials) still asserted the retired Assistant & Patrol heading on
/settings/system-ai; the panel renders Provider & Models now, per
settingsHeaderMeta. Copy-only alignment; the journey itself still needs
a live activation environment to run.
The provider-setup spec asserted the retired Assistant & Patrol shell:
old heading, old enable button, the old setup dialog names, and a Model
Overrides section that no longer renders (per-section overrides moved to
the Patrol, Assistant, and Service Context panels). The contracts are
re-pinned on the current flow: first enable opens the Set up Pulse
Intelligence dialog and submits provider credentials with no hardcoded
model, saved Patrol readiness warnings keep their provider and model
context, and a rejected save through Save provider settings keeps the
preflight recommendation in the failure alert.
The spec asserted the pre-rework AI settings shell (Assistant & Patrol
heading, Enable Assistant and Patrol button, and the retired setup
dialog). The durable contract survives on the current panel: pressing
Enable Pulse Intelligence without a configured provider issues no
settings update, no quickstart copy appears anywhere, and a direct
enable via the API still fails without resurrecting quickstart state.
Commit 0a9a29d63 moved deploy-demo-server.yml and update-demo-server.yml
from the static TS_AUTHKEY to the Tailscale OAuth client but left the
release promotion policy pin asserting TS_AUTHKEY, so the Canonical
Governance run failed at the promotion policy unit tests once the
completion guard false positive (fixed in 54a6118d1) stopped masking
the step. The pin now asserts the OAuth client id and secret and pins
the static key retirement.
The disclosure specs pinned pre-rework copy and routes: the telemetry
summary and PRIVACY.md were rewritten (one outbound usage-data scope),
chat action controls moved to the Assistant panel where the autonomous
option is gated on runtime autonomy (stubbed to the unblocked capability
set, since the e2e stack runs the community runtime), and the billing
Terms of Service link sits inside the collapsed manual key recovery
disclosure. With the .md content-type fix the shipped docs now render in
the popup, so the open-doc assertions exercise real navigation again.
The embedded frontend file server had no content type for .md, so every
in-app "Full details" / "Terms of Service" / security-guide link to
/docs/*.md answered application/octet-stream and the browser downloaded
the file instead of showing it. Shipped docs now serve as
text/plain; charset=utf-8 and open readable in the tab the app targets.
The guard judged substantive contract updates by diffing HEAD against
the index. In CI nothing is staged, the index equals HEAD, so every
contract file piped in via --files-from-stdin looked unchanged and the
guard blocked compliant pushes. Concretely, run 28944317805 blocked
7645965af even though its deployment-installability.md addition sits
inside the Current State section.
The guard now accepts --diff-base <ref> (requires --files-from-stdin),
resolves it to its merge base with HEAD so the comparison anchor
matches the three-dot changed-file list, and compares base vs HEAD
contract texts in that mode. Pre-commit keeps the index comparison.
The canonical-governance workflow passes the push or PR range base.
The header-framing spec pinned four retired standalone routes; platform
pages intentionally carry no page header, so the contract now covers the
utility surfaces that still render the shared PageHeader (alerts overview,
settings general, and Patrol, whose description follows the current copy),
and the vertical-alignment check pins alerts against Patrol.
Five Patrol tests drove the retired Configure Patrol dialog and the
"Automation:" status strip, both removed with the monitor-first workbench,
so they could only time out on the dead affordances. The durable contracts
are re-pinned where the behavior lives now: a rejected Patrol settings save
surfaces the server reason from the settings panel, a save that returns a
not-ready Patrol model surfaces the readiness blocker with provider and
model context, and the Patrol mode group stays clamped to Watch only with
the Pro modes disabled when the runtime lacks autonomy, even when the
server reports a stale full autonomy level. The scoped-trigger strip
wording tests had no surviving surface; workbench presentation is covered
by the monitor-first workbench spec.
The weekly release-dry-run schedule failed at 'Resolve rehearsal
metadata' because GitHub does not apply workflow_dispatch input
defaults to schedule events, so rollback_version arrived empty and
resolve_release_promotion.py rejected the run.
Scheduled runs now pass --derive-rollback-latest-stable, which fills
an empty rollback_version with the latest stable tag preceding the
rehearsal version (currently v6.0.4 for 6.0.5-rc.3). Manual dispatches
keep the explicit rollback_version requirement; the stale prefilled
5.1.29 default is removed so operators state the target themselves.
The deployment-installability contract records the scoped scheduled
exception.
Commit 346ba81ed rewrote the per-platform connection specs against the
consolidated Connected systems workspace but left the old spec paths in
RA20 evidence and two registry.json proof lists, failing release_ready.
Point them at the renamed workspace specs.
The per-platform connection workspaces under
/settings/infrastructure/platforms/* are retired compatibility paths;
connections now live in the consolidated Connected systems workspace, whose
table reads the unified /api/connections endpoint and whose add dialogs
still speak the unchanged per-platform REST APIs. The TrueNAS and VMware
specs are rewritten against that surface: table listing from
/api/connections, the add dialog's draft Test connection payload, the
monitored-system impact preview copy from monitoredSystemPresentation, the
capacity-denial alert (the server explanation now surfaces as a
notification while the dialog stays open), and the structured
unsupported-vCenter draft-test guidance, which kept its testid and copy in
the ConnectionEditor.
The demo-boundary spec's settings heading follows the same rename
(Infrastructure Operations -> Infrastructure); its remaining retired-route
references are tracked for the demo-contract pass.
On mobile viewports the settings navigation sits behind the Settings
drawer trigger rather than a persistent sidebar, and panel descriptions
are desktop-only copy (hidden sm:block). The spec now opens the drawer
before asserting the shared navigation and search on mobile projects and
scopes the description assertion to desktop, instead of failing on a
sidebar mobile intentionally does not show.
Commit 005100432 retired the unified-route e2e specs but left two
status.json evidence entries pointing at deleted files, failing
repo_ready and RA20. Re-point them at the surviving coverage the
retirement commit named: refresh resilience via the ported Proxmox
workloads stability spec, and TrueNAS workload surfaces via the
platform pages shell spec.
The landing page announced v6 GA but the repo README (the highest-traffic
owned surface, 6.1k stars) never did, and its platform list predated
vSphere support. Adds a v6 callout linking the v5 upgrade guide.
The perf spec measured Infrastructure/Workloads tab transitions, both
retired with the platform-first navigation, so it could only ever time out
looking for the removed infrastructure-page hook. It now measures
Proxmox/Docker primary-tab switches with per-platform ready signals and
keeps the same 2200ms default budgets (medians run 300-600ms on the local
stack). Budget env overrides follow the new direction names.
The standalone /infrastructure, /workloads, /storage, and /recovery routes
were deliberately retired with the platform-first navigation (abb6f86ae);
these specs existed to pin those routes' filter, handoff, and scoped-routing
contracts, so there is no platform-first surface for them to assert. The
subjects that do survive the rework are covered elsewhere: workload drawers
and filters by the platform-page and embedded-workloads specs, and refresh
resilience by the workloads stability spec being ported to the Proxmox page.
The settings shell copy moved (General, Billing & Usage, Provider & Models
and their descriptions); spec 15 now mirrors settingsHeaderMeta instead of
pre-rework titles. The first-run agent handoff pre-provisions the scoped
install token, so spec 11 asserts that contract rather than a Generate
token button, exempts the app shell's Patrol open-work polling from the
no-AI-bootstrap guard (it powers the Patrol nav badge on every route),
treats navigation-aborted auth probes as benign console noise, and targets
/settings/support/reporting directly now that the /operations alias is
retired. Spec 12 scopes migration-notice assertions to the settings content
because the global banner legitimately renders the same copy.
Specs 36/54/55 hardcoded the hot-dev URL (127.0.0.1:5173), which does not
exist in the dockerized stack or CI; they now resolve the shared runtime
base URL. Spec 36 also never authenticated (it only ever ran against an
already-authenticated dev session) and pinned fixture timestamps that aged
out of the history view's default period window.
The visual spec injects an inline stylesheet, which the nonce CSP active
outside hot-dev blocks, so it now runs with bypassCSP. The commercial
cancellation specs require a pre-provisioned live-Stripe fixture bundle
(test clocks, customer and subscription ids) that no CI environment
carries; they skip with the missing variables named instead of failing.
The first-session specs depend on /api/security/dev/reset-first-run, which
is gated on development mode; the test stack never set PULSE_DEV, so every
reset attempt answered 403 in both CI and local compose runs. The only other
effects of the flag in this stack are the localhost AllowedOrigins default
and env-gated dev toggles that remain off.
Playwright storage states persist cookies and localStorage only, while the
primary-API-token flow authenticates through sessionStorage. Storage states
captured after token auth were silently unauthenticated, so whole spec files
landed on the login screen whenever the token path happened to win. Storage
states now always come from the cookie-backed password login, cached once
per run and probed before reuse.
Explicit bearer/X-API-Token requests now go through a cookie-less request
context: the backend intentionally CSRF-rejects mutations that carry a
session cookie without a CSRF token even when the Authorization header is
valid, so token-scope coverage could never pass from a logged-in page.
Session-semantics specs use the new ensureSessionAuthenticated instead of
racing the token path.
First-run resets now refuse the implicitly resolved hot-dev runtime (the pid
fallback with no explicit base URL and no harness-written runtime state);
an ad-hoc spec run wiped the live dev backend's auth twice through that
path. Resets can also recover an instance stuck unauthenticated mid-reset by
re-reading the rotated bootstrap token through docker exec, and login()
retries through the backend's 10-per-minute login limiter instead of failing
on burst runs.
Renders UpdateBanner with a Pro runtime identity and asserts the in-app
apply affordance is gone (even when the plan reports canAutoUpdate) while the
Private Release Access portal link and archive/.sshsig steps render; a
community binary still gets the Apply button. Guards the Guard-2 downgrade fix.
The in-app updater and install.sh both fetch the public community build from
github.com/rcourtman/Pulse. On non-Docker Pro installs (systemd, proxmoxve,
source) the "Apply Update" button was live, so applying an update replaced the
separately compiled Pro binary with community and silently stripped Audit,
RBAC, Reporting, and SSO from a paying customer. Docker was already blocked;
nothing else was.
Add a dependency-free pkg/edition marker (defaults to community) that the Pro
binary flips via enterpriseruntime.Initialize, mirroring the existing
coreaudit.SetLogger / server.SetBusinessHooks registration seam. ApplyUpdate
now refuses when the edition is Pro, pointing at the Private Release Access
portal (https://pulserelay.pro/download.html) and the install.sh --archive
path. The gate keys off the compiled binary, not license state: a community
binary with an active license still self-updates as before.
The update banner hides the in-app apply affordance and shows portal
instructions for the Pro runtime, keyed off the existing runtime-identity
signal (runtime.build) rather than a new payload field, so nothing extra is
plumbed through the version API and the frontend reuses the canonical
"which binary am I" contract. The backend gate is the hard guarantee; the
banner is the UX layer.
Guard 2 of the Pro download/update experience spec.
The Kubernetes workloads, services, and configuration tabs kept their
shared-toolbar search and status in component-local signals, so the saved
views shipped with the Docker containers work could never capture a -term
exclusion. The shared toolbar now reads search (q) and status from the URL
with replace-style writes, mirroring the Docker containers table, and the
namespace facet key moves onto new KUBERNETES_QUERY_PARAMS constants. The
shared Workloads surface already URL-backs q/status in
useWorkloadsControlsState; a new test pins that behaviour instead.
Resetting filters now clears every param in one setSearchParams write.
Consecutive writes each merge against the pre-navigation URL because the
router commits inside an async transition, so the Docker containers
clear-all (search, status, host as three writes) resurrected the params the
first writes had just cleared; it now issues a single combined write too.
New tests cover the Kubernetes URL filters end to end and the single-write
reset on both platforms.
Mobile users could not save, apply, or set default views on any
savedViewsKey surface because SavedViewsMenu only rendered in the
desktop controls row. Render it in the mobile chip row, show that row
when a surface has saved views even with zero menu filters, drop the
duplicate Reset the wider chip row introduced, and left-anchor the
dropdown below the sm breakpoint so it stays on screen at 375px.
The public demo wrote ~110K resource_changes rows/day (restart 60K/day,
state_transition 45K/day), making the Changes timeline unreadable and
keeping unified_resources.db churning. Four generator-level engines,
all verified with before/after soaks against scratch mock backends:
- Per-tick flap probabilities ran 43,200x/day on the 2s update loop
(docker restart p=0.01/tick alone is ~430 restarts/day/container).
Churn rates are now expressed as events per day per entity and
converted per tick against the configured interval, tuned to a few
fleet-wide events per day with dwell times long enough to see.
- The pod scheduling reconciler fought the per-tick scenario re-pin
and fabricated a fresh random StartTime on every recovery; derived
uptime moved backwards, which change emission records as a restart.
The reconciler is now idempotent: stable per-pod park/reschedule
choices, StartTime never regenerated, and only pods it parked
itself (NodeNotReady/ClusterOffline/NodeLost) get recovered, so
curated Pending and ImagePullBackOff stories stay put.
- Swarm cluster objects were fabricated per host under one shared
cluster key, so registry dedupe alternated between the divergent
candidates every poll (service renames, status flips, node
re-parenting). One leader manager now reports services, tasks,
secrets, configs and the node inventory, like a real control plane.
- Demo docker host profiles cycled 2 hostnames across 4 online hosts,
collapsing canonical identities, and scripted-offline hosts had
their sighting refreshed right at the 2 minute staleness threshold,
sawtoothing them online/offline. Four distinct host profiles now
exist and offline hosts keep a stably stale sighting.
Before/after soak with a live client: pre-fix ~110-160 rows/min
sustained (restart ~45/min, matching the droplet's 60K/day); post-fix
zero rows/min at steady state with the curated degraded stories
(CrashLoop payments-worker, ImagePullBackOff, offline hosts) intact.
In a multi-manager swarm every manager reports the same cluster-scoped
objects (services, nodes, secrets, configs), and the registry dedupe
picked the candidate whose host had the freshest LastSeen. That ordering
flips between polls, so the winner alternated: swarm node resources
re-parented to whichever manager won (ParentID tracks the reporting
host), and services flipped name/status/updateStatus whenever the
managers' views differed slightly. Every rebuild then wrote phantom
relationship_change / state_transition / config_update rows into
resource_changes, the same class of churn as the registry-rebuild spam
fixed in 74131e56e. Mock mode had the same symptom and was fixed by
pinning a single reporting leader, but real deployments still hit this.
Select winners deterministically instead: richer candidate fields first
(unchanged intent), then managers with an available control plane, and
freshness only when the gap exceeds the docker stale threshold, i.e.
the losing manager has genuinely gone quiet; otherwise the lowest host
ID wins as a stable tiebreak. Equal candidates can no longer alternate.
Regression test rebuilds the registry across polls with two managers,
jittered LastSeen ordering, flipped snapshot host order, and slightly
divergent service views, asserting no change emission; it fails against
the old LastSeen-ordering rule with the exact swarm-node re-parenting
seen in production.
Support requests keep landing for a way to hide noisy containers (one-shot
jobs, helpers) from the Docker dashboard. Free-text search across the
platform tables now understands -term exclusions via a shared
splitSearchExclusions helper in searchQuery.ts, wired into the Docker,
Kubernetes, generic platform, and Workloads search predicates. Workloads
keeps its comma OR semantics; exclusions apply globally.
The Docker containers table now URL-backs its search (q) and status params
alongside the existing host param, so saved views capture the whole filter
state and a default view with exclusions keeps unwanted containers hidden
on every visit. Search tips on the containers toolbar document the syntax.
Proving run 28925348032 showed a shard can still hit the 45-minute job
timeout while the suite carries many real failures: shard 4 held the
visual crawl (5.3 minutes per attempt, currently failing) plus dozens of
14-second failing attempts, and with two retries the failure storm
outran the budget even under the 20-failure cap.
Retry once instead of twice on CI, and run the visual crawl only on the
desktop project. The mobile emulation projects keep their dedicated
mobile specs; a per-project 5-minute crawl was the single most expensive
line in the suite.
Refs #1515
The install command is generated under the Settings infrastructure
installer, not a "Settings > Agents" tab. Correct the agent log, installer
warning, and Machines tooltip to say "the Pulse UI" so the recovery step is
accurate.
Refs #1515
When an upgraded or restored Pulse server no longer recognises an agent's
API token, the report endpoint returns 401. The agent buffered and retried
that report forever with only a generic warning, and the server kept the
node green at its last known agent version because a Proxmox node stays
online via the PVE API poll even after its agent dies.
- Agent: special-case 401 on /api/agents/agent/report. Drop the report
instead of buffering it and log a throttled, actionable error pointing the
operator at the install command to mint a fresh token.
- Installer: verify_agent_server_registration now tells a rejected token
(401/403) apart from "agent has not reported yet" and prints the recovery
steps at install time instead of a vague soft warning.
- Status layer: resourceFromHost flags a stale agent (its host marked
offline by the staleness evaluator) and carries the agent's own last
report time. Coalescing keeps a node online via the PVE source but the
dead agent stays flagged, so its version is no longer presented as current.
The Machines table renders such versions as "(stale)".
Agents push to main every few minutes and a sharded run takes about 30,
so cancel-in-progress meant a busy main could never complete a verdict.
With cancel-in-progress off, the in-flight run finishes and GitHub
collapses queued runs to the newest pending one, so intermediate pushes
still skip without killing the run that is about to report.
contract_patch_has_substantive_change walked git diff --unified=1000
output and tracked the last ## header seen in the patch, so an edit
more than 1000 lines below its section header (e.g. deep inside
unified-resources.md's Current State) lost its section and was wrongly
reported as not substantive, forcing a contract-neutral bypass on
74131e56e.
Replace the patch walk with full-file attribution: diff the HEAD blob
against the staged blob and map every changed line to its section via
a per-line section map built from the complete file. Covers new
contract files (empty HEAD blob) and keeps metadata-only edits
non-substantive. Adds regression tests for edits >1000 lines below
their section header in both directions.
Bump vite ^6.4.2 to ^6.4.3 and pin patched transitives via overrides
(@babel/core ^7.29.6, js-yaml ^4.2.0). Clears 4 Dependabot alerts (2 high,
1 medium, 1 low). npm audit reports 0 vulnerabilities; npm ci, vite build,
vitest (6830 tests), and bundlesize all green.
Also gitignore stray pnpm artifacts. npm is canonical here (CI and the
production Dockerfile both use npm ci from package-lock.json); nothing
consumes pnpm. The unused pnpm-lock.yaml and pnpm-workspace.yaml were
removed separately this cycle and only ever produced Dependabot noise (13
alerts). Ignoring them stops the pair creeping back in, as happened after
the earlier cac5be2ca removal.