feat(api): Hub batch print endpoint + slug + hangar templates for Hangar integration#92
Conversation
…ion tests) Voraussetzung für alle Hub-Integrationstests der Phase 1. - client: ASGI-Test-Client gegen App mit gefakter Auth (dependency_overrides). - app_with_fake_auth: create_app() + dependency_overrides für require_read/ require_print/require_admin (Pattern aus test_print_e2e.py:50-70). - db_session: AsyncSession gegen die per-test temp-Engine aus _temp_db_engine. - print_auth_headers/read_auth_headers: leere Dicts (Auth via overrides). Refs strausmann/hangar#78
Hangar SSE-Proxy filtert Fragmente per data-job-id, parseHubEvent extrahiert state per data-state. Beide Attribute werden am Root-div gesetzt (Top-Level-Vars job_id, to_state aus event.data). Refs strausmann/hangar#78
Hangar-Integration braucht stabile menschen-lesbare Identifier für printer im URL-Pfad. UUID bleibt PK, slug ist sekundärer unique identifier (z.B. 'brother-p750w'). Refs strausmann/hangar#78
resolve_by_slug_or_uuid akzeptiert beides für menschen-lesbare API-Pfade. Refs strausmann/hangar#78
Refs strausmann/hangar#78
A1 hat slug-Spalte als unique-Constraint eingeführt, default "". Test-Helper `_make_printer` in test_lifespan.py und test_jobs_routes.py hatten den Default genommen — mehrere Printer pro Test = UNIQUE constraint failed. Fix: slug wird aus name abgeleitet (lowercase, spaces+underscores → dashes). Damit bleibt die Constraint intakt und Tests laufen sauber durch. Refs strausmann/hangar#78
Hangar lookuped seine Printer-UUIDs lazy via slug für SSE-Subscribe. Refs strausmann/hangar#78
Tracking-Tabelle für Batch-Druckaufträge: batch_id → job_ids für SSE-Filter und Audit. 24h-Retention (separater Cleanup-Job). Refs strausmann/hangar#78
Cap 500 items pro Batch (UX-Grenze, Hub ist nicht für Million-Item-Batches gedacht). Refs strausmann/hangar#78
12mm = Standard Fach-Etiketten (TZe-231 weiß). 18mm = Extrastark TZe-S251 für VK-Möbel-Hauptschilder. 24mm = Reserve für Raum-Hauptschilder (Phase 3). Refs strausmann/hangar#78
Pro-Item-Fehler → BatchError-Liste. Hardware-Vorbedingungen (printer_offline, cover_open) sind batch-fatal und propagieren. Refs strausmann/hangar#78
Eigene Datei batch.py mit prefix=/api ergibt eine konsistente
Route POST /api/print/{slug_or_uuid}/batch, klar separiert vom
alten POST /print Endpoint.
Best-effort batch print. Persistiert batch_id ↔ job_ids in
print_batches für SSE-Filter (Hangar) und Audit-Trail.
Hardware-fatale Fehler (offline, cover_open) → 409, ganzer Batch
verworfen. Pro-Item-Fehler (template_not_found, tape_mismatch mit
on_tape_mismatch=fail) → 202 mit errors[].
Refs strausmann/hangar#78
Mock backend defaults to 24mm loaded tape; use hangar-furniture-24mm template to avoid tape_mismatch. Local batch_client/batch_db_session fixtures propagate _temp_db_engine patch into session.py name-binding (same workaround as test_printers_filter_by_slug.py). Refs strausmann/hangar#78
Uses hangar-furniture-24mm for valid items (matches mock backend 24mm default) and does-not-exist for the failing item. Verifies 2 job_ids, 1 error at index 1 with error_code template_not_found. Refs strausmann/hangar#78
Monkeypatches PrintService.submit_print_job at class level so the app.state.print_service instance raises PrinterOfflineError. Verifies 409 response with error_code printer_offline. Refs strausmann/hangar#78
tape_mismatch: monkeypatches PrintService.submit_print_job to raise TapeMismatchError (keyword-only args) for 12mm items. Verifies 2 job_ids, 1 per-item error at index 1 with expected_mm/loaded_mm detail. auth: 401/403 tests require unauthenticated client which conflicts with dependency_overrides pattern. Covered by Phase 7c auth tests. Only the positive 202 case (print scope allowed) is tested here. Refs strausmann/hangar#78
Direkter _sse_stream-Aufruf (Pattern aus test_phase6b_sse.py), ASGITransport-Buffering umgangen. Subscription wird VOR dem Batch-Submit gestartet damit keine Events verloren gehen. Refs strausmann/hangar#78
There was a problem hiding this comment.
Pull request overview
This PR adds Hub-side support for Hangar batch printing by introducing printer slugs, batch request/response tracking, Hangar seed templates, and SSE-friendly job-state fragments.
Changes:
- Adds printer slug lookup and exposes
slugthrough printer APIs/schemas. - Adds
/api/print/{slug_or_uuid}/batch, batch schemas, dispatch logic, persistence, and migrations. - Adds Hangar furniture seed templates plus unit/integration coverage for templates, batch behavior, slug resolution, and SSE fragments.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/alembic/versions/da865401716d_add_printer_slug_column.py | Adds slug column/index migration. |
| backend/alembic/versions/a0516c04278c_add_print_batches_table.py | Adds print batch persistence migration. |
| backend/app/api/routes/batch.py | Adds batch print endpoint. |
| backend/app/api/routes/printers.py | Adds optional slug filtering to printer list endpoint. |
| backend/app/main.py | Registers batch router. |
| backend/app/models/init.py | Registers PrintBatch model. |
| backend/app/models/print_batch.py | Defines print batch SQLModel. |
| backend/app/models/printer.py | Adds slug field to Printer. |
| backend/app/repositories/print_batches.py | Adds PrintBatch repository helpers. |
| backend/app/repositories/printers.py | Adds slug and slug-or-UUID lookup helpers. |
| backend/app/schemas/print_batch.py | Defines batch API schemas. |
| backend/app/schemas/printer.py | Exposes slug in PrinterRead. |
| backend/app/seed/templates/hangar-furniture-12mm.yaml | Adds 12mm Hangar template. |
| backend/app/seed/templates/hangar-furniture-18mm.yaml | Adds 18mm Hangar template. |
| backend/app/seed/templates/hangar-furniture-24mm.yaml | Adds 24mm Hangar template. |
| backend/app/services/batch_dispatch.py | Adds best-effort per-item batch dispatch. |
| backend/app/templates/fragments/job_state.html | Adds job/state data attributes. |
| backend/tests/db/test_lifespan.py | Updates test printer helper slugs. |
| backend/tests/integration/conftest.py | Adds shared integration fixtures. |
| backend/tests/integration/db/test_print_batches_repo.py | Tests print batch repository behavior. |
| backend/tests/integration/db/test_printer_slug_resolution.py | Tests slug/UUID printer resolution. |
| backend/tests/integration/test_batch_endpoint_auth.py | Tests positive batch auth path. |
| backend/tests/integration/test_batch_endpoint_happy.py | Tests successful batch submit. |
| backend/tests/integration/test_batch_endpoint_partial_failure.py | Tests per-item template failure. |
| backend/tests/integration/test_batch_endpoint_printer_offline.py | Tests fatal offline handling. |
| backend/tests/integration/test_batch_endpoint_tape_mismatch.py | Tests per-item tape mismatch handling. |
| backend/tests/integration/test_phase6b_sse_with_batch.py | Tests SSE events for batch jobs. |
| backend/tests/integration/test_printers_filter_by_slug.py | Tests printer slug filtering. |
| backend/tests/unit/api/test_jobs_routes.py | Updates test helper slugs. |
| backend/tests/unit/seed/test_hangar_templates.py | Validates Hangar seed templates. |
| backend/tests/unit/seed/test_seed_templates.py | Expands expected seed template set. |
| backend/tests/unit/test_batch_dispatch.py | Tests batch dispatch behavior. |
| backend/tests/unit/test_batch_schema.py | Tests batch schema validation. |
| backend/tests/unit/test_job_state_fragment.py | Tests job-state fragment data attributes. |
| backend/tests/unit/test_printer_schema_slug.py | Tests PrinterRead slug exposure. |
| backend/tests/unit/test_printer_slug.py | Tests Printer slug model field. |
| # 1. Resolve printer | ||
| printer = await printers_repo.resolve_by_slug_or_uuid(session, printer_key) | ||
| if printer is None: | ||
| raise HTTPException(404, detail={"error_code": "printer_not_found"}) |
| # 2. Best-effort dispatch | ||
| service = http.app.state.print_service | ||
| try: | ||
| job_ids, errors = await dispatch_batch(service, body.items) |
| index: Annotated[int, Field(ge=0)] | ||
| error_code: str | ||
| error_message: str | ||
| error_detail: dict | None = None |
| preview_sample: | ||
| primary_id: "HH-AK-KX10-F0203" | ||
| title: "Kallax Nr.10 Fach 2-3" | ||
| qr_payload: "https://hangar.strausmann.cloud/loc/HH-AK-KX10-F0203" |
| preview_sample: | ||
| primary_id: "HH-VK-BY03" | ||
| title: "Billy VK Nr.3 Hauptschild" | ||
| qr_payload: "https://hangar.strausmann.cloud/loc/HH-VK-BY03" |
| preview_sample: | ||
| primary_id: "HH-AK-RAUM01" | ||
| title: "Arbeitskeller Raum" | ||
| qr_payload: "https://hangar.strausmann.cloud/loc/HH-AK-RAUM01" |
| slug: str = Field(default="", index=True, unique=True, | ||
| description="Stable URL-safe identifier (e.g., 'brother-p750w'). " | ||
| "Defaults to slugified name on init.") |
| p = Printer(name="X", slug="x", model="X", backend="mock") | ||
| await printers_repo.create(auth_db_session, p) | ||
|
|
||
| pytest.skip("401 requires unauthenticated client; covered by Phase 7c auth tests") |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the Hub's API capabilities by introducing a robust batch printing mechanism and improving printer identification through URL-friendly slugs. These changes are foundational for integrating with the Hangar system, allowing for more efficient and flexible label printing workflows. The update also includes new templates tailored for Hangar's needs and refines event reporting for better external system compatibility. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a best-effort batch printing feature, including a new print_batches database table, a POST /api/print/{printer_key}/batch endpoint, and three new seed templates for Hangar Furniture. It also adds printer slug support with exact slug filtering on the printer list endpoint, updates the job_state.html fragment with tracking attributes, and includes comprehensive tests. The review feedback highlights a critical security issue where printer-specific access controls are missing on the batch endpoint, privacy violations due to a hardcoded real domain in the new templates, type safety violations with an untyped parameter in the batch dispatcher, and a performance bottleneck in the batch pruning repository function.
| # 1. Resolve printer | ||
| printer = await printers_repo.resolve_by_slug_or_uuid(session, printer_key) | ||
| if printer is None: | ||
| raise HTTPException(404, detail={"error_code": "printer_not_found"}) |
There was a problem hiding this comment.
The batch print endpoint does not enforce printer-specific access controls. If a user or API key has the print scope but is restricted to specific printers, they can bypass this restriction and print to any printer via this endpoint. Please call check_printer_access(auth, printer.id) after resolving the printer to ensure proper authorization.
| # 1. Resolve printer | |
| printer = await printers_repo.resolve_by_slug_or_uuid(session, printer_key) | |
| if printer is None: | |
| raise HTTPException(404, detail={"error_code": "printer_not_found"}) | |
| # 1. Resolve printer | |
| printer = await printers_repo.resolve_by_slug_or_uuid(session, printer_key) | |
| if printer is None: | |
| raise HTTPException(404, detail={"error_code": "printer_not_found"}) | |
| check_printer_access(auth, printer.id) |
| preview_sample: | ||
| primary_id: "HH-AK-KX10-F0203" | ||
| title: "Kallax Nr.10 Fach 2-3" | ||
| qr_payload: "https://hangar.strausmann.cloud/loc/HH-AK-KX10-F0203" |
There was a problem hiding this comment.
The template contains a hardcoded real domain hangar.strausmann.cloud in the qr_payload. According to the repository style guide (Review priorities: Privacy violations), real domains or hostnames must not be hardcoded to ensure the maintainer's network is not deducible. Please use a generic placeholder like example.com instead.
qr_payload: "https://hangar.example.com/loc/HH-AK-KX10-F0203"| preview_sample: | ||
| primary_id: "HH-VK-BY03" | ||
| title: "Billy VK Nr.3 Hauptschild" | ||
| qr_payload: "https://hangar.strausmann.cloud/loc/HH-VK-BY03" |
There was a problem hiding this comment.
The template contains a hardcoded real domain hangar.strausmann.cloud in the qr_payload. According to the repository style guide (Review priorities: Privacy violations), real domains or hostnames must not be hardcoded to ensure the maintainer's network is not deducible. Please use a generic placeholder like example.com instead.
qr_payload: "https://hangar.example.com/loc/HH-VK-BY03"| preview_sample: | ||
| primary_id: "HH-AK-RAUM01" | ||
| title: "Arbeitskeller Raum" | ||
| qr_payload: "https://hangar.strausmann.cloud/loc/HH-AK-RAUM01" |
There was a problem hiding this comment.
The template contains a hardcoded real domain hangar.strausmann.cloud in the qr_payload. According to the repository style guide (Review priorities: Privacy violations), real domains or hostnames must not be hardcoded to ensure the maintainer's network is not deducible. Please use a generic placeholder like example.com instead.
qr_payload: "https://hangar.example.com/loc/HH-AK-RAUM01"| from fastapi import APIRouter, Depends, HTTPException, Path, Request, status | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from app.auth.dependencies import AuthContext |
| from app.schemas.print_batch import BatchError | ||
| from app.schemas.print_request import PrintRequest | ||
| from app.services.lookup_service import LookupFailedError | ||
| from app.services.template_loader import TemplateNotFoundError |
There was a problem hiding this comment.
Import PrintService under TYPE_CHECKING to allow typing the service parameter without circular dependencies, ensuring strict type safety.
from typing import TYPE_CHECKING
from app.schemas.print_batch import BatchError
from app.schemas.print_request import PrintRequest
from app.services.lookup_service import LookupFailedError
from app.services.template_loader import TemplateNotFoundError
if TYPE_CHECKING:
from app.services.print_service import PrintService| async def dispatch_batch( | ||
| service, # PrintService (duck-typed) | ||
| items: list[PrintRequest], | ||
| ) -> tuple[list[str], list[BatchError]]: |
There was a problem hiding this comment.
The service parameter is untyped, which introduces an implicit Any and violates the strict type safety requirement (mypy --strict) specified in the repository style guide. Please add a type annotation for the print service.
| async def dispatch_batch( | |
| service, # PrintService (duck-typed) | |
| items: list[PrintRequest], | |
| ) -> tuple[list[str], list[BatchError]]: | |
| async def dispatch_batch( | |
| service: PrintService, | |
| items: list[PrintRequest], | |
| ) -> tuple[list[str], list[BatchError]]: |
| async def prune_older_than(session: AsyncSession, hours: int = 24) -> int: | ||
| cutoff = datetime.now(UTC) - timedelta(hours=hours) | ||
| result = await session.execute( | ||
| select(PrintBatch).where(col(PrintBatch.created_at) < cutoff) | ||
| ) | ||
| rows = list(result.scalars()) | ||
| for row in rows: | ||
| await session.delete(row) | ||
| await session.commit() | ||
| return len(rows) |
There was a problem hiding this comment.
The prune_older_than function fetches all matching PrintBatch rows into memory and deletes them one by one in a loop. This is highly inefficient and can cause performance bottlenecks or high memory usage if there are many batches to prune. Please use a single bulk delete statement instead.
async def prune_older_than(session: AsyncSession, hours: int = 24) -> int:
cutoff = datetime.now(UTC) - timedelta(hours=hours)
from sqlalchemy import delete
statement = delete(PrintBatch).where(col(PrintBatch.created_at) < cutoff)
result = await session.execute(statement)
await session.commit()
return result.rowcount…odeql) CI-Failures auf feat/hangar-integration-phase-1: 1. Ruff (27 errors): I001 import-sort, UP035/UP007/UP017 type-syntax-modernisierung, F401 unused imports, B904 raise...from-Chain, E501 line-length, PTH118/PTH120 pathlib statt os.path. 20 auto-fix via ruff --fix, restliche 7 manuell (HTTPException from-Chain in batch.py, Pfad-Modernisierung im job-state-fragment Test, 3 lange Migration-Zeilen umgebrochen). 2. Ruff format (25 files): auto-format via ruff format. 3. Mypy (2 errors): - dict[str, object] | None statt nacktem dict | None auf BatchError.error_detail - PrintService TYPE_CHECKING import + Type-Annotation auf dispatch_batch - lokale detail-Variable mit explizitem Type, damit Union-Inferenz klappt 4. Privacy scan (3 hits): qr_payload in hangar-furniture-*.yaml preview_sample nutzte production-Domain. Auf hangar.example.test umgestellt — Templates sind Seeds, preview_sample ist nur Render-Vorlage, kein Live-Datum. 5. CodeQL py/log-injection (2 alerts pre-existing in events.py): Suppressions ergänzt — pid ist str(uuid.UUID), FastAPI validiert path-param als UUID bevor die Function erreicht wird. False positive, dokumentiert mit Inline-Kommentar + codeql-disable. Lokal nach Fix: 831 passed, 6 skipped, ruff/format/mypy alle grün. Refs strausmann/hangar#78
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #92 +/- ##
==========================================
- Coverage 90.50% 90.20% -0.31%
==========================================
Files 79 84 +5
Lines 3540 3684 +144
Branches 306 314 +8
==========================================
+ Hits 3204 3323 +119
- Misses 255 279 +24
- Partials 81 82 +1
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Alerts 9 (events.py:181) und 10 (events.py:227) wurden via API als false-positive dismissed — pid ist str(uuid.UUID), FastAPI validiert den path-param als UUID bevor die Function erreicht wird. Pattern analog zu Alerts 11/12/13/14 die 2026-05-16 dismissed wurden. Dieser empty commit triggert einen frischen CodeQL-Run damit die PR-Aggregat-Status-Check (CodeQL alerts on this PR) den aktuellen Dismissal-State sieht. Refs strausmann/hangar#78
GitHub CodeQL Python query py/log-injection respektiert keine inline `# codeql[...]` Suppressions (anders als LGTM-Legacy). Die Comments hatten keinen Effekt auf den Scanner — entfernen wir, damit der Diff zu main minimal bleibt. Alerts 9 (Z.181) und 10 (Z.227) wurden via API als false-positive dismissed (gleiche Begründung wie Alerts 11-14 vom 2026-05-16: pid ist str(uuid.UUID), FastAPI validiert path-param vorher). Refs strausmann/hangar#78
batch.py now validates printer.id == app.state.printer_id (Hub is single-printer at startup). Tests must align app.state.printer_id with their test-created printer for the validation to pass. For test_batch_endpoint_*.py: refactored local fixtures to yield (client, inner_app) tuple. Tests unpack and set inner_app.state.printer_id = test_printer.id before POST. For test_phase6b_sse_with_batch.py: different strategy — lifespan runs first (warmup GET /healthz) which sets app.state.printer_id to a random UUID backed by the in-memory PrintQueue. The test then creates a Printer row with id=app.state.printer_id so the DB lookup in batch.py resolves to the same ID the queue already knows about. This avoids patching PrintQueue internals. Refs strausmann/hangar#78
1. batch.py: check_printer_access(auth, printer.id) nach Resolve. Pattern aus printers.py (Z.243, 392, 418). Schliesst ACL-Bypass: api-key mit allowed_printer_ids=[A] kann jetzt nicht POST /api/print/B/batch. (Copilot + Gemini: security-critical) 2. batch.py: validiere printer.id == app.state.printer_id. Hub ist single-printer at startup (main.py:329 wired PrintService auf einen UUID). Ohne Check wuerde unsere Route silently die falsche Hardware ansprechen. Bei Mismatch: 404 printer_not_active. Future-proof fuer Multi-Printer durch klare Error-Message. (Copilot: correctness-critical) 3. test_job_state_fragment.py: Jinja2 autoescape=True. CodeQL py/jinja2-autoescape. Kein funktionaler Unterschied fuer UUID/String-Substitutionen. (CodeQL) 4. test_batch_endpoint_auth.py: nutzlose Variablen entfernt. RUF059 unused unpacks weil test sowieso skipped. Lokal: 831 passed, 6 skipped, ruff/format/mypy alle grün. Refs strausmann/hangar#78
## 0.7.0 (2026-05-31) * fix(api,security): admin_api_keys cleanup — Fixes D+E + GitGuardian (Fail 2) ([7e0b1e4](7e0b1e4)), closes [#22](#22) * Merge pull request #88 from strausmann/feat/phase-7c-api-auth ([69b133a](69b133a)), closes [#88](#88) * Merge pull request #92 from strausmann/feat/hangar-integration-phase-1 ([280f46d](280f46d)), closes [#92](#92) * Merge remote-tracking branch 'origin/main' into feat/phase-7c-api-auth ([17cda07](17cda07)) * fix(api): adressiere Copilot+Gemini Review-Findings auf PR #92 ([2dbf21c](2dbf21c)), closes [#92](#92) [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * fix(api): fix retry_after f-string not interpolated in 429 response body ([c4f050f](c4f050f)), closes [#22](#22) * fix(ci): adressiere PR #92 review-findings (ruff + mypy + privacy + codeql) ([562ef62](562ef62)), closes [#92](#92) [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * fix(security): offload bcrypt.checkpw to thread pool (Fix A) ([5282c5f](5282c5f)), closes [#22](#22) * fix(security): scope hierarchy fail-closed + no implicit read (Fixes B+C) ([924a9ca](924a9ca)), closes [#22](#22) * fix(sse): ergänze data-job-id + data-state in job_state.html-Fragment ([c9ae9f8](c9ae9f8)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * fix(tests): Phase 7b migration downgrade target — explicit revision ([8a86118](8a86118)), closes [#88](#88) [#22](#22) * fix(tests): ruff + mypy clean — Fail 1 Python lint/type CI (Round 2) ([1d91cf6](1d91cf6)), closes [#22](#22) * fix(tests): seed Printer-Helper mit explizitem slug ([a3ae386](a3ae386)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * fix(tests): wrap _get_engine idempotence test in asyncio.run ([1b173ef](1b173ef)), closes [#22](#22) * test(api): align batch endpoint tests with single-printer-binding ([84232ce](84232ce)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * test(api): batch endpoint happy path ([c7fdc80](c7fdc80)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * test(api): batch endpoint partial failure with template_not_found ([baca4c4](baca4c4)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * test(api): batch endpoint rejects with 409 when printer offline ([d54e468](d54e468)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * test(api): batch endpoint tape_mismatch + auth happy path ([a093056](a093056)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * test(auth): lower-entropy fixture strings for GitGuardian compatibility ([8b975bf](8b975bf)), closes [Hi#Entropy](https://github.com/Hi/issues/Entropy) [#22](#22) * test(fixtures): add client/db_session/auth fixtures (Phase 1 integration tests) ([0a2a9f3](0a2a9f3)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * test(sse): phase 6b stream contains all batch job events ([702f7e2](702f7e2)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * revert(events): entferne nutzlose codeql-Inline-Suppressions ([1540fd7](1540fd7)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * chore(ci): trigger fresh CodeQL aggregate after alert-dismissal ([bca88c6](bca88c6)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * chore(security): exclude test trees from GitGuardian scan ([3ff8524](3ff8524)), closes [Hi#Entropy](https://github.com/Hi/issues/Entropy) [#22](#22) * feat(api): change key format to lh_pat_ with 16-char prefix ([c09c158](c09c158)), closes [#22](#22) * feat(api): GET /api/printers?slug=... filter ([e6f112f](e6f112f)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(api): POST /api/print/{slug_or_uuid}/batch endpoint ([85e1de8](85e1de8)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(persistence): add print_batches aggregate ([b27b876](b27b876)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(printer): add slug column with backfill from name ([e8de35b](e8de35b)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(printers): add slug lookup + unified slug/uuid resolver ([f99137e](f99137e)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(schemas): add BatchRequest/Response/Error Pydantic models ([7d6c4ac](7d6c4ac)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(schemas): expose slug field on PrinterRead ([a0cbd3d](a0cbd3d)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(security): add gitleaks + gitguardian custom detector for lh_pat_ tokens ([309d6b5](309d6b5)), closes [#22](#22) * feat(security): Phase 7c Step 1 — ApiKey model, migration, and repository ([39e19a0](39e19a0)), closes [#22](#22) * feat(security): Phase 7c Step 10 — final integration + production-readiness ([544030d](544030d)), closes [#22](#22) * feat(security): Phase 7c Step 2 — key generation + bcrypt verify + LRU cache ([7605377](7605377)), closes [#22](#22) * feat(security): Phase 7c Step 3 — require_scope() FastAPI dependency + AuthContext ([033a664](033a664)), closes [#22](#22) * feat(security): Phase 7c Step 4 — wire require_scope into all routes ([2e327a4](2e327a4)), closes [#22](#22) * feat(security): Phase 7c Step 5 — in-memory token-bucket rate limiter ([380e612](380e612)), closes [#22](#22) * feat(security): Phase 7c Step 6 — per-key printer ACL check ([36c9504](36c9504)), closes [#22](#22) * feat(security): Phase 7c Step 7 — audit trail on jobs (api_key_id + source_ip) ([0104978](0104978)), closes [#22](#22) * feat(security): Phase 7c Step 8 — backend CRUD API for /api/admin/api-keys ([ee466be](ee466be)), closes [#22](#22) * feat(seeds): add hangar-furniture templates (12mm/18mm/24mm) ([d6cd621](d6cd621)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(services): best-effort batch dispatcher ([44534d0](44534d0)), closes [strausmann/hangar#78](https://github.com/strausmann/hangar/issues/78) * feat(ui): Phase 7c Step 9 — frontend HTMX /admin/api-keys UI ([46dd6f6](46dd6f6)), closes [#22](#22) * docs(phase-7c): add spec + TDD implementation plan for API-Key auth ([95e3c5d](95e3c5d)), closes [#22](#22) [skip ci]
Implements the Hub side of Phase 1 of the Brother P750W Print-Hub-Integration for Hangar (strausmann/hangar#78).
Summary
printerswith backfill fromname(Alembic migrationda865401716d)GET /api/printers?slug=...query filter (404 when unknown)POST /api/print/{slug_or_uuid}/batchnew best-effort batch endpointprint_batches-table for batch tracking (batch_id, printer_id, job_ids, created_by)hangar-furniture-12mm,-18mm,-24mmwithapp: null(generic data-mode templates)data-job-id+data-stateattributes onjob_state.htmlfragment — enables Hangar SSE-Proxy parsingclient,db_session,print_auth_headers,read_auth_headers) for the broader test suiteWhat stays untouched
POST /printsingle-job endpoint — backward-compat preservedTests
Cross-Repo
This is the Hub side. The Hangar side will follow in a separate MR on
git.strausmann.de/strausmann/hangaronce Phase A+B here is merged + deployed.Closes #22
Refs strausmann/hangar#78