Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e22c537
spec: printers.yaml weg, Drucker in DB + /admin/printers Admin-UI
strausmann Jun 14, 2026
f4890d6
spec(#124) Round-2: Round-1-Review-Findings adressiert (5 CRITICAL + …
strausmann Jun 14, 2026
83cd15d
spec(#124) Round-3: Round-2-Review-Findings adressiert (3 HIGH + 4 ME…
strausmann Jun 14, 2026
4e1d195
spec(#124) Round-4: code-quality Round-3-Findings adressiert (2 MED +…
strausmann Jun 14, 2026
291f3e5
plan(#124): Implementation-Plan in 9 Phasen (28 Tasks)
strausmann Jun 15, 2026
b0beb31
plan(#124) Round-2: Round-1-Review-Findings adressiert (2 CRITICAL + …
strausmann Jun 17, 2026
44b944c
plan(#124) Round-3: Round-2-Review-Findings adressiert (2 MED + 3 LOW)
strausmann Jun 17, 2026
5d37e7f
plan(#124) Round-4 FINAL: Round-3-Review-Findings adressiert (4 LOWs)
strausmann Jun 17, 2026
35287d0
spec(#124) RESET: Neue Spec basierend auf Live-State (ENV-VARS → DB)
strausmann Jun 19, 2026
e0a9259
spec(#124) Round-5: Live-State-Reset auf approved Round-4-Spec angewandt
strausmann Jun 19, 2026
29f0c23
spec(#124) Round-6: Round-5-Findings adressiert (3 HIGH + 2 MED + 3 L…
strausmann Jun 19, 2026
2b2b461
spec(#124) Round-6 FINAL: Working Draft mit known issues — Plan macht…
strausmann Jun 19, 2026
4eb6ffa
plan(#124) Round-5 NEW: Two-Container Plan mit Live-Verifikation-Pflicht
strausmann Jun 19, 2026
8d90c69
plan(#124) Round-6: Round-5-Findings adressiert (H1+M1+M2+LOWs)
strausmann Jun 19, 2026
5e7c103
chore(privacy): scrub private artefacts + plan Round-7 fixes (#124)
strausmann Jun 19, 2026
e8d831e
chore(privacy/spec): Gemini Round-8 Findings (#124) — 3 MED
strausmann Jun 20, 2026
fc4ae25
docs(spec/plan): Gemini Round-9 Findings (#124) — DB-Pfad-Konsistenz
strausmann Jun 20, 2026
c0e81f7
docs(#124): Phase 0 Live-State sammeln (alle Werte als ground truth)
strausmann Jun 20, 2026
4fdf7ef
feat(#124): SQLite SERIALIZABLE + existing WAL Pragma-Listener wieder…
strausmann Jun 20, 2026
82163e7
feat(#124): PrinterDisabledError Exception für Soft-Delete-Status
strausmann Jun 20, 2026
29c81b5
feat(#124): Alembic-Migration Schema-Erweiterung + printers_audit + B…
strausmann Jun 20, 2026
6886a55
fix(#124): Index-Drift zwischen Migration und PrinterAudit-Model beho…
strausmann Jun 20, 2026
bd6aa73
test(#124): RFC-5737 IPs (192.0.2.x) in test_migration_124.py (Repo-K…
strausmann Jun 20, 2026
b019bb2
feat(#124): derive_printer_id 4-arg mit timezone-aware created_at_utc
strausmann Jun 20, 2026
f9d8fa8
feat(#124): Pydantic-Schemas fuer Admin-API (SNMP verschachtelt)
strausmann Jun 20, 2026
833ac49
feat(#124): audit_redaction.py SNMP-Community Redaction Helper
strausmann Jun 20, 2026
2ba31e6
feat(#124): printer_model_registry fuer Frontend-Model-Dropdown
strausmann Jun 20, 2026
1eff078
feat(#124): PrinterAdminService CRUD + Flattening-Helper + Audit-Reco…
strausmann Jun 20, 2026
bd8d449
feat(#124): printers_repo enabled-Filter + GET /api/printers filtert …
strausmann Jun 20, 2026
711bd24
feat(#124): JSON-API /api/v1/admin/printers (6 Endpoints)
strausmann Jun 20, 2026
9c7adb0
feat(#124): PrintService enabled-Check + 409-Mapping für PrinterDisab…
strausmann Jun 20, 2026
bbf2b59
refactor(#124): PrinterConfigLoader + lifespan-Sync + 5 Test-Files en…
strausmann Jun 20, 2026
9083868
feat(#124): gorilla/csrf + existing Admin-API-Keys-Routes nachgerüstet
strausmann Jun 20, 2026
a3f30e8
feat(#124): oapi-codegen Regeneration für Admin-Printers-Endpoints (T…
strausmann Jun 20, 2026
11f1f97
feat(#124): Admin-UI Frontend-Handler + Templates + Router für Drucke…
strausmann Jun 20, 2026
342243b
fix(#124): Edit-Form SNMP-Prefill + RFC 5737 Placeholder
strausmann Jun 20, 2026
51bc230
fix(ci): ruff I001 import-order in main.py + RFC 5737 IPs in tests (#…
strausmann Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion backend/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@
# ... etc.


def _include_object(
obj: object,
name: str | None, # noqa: ARG001
type_: str,
reflected: bool, # noqa: ARG001
compare_to: object, # noqa: ARG001
) -> bool:
"""Filter expression-based indexes from autogenerate comparison.

SQLite cannot reflect text-expression indexes (e.g. ``created_at DESC``)
back from the database, so Alembic always sees a diff between the in-memory
model (which uses ``text("created_at DESC")``) and the reflected schema
(which returns the plain column name). We exclude any ``Index`` whose
expressions contain a SQLAlchemy ``TextClause`` from autogenerate so that
``alembic check`` stays green on SQLite dev/CI while the migration itself
still creates the correct expression index at upgrade time.
"""
if type_ == "index":
from sqlalchemy import Index as _Index
from sqlalchemy.sql.elements import TextClause

if isinstance(obj, _Index):
for col in obj.expressions:
if isinstance(col, TextClause):
return False
return True


def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.

Expand All @@ -49,14 +77,19 @@ def run_migrations_offline() -> None:
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
include_object=_include_object,
)

with context.begin_transaction():
context.run_migrations()


def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection,
target_metadata=target_metadata,
include_object=_include_object,
)

with context.begin_transaction():
context.run_migrations()
Expand Down
122 changes: 122 additions & 0 deletions backend/alembic/versions/42fbd015698d_printers_audit_and_backfill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""printers_audit_and_backfill

Issue #124: Schema-Erweiterung der printers-Tabelle um queue_timeout_s und
cut_defaults_half_cut, neue printers_audit-Tabelle für Änderungsprotokoll sowie
Backfill des connection.snmp-Blocks für Bestandsdrucker.

Revision ID: 42fbd015698d
Revises: 20260605b2c3d4e5
Create Date: 2026-06-20

"""

from __future__ import annotations

import json
from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "42fbd015698d"
down_revision: str | Sequence[str] | None = "20260605b2c3d4e5"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def _backfill_snmp(bind: sa.engine.Connection) -> None:
"""Befüllt connection.snmp für Bestandsdrucker ohne SNMP-Konfiguration.

Pro printers-Row: wenn connection.snmp fehlt UND connection.host vorhanden,
wird snmp = {"discover": false, "community": "public"} gesetzt.

Schutzklauseln:
- NULL connection wird übersprungen
- Fehlendes host-Feld wird übersprungen
- Bereits vorhandenes snmp-Feld bleibt unverändert (idempotent)

Diese Funktion ist als top-level Funktion definiert damit sie direkt
via ``await conn.run_sync(mig._backfill_snmp)`` in Tests testbar ist.
"""
rows = bind.execute(sa.text("SELECT id, connection FROM printers")).all()
for row in rows:
pid, conn_raw = row[0], row[1]
if conn_raw is None:
continue
conn_json: dict = json.loads(conn_raw) if isinstance(conn_raw, str) else conn_raw
if "host" not in conn_json:
continue
if "snmp" in conn_json:
continue # idempotent — bereits konfiguriert
conn_json["snmp"] = {"discover": False, "community": "public"}
bind.execute(
sa.text("UPDATE printers SET connection = :c WHERE id = :pid"),
{"c": json.dumps(conn_json), "pid": pid},
)


def upgrade() -> None:
"""Schema-Erweiterung: neue Spalten auf printers + printers_audit-Tabelle + Backfill."""
# 1. Neue Spalten auf printers (batch_alter_table für SQLite-Kompatibilität)
with op.batch_alter_table("printers", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"queue_timeout_s",
sa.Integer(),
nullable=False,
server_default="30",
)
)
batch_op.add_column(
sa.Column(
"cut_defaults_half_cut",
sa.Boolean(),
nullable=False,
server_default="0",
)
)

# 2. printers_audit-Tabelle (kein FK auf printers — Soft-Delete behält Rows)
op.create_table(
"printers_audit",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("printer_id", sa.Uuid(), nullable=False),
sa.Column("slug", sa.String(255), nullable=False),
sa.Column("action", sa.String(50), nullable=False),
sa.Column("before_json", sa.JSON(), nullable=True),
sa.Column("after_json", sa.JSON(), nullable=True),
sa.Column("updated_by", sa.String(255), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text("CURRENT_TIMESTAMP"),
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"idx_printers_audit_printer_id",
"printers_audit",
["printer_id"],
)
op.create_index(
"idx_printers_audit_created_at_desc",
"printers_audit",
[sa.text("created_at DESC")],
)

# 3. Backfill connection.snmp für Bestandsdrucker
bind = op.get_bind()
_backfill_snmp(bind)


def downgrade() -> None:
"""Downgrade ist ein no-op.

Die neuen Spalten und die printers_audit-Tabelle können nicht sicher
zurückgerollt werden ohne Datenverlust (Audit-Logs sind produktiv relevant).
Der SNMP-Backfill kann ebenfalls nicht rückgängig gemacht werden ohne
Originalzustand zu kennen. Downgrade wird daher nicht unterstützt.
"""
pass
Loading
Loading