Commit Graph
13 Commits
Author SHA1 Message Date
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