GET /deploys/catalog/apps -> bridge /api/catalog/apps (73 embedded apps,
single source of truth). POST /deploys/catalog/deploy {app,port,env} ->
validated (service-id charset, port 1024-65535, env name/value rules) then
proxied to bridge /api/catalog/deploy which runs the shipdeck CLI catalog
deploy (OCI image -> hardened systemd service; NO Docker daemon). Audited
as deploy.catalog with ok/seconds. 11 table-driven route tests incl.
validation rejections never reaching the bridge + bridge-failure 502 shape.
App Selector submit rewired from /apps/deploy (Docker chain) to
/deploys/catalog/deploy — same modal, same fields (port, mediaPath, plex
claim token as env), no new UI screens. Full suite 3510/3510 (156 suites).
- DC-160: /catalog/:appId read app.image/ports/env/volumes which live under
app.docker.* — every detail response was empty. Now reads docker.* and
surfaces defaultPort/subdomain/healthCheck/mediaMount/claimToken/difficulty/
setupInstructions for the deploy form. List/search use the templates own
category field (old hand-rolled CATEGORY_MAP mis-categorized apps).
- Shield (routes/shield.js + 5-line app.js mount) committed to source after
weeks as prod-only drift; regression test asserts the mount exists.
- New composition-root e2e suite: boots REAL createApp() on an ephemeral
port, probes the historically-broken routes through the REAL mount table
(wizard wiring, shield, catalog detail, deploys bridge) — the class of bug
all 3 P7 dogfood findings belonged to. 5 tests.
- Full suite 3499/3499 (155 suites); table-driven DC-160 test covers all 73
image templates.
Dogfood finding #3: upsertPerson resolved existing records by id only.
The wizard upserts a bootstrap admin AFTER a provider sync has already
created a person for the same Tailscale user (no id passed), so the same
providerUserId was duplicated into two records. Resolve existing by
providerUserId when no id is given; bootstrap Admin group + protectedAt are
preserved on the merge. New regression test; full suite 3414/3414.
- routes/ca.js: declare format before pfx/pem/crt dispatch (was ReferenceError
on every request that passed validation); add CA_CERT_FORMATS single source
of truth + hardened format extraction (string-coerce, whitelist)
- routes/caddycode.js: fix upstream-validation regexes (control-char classes)
- routes/logs.js: SSE/validation lint fixes
- routes/openclaw.js: remove useless regex escape in ALLOWED_PATH_RE
- fleet-validation.js + http-caddy-admin-origin test: eslint-disable for
intentional control-regex security sentinels
- ca-dc076.routes.test.js: regression test for declared format + behavioral
coverage of format validation (now pre-PKI)
Judge: Qwen lane (qwen3.8-max) grade B, 0 blocking, verdict
/tmp/judge-batch1-verdict.json. API suite 2859/2859 green.
The host-side uptime watchdog and on-host cron jobs curl Caddy with a stock
curl/8.5.0 UA from loopback and the host tailscale IP — ~300 GET /api/health
401 warn-events/day burying real perimeter signal (census: 339+20 of 5000
access.log lines). Dropping on UA alone would blind the store to external
curl scanners, so generic tool UAs (curl/) are now dropped ONLY when the
source remote_ip is one of this host's own addresses (DASHCADDY_SELF_IPS,
default loopback; start.sh derives 127.0.0.1 + tailscale ip -4, empty-safe).
remote_ip (TCP peer) is used, never the spoofable client_ip. DashCaddy-*
probe UAs stay unconditionally dropped. 9 regression pins cover the full
conjunction matrix incl. external-IP+curl KEPT and spoofed-XFF KEPT.
2858/2858 green (128 suites).
Judge: glm-4.6@zai-coding-paas cold-read round 1 = A clean (0 blocking),
both polish notes folded. URN urn:ump:s2sgitfepze65crtp57dpdw4gk4w7upsoi4tqcvfwahcsicyepba
- query().total: full-scan true count (was capped at offset+limit by an
early break — dashboard 24h stat read ≤1000 vs real ~46k)
- trim trigger/curer mismatch: byte trigger + line curer never converged
when avg line > ~524B (live avg 355B); now keeps maxDisk lines OR
≤80% byte budget, whichever retains fewer (TRIM_TARGET_FACTOR);
budget env-overridable via SECURITY_EVENT_TRIM_BYTES / opts.trimSizeLimit
- trim/append race: trim renamed over the file mid-append losing events
to the unlinked inode; now single-flight, queue-empty gated, holds the
write lock, error paths always release + lazily re-kick (no hot loop)
Judge: glm-4.6@zai-coding-paas adversarial cold-read, grade B clean
(0 blocking, 4 polish — 2 folded, 1 already-satisfied, 1 deferred as
judge-endorsed safer). 127 suites / 2849 tests green.