7676from app import __version__
7777from app .api .error_handlers import register_error_handlers
7878from app .api .routes import batch as batch_routes
79+ from app .api .routes import batches as batches_routes
7980from app .api .routes import events as events_routes
8081from app .api .routes import jobs as jobs_routes
8182from app .api .routes import lookup as lookup_routes
9192from app .db .engine import async_session , engine
9293from app .db .lifespan import (
9394 ensure_printer_state ,
94- recover_inflight_jobs ,
9595 run_migrations ,
9696 seed_templates ,
9797 upsert_runtime_printer ,
9898 verify_alembic_at_head ,
9999)
100100from app .db .session import get_session
101101from app .integrations .registry import IntegrationRegistry
102+ from app .models .printer import Printer as _Printer
102103from app .printer_backends import BackendRegistry
103104from app .printer_backends .exceptions import SnmpDiscoveryError
104105from app .printer_backends .snmp_helper import query_model_pjl
105106from app .printer_models .registry import ModelRegistry
106107from app .schemas .readiness import ReadinessResponse
108+ from app .services .cleanup_task import CleanupTask
107109from app .services .event_bus import EventBus
110+ from app .services .job_store_sqlite import SQLiteJobStore
108111from app .services .label_renderer import LabelRenderer
109112from app .services .lookup_service import AppLookupService
110113from app .services .print_queue import PrintQueue
@@ -270,13 +273,25 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
270273
271274 # 4. DB-bound init — plugin registry and template cache are populated.
272275 async with async_session () as s :
273- await recover_inflight_jobs (s )
276+ # Phase 2: recover_inflight_jobs() entfernt (Spec R1-C1) —
277+ # PrintQueue.start() übernimmt Recovery mit korrekter QUEUED/PRINTING-Differenzierung.
274278 await seed_templates (s , TemplateLoader )
275279 db_printer_id = await upsert_runtime_printer (s , settings )
276280 await ensure_printer_state (s )
277281 await s .commit ()
278282 # -------------------------------------------------------------------------
279283
284+ # Phase 2: JobStore + CleanupTask
285+ # 'async_session' ist die async_sessionmaker aus app.db.engine (R2-M5)
286+ job_store = SQLiteJobStore (async_session )
287+
288+ cleanup_task = CleanupTask (
289+ store = job_store ,
290+ retention_days = settings .job_retention_days ,
291+ )
292+ await cleanup_task .start ()
293+ app .state .cleanup_task = cleanup_task
294+
280295 discovery_host = settings .pt750w_host or ""
281296 if discovery_host and settings .printer_discover_via_snmp :
282297 model_id = await _resolve_model_id (settings , discovery_host )
@@ -296,15 +311,51 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
296311 tape_registry = TapeRegistry ()
297312 printer = driver .make_queue_printer (tape_registry , printer_id = db_printer_id )
298313
314+ if db_printer_id is None :
315+ # Wenn kein Host konfiguriert ist (Mock-Backend / CI), liefert
316+ # upsert_runtime_printer None zurück und fügt keine Printer-Row ein.
317+ # make_queue_printer erzeugt dann eine neue uuid4. Damit
318+ # jobs.printer_id (FK → printers.id) bei save_queued nicht verletzt
319+ # wird, legen wir hier eine Stub-Row an. slug wird auf str(id) gesetzt
320+ # (eindeutig durch UUID), damit der UNIQUE-Constraint nicht verletzt wird.
321+ _stub_slug = str (printer .id )
322+ async with async_session () as s :
323+ # Defensive idempotency: in non-mock production paths the printer_id
324+ # is explicit and would be reused, so the existing check matters
325+ # there. In mock paths (printer_id=None), a fresh uuid4 means
326+ # existing is always None.
327+ existing = await s .get (_Printer , printer .id )
328+ if existing is None :
329+ s .add (
330+ _Printer (
331+ id = printer .id ,
332+ name = f"stub-{ printer .id } " ,
333+ slug = _stub_slug ,
334+ model = model_id .lower (),
335+ backend = settings .printer_backend ,
336+ )
337+ )
338+ await s .commit ()
339+
299340 # --- SSE EventBus ---
300341 event_bus = EventBus (queue_size = settings .sse_queue_size )
301342 app .state .event_bus = event_bus
302343 # ----- end SSE ------
303344
345+ # Shared LabelRenderer reused by both PrintService, preview endpoint and
346+ # PrintQueue Recovery. Constructing it once avoids repeated font-loading
347+ # overhead on every POST /api/render/preview request.
348+ # Moved before PrintQueue construction so Recovery in queue.start() can use it.
349+ shared_renderer = LabelRenderer ()
350+ app .state .label_renderer = shared_renderer
351+
304352 pq_producer = PrintQueueProducer (bus = event_bus )
305353 queue = PrintQueue (
306354 printers = [printer ],
307355 on_state_change = pq_producer .handle_transition ,
356+ store = job_store ,
357+ renderer = shared_renderer ,
358+ loader = TemplateLoader ,
308359 )
309360 await queue .start ()
310361
@@ -329,18 +380,14 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
329380 app .state .printer_id = printer .id
330381 app .state .printer_host = discovery_host
331382 app .state .printer_snmp_community = settings .printer_snmp_community
332- # Shared LabelRenderer reused by both PrintService and the preview endpoint.
333- # Constructing it once avoids repeated font-loading overhead on every
334- # POST /api/render/preview request.
335- shared_renderer = LabelRenderer ()
336- app .state .label_renderer = shared_renderer
337383 app .state .print_service = PrintService (
338384 template_loader = TemplateLoader ,
339385 renderer = shared_renderer ,
340386 print_queue = queue ,
341387 lookup_service = AppLookupService (),
342388 printer_id = printer .id ,
343389 backend = backend ,
390+ store = job_store ,
344391 )
345392
346393 try :
@@ -349,6 +396,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
349396 if status_producer is not None :
350397 await status_producer .stop ()
351398 await queue .stop (timeout_s = settings .printer_queue_timeout_s )
399+ await cleanup_task .stop ()
352400 await engine .dispose ()
353401 # Close shared HTTP clients held by integration plugins that support it.
354402 # Plugins that pre-date connection pooling may not have aclose(); skip them.
@@ -597,6 +645,7 @@ async def readiness(
597645 register_error_handlers (app )
598646 app .include_router (print_router )
599647 app .include_router (batch_routes .router )
648+ app .include_router (batches_routes .router )
600649 app .include_router (events_routes .router )
601650 app .include_router (printers_routes .router )
602651 app .include_router (templates_routes .router )
0 commit comments