UX: implement visual product roadmap

This commit is contained in:
NuklearRabbit
2026-08-10 01:05:07 +02:00
parent 13ad2ba6a3
commit f2cdad194c
40 changed files with 1010 additions and 281 deletions
+43 -8
View File
@@ -2,10 +2,11 @@ from __future__ import annotations
import uuid
from collections.abc import Sequence
from datetime import datetime
from typing import Any
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.api.deps import get_db, require_operations_manager
@@ -14,7 +15,7 @@ from app.models.booking import Booking
from app.models.customer import Customer
from app.models.data_quality import DataQualityIssue
from app.models.vehicle import Vehicle
from app.schemas import AuditEventOut, CurrentUser
from app.schemas import AuditEventOut, AuditEventPageOut, CurrentUser
router = APIRouter(prefix="/api/v1/audit", tags=["audit"])
@@ -51,26 +52,51 @@ def _resolve_entity_refs(db: Session, events: Sequence[AuditEvent]) -> dict[uuid
return refs
@router.get("", response_model=list[AuditEventOut])
@router.get("", response_model=list[AuditEventOut] | AuditEventPageOut)
def list_audit_events(
actor_label: str | None = Query(default=None),
action: str | None = Query(default=None),
entity_type: str | None = Query(default=None),
entity_ref: str | None = Query(default=None, min_length=1, max_length=100),
correlation_id: str | None = Query(default=None),
limit: int = Query(default=100, le=500),
occurred_from: datetime | None = Query(default=None),
occurred_to: datetime | None = Query(default=None),
page: int | None = Query(default=None, ge=1),
page_size: int = Query(default=25, ge=1, le=25),
db: Session = Depends(get_db),
_user: CurrentUser = Depends(require_operations_manager),
) -> list[AuditEventOut]:
stmt = select(AuditEvent).order_by(AuditEvent.occurred_at.desc()).limit(limit)
) -> list[AuditEventOut] | AuditEventPageOut:
stmt = select(AuditEvent).order_by(AuditEvent.occurred_at.desc())
if actor_label:
stmt = stmt.where(AuditEvent.actor_label == actor_label)
if action:
stmt = stmt.where(AuditEvent.action == action)
if entity_type:
stmt = stmt.where(AuditEvent.entity_type == entity_type)
if entity_ref:
matched_ids: set[uuid.UUID] = set()
for model in _ENTITY_MODELS.values():
matched_ids.update(
db.scalars(select(model.id).where(model.public_ref.ilike(f"%{entity_ref.strip()}%"))).all()
)
if not matched_ids:
if page is None:
return []
return AuditEventPageOut(
items=[], page=1, page_size=page_size, total=0, total_pages=1
)
stmt = stmt.where(AuditEvent.entity_id.in_(matched_ids))
if correlation_id:
stmt = stmt.where(AuditEvent.correlation_id == correlation_id)
events = db.scalars(stmt).all()
if occurred_from:
stmt = stmt.where(AuditEvent.occurred_at >= occurred_from)
if occurred_to:
stmt = stmt.where(AuditEvent.occurred_at <= occurred_to)
total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0
page_number = page or 1
events = db.scalars(
stmt if page is None else stmt.offset((page_number - 1) * page_size).limit(page_size)
).all()
entity_refs = _resolve_entity_refs(db, events)
out = []
@@ -94,4 +120,13 @@ def list_audit_events(
metadata=e.metadata_json,
)
)
return out
if page is None:
return out
total_pages = max(1, (total + page_size - 1) // page_size)
return AuditEventPageOut(
items=out,
page=min(page_number, total_pages),
page_size=page_size,
total=total,
total_pages=total_pages,
)