Plan 87 phase C. A compose app on slots runs each slot as its own
project, <name>-a / <name>-b, rendered from the merged compose config:
container_name stripped, only the web service published, on the
slot's loopback port, and every named volume pinned to the name it has
today. Without the pin compose prefixes volumes with the project name
and slot B would start on empty ones; the real-Docker leg checks this.
Stateful services (a database, cache or queue with a named volume)
cannot run twice. They move once, on an explicit previewed split, into
a shared <name>-data project that both slots reach by the same names.
The webhook, template-update and rollback paths deploy through the
engine instead of stopping the stack. The --scale web=2 rolling
restart and the Webhook.zero_downtime flag are deleted: it assumed a
service named web, ignored its own health result, and no UI set it.
Compose slot images get immutable per-deployment tags and are pruned
with the rest.
Plan 87 phase D. Both slots share the database, so what changes it
runs once, before the switch, while the old release still serves:
the app's managed databases are dumped (a warning, never a blocker),
then the release command runs in a one-off container of the new
image. A failed release aborts the deploy with the live slot
untouched. The command comes from the app's setting, the Procfile
release: line, or serverkit.yaml; a rollback never re-runs it.
Rolling code back never rolls the database back: after a switch
back the slot card offers the pre-deploy snapshot as an explicit
second step with a data-loss warning. stop_old_before_release is
the announced-downtime fallback for schema changes that cannot stay
backward-compatible for one deploy.
Plan 87 phase F. A slot deploy job lists its real stages (Preflight,
Build, Boot slot X, Health gate, Switch, Watch) and advances through
them as the engine reaches each one, so PipelineStrip renders them
unchanged. The overview gets a slot card: which release is live,
which is the standby and how long it stays warm, with Switch back;
when slots are off it lists why the app does or does not qualify.
Settings gains the opt-in, watch window, standby minutes and the
shared-volume confirmation.
The tab-local title, retention blurb and standalone Refresh button
duplicated chrome the grid already provides (Refresh data lives in the
grid tools menu; per-row 'purges in Xd' carries the retention signal).
The page now leads with the view picker and table like other grids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The split point was clean: git_service.py L602-881 (Gitea install/
lifecycle/config) and 18 of git_bp's 32 routes (lifecycle + repo browsing
via GiteaAPIService) move into builtin-extensions/serverkit-git/backend/,
mounted back at the same /api/v1/git prefix when the extension is
installed. GiteaAPIService moves wholesale (its only consumer).
The deploy pipeline stays core untouched: clone/pull/deploy/webhook-verify
on GitService (all 5 core importers keep every method they call), the
webhook routes including the unauthenticated signature-verified receiver,
and the deployment routes. NginxService's gitea-config helpers stay core
as a deliberate two-speed seam the extension calls.
Upgrade parity: serverkit-git joins GATED_BUILTIN_SLUGS (auto-install only
when the panel-managed serverkit-gitea application exists) and
run_backend_acquisition now sweeps gated slugs too, so a panel that
installed the frontend-only extension picks the backend up once. The core
repo pickers keep three fail-soft getGitea* aliases that 404 into a clean
'not installed' state.
Proving suite test_git_split.py (7 tests); live smoke: builtin install
mounts all 18 routes. Render smoke green for the Git + Security pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds projects/vaults/shared-variables shots and drops services-cards, whose
list/cards toggle no longer exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(ui): one row of chrome per table
A table gets exactly one row of chrome:
[View name v] ....................... [search] [filter] [...]
and nothing else. The row count is gone from it entirely: GridViewPickers
components/PageSkeleton.jsx already owned `.sk-page` for its loading
archetypes, including `gap: $space-4`. The new shell reused the name, so every
page built on PageLayout inherited a 16px flex gap between the top bar and the
content — invisible on padded pages, but on Terminal/Logs it showed as a band
of page background under the bar, which is exactly the "weird space" reported.
The shell is `.sk-pageshell` now; the skeleton keeps `.sk-page`. The smoke test
gained a vgap assertion (content top must equal the bar's bottom edge), which
is what would have caught this in the first place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three detail panels rendered their own `.preview-drawer` — fixed positioning,
their own backdrop, their own slide animation, their own close button — so they
had no focus trap, no escape-to-close, and header chrome that only resembled
the shared drawer's. The Terminal process and service panels and Docker's
container logs panel now use ds/Drawer like everything else.
Left alone deliberately: the File Manager preview (it also renders inline as a
column pane, which a Radix sheet cannot do) and Docker's container inspector
(the `dx-*` workspace has its own language by design).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Git hand-rolled the tab-group frame — `page-container--full-bleed sk-tabgroup`
plus its own `__content`/`__inner` wrappers, duplicated across the loading
branch and the main return. It renders through the shared layout now, so the
frame comes from one place and the loading state matches the loaded page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The console panes were rounded, bordered cards floating inside a `0 16px 16px`
inset — the only page framed that way, which read as stray side padding plus
stray corners against an otherwise flush layout. It now renders through
PageLayout in `fill` mode, like the File Manager: the panes fill the content
region edge-to-edge and keep their internal dividers. Services stays a card
grid, so it re-pads itself instead of running to the edges.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`.sk-drawer__body` is the scroll region and deliberately carries no padding —
some drawers run edge-to-edge — so each body pads its own root. `.cron-form`
did not, leaving every label and input flush against the panel edges. Same
inset as `.cron-drawer` so the two cron drawers line up, plus a note on the DS
Drawer so the next body does not repeat it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Creating a job went through a plain modal with a preset <select> and a bare
cron text field — the schedule was something you had to already know how to
write. It now opens a drawer built on the shared SchedulePicker (presets ·
builder · advanced, with the humanized cadence and the next fire times), plus a
preview of the exact crontab line the form will write.
The drawer owns its own form state, so the page drops the form object, the
reset/open/submit handlers and the /cron/presets fetch that only fed the old
dropdown. The list head gains the section title and the "N of M jobs" count,
with the filter moved to the right where the mock has it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Monitors, a monitor's detail and 90-day uptime strip, Incidents, Events and
Jobs. These double as the render smoke test for the pages this round added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dashboard shots were clipped to `.dashboard-page`, which cropped the
sidebar out and made them the only images in the set without the app chrome
around them — next to Services and Docker in the README it read as a bug.
The clip was there for a reason: `.main-content` owns the scroll, so a plain
shot stops at the viewport and `fullPage` does not reach the bottom widgets.
The fix is a per-section viewport instead — a taller frame captures the whole
board WITH the navigation, and `capture.mjs` now honours `section.viewport`
for any page with the same internal-scroll shape.
Also stops the shots claiming work is in flight. The dashboard fixtures
answered the deploy pill with running jobs, so every capture carried a
"2 deploys running" pill over the sidebar. The pill is correct behaviour; it
just has nothing to do with a screenshot of an idle board, so the fixture now
reports settled jobs rather than the CSS hiding a true statement.
31/31 sections still render.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six new sections cover the pages rebuilt this round — the dashboard as it ships,
its edit mode, the widget library, the widget editor, the databases explorer and
the engine catalog drawer. 31/31 sections render.
The capture is a render smoke test, not just a picture, and it earned that here
by surfacing two real bugs that no test could see:
`SearchField` merges its `className` onto its OWN root, so `.skw-lib__search`
IS the `.sk-searchfield` element rather than a wrapper around one. The rule
styling it as a parent matched nothing, leaving the shared `flex: 1 1 240px`
in force — and inside a column-direction filter bar that 240px basis is a
HEIGHT. The widget library's search box was rendering 240px tall.
`GET /auth/me` answers `{user: {...}}`, but Databases read `.role` straight off
the envelope, so `isAdmin` was always false. Every admin got the read-only
console — and with the DDL builder now shipped, would never have seen the button
that runs a CREATE TABLE.
Also carries `postgresql.yaml`, which I left out of the extensions commit.
Without `engine.extensions: true` stock PostgreSQL is not a host for pgvector at
all; the extension tests passed only because the working tree had the change the
commit did not.
Fixture note for whoever edits these next: `/metrics/history` points are NESTED
(`cpu.percent`, `load['1m']`) and carry no network data, `/servers/available` is
a bare array, and `DeployPill` queries `/deployment-jobs` twice and sums — a
single unfiltered payload makes it claim eight running deploys.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fleet list carried a rail of its own — a health dial, a status nav and a
groups nav — plus a workbar, a command bar, bulk-select and a per-row action
menu. Every number those showed is a column or a filter here, so the page is
one filter row and one table: status segments with counts, a group select,
"4 of 4 servers · 2 online", then Server / Host / Agent / Status / CPU /
Memory / Disk / Last seen. Row actions are gone because ping and delete
already live on the server's own page, which is where rebooting or deleting a
machine belongs.
Offline and pending rows show a dash rather than empty gauges: a 0% bar is a
claim, and those hosts are not reporting.
Add Server was written as a right-side drawer, then pointed at the centred
Dialog primitive while keeping its own `position: fixed; right: 0` geometry.
The primitive's `left: 50%` and -50% transform still applied on top, which is
what rendered the panel half off-screen and clipped. It runs on the shared
Drawer now, so the slide-in, focus trap and overlay come from one place.
`.servers-page { padding: 0 }` was a leftover from the old full-bleed
workspace and was overriding the tab group's padding, running the table under
the right edge of the viewport. Removed, with its layout hook.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The summary band rendered above every section of the group, so "Total
backups" sat on top of Schedules, Storage and Settings, where it answered
nothing you opened those tabs to ask. It belongs to one section, and that
section is now an Overview rather than the archive table.
Overview answers "is my data safe?": protected resources, storage used,
success rate and next scheduled; an 18-week activity heatmap with its failure
days marked; a recent-activity feed; destination bars; latest snapshots. This
needed two endpoints the frontend had never called — /backups/policies and
/backups/runs — which is where the heatmap, the success rate and the
durations come from. A panel that has only ever taken manual backups falls
back to the archive listing instead of showing an empty page.
Schedules gets the mock's columns (frequency, retention, destination, next
fire, an on switch) and only its own action in the top bar; Create Backup was
answering a question nobody on that tab was asking. Snapshots moves its
search to the top bar with every other list page. Storage becomes destination
cards over the provider form.
Two places the mock has data we do not, left out rather than faked: per-policy
size and run-now on Schedules (the schedule API exposes neither), and
duration on Snapshots (filesystem entries do not record it).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fleet Monitor and /monitoring were the same questions at two scopes, so they
were two pages restating each other: both grew an alerts surface and both
grew thresholds. They are now one page with a scope, not two pages with a
gap between them.
The top bar carries a server picker, URL-backed as ?server= so the choice
survives a tab change, a refresh and a pasted link. Overview, Alerts and
Capacity all follow it. The fourth KPI tile is whichever reading the current
scope actually has — load average on the panel host, container count on an
agent — rather than a blank where the other one would be.
The overview is ported from the design mock: KPI band with trends and
sparklines, two trend charts, and a Host health strip below. That strip is
what the standalone heatmap became, except each card is a button that
re-scopes everything above it instead of a cell you can only read.
There is deliberately no second tab strip inside the page. Overview / Alerts
/ Rules / Capacity / Doctor live in the group's own top bar, the way the mock
puts them — a nav under a nav reads as two competing headers.
Also: the sidebar said "Observability" for a page, a route and a title that
all said "Monitoring", and the delivery channels rendered Discord, Slack and
a raw webhook with the same generic glyph. Both fixed.
/fleet-monitor redirects; the Servers tab strip loses the entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Search now lives in the top bar, right-most, on every list page — Services
kept its copy in the table toolbar, and Domains had none at all. The view
switch claims the free space so it stays right-aligned where a page gave its
toolbar search up; where one is still present it hands the margin back.
Domains loses its four-tile KPI strip. Every number on it was already the
count of a segment you would click to act on, so the segments carry them
instead: All 5 / Expiring SSL 1 / Attention 1. Search narrows the set the
counts come from, so they always describe what a click would show.
Fleet Proxy loses its title, subtitle and five-tile band — the tab strip
names the page and every tile re-counted a column of the table beneath it.
What the page is for now lives in the empty state, where it is needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The shared sheet sets padding and a gap on the panel itself, but
.sk-drawer lays out three full-height regions inside it — head, body,
foot — each managing its own padding. The panel's padding therefore inset
all of them: the header's divider stopped short of both edges and the
footer floated above a band of dead space instead of sitting on the
bottom edge.
Zero the panel's padding and gap, and give scrolling to __body alone —
with the panel itself scrollable the footer could not stay put either.
Fixed at .sk-drawer rather than per page, so every drawer gets it: the
template deploy and filter drawers had the same gap.
Measured after the slide-in settles: 0px below the footer, and head/foot
now span the full panel width.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Create WordPress Site" was a centred modal while services and templates
deploy from a right-hand drawer, and its own styling was broken: an icon
stranded on its own line, an unspaced list running straight into the
first field, and a footer that floated over the form.
Two causes, both structural. It rendered legacy .modal-body and
.modal-footer *inside* the shared Modal, which already supplies a body
and takes a footer prop — the chrome was nested twice. And its
.install-warning callout is styled under `.wordpress-page`, while the
dialog renders in a portal, so it is not a descendant of the page that
opened it and none of those rules ever applied.
So the drawer styling moves out of _templates.scss into the design system
as .sk-formdrawer, shared by both pages. That is the fix for the class of
bug, not just this instance: a portalled surface cannot rely on
page-scoped SCSS, so its styles belong somewhere unscoped. The block also
now covers bare <select> and checkbox rows, which otherwise render with
the browser's own white chrome on a dark panel, and the callout.
Verified in a browser: drawer settles at the right edge at its declared
560px, callout renders with its background and spacing, selects pick up
the surface colour, and no .modal-body/.modal-footer/.install-warning
remain in the tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every one of the ~40 loaders went through EmptyState's single hardcoded
shape — an avatar, three cards, three lines — whether the page was a
table, a console, a form or a chart. A skeleton exists to predict the
layout about to arrive so that arrival is a fill-in rather than a jump;
one shape reused everywhere mispredicts on all but the page it was drawn
for, which is worse than honest, because it promises a layout that never
comes.
PageSkeleton provides ten layout archetypes — panel, table, feed, cards,
detail, console, form, chart, tree, split — driven by SCSS so widths stay
responsive instead of being guessed in pixels. Repeated lines use a
deterministic ragged width, so they read as text and don't reshuffle each
render the way Math.random() would.
EmptyState takes `loadingVariant` (defaulting to the old `panel`, so
nothing regresses) and 41 call sites now name their real shape.
ResourceListPage picks table or cards from its own view state.
All ten are on the style guide under Feedback & Loading, and captured as
one screenshot so a regression in any of them is visible in one shot.
Captured bones (npm run capture:skeletons) remain the pixel-exact option
for individual regions; these archetypes are the broad-coverage answer
for the pages that will never be worth capturing one by one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recaptured against the ported design, plus new shots for the templates
catalog, its filter and deploy drawers, and the Services card view.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/deployments still reserved a second grid column for the detail pane plan 51
removed — the table sat at half width with its Started column clipped by
overflow:hidden. Single full-width panel now; pruned ~150 lines of dead
detail-pane SCSS; status badge + progress colors move from inline styles to
modifier classes; kinds humanized; skeleton loading rows; removed the topbar
New Service button duplicating the adjacent tab. Console: fills the viewport
(calc-based min-height), wider step rail (248px), flexible log search, denser
toolbar. Adds deploy-console screenshots (4 sections, mock-data captures).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop docs/screenshots/themes/ from the panel repo; THEMING.md and the
README roadmap now link to the registry's gallery instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One dashboard capture per theme (mock-data rig, capture-themes.mjs) under
docs/screenshots/themes/ with a grouped gallery README, linked from
THEMING.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the logo image at the top of the English and translated READMEs with the new docs/screenshots/poster.png banner, and change the tagline to "Deploy, manage, and monitor servers." (translated per language).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the stray /docs/screenshots ignore that overrode the existing negation (.gitignore:117-118), so the 23 mock-data README screenshots are committed assets again, and add the freshly regenerated set (ServiceDetail header selector updated in the local capture skill).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>