324 Commits
Author SHA1 Message Date
Juan DenisandClaude Fable 5 5aab23c19c docs(plan-76): completion record — decisions, commits, retained duplication
Names the resolved A and C decisions, the 2026-08-19 commit set, the
intentionally retained semantic duplication with owners and removal
conditions, and the deferred H items. Collection floor 4430 -> 4460.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 00:21:20 -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 1240827720 docs(plan-76): record the remote-execution seam closure
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 23:31:50 -04:00
Juan DenisandClaude Opus 5 5329436147 docs(plan-76): correct the swallow-and-answer-200 count
The recorded 56 was a measurement artefact — the probe counted any return it
could not resolve to a status, including the unexpected_response() calls the
migration had just added. The real count is 1. The remeasurement instead found
a hole in milestone B's ratchet, now closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:13:01 -04:00
Juan DenisandClaude Opus 5 429e036dd1 docs(plan-76): record E2 completion, F3 blob, G, and the A role checks
Also records why the remaining milestone A work (605 @jwt_required routes) is
not a mechanical swap: auth_required() accepts API keys where @jwt_required()
does not, so converting a route expands the authenticated surface and needs a
decision about which routes should be API-key reachable — not just an edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:07:20 -04:00
Juan DenisandClaude Opus 5 afdcaa0f66 docs(plan-76): record the C sniffing ratchet and the second-wave status
Names why the sniffing row is ratcheted rather than migrated: file_service owns
12 of the 23 sites and has eight callers, so converting it to raise ahead of the
envelope decision would leave two contracts on one service — the failure mode
the plan exists to remove.

Adds a status table for the five drift/bug rows and the five ratchets now
holding them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:49:03 -04:00
Juan DenisandClaude Opus 5 245f24dc55 docs(plan-76): record the E2 polling row
Corrects the audit's framing of the fix: the in-flight guard is not what stops
the schedule stacking — arming the next tick from the previous run's completion
is. Also flags the trap for the remaining 41 migrations: several sites use the
interval effect's dependency array as an out-of-band reload trigger, which
usePolling drops silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:46:20 -04:00
Juan DenisandClaude Opus 5 054cc33cdf docs(plan-76): record the milestone B crash-reporting row
Keeps the three ways the audit's measurement was off, and names the 56
swallow-and-answer-200 handlers as still open so B cannot read as finished.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:38:42 -04:00
Juan DenisandClaude Opus 5 ff09dd8fce docs(plan-76): record the F3 clipboard row
Also records the one non-mechanical part of the remaining F3 work: a
.then(onOk, onErr) chain cannot be repointed at copyToClipboard as-is, because
the helper resolves false instead of rejecting and the error callback would
silently stop running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:42:57 -04:00
Juan DenisandClaude Opus 5 9ff957edf7 docs(plan-76): record the milestone A identity/admin execution
Both drift/bug rows of milestone A are closed. The record keeps the four ways
the audit's own measurements were wrong, since the plan's premise is that these
numbers are the baselines everything else ratchets against:

- the API-key failure is a raised RuntimeError (a 500), not a None
- the inline population has a second, larger two-line shape (87 sites)
- a sixth _require_admin() lives in an extension with no installed live copy
- 12 of 19 private helpers were named get_current_user(), shadowing the door

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:34:56 -04:00
Juan DenisandClaude Opus 5 c8258990b7 docs(plan-76): record the measured second-wave adoption debt
Adds the 2026-08-18 duplication-audit table to plan 76: for each door the
first wave built, how much legacy traffic still walks around it, measured
rather than estimated.

The rows marked drift/bug are the reason this is not filed as tidy-up —
they are behaviour differences shipping today, not repetition. Inline
`User.query.get(get_jwt_identity())` returns None for API-key callers
because only rbac checks g.api_key_user; locally-swallowed exceptions skip
the 500 handler and never reach the error tracker; status codes chosen by
sniffing error strings; pollers with no in-flight guard; raw
navigator.clipboard on HTTP-served panels, where SSL being optional by
policy means those copy buttons simply do not work.

Two rules recorded with it: every row gets a ratchet before its migration
starts (a migration without one regrows), and the drift/bug rows are
scheduled like fixes rather than "migrate when touched".

Scope boundary noted so the three plans do not overlap: the services-layer
subprocess/docker/nginx/SQL-exec doors are plan 75 Round 2 (§G); model
mixins, secret handling, status vocabulary, realtime convergence and
shared infra utilities are plan 77.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 12:06:00 -04:00
Juan Denis 6b0144f0dd docs: record convergence execution results 2026-08-18 03:33:36 -04:00
Juan Denis 6a83b876a4 docs: add architecture convergence plan 2026-08-18 02:37:37 -04:00
Juan Denis ea3c9f7267 docs: add documentation map and point at auto-generated API docs
Separate user docs, extension-author docs and internal working docs; note
that Swagger UI at /api/v1/docs is the source of truth over API.md
2026-08-16 18:35:35 -04:00
Juan DenisandClaude Opus 5 0bb7e4ab54 fix(install): derive the real client IP on a stock host install
install.sh wrote no TRUST_PROXY_HEADERS line at all, so the code default (False)
applied while every host install serves the panel through its own nginx. Per-IP
rate limits, the login brute-force throttle and every audit-log entry therefore
keyed on 127.0.0.1, which quietly weakened all three -- one abusive client shared
a bucket with everyone else. .env.example claimed true, so the two disagreed and
it looked configured.

Trusting X-Forwarded-For is only safe when nothing can reach gunicorn without
traversing a proxy we control, so the switch follows that condition rather than
assuming it. On a host install it holds: render_service_unit binds gunicorn to
127.0.0.1 and bootstrap_firewall opens SSH + 80/443 + PANEL_PORT and never the
backend port. backend_bind_host() is now the single source of truth for both the
systemd unit and the trust decision so the two cannot drift, and
backend_is_loopback_only() gates the setting.

- stock install: TRUST_PROXY_HEADERS=true, TRUSTED_PROXY_HOPS=1
- SERVERKIT_EXTERNAL_PROXY=1: true with 2 hops, unchanged
- SERVERKIT_BIND_HOST not loopback: false plus a warning. The port is directly
  reachable there, so the header is attacker-chosen, and a forged IP is worse
  than a wrong one because it defeats the throttles that consume it.

Re-runs backfill it into installs that predate this, via _env_default_key, which
fills a gap but never overrides an uncommented value -- flipping someone's
deliberate false back on would be the worst kind of surprise.

.env.example moves to false on purpose: that file is the Docker/manual path and
docker-compose.yml publishes 5000 on all interfaces, so true was the unsafe
default there. Host installs never read it. SECURITY.md claimed "the shipped
nginx deploy sets it", which was untrue until now.

T44 covers all six behaviours, each mutation-checked by line number: disabling
the branch, inverting it to trust an exposed port, and disabling the backfill
each fail exactly one assertion and no others.

Shell suite 121 passed. Backend suite 3542 passed, 48 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:52:55 -04:00
Juan DenisandClaude Opus 5 2624816680 docs: how to run the panel behind your own reverse proxy
Issue #96: an operator fronting the panel with Caddy and a smallstep private CA
hit a redirect loop, and there was no documented path to follow. The
architecture already tolerated this deployment -- the fix was a config file we
already ship -- but nothing said so anywhere.

Adds an INSTALLATION.md section covering both topologies (external proxy ->
ServerKit nginx, full features, HOPS=2; external proxy -> the app directly,
panel-only, HOPS=1), why the loop happens, how to count TRUSTED_PROXY_HOPS,
Caddy (with acme_ca for a private CA), Traefik, Nginx Proxy Manager and plain
nginx snippets, what a private CA does and does not require (the panel never
sees it; a remote agent's host must trust the root), and a symptom-to-cause
table for the loop, stale HSTS, the rejected websocket and wrong client IPs.

Also corrects the Docker documentation, which had drifted badly:

- README (en/es/pt/zh-CN), INSTALLATION and DEPLOYMENT all told people to run
  `docker compose up -d` and reach the panel at http://localhost. It starts the
  all-in-one container on :5000, and a containerised panel cannot manage its own
  host -- now stated up front rather than discovered.
- DEPLOYMENT is a host-install guide that told operators to run
  `docker compose build && up` to fix a panel, which would build a second panel
  beside theirs, and carried a certbot cron copying certs and restarting a
  container that does not exist on a host install.
- CLAUDE.md, AGENTS.md, CONTRIBUTING.md and LOCAL_DEVELOPMENT.md (x3) pointed at
  a docker-compose.dev.yml that has never existed; the snippet offering to
  create it referenced a frontend/Dockerfile.dev that does not exist either.

Every anchor introduced here was checked to resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:07:27 -04:00
Juan DenisandClaude Opus 5 8df7350f41 docs(security): stop claiming extension permissions are enforced
Plan 55 Phase 4. EXTENSIONS.md said `permissions` is "**enforced** by the SDK
gate: require_permission(slug, "docker") raises unless docker is declared".
That sentence is true about the function and misleading about the system: the
gate only fires where something calls it, and nothing routes Docker through it.
The SDK exposes no docker, shell, filesystem or network helper at all, and hands
out `db` as raw SQLAlchemy, so an extension that wants any of them imports the
host module directly and no in-process check is involved. Declaring `docker`
does not stop an extension using Docker, and not declaring it does not either.

Both documents now state which capabilities are actually mediated -- today
`agent.command:<action>` and nothing else, because the SDK is the only route to
an agent -- in a per-capability table, and say plainly what declaring does
instead: it tells the operator at install time what the extension claims to
need, and lets them refuse.

SECURITY.md gains an Extension Trust Model section next to the agent one. It
leads with the part that matters most and was never written down: an extension
is in-process code running with the panel's privileges against the panel's
database, nothing sandboxes it, and no permission string changes that. Signing
and sha256 pinning are described for what they are -- integrity of what you
installed, not containment of what it does -- along with the unsigned-prompts,
bad-signature-fails split, and the pip opt-in.

It also explains why the new detail page reports "cannot be observed" rather
than a zero count for unenforceable permissions: absence of evidence is not
evidence of absence, and a "never used" badge there would imply a boundary that
does not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:50:30 -04:00
Juan DenisandClaude Opus 5 c07c73fa5c chore(screenshots): capture the converged Docker, Terminal and Databases tables
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 02:16:47 -04:00
Juan DenisandClaude Opus 5 3d296ff9a4 chore(screenshots): capture the unified incidents table and the SSL conversion
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:03:03 -04:00
Juan DenisandClaude Opus 5 36f926ba9a chore(screenshots): capture the selection bar and the fixed KPI band
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 00:19:11 -04:00
Juan DenisandClaude Opus 5 db432afc25 chore(screenshots): refresh captures
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 23:56:54 -04:00
Juan DenisandClaude Opus 5 8eccbf4e23 chore(screenshots): refresh mock-data captures for the converged list pages
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>
2026-08-12 23:49:52 -04:00
Juan Denis 116c416ec1 @
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
2026-08-12 23:47:58 -04:00
Juan Denis 065b6ed61b docs: consent-gate matrix + seam lifecycle in EXTENSIONS.md 2026-08-05 13:36:33 -04:00
Juan Denis b159c7a5bd docs: WP extraction complete — core data seam, Tier C status 2026-08-05 02:38:41 -04:00
Juan Denis 964f8eee7d docs: hook registration seams + core data seam in EXTENSIONS.md 2026-08-05 01:18:33 -04:00
Juan Denis ef88b765d7 docs: extraction audit reflects phase-4 frontend relocation 2026-08-05 00:48:21 -04:00
Juan Denis a4329124dd chore(extensions): author signing tool + signing spec docs
scripts/sign-extension.mjs signs a bundle with an ed25519 author key;
EXTENSIONS.md documents the format, key model, and verdict behaviors.
Generated private keys live in gitignored scripts/keys/.
2026-08-04 23:34:34 -04:00
Juan Denis 9b531e14b3 Add demo video badge to README variants
Adds a new YouTube “Watch the Demo” badge/link near the top badges in the main README and the Spanish, Portuguese, and Simplified Chinese README translations so users can quickly find the product walkthrough.
2026-08-03 00:17:29 -04:00
Juan Denis 937cbb2b81 feat(setup): capacity-aware install profiles and resource advisory
- install_profile_service: record what install.sh put on the box
  (minimal/standard/full), recommended from detected hardware and
  overridable; thresholds mirrored by recommend_profile in install.sh
- setup wizard: replace the hard ResourceGate/SetupStepTier with
  SetupStepCapacity + SetupStepSecurity
- ResourceAdvisory: inline capacity warning at the point of action
  (WordPress/Docker/Services/Deployments), backed by headroom.fits
- RequiresDocker component for docker-dependent surfaces
- install.sh/update.sh: profile detection + tests
  (test_install.sh, test_update.sh, test_server_capacity.py)
2026-07-31 14:56:37 -04:00
Juan DenisandClaude Opus 5 4effb201f4 fix(ui): rename the page shell off .sk-page (name collision)
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>
2026-07-28 22:55:54 -04:00
Juan DenisandClaude Opus 5 fd68d912ce refactor(ui): move the hand-rolled slide-overs onto the DS Drawer
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>
2026-07-28 22:50:01 -04:00
Juan DenisandClaude Opus 5 4a7fe8e875 refactor(ui): move Git onto PageLayout
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>
2026-07-28 22:33:16 -04:00
Juan DenisandClaude Opus 5 9b27b2e362 refactor(ui): run Terminal/Logs as a flush workspace on the shared shell
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>
2026-07-28 22:33:16 -04:00
Juan DenisandClaude Opus 5 f7ff4df043 fix(cron): pad the create drawer's body
`.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>
2026-07-28 22:33:05 -04:00
Juan DenisandClaude Opus 5 62507f750c feat(cron): rebuild the create surface as a drawer on SchedulePicker
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>
2026-07-28 22:08:03 -04:00
Juan DenisandClaude Opus 5 e55968f177 docs(screenshots): capture the new Monitoring surfaces
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>
2026-07-28 00:34:25 -04:00
Juan DenisandClaude Opus 5 e172289850 docs(screenshots): show the app, not just the page
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>
2026-07-26 11:16:44 -04:00
Juan DenisandClaude Opus 5 640e7e827e docs(screenshots): recapture, and fix two bugs the capture exposed
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>
2026-07-26 09:59:06 -04:00
Juan Denis b5160e90bf chore: clean root and docs — drop recovery artifacts, relocate stale audits
- Remove patch_localhost.py (one-off patch script, served its purpose)
- Remove RECOVERY.md + docs/RECOVERED_COMMITS.txt (July-6 incident artifacts)
- Move completed audits and Jan-era strategy docs to docs/audits/
- Update .gitignore for relocated UI_CONSISTENCY_AUDIT.md
2026-07-25 22:22:33 -04:00
Juan Denis 2d3bb33a34 docs: expand README requirements beyond Ubuntu, note homelab/ARM support
- List all installer-supported distros (Ubuntu 22.04+, Debian 12+,
  Fedora, RHEL/Rocky/AlmaLinux 9+) instead of Ubuntu only
- Add Arch row (x86_64, ARM64) matching installer platform detection
- Clarify Docker is optional for the panel itself
- Note the panel's ~180 MB RAM / ~500 MB disk footprint and that it
  runs on 1 GB VPSs, spare laptops, and Raspberry Pi homelabs
2026-07-25 20:40:11 -04:00
Juan DenisandClaude Opus 5 92e0750262 feat(sdk): a second widget surface — types users place, not just slots
`contributions.widgets` mounts a component at a host-chosen slot. That is the
wrong shape for the new dashboard, where the user decides what goes on a board
and where. So this adds `dashboard_widgets` alongside it: a registry of
placeable widget TYPES that surface in the widget library, each instance
positioned, sized and configured by the person looking at it.

The two are easy to confuse, so the docstring, the manifest spec and
EXTENSIONS.md all now say plainly which is which, and a test asserts a
dashboard widget never leaks into the fixed-slot list.

SDK_VERSION 1.0.0 -> 1.1.0 (added surface, older extensions unaffected),
bumped in lock-step across the JS export and the backend mirror.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:24:41 -04:00
Juan DenisandClaude Opus 5 8e6b53d77b refactor(servers): one list instead of a rail, and give Add Server its drawer back
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>
2026-07-25 19:03:48 -04:00
Juan DenisandClaude Opus 5 98ea0f6b10 feat(backups): port the design's Overview, Schedules, Snapshots and Storage
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>
2026-07-25 19:03:34 -04:00
Juan DenisandClaude Opus 5 b413148194 feat(monitoring): fold Fleet Monitor in, and scope the page to a server
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>
2026-07-25 19:03:15 -04:00
Juan DenisandClaude Opus 5 d944b695e7 refactor(ui): one place for search, and drop the KPI restatements
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>
2026-07-25 19:02:58 -04:00
Juan DenisandClaude Opus 5 593dd11a8b docs(registries): write down the test for what earns a registry
"Should X be a registry too?" gets re-asked every few months, and the answer is
usually no. This records the rule and the per-catalog verdicts so the assessment
does not get redone from scratch.

A catalog earns a registry only when all three hold: it changes faster than the
panel releases, it is data rather than code (a new entry must work on an
existing panel with no code change), and someone outside the release cycle would
contribute to it. Extensions, themes and app templates pass. Seven other
catalogs were assessed and rejected, each with the reason -- server templates,
notification events, AI providers, DB tuning settings, sanitization profiles,
agent survey probes and build packs.

Also names the failure mode that makes a bad extraction expensive: panels that
have not upgraded read the registry forever, so an entry that needs new panel
code either gets ignored or renders as a broken item. That is what forces a
min_panel_version gate, and why serverkit-extensions has one and the other two
do not need one.

Records one open candidate rather than leaving it implicit. YARA rules pass all
three tests and have no update path -- 16 curated rules frozen at release time.
That got sharper this session: the image now builds without ClamAV by default,
and ClamAV was the component with a self-updating feed via freshclam, so YARA is
now the only malware detection in a default container. Operators can already
drop custom .yar files in CUSTOM_RULES_DIR; what is missing is a curated
upstream. Not built -- noted so the gap is deliberate.

Adds the templates registry and this doc to the README documentation table.
Every claim was checked against the code rather than written from memory: rule
counts (16 in both BUILTIN_RULES and webshells.yar), the CUSTOM_RULES_DIR
default, ARG INSTALL_CLAMAV=false, and that ServerTemplateService already
exposes create_template so its 12-entry library is a seed, not a catalog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:53:17 -04:00
Juan DenisandClaude Opus 5 dd402892e1 docs(readme): lead with what the panel actually measures
Adds a "By the Numbers" table under the badges in all four READMEs (EN, ES, PT,
zh-CN). Every figure is measured from this repo at this commit, not estimated:

  1,519 REST endpoints   walked from app.url_map after create_app(); a raw
                         @route grep undercounts at 1,276 because plugins
                         register routes dynamically
    106 app templates    backend/templates/*.yaml, matches index.json
  2,633 backend tests    pytest tests --collect-only
1.75 MB gzipped UI       frontend/dist, gzip -6 over every asset
 ~180 MB resident        gunicorn --workers 1 --threads 100, FLASK_ENV=production
 501 MB image            docker build . on linux/amd64

Three claims were corrected against the code rather than written from memory:
the OpenAPI docs live at /api/v1/docs (not /api/docs); "no telemetry" was wrong
-- there is a telemetry subsystem, it just writes local SystemEvent rows -- so
it now says nothing phones home, which was verified (no sentry/posthog/segment/
mixpanel, no version ping, and the SPA's only external URLs are documentation
links, not asset loads).

Numbers only ever grow here, so a stale figure undercounts rather than
overstates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:15:44 -04:00
Juan DenisandClaude Opus 5 df3524b202 docs(extensions): document the six contribution surfaces
The SDK table listed the surfaces an extension consumes but not the ones it can
now contribute to, and deploys had been missing since it shipped. Adds all six
with one worked example each, plus the rule that ties them together: namespace
what you register, the panel owns the UI and normalises what you hand back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:03:01 -04:00
Juan DenisandClaude Opus 5 374404e43d fix(ui): let drawer footers reach the bottom edge
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>
2026-07-25 12:12:59 -04:00