Commit Graph
2299 Commits
Author SHA1 Message Date
Juan DenisandClaude Opus 5 30b780f4e4 fix(status): a null JSON body no longer 500s the status-page routes
PR #122 review. Four routes passed request.get_json() straight into the
service, which does `if field in data` and raises TypeError on None.

Copilot reported the trigger as "empty/invalid JSON bodies"; measured on
Flask 3.1.3 that is wrong — an empty body is already a 400 and a wrong
content-type a 415, both raised by Flask before the view runs. The body
that actually reaches the view is literal `null`, which is valid JSON and
decodes to None. Same fix, accurate reason.

Also corrects plugin.json, which claimed installing the extension serves
the public /status/<slug> pages; the registry catalog says that page
stays core, and the two contradicted each other.

Both copies edited — app/plugins/ is the live copy the loader imports,
builtin-extensions/ is the source — and a test pins that they agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:34:20 -04:00
Juan Denis bcf53f3e84 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-31 00:11:38 -04:00
Juan DenisandClaude Opus 5 f724a1fbca fix(notifications): escape interpolated values in Telegram alerts
send_telegram posts with parse_mode='HTML' but interpolated the hostname,
alert type, message, value and threshold straight into the body. Telegram
then parses those as markup, so a message as ordinary as

    Disk usage > 90% on /var & /tmp

is rejected with "can't parse entities" and the entire notification is
dropped — precisely when something is already wrong on the box. A value
containing <b> could also inject markup into the rendered alert.

Escape every substituted value with html.escape(quote=False); the
template's own tags are untouched.

Proving tests pinned against the unfixed service first: the three
escaping cases fail without this change, the literal-markup case passes,
so they test the behaviour and not the implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:07:47 -04:00
Juan DenisandClaude Opus 5 48871b07e6 fix(dev): dev.sh notices a new requirement instead of booting into ImportError
The venv check probed four imports (dotenv, flask, flask_socketio,
sqlalchemy). Those catch a missing or broken venv, but they are blind to
the common case: a dependency is added to requirements.txt, all four
imports still succeed, dev.sh reports nothing, and the backend dies on
the fifth import at startup. That has derailed several sessions.

Stamp a sha256 of requirements.txt into the venv after a successful
install and compare on each start, so drift triggers a sync. The import
probe stays as the broken-venv check; the hash covers what it cannot see.

Guards: the hash is computed with the venv's own python (no dependency on
sha256sum being installed), an unreadable requirements.txt yields an
empty hash and leaves a working venv alone rather than reinstalling every
start, and the marker is written only after a successful install so a
failed one retries.

Verified by driving the decision logic with pip stubbed: no marker →
install, marker current → skip, requirement appended → install, settled →
skip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:05:47 -04:00
github-actions[bot] d39f2a8d9d chore: bump version to 1.9.12 [skip ci] 2026-08-31 03:54:38 +00:00
Juan DenisandClaude Opus 5 12c863b230 fix(templates): repo deploys send the build inputs the template declares
The deploy drawer posted only url/branch/app_type/build_method/port, so a
repo template's `build_command`, `start_command` and `dockerfile_path`
were decorative — declared in the YAML, read by nobody. The endpoint has
always accepted custom_build_cmd / custom_start_cmd / dockerfile_path;
only the caller was dropping them.

The visible cost was nixpacks builds: with no start command it falls back
to `npm start`, so any repo whose package.json has no `start` script
produced an image that built fine and could not boot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 23:47:14 -04:00
Juan Denis 37cf8b6e84 Merge plan 52 cutover: the published extensions leave the tree
localkit, email, cloudflare-ops and git are now published, signed and in
the registry index, so the panel no longer needs in-tree copies of them.
This removes ~29k lines that existed in two places at once and flips the
bundled registry so they install from the catalog like any other
extension.

Unblocked by the operator publishing all nine repos (2026-08-30); the two
registry-mirror tests that plan 83 expected to stay red now pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	builtin-extensions/serverkit-cloudflare-ops/frontend/components/StoragePanel.jsx
#	frontend/src/plugins/serverkit-cloudflare-ops/components/StoragePanel.jsx
2026-08-30 22:13:01 -04:00
Juan DenisandClaude Opus 5 7b9eb43a9a test: lower the count floor for the extensions that left the tree
The cutover removes four builtin extensions, so collection legitimately
drops. Verified against dev by diffing collected node ids rather than
trusting the delta:

  -7  test_extension_manifests  the 7 plugin.json files of the four
                                departing extensions (builtin + frontend)
  -4  test_builtin_extension_drift  anchor swap, email (7 py files) ->
                                    status (3)
   0  test_files_rbac / *_extraction  param-id churn only

No feature lost its proving tests. 4799 -> 4791.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 22:12:05 -04:00
Juan DenisandClaude Opus 5 18767c2244 test(drift): re-anchor the builtin-copy guard on serverkit-status
serverkit-email's live copy under app/plugins/ was the only git-TRACKED
one, which is what kept test_builtin_extension_drift non-vacuous on a
fresh clone. Plan 52's cutover deletes it, so on CI the guard had zero
pairs to compare and test_drift_check_is_not_vacuous fired -- correctly.
It still passed on a dev box, where the gitignored live copies exist, so
the failure only reproduces in a clean checkout.

Track serverkit-status's live copy instead (verified byte-identical to
builtin-extensions/serverkit-status/backend), un-ignore just that path,
and update both docstrings to name the new anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 22:07:53 -04:00
Juan Denis d597173ed0 Merge remote-tracking branch 'origin/main' into dev 2026-08-30 21:48:27 -04:00
Juan Denis 3ff85b1598 Merge pull request #121 from jhd3197/dev
Split the security suite and the last built-ins into extensions
v1.9.11
2026-08-30 21:30:45 -04:00
Juan DenisandClaude Fable 5 cea1770562 fix(review): posture installs survive unchecked overlaps; translatable storage empty-states
PR #121 review follow-ups. The setup summary de-duplicated the install
queue with recommendations winning, so a security-posture extension
that also appeared as an unchecked recommendation was silently dropped
— the posture half now wins regardless of checkbox state. The
Cloudflare storage panel templated English around a translated title
(`No ${title.toLowerCase()} yet.`); each section now passes a fully
translated emptyHint instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 20:25:17 -04:00
Juan Denis 4b65c1e638 Merge remote-tracking branch 'origin/main' into dev 2026-08-30 20:22:18 -04:00
github-actions[bot] 3df4fb8446 chore: bump version to 1.9.11 [skip ci] 2026-08-30 20:32:52 +00:00
Juan Denis d764f50809 Merge branch 'dev' into feature/plan52-cutover
# Conflicts:
#	backend/app/data/registry_index.json
#	frontend/src/plugins/serverkit-git/plugin.json
2026-08-30 16:24:27 -04:00
Juan DenisandClaude Fable 5 2c3b63b1f6 chore: re-vendor the bundled registry index from the published catalog
The nine extracted extensions are now published, signed, and merged
into the serverkit-extensions main catalog; the bundled offline copy
picks up the five new security-suite entries and the source/sha256/
signature fields on localkit, email, cloudflare-ops, and git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:23:48 -04:00
Juan DenisandClaude Fable 5 6234832c46 chore(preflight): gate builtin-frontend drift locally
The Extensions CI drift check (sync-builtin-frontends.mjs --check) had
no local counterpart, so a builtin-extensions/ manifest edit without a
re-sync reached CI unrun — exactly how the serverkit-git split went red
on the next PR. Same contract as the migration-inventory gate: a stale
copy is re-synced on the spot and flagged so the diff gets committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:08:09 -04:00
Juan DenisandClaude Fable 5 91d464f871 chore: re-sync the pre-bundled serverkit-git plugin.json
The plan 52 Phase 6 git split updated the source manifest under
builtin-extensions/ (shell permission, entry_point, url_prefix,
min_panel_version) without running sync-builtin-frontends.mjs, so the
Extensions CI drift check went red on the next PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:00:50 -04:00
Juan DenisandClaude Fable 5 1b9f0061c7 fix(apps): name-clash errors point at the Recycle Bin for tombstones
A deleted app deliberately keeps reserving its name (Plan 70: a restore
must never collide), but every clash site said only "already exists" —
pointing the operator at an app that is in no list (GH report: delete an
app, then a same-name deploy fails with no way forward).

app_name_clash_error() now distinguishes a tombstone from a live row and
names the release path (restore or purge from Settings -> Recycle Bin);
all five clash sites go through it. Live-row wording is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 03:20:34 -04:00
Juan DenisandClaude Fable 5 d3e3564c7d fix(templates): surface validate-install errors in the deploy drawer
The API client throws on any non-2xx, so a 400 from validate-install
never reached the `!validation.valid` branch — and the catch only read
`err.data.error` (singular), while this endpoint returns an `errors`
array. The operator saw "Request failed (400)" instead of the actual
reason (e.g. the app name being reserved by the Recycle Bin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 03:20:27 -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 02967b0547 cutover(git): serverkit-git leaves the tree (plan 52 Phase 6)
builtin-extensions/serverkit-git + the tracked frontend live copy removed;
the bundled registry entry flips to bundled:false at v1.1.0 (the version
that carries the Gitea backend) with the local zip's sha256; the gated
auto-install (_gitea_was_used) moves to REGISTRY_GATED_SLUGS, leaving
GATED_BUILTIN_SLUGS empty with the machinery intact.

i18n: en.json regenerated (the app.git/app.email/app.cloudflare* namespaces
ride with their extensions, which carry en+es bundles; other languages are
recoverable from git history), all locales pruned, intl-door ratchet
tightens 123 -> 115.

Expected red on this branch: the two registry-mirror tests, until the four
repos are published + signed + re-vendored (see docs/plans/83).

OPERATOR: publish jhd3197/serverkit-git, sign, sync-registry-index.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:53:27 -04:00
Juan DenisandClaude Fable 5 665d6775e4 cutover(cloudflare-ops): the last in-place flagship leaves the tree (plan 52 Phase 2)
builtin-extensions/serverkit-cloudflare-ops + the tracked frontend live
copy removed; FLAGSHIP_SLUGS is now empty (machinery kept);
plugins-manifest.json empties; the bundled registry entry flips to
bundled:false with the local zip's sha256. cloudflare_ops_bridge resolves
the copy-installed package FIRST so a registry install works without the
builtin registration path.

Tests: new conftest.mount_sibling_extension mirrors a real install from
the sibling checkout (the _wp_support.ensure_plugin analogue) and a
cf_extension fixture forces a private app; the zone-settings and round2
suites mount it autouse (74/74 green), the extraction suite pins the
lean-panel shape, and the flagship-era onboarding tests prove the
machinery with a fabricated member instead.

OPERATOR: publish jhd3197/serverkit-cloudflare-ops, sign,
sync-registry-index.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:51:17 -04:00
Juan DenisandClaude Fable 5 43fec8030c cutover(email): serverkit-email leaves the tree (plan 52 Phase 2, D8)
builtin-extensions/serverkit-email + both tracked live copies removed; the
bundled registry entry flips to bundled:false with the local zip's sha256;
the gated auto-install (_email_was_configured) moves from
GATED_BUILTIN_SLUGS to REGISTRY_GATED_SLUGS so mail users still get it back
once the entry is published.

Panel-core tests re-home onto the sibling checkout via the new
conftest.sibling_extension_dir helper (skip cleanly when absent): the
extraction suite installs from the serverkit-email repo, the dovecot-probe
suite loads its module from there. All 39 affected tests green with the
sibling present.

OPERATOR: publish jhd3197/serverkit-email, sign, sync-registry-index.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:45:22 -04:00
Juan DenisandClaude Fable 5 4eda066264 cutover(localkit): serverkit-localkit leaves the tree (plan 52 Phase 2)
builtin-extensions/serverkit-localkit removed; the bundled registry entry
flips to bundled:false pointing at the (to-be-published) v0.2.0 release
asset with the sha256 of the locally built zip.

OPERATOR: publish jhd3197/serverkit-localkit, let its release workflow (or
update-registry.mjs) upsert + sign the registry entry, then run
scripts/sync-registry-index.sh - which replaces this hand-staged entry with
the published, signed one. Until then the two registry-mirror tests
(signature pin + published-index match) are red BY DESIGN on this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:43:36 -04:00
Juan DenisandClaude Fable 5 b72c1d0014 docs: check off the extraction + security-suite roadmap items
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:40:10 -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 53757bf96b test: drop re-homed security suites from the seed one-door baselines
test_malware_scan_job.py / test_security_scan_jobs.py moved to their
extension repos with the security split; the staleness guard rightly
flagged them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:27:10 -04:00
Juan DenisandClaude Fable 5 2e0330d84d refactor(git): relocate the Git page into the serverkit-git extension (plan 52 Phase 6, frontend half)
Same relocation pattern as email/cloudflare-ops: pages/Git.jsx (1920 lines),
its SCSS partial (with the %sk-state placeholder inlined - @extend cannot
cross compile units) and a new Gitea-only API client move into
builtin-extensions/serverkit-git/frontend/. The page's two halves now show
their seam: Gitea self-host calls go through the extension's own client;
webhook + deployment calls stay on the core api object (they ARE the deploy
pipeline and exist on every panel).

Core files.js sheds the Gitea-half methods but keeps the three fail-soft
getGitea* aliases the app-creation repo pickers probe (they 404 and degrade
to 'not installed' when the extension is absent). serverkit-sdk adds the
ds/status helpers (statusKind & co). pages/ ratchets tighten: raw api calls
431->405, toast extractions 264->218.

Frontend build green, lint fully green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:04:30 -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 DenisandClaude Fable 5 8b61ffbe01 refactor(security): jail service owns fail2ban status probing (plan 47 Ph3c inversion)
Fail2banJailService gains get_fail2ban_status / get_fail2ban_jail_status /
unban_ip (moved verbatim from SecurityService) and stops lazy-importing
SecurityService for its own per-site status; Setup Health probes through the
jail service too. This is the read/undo half core keeps when the fail2ban
management surface moves to the serverkit-fail2ban extension - pure
fail2ban-client probes that work with or without the extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 13:36:08 -04:00
Juan DenisandClaude Fable 5 3057d56977 refactor(cloudflare-ops): relocate the zone-ops frontend into the extension (plan 52 Phase 2 prep)
Same WP-Phase-4 pattern as the email relocation: the CloudflareZoneSettings
page, the WAF/Workers/Tunnels/Storage panels, the /cloudflare API client and
the SCSS partial move into builtin-extensions/serverkit-cloudflare-ops/
frontend/; the extension index.jsx imports locally and side-effect-imports
the SCSS. Core drops the cloudflareMethods spread from the api object and
the main.scss partial - DNS records and the Cloudflare connection stay core.

serverkit-sdk (still the unreleased 1.4.0 bump) adds Tabs kit, PageLoader
and copyToClipboard; vendor shim regenerated. The pages/ raw-api ratchet
tightens 460 -> 431 (the two relocated pages left pages/).

Frontend build green, lint fully green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 13:23:44 -04:00
Juan DenisandClaude Fable 5 f6d9dea954 refactor(email): relocate the Email page into the serverkit-email extension (plan 52 Phase 2 prep)
WP-Phase-4 pattern, in-tree Tier C: pages/Email.jsx, its SCSS partial and a
new self-contained /email API client move into
builtin-extensions/serverkit-email/frontend/; the extension index.jsx now
imports locally (no more core re-export shim) and side-effect-imports the
SCSS so it compiles through the extension's module graph. main.scss drops
the _email partial.

Core keeps only the email-adjacent surfaces core Settings renders: the
DNS-provider methods (core dns_providers blueprint) and the relay tile
methods. serverkit-sdk grows the generic list-page chrome the relocation
needed (DataTableFooter/ListToolbar/SearchField/SortChipBar, ds/grid saved-
view chrome, PageLayout, useTableSort, useColumnVisibility) - SDK 1.4.0,
backend mirror bumped in lock-step; vendor shim regenerated.

Frontend build green, lint 0 errors, i18n check no orphans.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 13:19:21 -04:00
Juan Denis 2cb2b36474 Merge pull request #120 from jhd3197/dev
Policy-driven scaling, a provider SDK, and all-25 distro coverage
v1.9.10
2026-08-29 04:39:04 -04:00
Juan Denis 4247e9270c Merge origin/dev (version bump) 2026-08-29 04:31:10 -04:00
Juan Denis 71ce9f28e8 Merge main into dev to update PR #120 2026-08-29 04:31:04 -04:00
github-actions[bot] 989a5d60e6 chore: bump version to 1.9.10 [skip ci] 2026-08-29 08:21:23 +00:00
Juan DenisandClaude Fable 5 7a87e98bb6 chore(registry): re-vendor bundled index from the published signed catalog
serverkit-extensions#3 published the signed first-party catalog and
restored serverkit-redis (re-pinned to the rebuilt v1.0.1 asset). The
bundled mirror already carried the signatures; this picks up the
serverkit-redis entry so test_matches_published_index agrees with main
again. Generated by scripts/sync-registry-index.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 04:09:46 -04:00
Juan DenisandClaude Fable 5 5d2b3dd5ca fix(scale): roll back on any set_policy commit failure
The rollback guard only caught ValueError from field validation, but
db.session.commit() itself can raise SQLAlchemy errors (IntegrityError,
OperationalError), leaving callers a dirty session despite the comment's
promise. Catch everything, roll back, re-raise. (Copilot review on #120)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 04:06:07 -04:00
Juan DenisandClaude Fable 5 2c7f13051f fix(scale): normalize backend nulls before they reach the policy form
The backend stores an unset service_name as null; spreading the API
response over the defaults pushed that null into a controlled <Input>,
flipping it uncontrolled. normalizeScalePolicy now coerces strings and
integer fields back to form-safe values. (Copilot review on #120)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 04:06:07 -04:00
Juan DenisandClaude Fable 5 e1f371b585 test(templates): roadmap icon contract follows the registry-hosted icons
586f556b/025fa0be deliberately moved template icons off inline data URIs
onto https://serverkit.ai/imgs/template-icons/ (proxied from the
serverkit-templates repo), but test_roadmap_template_conventions still
pinned the old offline-inline contract, failing all 41 hosted roadmap
templates in CI. The contract now accepts the registry-hosted icon URL,
keeping the inline data-URI form only for templates with no public logo
(prompture-hub).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 04:03:13 -04:00
Juan Denis ab27c7b058 fix(distro): patch all 6 failing distro-compatibility probes
- Harness: bootstrap find/awk in distro-compatibility.py (AL2023,
  Tumbleweed, SLES BCI minimal images lack them; the source-level
  suites legitimately use both)
- Python gate 3.11-3.14: Arch/Manjaro rolling ship only python 3.14
  with no versioned fallback packages (same trap as Debian 13/#99);
  align scripts/update.sh, which had drifted to a 3.11-3.12 gate
- Gentoo: add emerge as a supported installer package-manager path
  (install.sh, scripts/lib/pkg.sh, distro.py family, deps manifest);
  provision via slotted dev-lang/python:3.13 -> 3.12
- Tests: move out-of-range stubs above the new ceiling, shadow all
  locate_python candidate names (real python3.14/3.13 in Manjaro/
  Gentoo images were winning over fixtures), cover the 7th manager
- Catalog: Gentoo is no longer a documented expected failure

Verified: test_lib 32/32, test_install 124/124, test_update 76/76,
pytest test_serverkit_installer 46/46, and all 6 previously failing
images now pass both quick and provision container probes (12/12).
2026-08-28 23:31:36 -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 Fable 5 1dfd5f52c1 test(wordpress): seed the HA drift fixture through make_application
test_wordpress_ha_foundations constructed Application(...) by hand, which
the seed-one-door ratchet refuses for new files. The factory also seeds
the owning admin, so the hand-rolled User goes too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 22:20:34 -04:00
Juan DenisandClaude Fable 5 1316feaad3 refactor(scale): ContainerScaleService owns its rollback
set_policy assigned fields to the ORM row and then raised ValueError
mid-validation, leaving the API route to clean up the dirty session with a
direct db.session.rollback() — a new controller-boundary crossing the
inventory ratchet rightly refused. The service now wraps apply+commit and
rolls back before re-raising, so callers never inherit a dirty session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 22:20:33 -04:00
Juan DenisandClaude Fable 5 fb8d6bfbf8 fix(templates): the deploy drawer's domain promise now sticks
The drawer showed '<name>.<base> — Published automatically once the deploy
finishes' whenever a managed base domain existed, but publishing was gated
on a per-template auto_domain flag the operator can't see. For most of the
catalog the install landed on host:port instead — a link a default firewall
doesn't even expose — and the domain had to be redone by hand in the
service's Domain settings.

The promise on screen is now the contract: when the drawer displays a
domain it sends auto_domain: true, which rides the install plan to the
finalizer and overrides the template's silence (the forced flag also feeds
the SERVICE_FQDN magic context, so tokens and the published host agree).
Absent flag = the template YAML still decides, as before.

The drawer also stops promising what can't be delivered: the domain field
only renders for compose templates targeting the local server — the panel's
nginx can't proxy a remote container, and the repo pipeline has no
auto-publish step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 21:59:26 -04:00