27 Commits
Author SHA1 Message Date
Juan DenisandClaude Fable 5 4fcd0db4d5 chore(frontend): delete the dead useContainerStatus hook and its legacy-poller entry
The hook has zero importers yet was counted in the LEGACY_POLLERS baseline,
holding a phantom slot in the ratchet (a place a new raw setInterval could
hide). Poller debt shrinks 11 sites/10 files -> 10 sites/9 files; the
migration inventory is regenerated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:14:36 -04:00
Juan DenisandClaude Fable 5 5d875d8a09 refactor(scss): status-variant mixin — 174 re-rolled color pairs converge (plan 77 D4)
@mixin status-variant($color, $bg, $border) in _mixins.scss is the one
shape for coloring status-bearing elements; every mechanical
background/color token pair across 40+ page/component partials now
includes it (pass-through args -> identical compiled CSS values, build
green). check-status-scss.mjs holds the raw-pair count at zero and runs
inside npm run lint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:35:28 -04:00
Juan DenisandClaude Fable 5 dc32ac6e86 refactor(frontend): one status->tone vocabulary in ds/status.js (plan 77 D3)
statusKind()/statusVariant()/statusDotClass()/statusLabel() drive both
Pill kinds and Badge variants from ONE table; ~40 per-page maps across
35+ files migrate to it. Scoped overrides stay in the same file:
serviceStatusKind (running IS the healthy steady state for services/
units/previews -> green), processStateVariant, alertStatusKind,
envActionKind, impactTone. Unifications: run-shaped 'running' is cyan
everywhere (Git.jsx's amber gone), QueueDetail/QueueOperations' invalid
blue/yellow kinds fixed, severity tones (critical/high/medium/low)
agree across security surfaces. Ratchet: npm run check:status fails on
new local maps outside ds/status.js (frozen allowlist for domain maps
and D4-territory hex/css-class renderers). Lint + build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 04:49:35 -04:00
Juan DenisandClaude Fable 5 3953090526 feat(conformance): generated remaining-migration inventory (H closure)
docs/MIGRATION_INVENTORY.md is generated from the live censuses and the
frontend boundary checker's new --inventory JSON mode — no hand-typed
numbers, so the plan cannot claim completion while a door is bypassed. A
freshness test fails whenever a ratchet number moves without regenerating
the document, making the regeneration part of every migration commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 00:16:39 -04:00
Juan DenisandClaude Fable 5 39a6e35d42 test(contracts): ratchet the four remaining adoption populations
Backend: hand-shaped {'error': ...} bodies in app/api get an AST census
and ceiling (1159) — milestone B's migrate-when-touched policy made
enforceable. Frontend: check-frontend-boundaries.mjs gains adoption
ceilings for raw api.* calls in pages (460, E1), per-page toast.error
extraction (264, E1), form-group blocks (343, F2), and unencoded query
interpolations in services/api (97, C4-interim) — totals that may only go
down, with a slack guard so a ceiling cannot drift above reality.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 00:13:59 -04:00
Juan DenisandClaude Opus 5 38ba85f07a test(styles): ratchet the colour literals runtime skins cannot reach
Plan 76 milestone G, last item. 151 hex literals live outside the token files
across 35 partials. A theme is a JSON map of CSS custom properties (plan 60), so
only values written as var(--token) change when someone switches skin — every
literal is a spot that stays the default palette on every theme, which is a
functional hole in the themes feature rather than untidiness.

Per-file exact counts rather than a ban, for the reason milestone G gives
explicitly: legitimate brand and data-visualisation colours stay. They just have
to be listed, so the number is honest and can only go down.

Not migrated: picking the right token per site is a semantic judgement (is this
`--green` or `--success`?) with visual consequences, not a substitution — the
same reason .empty-state was ratcheted rather than merged.

Verified non-vacuous: adding one literal to _terminal.scss fails it (20 vs 19),
and it passes again on revert.

Gate: boundary check green, npm run lint 0 errors / 953 warnings, build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:09:50 -04:00
Juan DenisandClaude Opus 5 b182deaff9 test(styles): ratchet class ownership across partials
Plan 76 milestone G. 114 top-level class definitions share 50 names across
partials, so those classes have no owner: every definition applies, merged
per-property in main.scss import order, and editing one file changes only the
properties that file happens to win.

`.empty-state` is the worked example the audit named. Three partials define it —
components/_empty-state.scss (flex column, min-height 200px), _cards.scss (card
background, solid border, radius), _users.scss (padding, colour) — and the
compiled sheet carries five competing top-level rules. What renders takes its
background and *dashed* border from one, its padding and display from another,
and its colour from a third. No author wrote that element.

Ratcheted rather than merged, on purpose. Reproducing the cascade's computed
result has no byte-identical-output proof available (unlike the @keyframes
sweep, where deleting seven dead copies left the CSS unchanged to the byte), so
it needs eyes on the affected pages — plan invariant 9, no repo-wide mechanical
rewrite without behavioural tests.

The ratchet counts DEFINITIONS, not names: counting names would let an
already-shared class spread to a fourth and fifth partial without moving the
number, which is the failure mode worth stopping. Mutation-checked both ways —
adding a definition of an existing shared class and introducing a brand-new
shared name each fail it, and both pass on revert.

Wired into `npm run lint`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:59:28 -04:00
Juan DenisandClaude Opus 5 0b3854824d fix(styles): one definition per @keyframes name
Plan 76 milestone G. A @keyframes name is global and last-definition-wins across
every partial main.scss pulls in, so a duplicate name is not wasted bytes — one
copy is silently dead, and which one depends on import order.

`spin` was defined 8 times. The build already emitted exactly one, so seven were
dead: deleting them left the compiled CSS byte-for-byte identical (same content
hash, 958878 bytes), which is the proof rather than an argument. Three of the
seven also differed — `to`-only rather than `from`/`to` — so anyone editing
those was changing nothing.

`pulse-dot` was the one that actually mattered: components/_uptime defined it
with a scale (1 -> 1.2) and pages/_services defined it opacity-only, and
_services is imported later, so the uptime dot has been running the services
animation. Renamed the uptime one to `uptime-pulse-dot`; service-detail keeps
the shared `pulse-dot` it already got.

**Visual change**: the uptime status dot now scales as its own stylesheet always
said it should. Nothing else moves.

`pulse` (identical twice) and `drawer-fade`/`drawer-slide` (identical in
_docker and _file-manager) are consolidated into base/_utilities.scss, so
neither page owns the other page's animation.

Ratchet: the boundary check now scans every .scss partial and fails on any
@keyframes name defined more than once. 66 unique names, zero duplicates.
Verified non-vacuous: an injected second `spin` fails it, revert passes.

Gate: npm run build clean, npm run lint 0 errors / 953 warnings, boundary check
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:56:55 -04:00
Juan DenisandClaude Opus 5 89c2e13233 refactor(ui): move the last fetching pollers onto usePolling
Plan 76 milestone E2, continuing 0bfcf69c. Ratchet 16 sites / 12 files -> 11 / 10.

Dashboard's remote-metrics poll and its socket-down HTTP fallback, ImportWizard's
active-import poll, and TestSandbox's run poll plus its open-log fan-out.

TestSandbox's log refresher fanned out one request per open log per tick and
returned nothing, so a tick reported done before any of them had come back. It
now returns a single Promise.all, which is what the in-flight guard needs to
mean anything for a fan-out.

That leaves 11 raw setInterval sites, and they are deliberate:

- 5 are clock ticks with nothing to overlap and nothing to fetch — an elapsed
  timer, a countdown, a board refresh pulse, two wall clocks.
- 3 are the socket-fallback pollers in useMetrics, useContainerStatus and
  useDeployJobStream, driven by imperative startPolling()/stopPolling() from
  socket handlers with a 3s grace period. Expressing that as a declarative
  `enabled` is a real restructure of each hook and they only poll when the
  socket is already down.
- 2 belong to the serverkit-gui extension, whose source is the sibling repo;
  its frame loop already carries its own in-flight guard.
- 1 is ServerGui's user-pausable capture interval.

The ratchet keeps all of them at exact counts, so the reasoning above has to be
revisited rather than forgotten if one changes.

Gate: npm run build clean, npm run lint 0 errors / 953 warnings, npm test 72
passed, boundary check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:53:28 -04:00
Juan DenisandClaude Opus 5 0bfcf69c0b refactor(ui): make the two job pollers declarative, and finish the container tabs
Plan 76 milestone E2, continuing 89c3896e. Ratchet 22 sites / 15 files -> 16 / 12.

DoctorPanel's fleet sweep and ScannerTab's malware scan were imperative
pollers: a function started an interval, and the interval cleared itself once
the job reached a terminal status, with a second clearInterval in an unmount
cleanup to catch the case where it never did. Both now hold the job id in state
and let usePolling own the schedule —

    const pollSweep = useCallback((jobId) => setSweepJobId(jobId), []);
    usePolling(tick, SWEEP_POLL_MS, { enabled: Boolean(sweepJobId) });

which is what makes the visibility pause and the in-flight guard apply to them
too. A job poller is the shape that most wants both: it runs while someone
watches a spinner, and its endpoint is doing real work.

Also ContainersTab's stats poll and log tail, and OnboardingWizard's status
poll (its 1s elapsed-time clock stays a plain interval — nothing to overlap).

Two `useRef`s and one `clearInterval` in an unmount cleanup went with them.

Gate: npm run build clean, npm run lint 0 errors / 953 warnings, npm test 72
passed, boundary check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:51:30 -04:00
Juan DenisandClaude Opus 5 89c3896ec4 refactor(ui): move the log-tail pollers onto usePolling
Plan 76 milestone E2, continuing f9657d38. Ratchet 27 sites / 18 files -> 22 / 15.

Terminal's three tab components (file logs, journal, processes), ServicesTab's
unit log tail, and the GUI extension's synthetic desktop. All were the
auto-refresh tail shape: poll only while the toggle is on, never fetch on mount
from the interval effect.

These are the sites where a hidden tab mattered most — a log tail at 3s left
open in a background tab is the single most wasteful poller in the panel, and
every one of them also re-ran on `lineCount`/`priority`/`sort` changes via a
dependency array with `// eslint-disable-line` on it. That silent restart is now
explicit: the tail's own load effects still handle those, the poll handles the
repeat.

Four `intervalRef`s and one `logIntervalRef` went with them.

Gate: npm run build clean, npm run lint 0 errors / 953 warnings, npm test 72
passed, boundary check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:48:31 -04:00
Juan DenisandClaude Opus 5 f9657d38de refactor(ui): move four multi-request pollers onto usePolling
Plan 76 milestone E2, continuing 3ac566cb. Ratchet 31 sites / 22 files -> 27 / 18.

QueueDetail and QueueOperations each fired two requests per tick. Their ticks
now return a single awaited Promise.all, because the in-flight guard only knows
a tick has finished when the promise it was handed settles — a bare `.then()`
inside the callback reports done immediately and lets the next tick start on top
of a request still in flight, which is the exact overlap the hook exists to
prevent.

ServerDetail and LinkPanelForm keep their mount-time load as its own effect;
the poll is `immediate: false` so nothing fetches twice.

Gate: npm run build clean, npm run lint 0 errors / 953 warnings, npm test 72
passed, boundary check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:46:31 -04:00
Juan DenisandClaude Opus 5 1503c493f4 refactor(ui): move ten more pollers onto usePolling
Plan 76 milestone E2, continuing f21abe7a. Poller ratchet 41 sites / 31 files
-> 31 / 22.

Migrated: Jobs, Monitors, MonitorDetail, DeliveryLog, Deployments, Databases,
VulnerabilityTab, and the service-detail Logs/Metrics/Overview tabs.

The trap flagged in the plan bit on four of them, so it is worth restating: the
interval effect's dependency array was doing double duty as the "reload because
the query changed" trigger —

    useEffect(() => {
        load();                                  // <- reload on filter change
        pollRef.current = setInterval(load, POLL_MS);
        return () => clearInterval(pollRef.current);
    }, [isAdmin, load]);                         // <- load() is a useCallback of the filters

usePolling holds the callback in a ref precisely so it does not restart on every
render, which means a naive swap silently drops that reload and nothing fails to
compile. Each of those four keeps an explicit `useEffect(() => load(), [load])`
alongside the poll, with `immediate: false` so mount does not fetch twice.

Six magic interval literals became named constants while their lines were being
touched (LOG_REFRESH_MS, METRICS_REFRESH_MS, SCAN_POLL_MS, DEPLOY_POLL_MS,
ENGINE_POLL_MS).

Not migrated, and not mechanical: useMetrics, useContainerStatus and
useDeployJobStream are socket-fallback pollers driven by imperative
startPolling()/stopPolling() calls from socket event handlers, with a 3s grace
period before falling back. Expressing that as a declarative `enabled` flag is a
real restructure of each hook, and they only poll when the socket is already
down. Left for when they are next touched.

Gate: npm run build clean, npm run lint 0 errors / 953 warnings (one below the
954 baseline — four orphaned useRef imports removed), npm test 72 passed,
boundary check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:44:46 -04:00
Juan DenisandClaude Opus 5 375677ab0c fix(ui): one download helper, and delete the dead wordpress client
Plan 76 milestone F3 (the blob half) plus the C4-interim dead-file deletion the
audit called out.

The six-line "save this to disk" anchor ritual was pasted into 14 components,
and the copies had drifted in two ways that decide whether the download
actually happens:

- only 3 of 14 appended the anchor to the document. Chrome dispatches a click
  on a detached anchor; Firefox has not always done so, which makes "the
  download button does nothing" a browser-specific bug that never reproduces
  for whoever wrote it.
- most revoked the object URL on the very next line after click(). The download
  starts asynchronously, so revoking immediately can cancel it — showing up as
  a flaky empty-or-missing file rather than a clean failure.

utils/downloadBlob.js does both correctly (append, click, remove, revoke on a
later task) and accepts either a string or an existing Blob, which is what
FleetCapacityPanel needs since its CSV arrives from the API already blobbed.

Also deletes src/services/wordpress.js: 494 lines, zero importers, and a stale
duplicate of the live src/services/api/wordpress.js that the ApiService barrel
actually wires in. Verified by import graph and a clean production build.

Ratchet: the boundary check now fails on any `.download =` assignment outside
the helper — a zero baseline, not a shrinking one, since every site is
migrated. Verified non-vacuous: an injected anchor fails it, revert passes.

Gate: npm run build clean, npm run lint 0 errors / 954 warnings (unchanged),
npm test 72 passed, boundary check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:39:36 -04:00
Juan DenisandClaude Opus 5 f21abe7af5 feat(ui): one visibility-aware polling convention, and a ratchet on the rest
Plan 76 milestone E2, drift/bug row 4. 45 hand-rolled setInterval sites across
35 files, with `visibilitychange` handled in exactly one of them and an
in-flight guard in exactly one of them — the documented poller-stampede shape.

Two failures every hand-rolled `setInterval(load, 5000)` shares:

- it starts a request every 5s whether or not the previous one returned. A slow
  endpoint does not slow the poller down, it makes it stack; each extra request
  makes the server slower, which makes the next overlap more likely. This is
  what a DevTools capture of /domains showed as a wall of pending rows.
- it keeps polling a tab nobody is looking at, where browsers throttle timers
  unevenly anyway, so it is not even a reliable refresh.

utils/pollScheduler.js holds the rules as a plain object so they are testable
without a DOM (the repo has no jsdom — this follows formState.js/useForm).
hooks/usePolling.js is the React binding, and useServerQuery gains
`refetchInterval` built on the same scheduler, so there is one implementation
rather than a second one for query users.

Two design points worth stating, since both differ from what the sites did:

- the next tick is armed by the previous run *finishing*, not by setInterval.
  With setInterval the gap is measured tick-to-tick regardless of how long the
  work took, so slow work silently turns a 5s gap into no gap. The in-flight
  guard therefore protects the out-of-band paths — refresh() and the visibility
  catch-up — rather than the schedule, and the test says so instead of implying
  a guard the schedule can no longer exercise.
- the callback is held in a ref, so an inline arrow re-created each render does
  not restart the timer. That is the usual reason a hand-rolled poller runs far
  faster than its stated interval.

Migrated four as reference implementations, per the plan's "migrate a
representative flow before broader adoption": DeployPill (which had worked all
of this out by hand and loses ~20 lines), MonitorsSummary, appdetail/LogsTab,
and ProcessListPanel. MonitorsSummary keeps its `refreshKey` prop as an explicit
out-of-band refresh rather than folding it into the schedule.

Ratchet: check-frontend-boundaries.mjs gains LEGACY_POLLERS with exact per-file
counts, now 41 sites in 31 files, so every future cleanup must shrink it and a
new bare setInterval fails lint. Genuine non-fetching timers (clock ticks,
countdowns) are listed too rather than pattern-matched away — they are harmless
but the exact count is what makes the ratchet trustworthy.

Gate: npm test (72 passed, 11 of them new), npm run lint (0 errors; 954
warnings, one fewer than the 955 baseline), npm run build clean, boundary check
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:45:59 -04:00
Juan DenisandClaude Opus 5 e1a5a5e102 fix(ui): route every copy button through the clipboard door
Plan 76 milestone F3, drift/bug row 5. 33 call sites in 25 files reached for
navigator.clipboard.writeText directly, against 4 files using the shared
copyToClipboard/useClipboard helpers.

This is a fix rather than a tidy-up: navigator.clipboard is undefined in an
insecure context, and ServerKit's SSL is optional by policy, so on an
HTTP-served panel every one of those copy buttons did nothing. The failure was
silent at most sites — a bare fire-and-forget writeText, or a `.catch` that
swallowed. utils/clipboard.js already carries the execCommand fallback that
makes them work; it just had almost no callers.

Two of the bypasses were local functions *named* copyToClipboard
(ServerSettingsTab, Downloads) — the same shadowing shape as the backend's
private get_current_user() helpers, and the reason the call sites read as
though they were already correct. ServerSettingsTab's was a hand-rolled
useClipboard (copy + toast) and now uses the hook; Downloads' is renamed
copyCommand since it also owns per-command copied state.

Behaviour preserved per site, with two deliberate improvements the helper's
contract allows:

- state that was set unconditionally (`copied` flags, keyed copy markers) is
  now set only when the copy actually succeeded
- CreateTableModal's "this browser will not give the page clipboard access"
  notice is gone; the fallback is what it was warning about

`.then(onOk, onErr)` chains became `.then(ok => ...)` because copyToClipboard
resolves false instead of rejecting — the second callback would never have run.

The existing frontend boundary ratchet held an exact per-file legacy baseline;
it is now empty and documented as needing to stay that way, so a new direct
call fails `npm run lint`.

Gate: node scripts/check-frontend-boundaries.mjs (0 clipboard files ratcheted),
npm run lint (0 errors, 955 pre-existing warnings — unchanged), npm test
(61 passed), npm run build (clean).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:42:14 -04:00
Juan Denis aa3407750c refactor(extensions): own remote access styles 2026-08-18 03:14:15 -04:00
Juan Denis 2ee8d9ed42 refactor(frontend): centralize active workspace state 2026-08-18 02:58:20 -04:00
Juan Denis 4c4ccb4a18 refactor(frontend): enforce shared browser boundaries 2026-08-18 02:48:56 -04:00
Juan DenisandClaude Opus 5 a8e9f605e4 fix(skeletons): make bone capture reliable, and cap what it will bake
The capture tool shipped with two targets and had evidently never been
run past them — every one of them failed for reasons the tool could not
report.

Three bugs, in the order they bit:

- login filled the first `input[type="text"]` on the page, which is a 2FA
  digit box rendered above the real form. The credential form stayed
  empty, so every target then timed out waiting for content on a page
  that was still /login. Target #email/#password, and fail loudly rather
  than silently proceeding unauthenticated.

- The API client clears both tokens from localStorage whenever any
  request 401s, and one admin-only endpoint doing so was enough to bounce
  every later target back to /login. The captured tokens are now re-seeded
  before app code runs on each navigation, with a re-auth-and-retry as
  backstop.

- A bare "waitForSelector timeout" hid all of the above. Failures now say
  which cause applies: bounced to /login, root selector absent, or the
  page rendered its empty state.

Targets grew to services and domains alongside ssl and wordpress-list.
/templates is deliberately excluded: its catalog measures ~975 bones over
5700px — a skeleton nobody scrolls, at a node count costing more to
animate than the page costs to load. A MAX_BONES cap now rejects that
class of capture outright, pointing at the PageSkeleton archetypes, and
the target list documents that a page only qualifies once the dev
database reliably has rows for it — bones measured from an empty page
would predict that nothing is coming.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 05:05:39 -04:00
Juan DenisandClaude Opus 4.8 c20a9f14f2 feat(themes): backend model, API, and registry pipeline (plan 60 Phase 2-3)
Themes are data, not code — so the backend is a simple validated-JSON store,
no zips/checksums/permissions.

- Theme model + migration 078_themes
- theme_tokens.py: server-side mirror of the token whitelist + validators
  (the gate: a malicious/malformed theme never reaches the DB)
- theme_service: bundled-seed loading, import/list/delete, panel default,
  pre-auth public/active
- /api/v1/themes: /installed, /import (admin), DELETE (admin), /default
  get+set (admin), unauthenticated /public/active for login/setup, plus
  /registry + /registry/<slug>/install
- theme_registry_service: offline-tolerant registry fetch (TTL + last-good +
  bundled fallback), mirrors the extension registry pattern
- bundled seed themes synced to app/data/themes + themes_index.json
- 18 proving tests (server-side validation sanitizes url()/;/} smuggling,
  admin gate, panel-default → public/active, offline fallback, install flow)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 00:24:00 -04:00
Juan DenisandClaude Opus 4.8 0016b1ff22 feat(themes): theme gallery + runtime skins (plan 60 Phase 1)
A ServerKit theme is data, not code: a validated map of CSS custom-property
tokens. This lands the client-side foundation.

- themeTokens.js: canonical token whitelist, legacy-alias table, and strict
  value validators (reject url()/@/;/}/expression, type-check per group)
- applySkin.js: runtime skin applier via setProperty (injection-safe); dark/
  light resolution with stock-light fallback for dark-only themes
- ThemeContext: per-user skin selection (localStorage), live hover preview,
  and accent precedence workspace > user > skin > default
- 6 bundled seed themes (default/paper/nord-deep/gruvbox/phosphor/high-contrast)
- Theme Gallery in Settings > Appearance (instant apply, 4-swatch cards) +
  command-palette index entry
- hardcoded-hex audit: 3 exact token-value leaks fixed to var(--token)
- check-theme-tokens.mjs proving guard wired into the lint chain

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:55:19 -04:00
Juan DenisandClaude Opus 4.8 8a9aa03d8f chore(frontend): drop unused eslint-disable in capture-skeletons script
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 19:23:59 -04:00
Juan DenisandClaude Opus 4.8 3333ede87b feat(frontend): runtime self-captured skeleton bones (plan 50 phase 3)
Add useSelfCapturedBones(key, {ready}): after a page's real content renders it
snapshots the region into bones (shared src/utils/snapshotBones.js) and caches
them in localStorage under sk-bones:<key>, versioned so stale layouts are purged;
the next load replays a pixel-accurate skeleton. First-ever visit has no cache and
falls back to the composed skeleton — no regression.

SkeletonBoundary now forwards its ref to the measured container, and the SSL page
is wired as the demonstration. The capture script (phase 2) is DRY'd to inject the
same shared snapshotBones source rather than an inline copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 19:20:19 -04:00
Juan DenisandClaude Opus 4.8 67142efd13 feat(frontend): dev-time skeleton bone capture + bones replay (plan 50 phase 2)
Add an accuracy path for skeletons: a dev-only `npm run capture:skeletons` tool
(scripts/capture-skeletons.mjs) drives a headless, logged-in dev session, measures
designated page regions into boneyard-format bones ({x,y,w,h,r}; x/w %, y/h px),
and bakes them to frontend/src/skeletons/*.json as static assets. Nothing browser-
or capture-related ships in the product.

SkeletonBoundary gains an optional `bones` prop that replays a captured layout via
a new `renderBones` mapper — SCSS-classed absolutely-positioned placeholders reusing
the shared .skeleton look (shimmer + reduced-motion). boneyard-js is added as a
devDependency for parity/format reference (never imported by shipped code). Ships a
README runbook and an example.json documenting the format.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 19:15:46 -04:00
Juan DenisandClaude Opus 4.8 e0a47bbb9d feat(palette): settings search — deep-link focus + completeness guard
useSettingFocus rides the useFocusParam rail with a new `setting` kind: a
palette result navigates to /settings/<tab>?focus=setting:<id>, and the target
card scrolls into view and flashes a token-based highlight for ~2s. Wires the
headline Security (2FA card + admin require-2FA policy, now mounted) and
Notifications (email/SMTP, channels, severity, categories, quiet hours) cards.

Adds scripts/check-settings-index.mjs (dependency-free) to the lint step so
every settings tab must carry at least one index entry — a new tab can't ship
invisible to search.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:42:57 -04:00
Juan DenisandClaude Opus 4.8 326bc89679 feat(ui): redesign foundation — tokens, self-hosted fonts, primitives, sidebar
Phase 0 of the ServerKit UI redesign (see docs/REDESIGN_MAP.md):
- Remap design tokens to the new infra-console palette behind existing names; add prototype token names (surface ramp, cyan/violet, accent-bright/-bg). Dark/light/system.
- Self-host IBM Plex (woff2 vendored via scripts/fetch-fonts.mjs); no Google Fonts CDN.
- Runtime-derive the full accent ramp in ThemeContext.
- Add shared design-system primitives in components/ds (Pill, EnvTag, SegControl, MetricCard, Gauge, ServiceTile, Sparkline, AreaChart, ScoreGauge, Feed, Drawer).
- Restyle the sidebar (gradient rail, mono section headers, left accent bar) and switch the brand to the demo's gradient-tile logo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:34:28 -04:00