AI Discovery ran a separate model-backed service scanner beside Patrol
and the Assistant: its own store, scheduler, model route, API, tool and
drawer UI. No telemetry counter ever measured its use (the telemetry
discovery_enabled flag OR'd it with network subnet discovery), the
maintainer judged it a flop, and the context it produced is already
available to the models through pulse_query, pulse_read and the
unified-resource context sections.
This deletes internal/servicediscovery, internal/ai/infradiscovery, the
pulse_discovery tool, the /api/discovery routes, the mock Discovery
fixtures, the Discovery tabs in the resource, guest, node and Docker host
drawers, the identified-service cards, readiness badges, provenance
markers, suggested web-interface URLs, the Workloads AI Context column,
the chat "Discovery is off" hint and the AI Service Context settings page.
The old settings route redirects to Assistant settings. The model-bound
sanitizer allow-list existed only to pass Discovery context through, so it
goes too, along with the CopyableCodeRow primitive that only the
Discovery tab used.
Network subnet discovery (/api/discover, the Infrastructure discovery
settings) and the discoveryTarget agent coordinates are separate features
and stay. Stored discovery_enabled, discovery_interval_hours and
discovery_model AI settings are ignored on load; the settings API decoder
is lenient, so older frontends saving those fields still succeed.
canonical-platform-audit.mjs still carried 18 rules and 15 allowlist
entries for modules deleted between 2026-04-28 and 2026-07-10, mostly
as orphans of removed pages: the PMG presenters and ServiceHealthBadge,
deployStatus and deployFlow, infrastructureEmptyState, the AI quickstart,
explore and session-diff presenters, clusterEndpoint and
configuredNodeCapability, the Ceph summary card and ZFS health map
presenters, problemResource, and the storageSourceOptions, resourceBadges
and Settings reportingResourceTypes component shims. Fourteen messages
told authors "Use @/<module> instead" for a file that does not exist, a
fifteenth pointed at the deleted PMG ServiceHealthBadge, and three more
banned imports from shims the type check already rejects.
Applying all 18 regexes to every .ts and .tsx under src, tests and
allowlisted modules included (2,458 files), finds three matches: the
not.toContain assertions in frontendResourceTypeBoundaries.test.ts that
keep three old PMG strings out of ProxmoxMailGatewayDrawer. Those tests
stay. Over the 8,310 tracked text files the only other matches are the
rules' own regex text in this script. A probe that plants one forbidden
string per rule reports all 18, so the empty result is not a blind scan.
I looked for a live owner to point each message at instead of deleting
the rule, and found none. The PMG drawer and table, the Ceph table and
drawer, the cluster-agent deploy dialog and the cluster members section
all survive, but each keeps its own copy and styling inline, so there is
no module a message could name, and none of their code has the shape a
pattern matches. cephSummaryPresentation.ts is live but derives cluster
data and holds no styling.
canonical-pmg/no-local-service-health-badge could not match real code at
all. Its regex ends in ">\b", which needs a word character glued to the
">", but a closing type-argument ">" in valid TypeScript is followed by
a space, "=" or similar. The line it was written for (MailGateway.tsx:80
at e71916f246, "...}> = (props) => {") does not match it.
With the 15 allowlist entries gone those paths are scanned like any
other. A planted "const niceAxisMax = (" at each of the 15 passes under
the old script, which exempts those paths, and is reported 15 times by
the new one; a control copy at a path outside the allowlist is flagged
by both. The audit passes on the clean tree before and after.
Nothing else reads these rule ids: no test pins them and no contract
describes them. frontend-primitives.md and patrol-intelligence.md name
this script only for the locale and Patrol rules. The change is script
only, so no runtime output changes and the browser verification guard
lists no changed paths.
Contract-Neutral: Only residual guard demand is the platform support manifest frontend projection proof that the canonical-platform-audit.mjs path policy names (resource.test.ts, sourcePlatformOptions.test.ts, sourcePlatforms.test.ts). This commit deletes 18 dead audit rules and 15 allowlist entries for deleted modules and leaves the projection sync check untouched, so editing those tests would be fabricated proof. No subsystem contract describes the removed rules (frontend-primitives.md and patrol-intelligence.md name this script only for the locale and Patrol rules), so there is no contract text to change.
Reconcile PR2751's removed Terms consumer with current main. Inspect actual surviving JSX for the shared primitive, shipped Terms route and opener safety, with whole-removal and unsafe-link controls. Keep all surviving fixed consumers and runtime source unchanged.
Contract-Neutral: Test and consumer-registry reconciliation only; no runtime, route, documentation destination or shared external-link safety contract changes.
Change-source: pulse-maintainer
Merge exact upstream 7f466fc183 without re-parenting its reviewed history. Retain both independent contract sections in the two documentation conflicts; no frontend resolution or runtime correction.
Change-source: pulse-maintainer
canonical-platform-audit.mjs still carried eight canonical-recovery/*
rules and eight allowlist entries for presenter modules that went away
with the aggregate Recovery page: recoveryActionPresentation,
recoveryEmptyStatePresentation, recoveryRecordPresentation,
recoveryFilterChipPresentation, recoveryIssuePresentation,
recoverySummaryPresentation, recoveryStatusPresentation and
recoveryTablePresentation (deleted in ae65c5e9a2, c40455806f and
c26665a63a on 2026-05-26 and 7d6b447c59 on 2026-05-14). Each rule told
authors "Use @/utils/<module> instead", naming a file that does not
exist, so a hit could not be fixed the way the message said.
The rules matched Recovery page code. protectedStaleOnly(),
ISSUE_RAIL_CLASS, RECOVERY_TIME_RANGE_LABELS, rollupSubjectLabel,
groupHeaderRowClass and the "No protected items yet" copy were each last
changed by 66fb198cc6, which deleted that page. Applying all eight
regexes to every .ts and .tsx file under src, tests and allowlisted
modules included (2,413 files), finds no match. Over the 8,026 tracked
text files in the repo the only matches are the empty-state rule's own
regex text in this script. A probe that plants one forbidden string per
rule reports all eight, so the empty result is not a blind scan.
With the eight allowlist entries gone the audit scans those paths like
any other. A planted "const niceAxisMax = (" at each of the eight paths
passes under the old script and is reported eight times by the new one.
The audit passes on the clean tree before and after.
The date, timeline, timeline chart and outcome rules stay, with their
three allowlist entries: those presenter modules still exist and the
messages point at them. Fifteen other allowlist entries name modules
that are also gone (PMG, AI, deploy and a few more); they belong to
those surfaces' own cleanups.
Nothing else reads these rule ids: no test pins them and no contract
describes them. frontend-primitives.md and patrol-intelligence.md name
this script only for the locale and Patrol rules. The change is script
only, so no runtime output changes and the browser verification guard
lists no changed paths.
Contract-Neutral: Only residual guard demand is the platform support manifest frontend projection proof that the canonical-platform-audit.mjs path policy names (resource.test.ts, sourcePlatformOptions.test.ts, sourcePlatforms.test.ts). This commit deletes eight dead canonical-recovery audit rules and eight allowlist entries for deleted modules and leaves the projection sync check untouched, so editing those tests would be fabricated proof. No subsystem contract describes the removed rules (frontend-primitives.md and patrol-intelligence.md name this script only for the locale and Patrol rules), so there is no contract text to change.
The aggregate Recovery page and its rollup, facet and series hooks went on
2026-05-26, and #2728 removed the recovery query serializer. Several
recovery helpers kept exports that nothing in the shipped app reads. A
TypeScript-compiler export graph from src/index.tsx finds no reachable
importer of any of them, and git log -S dates when each lost its reader:
- recoveryPlatformModel: the rollup normalizers and getRecoveryRollupPlatforms
(last reader useRecoveryRollups.ts, 2026-05-26).
- recoveryTimelinePresentation: the tooltip-row, point-total, day-filter and
column aria-label builders (the page's timeline, 2026-05-26). The tooltip
rows were the only reader of recoveryArtifactModePresentation.ts, so that
module, its test and the audit rule that pointed page code at it go too.
- recoveryTimelineChartPresentation: the range-group class and bar
min-width helper (unread since the timeline changes of 2026-03-29).
- recoveryOutcomePresentation: RECOVERY_OUTCOMES (dashboard retirement,
2026-04-29) and the bar and text class helpers.
- recoveryDatePresentation: the date-search resolver and its normaliser,
and formatRecoveryTimeOnly (2026-05-26).
- types/recovery: the rollup, series and facet types.
The TrueNAS Protection tab also fell back to display.subjectLabel,
display.subjectType and subjectRef for labels, sorting and search. Its
points come only from useRecoveryPoints, which runs every
/api/recovery/points response through normalizeRecoveryPointsResponse.
That decode folds those fields onto display.itemLabel, display.itemType
and itemRef and drops the originals, so the fallbacks never fired. The
subject names now live on RecoveryPointTransport and
RecoveryPointDisplayTransport only, which turns any future read of them
off a normalized point into a type error instead of a silent no-op.
The points handler sends the item label only as display.subjectLabel, so
that fold is what names TrueNAS rows. recoveryTransport.test.ts now drives
backend-shaped points through useRecoveryPoints into the TrueNAS sort and
search, and pins the type split with expectTypeOf. Breaking the fold fails
the runtime test; re-adding subjectRef to RecoveryPoint fails the type check.
Tests that pinned the subject fallbacks now pin the itemLabel and itemRef
arms they stood beside. Coverage of the surviving modules is unchanged:
matched by normalised source text, no statement or branch covered before
is uncovered after.
canonical-platform-audit.mjs loses the artifact-mode rule and allowlist
entry, and the formatTimeOnly, range-group and bar min-width alternatives
of two recovery rules, since the helpers they pointed at are gone. A planted
niceAxisMax, labelEvery or tick-label tint string still trips the retained
alternatives.
The shipped JavaScript changes only by the removed code. Of 189 dist files,
93 are byte-identical and 93 differ only in chunk-hash references and SRI
strings; the TrueNAS chunk loses the datasetLabel subject terms, the
truenasPageModel chunk loses the subject search and sort terms, and the CSS
loses four rules no source uses any more (bg-gray-400 and three min-w
classes). The bundle-size check passes with no chunk renamed. Before and
after builds driven by one browser script show the same TrueNAS Protection
rows, search, filters and detail panel and the same Proxmox Backups chart,
tooltip and day selection at desktop and phone width.
registry.json, storage-recovery.md, frontend-primitives.md and
unified-resources.md drop the deleted module and describe the decode
boundary as it now stands; two storage-recovery and unified-resources
sentences that still described rollup, facet and series reads in the
frontend are corrected.
Contract-Neutral: Only residual guard demand is the platform support manifest frontend projection proof that the canonical-platform-audit.mjs path policy names (resource.test.ts, sourcePlatformOptions.test.ts, sourcePlatforms.test.ts). This commit deletes one recovery audit rule, one allowlist entry and retired alternatives in two other recovery rules, and leaves the projection sync check untouched, so editing those tests would be fabricated proof. storage-recovery, frontend-primitives and unified-resources carry substantive notes, and recoveryTransport.test.ts and frontendResourceTypeBoundaries.test.ts are the staged recovery-product-surface proofs.
Additively incorporate exact observed upstream a9f23501eb following successful bounded refresh 7a6fff3c87105d65e8e5cb5b45e82d27bffdedd05ff2c846e086aa27f2daee21. Retain starting reviewed tip d6a0244ede and both earlier full-SHA compositions. This batch remains deferred for its outstanding canonical, browser and security evidence; no source validation or release readiness is inferred.
Change-source: pulse-maintainer
The Workloads View disclosure rendered `ChartVisibilityToggleButton`
only inside `<Show when={props.onChartsToggle}>`, and no surface passes
`onChartsToggle` or `chartsCollapsed` since the workload summary chart
section was deleted with the summary components. The button had no other
production consumer.
This removes:
- the `Summary` block and import in `WorkloadsFilter.tsx`, and the
`chartsCollapsed` / `onChartsToggle` props in `workloadsFilterModel.ts`
- `ChartVisibilityToggleButton` and its icon import in
`shared/FilterToolbar.tsx`
- the `chart-visibility-toggle-button` rule and
`chart-visibility-local-toggle-labels` pattern guard in
`scripts/shared-template-registry.json`
- the unit tests for the button and the WorkloadsFilter charts describe
The guardrail case that required the primitive now keeps the exact
retired registry ids, export and props out. It deliberately does not
block unrelated future chart controls: a returning summary section is a
product decision that should register its own primitive.
Contract prose: `frontend-primitives.md` (View-disclosure list, the
FilterToolbar rule, the registry paragraph, the FilterBar view-options
list), `performance-and-scalability.md` (the workload chart-visibility
sentence) and `unified-resources.md` rule 12 now say no chart show/hide
control exists instead of describing the deleted one.
Verification:
- frontend `src/features`, `src/components/shared`,
`src/components/Workloads`, `src/utils` suites on pulse-dev: 783 files
passed, one load-induced 5s timeout in `AgentsMachinesTable.test.tsx`
that passes alone (32/32) and does not reference these symbols; tsc,
prettier and the shared-template audit pass
- browser: production builds of this change and of main's three runtime
files, served against the 50-node mock rig, Proxmox and vSphere
overviews at 1440x900 and 390x844 (phone via Filters > View). The View
preferences disclosure shows identical headings, controls and pressed
states before and after, with no Summary heading, no Charts button, no
horizontal overflow and no console errors. The after bundle no longer
carries the `Show charts` copy.
Additively incorporate exact upstream main 533027ee06 without rewriting accepted maintenance. Resolve the storage-recovery contract insertion by retaining both the reviewed silent-source History fence and upstream SAML/method constraints. No runtime conflict resolution is introduced; final-content evidence and exact-head checks remain required before publication.
Change-source: pulse-maintainer
WorkloadsFilter rendered ChartVisibilityToggleButton only when a surface
passed onChartsToggle, and none has since the workload summary chart
section was deleted with the summary components. The button had no other
production consumer, so the shared primitive, its two shared-template
registry entries, the filter-model props and the tests that pinned them
described a Show/Hide charts control that nobody could reach.
Delete them. The guardrail that required the primitive now keeps the
exact retired registry ids, export and props out: a returning summary
section is a product decision that should register its own control.
Contract prose in frontend-primitives, performance-and-scalability and
unified-resources now says no chart show/hide control exists instead of
describing the deleted one.
No visible change: the Workloads View disclosure on the Proxmox and
vSphere overviews shows the same sections and controls before and after,
at desktop and phone widths.
Below the lg breakpoint, the inline drawer's Identity section rendered
alias and IP chips as hand-rolled nowrap spans inside a fixed-layout
table cell. A generated alias such as
proxmox-lxc-docker:Production West:pve1:113 or
k8s-production-1:deployment:redis-ffcd-b17c78f012 is wider than that
value cell on a phone, so the cell's hidden overflow and the drawer's
clipping content shell cut it off with no ellipsis. On a 390px mock
estate 53 chips across Docker and Kubernetes drawers were cut. The part
that tells two aliases apart sits at the end, and a phone has no hover
to show the title.
MetadataBadge owns read-only chip whitespace handling, so it gains an
opt-in wrap mode that caps the chip at its container and breaks inside
a token only where no space or hyphen fits. The drawer's IP, alias and
alias-count chips now compose MetadataBadge, and the identity values
wrap in full. The default stays nowrap, so existing badge consumers are
unchanged, and desktop drawers still show each alias on one line.
The shared template registry now requires ResourceDetailSummary to
compose MetadataBadge and forbids the old hand-rolled chip classes
there. A mobile integration check opens a Docker container drawer and
fails if any Identity-section element or text crosses the detail cell,
table wrapper or drawer shell; it fails on the old code and passes with
this change.
Additively merge upstream 1ac600ec2b without changing accepted commit identities. Compose the shared filesystem threshold resolver with the retained explicit host and linked-node override precedence and canonical metric release; extend the existing regression across configuration save and next report. Keep upstream live-config ownership and Patrol cleanup, alongside all reviewed monitoring work.
Change-source: pulse-maintainer
usePatrolIntelligenceState() still returned summaryStats, a closure that
counted critical, warning and active Patrol findings plus fixed outcomes.
Its only reader was PatrolIntelligenceSummary.tsx, deleted in 27ba85ddc6
(2026-06-23). A type-checker scan of the whole program (src and tests)
finds no reference to the member outside the hook's return object, and
nothing spreads, iterates or index-reads the returned state, so the
closure and its return entry go.
The canonical-patrol/no-local-summary-card-presentation rule in
canonical-platform-audit.mjs matched only text shaped like that card's
tint ternaries on summaryStats().criticalFindings, warningFindings and
fixedCount, and told authors to use patrolSummaryPresentation, which no
longer holds any summary-card presentation. With the accessor gone the
rule guards a name nothing can call, so it goes, along with the three
not.toContain guards in frontendResourceTypeBoundaries.test.ts that
pinned the same strings. patrolSummaryPresentation.ts also leaves the
audit allowlist: its remaining recency and verification presenters trip
no rule, and without the entry the audit scans the module like any other
(a planted "Loading messages..." string is now reported).
The JavaScript output changes only by the removed code. Of 187 dist
files, 94 are byte-identical and 92 differ only in chunk-hash references
and SRI strings. In the AIIntelligence chunk, after normalising hashes
and short identifiers, the only residual is the summaryStats closure and
its entry in the returned state. The bundle-size check passes. The Patrol
Inbox, Activity and Protection tabs render without console errors at
desktop and phone widths.
The contracts now record that the hook derives no client-side severity
tally, that the page's open-issue severity counts come from the This week
card's digest read, and that the audit carries no Patrol summary-card
rule. The patrol-intelligence section-owner sentence named a summary
surface that no longer exists; it now names the live tab panels.
Contract-Neutral: Only residual guard demand is the platform support manifest frontend projection proof that the canonical-platform-audit.mjs path policy names (resource.test.ts, sourcePlatformOptions.test.ts, sourcePlatforms.test.ts). This commit deletes a Patrol summary-card rule and one allowlist entry and leaves the projection sync check untouched, so editing those tests would be fabricated proof. patrol-intelligence, api-contracts and frontend-primitives carry substantive notes, and frontendResourceTypeBoundaries.test.ts is the staged patrol page-and-state proof.
Additively integrate published main dcb69cfb02 while retaining reviewed tip 52fe83d266 and every accepted commit. Preserve both subsystem contracts and independent alert tests. Reconcile the reactive row-history helper with reviewed memory provenance and visible freshness cues; exact source, browser and protected checks remain required.
Change-source: pulse-maintainer
PlatformResourceTable was the only renderer of UnifiedResourceTable, and
nothing imports it. Every platform page (Proxmox, Docker, Kubernetes,
TrueNAS, vSphere, Machines) renders its own native tables, and
/infrastructure only shows the opening-workspace placeholder. A
production build of main already tree-shook the whole cluster (none of
its strings ship), yet it still carried 11 source files, 7 test files,
registry ownership in two subsystems and contract rules for a table
nobody sees.
Removed: PlatformResourceTable and its PLATFORM_STATUS_FILTER_OPTIONS;
UnifiedResourceTable with its host, service, PBS and PMG cards, source
badge cell, state hook, viewport sync and models; and
ClusterDeployBanner, which only the host card rendered (the live cluster
deploy flow is the Settings ClusterAgentDeployDialog). Their tests go
with them.
Coverage for live code those tests also exercised is kept: the detail
mapper no-fetch boundary and the online boolean sensor case move to
resourceDetailMappers.test.ts, the ResourceFacetSummary render cases
move to their own test file, and both files become registered proofs for
the policies that govern their subjects. Guardrail tests, the shared
template registry, registry.json, status.json evidence and the
performance, unified-resources, frontend-primitives and cloud-paid
contracts drop the deleted paths and the rules that only governed them.
Rollup used to name the large shared chunk after sharedPlatformPage; it
now names it MetricBar, so the bundle baseline entry is re-keyed at its
measured size.
Preserve published helper filesystem collection and vSphere activity commits. Resolve mirrored helper capability documentation to the upstream implementation account and retain both parents\x27 independent helper regression tests. No frontend source resolution or receipt rebinding.
Change-source: pulse-maintainer
The vSphere Activity tab titled every vCenter event with its class name
("HostConnectedEvent", "HostProfileAppliedEvent"), and its When column
clipped the time on every row at most widths.
Pulse never asked vCenter for the readable message: QueryEvents leaves
out fullFormattedMessage unless the filter sets disableFullMessage, so
on a real vCenter the drawer's Message row was always empty too. The
client now sends disableFullMessage: false, and the provider titles an
event change with that message, falling back to the class. That also
gives Patrol's change context a readable reason. The page model prefers
the message for rows stored before this change, and keeps the class as
an Event type row in the drawer and in search.
When now reads as an age ("1d ago"), as Kubernetes events do, with the
exact time on hover and in the drawer. A vCenter time before 2000 (its
unset marker) still shows as "-". The Activity table moves from the
date-time registry rule to the relative-time rule.
Native runtime verification now needs the real frontend build on Windows as well as Unix. Invoke the already installed Vite CLI with the same Node executable, keeping arguments out of a shell and avoiding npx.cmd executable handling. The asset inputs, build arguments, lock, documentation sync and embed copy are unchanged.
Contract-Neutral: Build command portability only; frontend content, API and agent lifecycle contracts are unchanged.
Change-source: pulse-maintainer
This clears the last shared-template audit findings. Combined with
#2546, the audit then passes.
- **Backup export and import warnings:** the "migration guide" links
were raw `target="_blank"` anchors. They now use `ExternalTextLink`. Its
new `inlineInherit` variant keeps the warning's own amber or yellow
colour and underline, so a link inside a tinted callout sentence doesn't
switch to link blue.
- **External watchdog:** the Refresh and Show/Hide buttons now use the
shared outline `Button` instead of a copied border shell. On desktop
they look and measure the same as on main (26px and 38px). On phones
they become 44px touch targets.
- `BackupTransferDialogs.tsx` is now a registered required consumer of
`ExternalTextLink`, and the guardrail pins the new variant.
Verification:
- Checked live on a mock stack against main, in light and dark mode at
1440px and at 390px.
- The links keep their callout colour, stay inline and open in a new tab
with noopener.
- The receipt is included.
The shared template audit still flagged hand-built controls in two
places: the backup export and import warnings opened the migration
guide through raw new-tab anchors, and the external watchdog section
copied the outline button shell for Refresh and Show/Hide.
The guide links now compose ExternalTextLink. Its new inlineInherit
variant keeps the warning's own colour and underline, so a link inside
a tinted callout sentence does not switch to link blue. The watchdog
buttons compose Button (outline, xs and settingsAction), which keeps
their desktop look and gives them a 44px touch target on phones.
The shared template audit listed Settings, Alerts and Discovery forms
still building their own controls. The Thresholds CPU averaging selects
and the Discovery availability proposal now compose FormSelect (the
proposal's name field takes the shared field classes beside them), and
an availability check's request body composes FormTextarea, which links
its help text and, with sm:min-h-24, finally gets the height its class
asked for on desktop.
FormSelect's default chrome set appearance-none and drew no arrow, so
every default select in Settings read as a text field. A
form-select-arrow utility draws the caret in the padding the class
already reserved, and hands the native arrow back in forced-colors mode.
Two registry entries were wrong: AlertEscalationSection has used
checkboxes instead of a select since its destinations became
per-escalation, and the inline detail row rule matched a test file;
test files are now excluded from it.
Replace the rejected global host-network proposal with the unchanged private namespace on every platform, including native linux-386. Carry successful rows and omitted VMIDs through operation v2 without clearing degraded health or renewing omitted measurements; retain strict complete-only v1 compatibility. Restart the installer-owned helper only after isolated-unit and socket checks, and document the unresolved pmxcfs IPC limitation rather than claiming PVE recovery.
Change-source: pulse-maintainer
The shared-template rule required every TrueNAS table to use the shared
title-case helper. The storage table used it only for the risk-level
pill, which the Health reason replaced, so it no longer formats any raw
provider value and the required-consumer check failed. The scope-wide
guard against a local titleCase helper still covers the file.
Light-mode text on tinted callouts, banners and selection cards drew at
25% alpha: CalloutCard titles and bodies in every tone (the Security
overview's "Recommended hardening steps"), the Alerts thresholds help,
the update-channel card titles, Patrol and Action banners. The Tailwind
v3 config redefined each colour's 900 and 950 shades as 25% alpha so
dark-mode backgrounds could tint, which also faded every light-mode
text-*-900. The Tailwind 4 migration kept that parity by rewriting the
usages to -900/25.
Every light-mode text-*-900/25 and text-*-950/25, hover included, now
uses the solid shade. Dark variants, backgrounds, borders and rings keep
their tint, so dark mode is unchanged.
Contract-Neutral: restores light-mode text colour tokens only; no behaviour, data, route or contract change
GHSA-vfj7-8cjw-p6xm affects every braces release and has no fix, so the
required frontend audit failed with seven high findings on any pull
request that changes the dependency graph. Tailwind CSS 3 (chokidar,
fast-glob, micromatch) and jscpd 4 (@jscpd/finder, fast-glob) were the
only paths in, both dev-only, and no override can remove braces from
fast-glob or micromatch. Tailwind CSS 4.3.3 through @tailwindcss/vite and
jscpd 5.4.0 leave none; npm audit over the full graph reports zero.
The stylesheet keeps the v3 rendering. It pins the v3 palette (inlined,
so opacity modifiers compile to static colors), font stacks and line
heights, keeps v3's cascade order by placing former component and
unlayered rules around the generated utilities, and restores v3 hover on
touch devices, gray-200 default borders, the 3px blue ring, placeholder
color, button cursor, table-cell padding and the space-* and divide-*
child selectors. The 900/950 shades Pulse overrode with 25% alpha are
opaque now and their callers say /25, because v4 multiplies an opacity
modifier into a translucent color instead of replacing it. Opacity
modifiers on var() colors that v3 never generated are removed.
Utility order changed from lexical to numeric, so a few caller overrides
that silently lost in v3 would start to win. Touch targets keep their
authored 44px phone size (the Machines row stays 32px); cosmetic
overrides v3 never applied are removed, and overrides it did apply use
the important suffix.
Contract-Neutral: Tailwind CSS 4 migration for GHSA-vfj7-8cjw-p6xm: utility class renames and stylesheet compatibility, no subsystem contract change
Remove duplicate keyboard and disclosure semantics from static data-table rows across the platform surfaces. Preserve whole-row pointer convenience while leaving focus, accessible naming and expanded state on each existing native disclosure button.
The populated TrueNAS browser check also exposed an invalid combobox role on inline-only search completion, so keep it as a native textbox with aria-autocomplete.
Change-source: pulse-maintainer
Contract-Neutral: Accessibility semantics and regression coverage only; no product or API boundary changed
Any *_SPEC.md, *_PLAN.md, or *_CONTRACT.md under docs/ must now carry a
Status: line (or a ## Status section) in its header, enforced by the
pre-push lint. A spec, plan, or contract is a record of a decision, not
demand; the 2026-09-01 Home wall case showed what happens when a stale
handoff spec reads as a current signal. Subsystem contract markdown is
governed separately and is not scanned.
The commercial cancellation/reactivation e2e test plan gets the one
missing line so the existing set passes.
Offline guard for the pulseapp.io class: any domain-shaped token
containing 'pulse' in published surfaces (root markdown, docs and
mirrors, non-test frontend src and Go source) must be pulserelay.pro,
1mk.app, or a documented placeholder. Catches the invented
security@pulseapp.io / docs.pulseapp.io (10 months as the published
security contact, bouncing every disclosure) and the pulse.app
OpenRouter referer fixed in b1240c6ca. Runs in npm run lint, so the
pre-push hook enforces it.
The previous commit unintentionally flipped the mode to 100755 via a
temp-index cacheinfo; the script is invoked as 'node scripts/...'
everywhere, so keep the non-executable mode it has always had.
Vite injects modulepreload links for the entry chunk's transitive
static import graph, not just its direct imports. The posture check in
check-bundle-size.mjs allowed only direct imports, so a legitimate
future chunk-graph change (a preloaded vendor chunk statically
importing another chunk) would have false-failed the build and invited
a hasty weakening of the check. Compute the allowed preload set as the
static-import closure reachable from the entry instead; lazy route
chunks stay excluded because they are only reachable dynamically.
Re-verified: healthy build passes; injected lazy-chunk preload fails;
rebuilding with preloadDynamicChunks: true fails with 47 lazy-chunk
violations.
The deployment-installability verification policy routed
frontend-modern/vite.config.ts through the dev-runtime orchestration
proof set, all of which exercise the unbuilt hot-dev runtime. No
accepted proof could observe production build output, which is why
c4af728c0 (preload posture change) needed
PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT.
Split vite.config.ts into its own frontend-build-output path policy:
the accepted set keeps every dev-runtime proof, so dev-server-facing
edits are unchanged, and adds
frontend-modern/scripts/check-bundle-size.mjs, which now also asserts
the built index.html posture the contract clause pins: modulepreload
links limited to the entry's static imports (no lazy route chunks) and
import map integrity coverage of every built JS asset. The guard test
pins the new policy's accepted set.
Verified against the built output: flipping preloadDynamicChunks to
true fails the check with 47 lazy-chunk preload violations; the
healthy build passes. Full canonical-governance chain run locally, all
exit 0.