273 Commits
Author SHA1 Message Date
Juan Denis 7dfc0c8b68 docs: align award badge heights across README languages 2026-09-07 02:36:37 -04:00
Juan Denis 63d1fe8e4e docs: show deployment videos side by side in all languages 2026-09-07 02:35:15 -04:00
Juan Denis cacd3a2625 docs: add deployment video walkthrough in all README languages 2026-09-07 02:32:59 -04:00
Juan Denis ccba314042 docs: unify README asset size into one total 2026-09-05 14:57:27 -04:00
Juan Denis 2de3a971c5 docs: publish reproducible README measurements
Generate all four README tables from reviewed source and build snapshots, distinguish collection and asset sizes from runtime results, and remove unsupported footprint claims. Add a consistency check in CI and document how to reproduce profiling and measurements.
2026-09-05 14:42:28 -04:00
Juan Denis d67e5d07d0 fix: complete audit follow-ups and add regression coverage
Centralize backup schedule calculations and shared request/form flows, encode query values, and move repository finalization into its service. Add browser CI coverage, repair stale test fixtures, and provide opt-in request and build measurement tools.
2026-09-05 14:41:15 -04:00
Juan Denis aae180aaef docs: refresh migration inventory after reuse cleanup 2026-09-05 13:55:48 -04:00
Juan Denis 3068fc81f0 docs: record security review and remediation handoff
Preserve synthetic historical probes, document fixes and validation, and list upgrade behavior and remaining hardening work with the local implementation commits.
2026-09-05 04:30:47 -04:00
Juan Denis c8c93a8e0c fix(auth): enforce MFA, scoped keys, and revocable sessions
Add session-family revocation and migration 097, require current credentials for password changes, constrain API keys, and require verified WebAuthn authentication. Update browser token handling and add boundary regressions. Existing JWTs require fresh login; restricted keys deny undeclared endpoints.
2026-09-05 04:29:16 -04:00
Rebertt RodriguesandJuan Denis a092ba1afe fix(dashboard): avoid content blockers on metrics history requests (#133)
Co-authored-by: Juan Denis <jhd3197@gmail.com>
2026-09-04 22:20:22 -04:00
Juan DenisandClaude Opus 5 4dfb468f77 chore(connect): regenerate the surface, inventory and locale files
Generated files the connect work moved and nobody re-ran:

- docs/API_SURFACE.md gains GET /connect/status
- docs/MIGRATION_INVENTORY.md: the hand-shaped error bodies count is 1145
  (ceiling 1150) for the 409 the backups PUT now answers when Cloud owns
  the destination
- en.json gains app.backups.storageManagedByCloud, so the sentence the
  read-only backup form shows is translatable like every other string

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q56AfqpgKjio4n9KYubvZ
2026-09-03 00:19:35 -04:00
Juan DenisandClaude Opus 5 503dedd95c fix(apps): start/stop/restart a docker app that has no compose project
`app_type == 'docker'` was being read as "compose-managed", but the build-pack
path deploys by building an image and running a single container
(DeploymentService._deploy_docker, named serverkit-app-<id>) and never writes
a compose file. start/stop/restart called `docker compose` anyway, which -- in
a directory with nothing to read -- fails with:

    no configuration file provided: not found

So every build-pack app returned 400 from the panel's start, stop and restart
buttons and could not be run at all. There was already a materialize-compose
guard for BYO-image apps, but it only fires when root_path is NULL; a
build-pack app has source on disk, so it fell straight through to compose.

Decide on the artifact rather than the type: `_has_local_compose_project()`
looks for a compose file on disk (the app's own `compose_file`, else the four
default names). When there is one, nothing changes. When there isn't, drive
the container the deploy actually created.

Stop treats an already-gone container as stopped -- reporting that as a
failure would strand the app in `running` forever. Start and restart explain
that the app has not been deployed yet rather than surfacing a docker error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 01:34:30 -04:00
Juan DenisandClaude Opus 5 332ad73770 fix(walkthroughs): accept the step-level shorthand in extension manifests
Core walkthroughs author single-condition steps with `signal:`, `check:` or
`route:` directly on the step -- 24 sites in frontend/src/data/walkthroughs.js
do it -- and the frontend registry's completionFor() has always desugared
them. The backend validator did not: `_STEP_FIELDS` rejected all three, so an
extension that copied the pattern passed `new-extension --validate` and was
then refused at install with an "unsupported fields" error.

Teach the backend the same shorthand rather than take it away from core.
`_completion_for()` mirrors completionFor() exactly (an explicit `completion`
block still wins) and returns which spelling supplied the value, so problems
are reported against the key the author wrote -- `steps[0].signal`, not
`steps[0].completion.signal`. The shorthand is a spelling, not an escape
hatch: the same token regexes and length bounds apply either way.

The published schema gains the three properties so the contract, the
validator and the frontend now agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 00:46:35 -04:00
Juan DenisandClaude Opus 5 c8ae7f35f2 chore(docs): regenerate the API surface and migration inventory
Both committed inventories went stale behind the walkthrough work, so the
two ratchets that pin them failed on dev:

  test_api_surface_inventory::test_committed_surface_matches_served_surface
  test_migration_inventory::test_the_committed_inventory_matches_the_measured_state

API_SURFACE picks up GET and PUT /walkthroughs/definitions, the endpoints the
Walkthrough Studio contribution contract added. MIGRATION_INVENTORY moves the
hand-shaped {'error': ...} census 1141 -> 1142 against an unchanged ceiling of
1150; the routes above are the new sites and the ceiling still holds.

Regenerated, not hand-edited:
  SERVERKIT_UPDATE_API_SURFACE=1 pytest tests/test_api_surface_inventory.py
  python scripts/generate-migration-inventory.py

Version Bump failed only as a dependent of Backend CI ("Sibling CI failed;
VERSION will not advance") and needs no change of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 00:21:38 -04:00
Juan Denis cfcf965e24 feat: add declarative walkthrough contribution contract
Extensions can now contribute walkthroughs via contributions.walkthroughs
in plugin.json. The backend validates definitions against a shared
contract (route, signal, manual, and check completion types), transports
them through the dynamic contribution envelope, and persists custom
walkthroughs from the Studio. Includes the JSON Schema, SDK version gate,
and extension scaffold support.
2026-08-31 23:14:29 -04:00
Juan DenisandClaude Fable 5 5417c39100 perf(models): index every foreign-key column
An audit found 102 FK columns with no index — every join, per-parent
listing, and the delete cascades from _delete_cascade_policy scanned
whole tables on them (deployments.app_id, audit_logs.user_id,
dns_records.zone_id, webhook_logs.webhook_id, ...). All FK columns now
declare index=True; migration 096 creates the same indexes on existing
installs, deriving the list from the live schema at upgrade time (so
it is idempotent and cannot drift from what an install actually has).

Ratchet: test_every_fk_column_is_indexed pins the rule with no
allowlist — a new FK column is born indexed or the build fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:42:53 -04:00
Juan DenisandClaude Fable 5 5bfb0f6747 refactor(recycle-bin): drop the page-local header
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>
2026-08-31 19:35:53 -04:00
Juan DenisandClaude Fable 5 160bf6dab7 feat(panel): one-click self-update from the About tab
The About tab could only detect a new release and link to it; applying the
update meant SSH and scripts/update.sh. Wire that script to the UI:

- panel_update_service launches update.sh via systemd-run in its own
  transient unit — a plain subprocess child would be killed when the
  updater restarts the panel's own systemd unit. Status is stateless
  (transient-unit state + the updater's own update-*.log) so it survives
  the mid-update backend restart.
- Capability gates keep the button honest: root systemd installs only.
  Docker installs get the docker compose pull instructions; dev servers
  and hosts without systemd-run get a reason instead of a button.
- GET/POST /api/v1/system/update (admin_required, confirm-gated,
  audited). API surface doc regenerated.
- About tab gains Update Now with inline confirm, live log tail while
  the update runs, a restarting phase that tolerates the panel being
  briefly unreachable, and success/rollback/timeout outcomes. New
  aboutTab locale keys in en + es.

The heavy lifting (blue/green switch, migrations, health check,
automatic rollback) stays in update.sh, which is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 08:09:52 -04:00
Juan DenisandClaude Opus 5 e181ae7f44 fix(authz census): count extension routes from a source git tracks
a31cc5d3 widened the jwt-only census to app/plugins and shipped a ceiling
of 611 measured on a dev box. CI measured 591 and went red: most
app/plugins/ live copies are GITIGNORED, so the count depended on which
extensions a machine happened to have installed. That is the same
machine-dependence the builtin-copy drift census exists to prevent, and
this walked straight into it.

Count builtin-extensions/ instead -- the source, pinned equal to the live
copy by test_builtin_extension_drift.py, and tracked. Filter to
git-tracked files as well, because extractions leave stale ignored
directories behind (cloudflare-ops, git, localkit after plan 52) that
would otherwise be counted on some working copies but not others.

Stable population: 608 (35 of them extension routes across
cloud-provision, remote-access and status). Also regenerates
docs/MIGRATION_INVENTORY.md, whose row is derived from this ceiling, and
points the CLAUDE.md / AGENTS.md notes at the scope actually scanned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:53:15 -04:00
Juan DenisandClaude Fable 5 aee50fea0c cutover: contracts follow the flagship departure
API surface regenerated (the flagship-seeded cloudflare zone routes no
longer boot with a lean panel); the frontend route contract gains an
explicit EXTRACTED_EXTENSION_PREFIXES excuse list for the /git, /email and
/cloudflare prefixes core clients probe fail-soft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:57:57 -04:00
Juan DenisandClaude Fable 5 4be264ccd5 chore: migration inventory picks up the 511 controller-boundary count
Second regeneration - the first ran before the boundary baseline rewrite
landed, so it still measured 514.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:37:51 -04:00
Juan DenisandClaude Fable 5 2bdddd4d77 chore: regenerate ratchet inventories after the git split
Controller-boundary baseline drops the three api/security.py crossings that
moved to serverkit-image-scan; API surface sheds the 18 Gitea operations;
migration inventory regenerated by preflight; test-count floor raised
4735 -> 4799 (SERVERKIT_CLEAN_COLLECT).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:35:38 -04:00
Juan DenisandClaude Fable 5 db59ad2da6 feat(git)!: split the Gitea self-host half into the serverkit-git extension (plan 52 Phase 6)
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>
2026-08-29 14:30:07 -04:00
Juan DenisandClaude Fable 5 ab828d31e6 docs: record the security-suite extraction + email/cloudflare frontend relocations
ARCHITECTURE gains the security-extension seam note; CORE_SLIM Tier B rows
3/7 marked done; CHANGELOG Unreleased entry for the lean-by-default
Security page. (Plan docs 47/55 updated too — they live in the git-ignored
docs/plans/.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 13:56:46 -04:00
Juan DenisandClaude Fable 5 9f92850cbd feat(security)!: extract the install-gated security suite to extensions (plan 47 Ph3b-5 / plan 55 Ph3)
The 1791-line security omnibus splits along tool lines and the install-gated
surfaces leave core for five standalone extensions - serverkit-clamav
(ClamAV+YARA scanning, quarantine, scan jobs), serverkit-fail2ban
(management UI; probes stay core on Fail2banJailService), serverkit-lynis,
serverkit-auto-updates, and serverkit-image-scan (headless CVE/SBOM
scanning; models stay core per the plan 52 D1 data seam). Each mounts its
routes back at the same /api/v1/security prefix when installed, so route
shapes never change; absent extensions mean plain 404s, no dead UI.

Core keeps the zero-host-package baseline: status/config, legacy integrity,
suspicious activity/events, SSH keys, IP lists, the audit (fail2ban check
now probes via the jail service; updates check was already self-contained),
scoped FIM, and the scan/alert logs + notification helpers extensions write
through. Job handlers for security.malware_scan/lynis_scan/image_scan now
come exclusively from the extensions' manifest jobs key.

Security page becomes core-tabs-only; extension tabs contribute into the
security tab group (3a groupId). Overview posture drops the hardcoded
ClamAV rows and offers the Marketplace when no security extension is
installed. Upgrade parity: run_registry_auto_install() installs each
extension from the registry once, gated on the host tool actually being
present, fail-soft until the registry entry is published.

Wizard round 2 (Ph5): a security-posture chooser (minimal/recommended/
hardened) on the Summary step, catalog-resolved so unpublished slugs never
render dead installs; persisted as onboarding_security_posture.

Moved test suites re-home to the extension repos; panel keeps
test_security_suite_extraction.py (18 proving tests: absent-shape 404s,
lean summary, jail-service probes, migration gates, posture wiring).
docs/API_SURFACE.md regenerated (34 operations moved out).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 13:53:04 -04:00
Juan Denis 9a09a392cf test: add all-25 distro compatibility discovery 2026-08-28 23:01:48 -04:00
Juan DenisandClaude Fable 5 69b5fbe9eb docs(api): regenerate API_SURFACE.md for the connection provider routes
The seven /connections/providers operations shipped in 939c77b6 without
the surface pin being regenerated; the diff is exactly those routes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 22:20:34 -04:00
Juan DenisandClaude Opus 5 5e7bb17e9b docs(readme): sync Docker pulls and Uneed badges to translations
The es/pt/zh-CN READMEs had drifted from the English one: neither carried
the Uneed POTD3 badge, and none had the new Docker Hub pulls badge. Adds
both in the same positions, leaving the intentionally translated demo
label and the ../LICENSE relative path untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GasJpPFj8JmAV7QCJhCG52
2026-08-28 13:25:04 -04:00
Juan Denis 49b44ab945 security(extensions): trust signed first-party catalog 2026-08-28 00:56:12 -04:00
Juan Denis 939c77b648 feat(connections): add provider SDK contract 2026-08-28 00:48:36 -04:00
Juan Denis bba169f04a feat(wordpress): shared-state contract and login rate limiting at the edge
Pin the Compose template's full-tree wordpress_html named volume as the
supported same-host shared-state contract (uploads, plugins, themes, disk
page cache), and make WordPress vhost publishing install shared
wp-login.php/xmlrpc.php limit_req zones keyed by server name + client IP.
Generic Docker vhosts are unaffected; drift repair regenerates existing
WordPress vhosts with the policy. Spec and roadmap rows move from Partial
to Shipped with their real-host boundaries recorded.
2026-08-27 22:35:54 -04:00
Juan Denis bb675f0d9a feat(apps): policy-driven replica scaling for local Compose apps
Add ContainerScaleService policies (min/max replicas, CPU thresholds,
cooldown) persisted via migration 093, with manual scale, single
evaluation, and an admin sweep endpoint. Container Ops panel exposes
policy editing; the configured minimum acts as a floor on the next
evaluation. Frontend node:test suites pin policy normalization and the
API contract.
2026-08-27 22:35:43 -04:00
Juan DenisandClaude Fable 5 0fe6d090fd test(api): pin the served API surface to a committed inventory
OpenAPIService.generate_spec() existed but nothing snapshotted it, so a
route rename/removal/method change shipped invisibly to API-key
consumers, the CLI, and agents. docs/API_SURFACE.md now pins the sorted
METHOD /path inventory (1418 operations) with the same
regenerate-and-review contract as docs/MIGRATION_INVENTORY.md: an
intentional surface change regenerates the doc in the same commit
(SERVERKIT_UPDATE_API_SURFACE=1 pytest
tests/test_api_surface_inventory.py) so the diff is the review.
Method+path only on purpose — summaries and schemas churn without
breaking anyone. Named *_inventory so preflight's globs run it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 05:03:40 -04:00
Juan DenisandClaude Fable 5 e29872f0cd fix(restore-points): satisfy jwt-only and seed-factory ratchets
The three restore-point read routes authenticated with bare
@jwt_required(), pushing the census to 579 against a 577 ceiling.
They now use the auth_required() policy door like the file's mutating
routes (which already accept API keys via @developer_required), keeping
read access consistent with write access for API-key callers; scope
authorization stays in-route. test_restore_point_env.py hand-rolled
Application/User seeds — switched to the make_application/make_user
factories. Migration inventory regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 02:52:59 -04:00
Juan Denis 515d960614 chore(ratchets): regenerate the boundary baseline, error ceiling and migration inventory
The recipes Phase 0 commit moved all three measured counts without
regenerating their baselines. The new api/recipes.py persistence
crossings are owned and documented in the baseline; the hand-shaped
error census is unchanged by this branch.
2026-08-21 23:05:56 -04:00
Juan Denis 811cf7dde9 feat: add resumable recipe execution foundation 2026-08-21 21:47:24 -04:00
Juan DenisandClaude Opus 5 550dd47585 fix(ratchets): regenerate the boundary baseline and the migration inventory
Two more shards were red on generated artifacts nobody regenerated:

- api_controller_boundary_baseline.json: the attachment round rewrote
  _persist_user_message to build the row before adding it, so the fingerprint
  moved from `db.session.add(AiMessage(...))` to `db.session.add(message)`.
  Same crossing, same category, count unchanged at 509 -- one line.

- docs/MIGRATION_INVENTORY.md: three measured numbers had moved without a
  regeneration -- bare @jwt_required() routes 577 -> 576, and the two frontend
  rows whose totals come from maps inside check-frontend-boundaries.mjs
  (setInterval pollers 10 -> 9, hex colour literals 151 -> 150). All three are
  deterministic AST/constant counts, so these are CI's numbers, not this box's.

The reason nobody caught the inventory locally is the third fix here: the
generator hardcoded `node`, and under WSL the Windows nvm directory is on PATH
but exposes only `node.exe`, so test_migration_inventory.py skipped on the dev
box. Both the generator and the test now resolve $NODE / node / node.exe, and
the test runs instead of skipping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 20:14:34 -04:00
Juan Denis 2825c536a8 fix(i18n): address review — reset locale readiness, typed error for bad language, correct parity test reference
- LocaleContext: reset ready on each language change so non-default
  locale bundles trigger a rerender once active
- auth PUT /me: raise typed ValidationError (auth.unsupported_language)
  instead of a hand-shaped error body; error-shape ceiling 1145 -> 1144
- i18n.py docstring: point at the real parity test
  (test_user_language_preference.py)
2026-08-20 19:04:09 -04:00
Juan Denis ae95ac4478 docs: regenerate MIGRATION_INVENTORY to match measured state 2026-08-20 18:41:14 -04:00
Juan DenisandClaude Opus 5 9c0ae9e957 feat(extensions): enforce the SDK gate at install; localize the GUI builtin
Follow-up audit of the extension surface, across all of them rather than one.

THE COMPATIBILITY HOLE. Two places documented an install-time `sdk_version`
check as existing -- app/utils/sdk.py's docstring ("checked at install (manifest
lint) and at load") and the runtime loader's comment, which named
`_assert_manifest_sdk_compatible` -- but `sdk_version_satisfies` had ZERO
production callers; only its own unit test. Consequences split by extension
kind: a runtime ESM extension installed cleanly and failed later at the load
gate, while a builtin/in-repo extension never passes that gate at all, so its
range went unchecked everywhere. The function now exists and runs at install,
beside the min/max_panel_version check, and refuses with what the operator
needs to act: what the extension wants, what this panel offers. Fail-open on a
missing range, matching the load gate's 'grace'. Proved both directions.

This matters now because SDK 1.3.0 added the localization surface and made
i18next/react-i18next shared singletons: an extension that externalizes them
needs a panel at 1.3.0+. Pinning `^1.3.0` is what turns a white screen on an
older panel into a readable message. The bump is additive, so anything pinned
to ^1.2.0 keeps installing and loading. Documented as a version table.

THE AUDIT. Measured, not assumed -- both censuses now take `--root` so they can
run against any extension repo (and refuse `--update` there, since the ceilings
belong to the panel):

  2,105 untranslated literals across 13 extension repos (WordPress alone 1,099
  in 44 files), plus 78 direct toLocale*/Intl calls that follow the BROWSER's
  locale (WordPress 56). None of it was visible to any check before, because
  every census stopped at frontend/src.

serverkit-gui -- the one builtin with real UI -- is converted as the worked
example, importing useTranslation/useFormat from 'serverkit-sdk' exactly as a
third-party extension would. That also closes its own toLocaleTimeString.
Ceiling 8055 -> 8034, intl door 124 -> 123.

The probe now asserts the claim the SDK surface rests on: an extension
resolving i18next through the shared vendor gets the HOST's INITIALISED
instance, and an extension-style t() returns Spanish. A second instance would
be uninitialised and every t() in that extension would render English forever
with nothing reporting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:31:42 -04:00
Juan DenisandClaude Opus 5 cff35cc651 feat(sdk): share i18next with extensions; generate the vendor shims
SDK 1.2.0 -> 1.3.0 (additive).

Auditing the extension surface for plan 79 found the localization gap I
expected and two pre-existing bugs I did not.

The gap: extensions had no way to translate anything. The SDK now re-exports
the HOST's translation surface -- useTranslation, Trans, t, useLocale,
useLabel, useFormat and the intl door -- and i18next/react-i18next join the
shared vendor singletons. That sharing is the load-bearing part: an extension
bundling its own i18next would get a separate, uninitialised instance, so
every t() in it would render its English default forever and nothing would
report it. Exactly the "two React copies" hazard vendorShare already documents,
and exactly the failure mode this plan hit twice in core. They also join
dedupe/optimizeDeps for the same reason React is there.

The bugs, both caused by the same duplication:

- The import map listed `react` and `react-router-dom`, but no shim existed
  for either. A runtime extension importing bare `react` 404'd at load.
- The serverkit-sdk shim re-exported 26 of the SDK's 71 exports. An extension
  importing Button, Modal, EmptyState, formatBytes or timeAgo from the SDK got
  `undefined`. The shim's own comment claimed parity was "asserted by the
  frontend build + backend test_sdk_contract"; nothing asserted it.

Three files independently restated the same module list -- the import map, the
namespace capture, and one hand-written shim per module -- so they drifted.
Now vendorManifest.js is the list, generate-vendor-shims.mjs derives every
shim's exports (from the real package's namespace, or from the SDK entry's own
export statements -- never typed by hand), the import map is built from the
same manifest, and `--check` fails the build on drift. 26 -> 85 exports on the
SDK shim, 6 -> 8 shims, all present.

Manifest contributions gain an optional `labelKey` beside `label` for nav, tabs
and command-palette entries, resolved at render by the same translateLabel core
uses -- data cannot call t(), and a label resolved at import would translate
once and never follow a switch. Omitting it renders `label` verbatim, which is
what every existing extension does, so nothing breaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:16:04 -04:00
Juan DenisandClaude Opus 5 7311abe61f docs(inventory): regenerate the migration inventory after the authz sweep
The workspace-authz round converted 28 routes off bare @jwt_required()
onto role decorators and ratcheted JWT_ONLY_CEILING 605 -> 577, but the
generated inventory was never regenerated, so CI's freshness test failed.

Regenerated from the same censuses that enforce the ratchets:

  bare @jwt_required()        605 -> 577 (ceiling 605 -> 577)
  hand-shaped error bodies   1159 -> 1158 (ceiling stays 1159, 1 of 25 slack)
  controller-boundary         510 -> 509 (baseline json already at 509)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:29:52 -04:00
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 c803ee5012 fix(census): measure only tracked files — the instruments count what ships
The raw-subprocess census and the thread-ownership guard walked the
filesystem under backend/app, which on a dev box also holds *installed*
extension copies under app/plugins/ that git does not track. The census
therefore measured 28 on dev machines but 24 on CI's clean checkout — four
sites of invisible ceiling slack, a stale thread classification for the
locally-installed analytics extension, and a red migration-inventory
freshness test on every CI run since the range landed.

Both instruments now intersect their walk with git ls-files (falling back
to walking everything when git cannot answer), the analytics thread entry
is removed (extension repos own their own threads), the ceiling ratchets
down 28 -> 24, and the inventory is regenerated. Proving test:
test_untracked_files_are_not_counted plants an untracked file with a raw
call inside app/ and asserts the census does not see it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:06:38 -04:00
Juan DenisandClaude Fable 5 8753ae6768 docs(plans): fix two pre-rebase hashes and the drifted Server query-site count
The plan-76 completion record cited a75f435f (clipboard) and 3ac566cb (first
poller wave) — pre-rebase hashes unreachable from HEAD; the landed commits
are e1a5a5e1 and 1503c493. The plan-77 record's '98 query sites' for the
Server SoftDelete tail measures 99 today, so it is recorded as approximate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:52:08 -04:00
Juan DenisandClaude Fable 5 0034b25a55 docs(architecture): describe the auth policy layer, typed errors, and realtime registry
Four passages still described the pre-convergence world: bare @jwt_required()
as the route guard (now the frozen legacy population behind auth_required()
and the rbac identity door), plain JSON errors (now typed envelopes with
request ids), sockets.py as a pile of handlers (now the channel registry,
room grammar, and run envelope), and deploy_log/deploy_status as the deploy
channel (now a compatibility dual-emit under run_log/run_status). Also names
the model mixin suite and canonical status vocabulary in the Models row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:51:32 -04:00
Juan DenisandClaude Fable 5 c5ade5a2bc docs: regenerate MIGRATION_INVENTORY (runs endpoint now on auth_required)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 06:06:08 -04:00
Juan DenisandClaude Fable 5 a3e907e97f docs(plan-77): completion record — decisions, scope calls, retained tail
All workstreams A-G executed 2026-08-19; open questions resolved
(D1 read-edge normalize, C2 permanent dual-read on the one key, B2
full-dict + _query shaping, E1 dual-emit); deliberate tail named with
owners (Server soft-delete pass, template/security_feed remote-index
non-adopters, the two raw log channels). BASELINE_COUNT 4460 -> 4591.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:50:35 -04:00
Juan DenisandClaude Fable 5 b0589e95db feat(realtime): generalized run envelope — one stream door for every run kind (plan 77 E1)
RunLogStream accepts any RunLike (__run_kind__ + id + to_dict, optional
result/step members): one room grammar run_<kind>_<id>, one batched
persisted run_log/run_status event pair, and a GET
/api/v1/runs/<kind>/<id>/logs?after_id polling twin over the new
run_log_entries table (migration 087). Deploys keep DeploymentJobLog and
dual-emit the legacy deploy_log/deploy_status during the migration
window. The unified Job runtime — which had NO live channel — adopts it
in the consumer (start/succeeded/failed frames; handlers can log via
stream_for(job)). The dead pipeline_* scaffolding was retired in E2.
Gate: emit -> persisted row -> after_id resync contract test; consumer
adoption test. MIGRATION_INVENTORY.md regenerated (census moved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:13:02 -04:00
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