Commit 784decc
authored
feat(api): Phase 7b foundation — init, datetime-TZ, /readiness, status cache, proxy widening (#75)
* docs(api): Phase 7b foundation implementation plan
Bite-sized TDD plan derived from the merged Phase 7b spec, covering
all 9 clusters in dependency order: datetime-TZ → printer identity →
lifespan init-order → alembic verify → /readiness → status cache →
frontend proxy → README → verification.
Refs #22
* feat(api): add serialize_datetime_utc helper for RFC3339 with Z
Go frontend oapi-codegen rejects naive datetimes. Helper normalises any
datetime to a timezone-aware ISO string before serialisation.
Refs #22
* fix(api): TemplateRead emits RFC3339 datetimes with Z suffix
Go oapi-codegen client rejected naive datetimes from /api/templates
with `parsing time "..." cannot parse "" as "Z07:00"`. Apply the new
serialize_datetime_utc helper via @field_serializer.
Refs #22
* refactor(api): hoist api_client_with_seed fixture into conftest
Centralises the API integration test fixture so Phase 7b Task B3 (PrinterRead
and JobRead) can reuse it without duplication or cross-file imports.
Refs #22
* fix(api): PrinterRead + JobRead emit RFC3339 datetimes with Z suffix
Same Go-oapi-codegen contract fix as TemplateRead. JobRead.started_at
and finished_at each get their own serializer that handles the nullable
case. conftest.py re-discovers IntegrationRegistry after lifespan
shutdown so the api_client_with_seed fixture works for all tests in
sequence, not just the first one.
Refs #22
* refactor(api): SQLAlchemy datetime columns are timezone-aware UTC
Every model column (templates/printers/jobs/presets/printer_state/
printer_status_cache) now uses DateTime(timezone=True) with
default_factory=lambda: datetime.now(UTC). Fresh inserts
write tz-aware values that survive the SQLite roundtrip.
Existing rows are migrated by the Phase 7b alembic data migration
in Task B5.
Refs #22
* fix(api): alembic data migration normalises naive datetimes to UTC
Existing rows from Phase 5 inserts contain naive datetimes that break
the Go frontend's RFC3339 parser. Migration appends '+00:00' to any
value without an explicit TZ marker across templates/printers/jobs/
presets/printer_state/printer_status_cache. Idempotent via WHERE
NOT LIKE '%+%' AND NOT LIKE '%Z'.
SQLite is dynamically typed so no ALTER TABLE is needed — the new
column types from the previous commit only affect new inserts via
the SQLAlchemy layer.
Refs #22
* fix(integration): suppress alembic fileConfig in migration test to restore caplog
Alembic's command.upgrade() calls logging.config.fileConfig() which, by
default, uses disable_existing_loggers=True. This marks every logger not
explicitly named in alembic.ini — including app.integrations — as
logger.disabled=True. Any _logger.error()/_logger.exception() call on a
disabled logger silently drops the record, breaking caplog assertions in
test_discovery.py tests that ran after the migration tests.
The fix mirrors the guard already present in app/db/lifespan.py:
set cfg.attributes["configure_logger"] = False so alembic skips its
logging reconfiguration entirely. The four previously failing caplog
assertions now pass in all orderings.
Refs #22
* feat(api): derive_printer_id helper for deterministic UUIDv5
Lifespan can now compute a stable printer.id from env config
(model, host, port) so the runtime printer and the DB row share
the same id across restarts. Phase 7b Cluster 1b prep work.
Refs #22
* feat(api): upsert_runtime_printer lifespan helper
Creates or refreshes one DB Printer row from env config, keyed by the
deterministic UUIDv5 from derive_printer_id(model, host, port). Returns
None for the mock backend so the lifespan can no-op when no printer is
configured. Idempotent across restarts.
Refs #22
* refactor(api): driver.make_queue_printer accepts optional printer_id
Lifespan can now hand the DB-deterministic UUID (from upsert_runtime_printer)
to the in-memory queue printer so app.state.printer_id matches the DB row.
Backwards compatible — omitting the parameter falls back to uuid4().
_PrinterLike.id and Job.printer_id promoted from str to UUID throughout
the in-memory queue stack (print_queue, job_lifecycle, print_service) to
maintain type consistency end-to-end.
Refs #22
* fix(api): seed_templates aborts on empty loader cache instead of silent no-op
Catches the Phase 7a bug pattern where lifespan called seed_templates
before TemplateLoader.load_dir() — cache empty, 0 rows upserted, no
error, UI shows no templates. The defensive RuntimeError surfaces the
misordering at startup so it cannot reach production silently.
Refs #22
* fix(api): re-order lifespan — load_dir before seed_templates + upsert printer
Calls plugin discovery and TemplateLoader.load_dir() before
seed_templates(), and adds upsert_runtime_printer(s, settings) between
seed_templates and ensure_printer_state. Hands the resulting DB UUID to
driver.make_queue_printer so app.state.printer_id matches the DB row.
Closes the Phase 7a bug where a fresh deploy showed 0 templates and 0
printers in the UI. Removes the now-unnecessary D1 monkey-patches in
test fixtures.
Refs #22
* feat(api): verify_alembic_at_head fails fast on revision drift
Lifespan calls verify_alembic_at_head(settings) right after
run_migrations(). If the DB revision deviates from the script head
(e.g. partial migration, downgrade, missing script file) the lifespan
raises with a clear message before any ORM query runs.
Takes settings explicitly (C2/D2 testability pattern) so unit tests
can verify against ad-hoc DBs without monkey-patching get_settings().
Sync alembic work runs inside asyncio.to_thread to keep the event loop
unblocked. configure_logger=False prevents alembic from clobbering
pytest caplog handlers (Phase 7b B6 learning).
Fixtures in test_lifespan.py and tests/integration/conftest.py
extended to patch verify_alembic_at_head to a no-op alongside
run_migrations, because create_all() does not populate alembic_version.
Refs #22
* feat(api): readiness response schema (CheckStatus + ReadinessResponse)
Frozen Pydantic models for the new /readiness deep-check endpoint
introduced by Phase 7b Cluster 1e.
Refs #22
* feat(api): readiness aggregator — database/alembic/templates/printer_runtime
First four checks for the /readiness deep-check endpoint plus the
ready/degraded/not-ready aggregation. Endpoint wiring lands in F4;
remaining 4 checks (printer_db_sync, snmp_discovery, print_queue,
sse_bus) land in F3.
Refs #22
* feat(api): readiness aggregator — remaining 4 checks
printer_db_sync, snmp_discovery (<90s ok / <600s stale / else fail),
print_queue worker liveness, sse_bus subscriber capacity. Completes
Cluster 1e aggregator. F4 wires the FastAPI route.
Refs #22
* feat(api): expose /readiness deep-check endpoint
Returns HTTP 200 with body.status in {ready, degraded} when the
critical checks pass; 503 with status=not-ready when database/
alembic/template_seed fail. Pangolin can switch its healthcheck.path
to /readiness — Docker keeps polling /healthz for liveness-only.
Refs #22
* test(api): regression guard — /healthz must answer 200 even when DB broken
Locks in the Cluster 1e contract: liveness probe is restart-relevant
(must NOT touch the DB), readiness probe owns the deep checks.
Prevents accidental DB queries sneaking back into /healthz.
Refs #22
* feat(status): StatusProbeProducer persists printer_status_cache rows
Every probe success writes parsed JSON + captured_at; SNMP timeouts
persist online=False + last_error while preserving the prior parsed
snapshot. No schema change — uses Phase 5 columns.
Refs #22
* feat(status): PrinterStatus carries cache freshness + offline reason
Adds captured_at, last_probe_age_s, last_error, note to the response
of /api/printers/{id}/status so the UI can render staleness and offline
reasons instead of guessing.
Refs #22
* fix(status): /api/printers/{id}/status reads from cache, no sync SNMP
Eliminates the 5-second block when the printer is offline. The probe
worker keeps printer_status_cache fresh in the background; this
endpoint returns whatever is there in <10ms.
Refs #22
* feat(ui): proxy /docs, /openapi.json, /redoc to the backend
Swagger UI and the raw OpenAPI document are now reachable behind the
public domain (which sits behind Pangolin SSO + the Basic-Auth bypass).
Closes the 404 reported in the hhdocker02 production smoke test.
Refs #22
* docs(api): document /healthz vs /readiness contract in the README
Explains the liveness/readiness split introduced in Phase 7b Cluster
1e and links to the spec for the full check list. Recommends using
/readiness for reverse-proxy routing checks while keeping /healthz
on Docker container healthchecks.
Refs #22
* fix(api): readiness sse_bus check supports real EventBus + Settings cap
The real EventBus exposes distinct_subscriber_count() (no zero-arg
subscriber_count) and reads its cap from settings.sse_max_subscribers
(no max_subscribers attribute). Probe both surfaces so production and
unit-test fakes both report correct subscriber counts and caps.
Refs #22
* fix(api): populate PrinterStatus.tape_loaded + error_state from cache
Bot reviews (Copilot + Gemini, identical HIGH-priority finding on PR
#75) flagged that the G3 endpoint rewrite stopped populating the
schema's tape_loaded and error_state fields — they were always null.
Map the cache JSON: loaded_tape_mm=12 → tape_loaded="12mm",
error_flags=[...] → error_state="flag1, flag2". Existing test
test_status_endpoint_returns_cached_tape_data extended to lock the
contract.
Also sanitises two private hostname references in the plan file that
tripped the Privacy / secret scan workflow.
Refs #221 parent c5a7964 commit 784decc
59 files changed
Lines changed: 5358 additions & 365 deletions
File tree
- backend
- alembic/versions
- app
- api/routes
- db
- models
- printer_models
- schemas
- services
- producers
- tests
- db
- integration
- api
- db
- unit
- api
- models
- printer_models
- schemas
- services
- docs/superpowers/plans
- frontend/cmd/server
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
95 | 111 | | |
96 | 112 | | |
97 | 113 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
35 | | - | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
26 | 25 | | |
27 | 26 | | |
28 | 27 | | |
| |||
166 | 165 | | |
167 | 166 | | |
168 | 167 | | |
169 | | - | |
| 168 | + | |
170 | 169 | | |
171 | | - | |
172 | | - | |
173 | | - | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
174 | 175 | | |
175 | 176 | | |
176 | 177 | | |
177 | 178 | | |
178 | 179 | | |
179 | 180 | | |
180 | | - | |
181 | | - | |
| 181 | + | |
| 182 | + | |
182 | 183 | | |
183 | | - | |
184 | | - | |
185 | | - | |
186 | | - | |
187 | | - | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
188 | 191 | | |
189 | 192 | | |
190 | | - | |
191 | | - | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | | - | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
199 | 198 | | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
| 199 | + | |
| 200 | + | |
213 | 201 | | |
214 | | - | |
215 | | - | |
216 | | - | |
217 | | - | |
218 | | - | |
219 | | - | |
220 | | - | |
| 202 | + | |
| 203 | + | |
221 | 204 | | |
222 | 205 | | |
223 | 206 | | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
228 | 213 | | |
229 | 214 | | |
230 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | | - | |
12 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
13 | 21 | | |
14 | 22 | | |
15 | 23 | | |
16 | 24 | | |
| 25 | + | |
| 26 | + | |
17 | 27 | | |
18 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
19 | 32 | | |
20 | 33 | | |
21 | 34 | | |
| |||
49 | 62 | | |
50 | 63 | | |
51 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
52 | 114 | | |
53 | 115 | | |
54 | 116 | | |
| |||
70 | 132 | | |
71 | 133 | | |
72 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
73 | 138 | | |
74 | 139 | | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
75 | 145 | | |
76 | 146 | | |
77 | 147 | | |
| |||
102 | 172 | | |
103 | 173 | | |
104 | 174 | | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
0 commit comments