Files
plm-lottery/app/api/routes/bug_reports.py
davideandClaude Sonnet 5 e5af15087c Polish /report-bug's visual design and refine bug report semantics
Design pass on the bug report page, staying inside the site's existing
design system (tokens, IBM Plex Sans, card/badge/pill components, stroke
icon set) rather than introducing a new one:
- A slim top bar (brand mark + back-to-home pill button + language switcher)
  replaces the bare floating heading, so the page reads as part of the
  product instead of an orphaned form.
- The "write in English" notice moves inside the form card, right above the
  field it applies to, and switches from the amber "needs attention" tone to
  an accent-tinted info tone, so it doesn't visually collide with the
  bug-status badges' own use of amber for "not read yet".
- "Your reports" is promoted to a proper labeled section with a cleaner row
  layout (truncated description with a title tooltip, compact date).
- A character counter on the description field.
- The back-to-home control is now a bordered pill with an arrow icon instead
  of a bare text link with a hardcoded "←", which also meant dropping that
  hardcoded arrow from all 7 translations.

Also, two content refinements based on feedback:
- Max description length dropped from 5000 to 2000 characters, enforced on
  both the textarea and the API's Pydantic validator.
- The "read" status is relabeled from a passive "read"/"letta" to an active
  "acknowledged"/"presa in carico" (and each other language's own equivalent
  helpdesk term) — it communicates a team is on it, not just that someone
  glanced at it. Only the label changed; the underlying "read" status value
  in the API/DB is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 16:28:37 +02:00

80 lines
2.4 KiB
Python

from fastapi import APIRouter, Depends, status
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.timeutil import isoformat_utc
from app.auth.dependencies import get_current_user, get_optional_user
from app.db.models import BugReport, User
from app.db.session import get_session
router = APIRouter(prefix="/bug-reports", tags=["bug-reports"])
class BugReportCreate(BaseModel):
description: str = Field(min_length=1, max_length=2000)
contact: str | None = Field(default=None, max_length=256)
@field_validator("description")
@classmethod
def _description_not_blank(cls, value: str) -> str:
value = value.strip()
if not value:
raise ValueError("description must not be blank")
return value
@field_validator("contact")
@classmethod
def _contact_stripped(cls, value: str | None) -> str | None:
if value is None:
return None
value = value.strip()
return value or None
class BugReportResponse(BaseModel):
id: int
@router.post("", response_model=BugReportResponse, status_code=status.HTTP_201_CREATED)
async def create_bug_report(
body: BugReportCreate,
user: User | None = Depends(get_optional_user),
session: AsyncSession = Depends(get_session),
) -> BugReportResponse:
report = BugReport(
description=body.description,
contact=body.contact,
user_id=user.id if user is not None else None,
)
session.add(report)
await session.commit()
return BugReportResponse(id=report.id)
class MyBugReportResponse(BaseModel):
id: int
description: str
status: str
created_at: str
@router.get("/mine", response_model=list[MyBugReportResponse])
async def list_my_bug_reports(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> list[MyBugReportResponse]:
"""The one user-facing history view for bug reports (anonymous submissions have
no user to attribute this to, so this only ever covers ones filed while logged in)."""
reports = (
await session.scalars(
select(BugReport).where(BugReport.user_id == user.id).order_by(BugReport.id.desc())
)
).all()
return [
MyBugReportResponse(
id=r.id, description=r.description, status=r.status, created_at=isoformat_utc(r.created_at)
)
for r in reports
]