Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
TimestampMixin (byte-identical to the hand-written pair — zero schema
migrations) adopted by 52 classes across 44 model files; the utcnow
tz-aware migration is now a one-file edit. uuid_pk() replaces the 13
pasted String(36) uuid4 lambdas across 7 files (ai.py's hex variant
stays grandfathered, documented). Deviant shapes (indexed created_at,
sanitization_profile's default-less updated_at, created-only models)
deliberately keep their own columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 of the July-5-7 recovery rebuild (plan 38). Backend now boots.
- models: reconstruct DnsCutoverSnapshot (from bytecode) + FleetDoctorResult
(from migration 063 DDL); restore lost columns applications.ports/
bootstrap_done, app_volumes.declared_size, dns_provider_configs.origin_ca_key
- migrations: recreate the 15 missing revisions 056/057/060/062/063/064/065/
067/068/069/070-074, reconciling the chain to a single linear head (074).
add_column revisions guard on live schema so a fresh-DB upgrade reaches head
- rbac: restore _APP_ROLE_RANK + require_workspace_access/require_workspace_role/
app_access_tier/require_app_member (plan 19/29 4-tier authz), recovered from
bytecode; unblocks api/cron.py import
- services: reconstruct cron_run_service.CronRunService (record_run/recent_runs/
stats, edge-triggered failure/recovery transitions) from bytecode
Gate: create_app() imports clean, db.create_all() OK, flask db upgrade on a
fresh SQLite DB reaches head 074 (linear chain).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give a service tracked, named persistent storage that survives redeploys,
replacing fragile relative bind mounts (./mysql-data:/var/lib/mysql).
- AppVolume model (namespaced serverkit-app-{id}-{slug}, unique per mount path)
+ migration 043.
- volume_service: create (docker volume + row, dup-path refusal), list_for_app
(rows + live inspect), delete (row-only by default; wipe only when the app is
stopped and no running container uses it), measure, convert_bind_mount,
run_args, compose_fragment, host_paths_for_app.
- Wired into the single-container deploy (_deploy_docker passes -v specs to
docker run) and new compose stacks (create_docker_app gains named_volumes ->
top-level volumes: block).
- API under /api/v1/apps/<id>/volumes (list/attach/detach?wipe=/convert), ACL
via ResourceGrantService.
- Frontend: Storage panel in Service detail Settings (attach/detach with an
off-by-default "also delete data" checkbox, disabled while running).
- Tests: create + namespaced volume, dup-path refusal, wipe safety, single-
container -v proving test, compose top-level named-volume generation, convert,
API CRUD.
Scoping: volumes are backup-addressable (mountpoints exposed; protect via the
files target) rather than force-folded into the app's single-parent incremental
tar chain, which would break restore. Live compose files (esp. the WP multidev
stack) are not rewritten -- convert_bind_mount is the opt-in migration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>