# DAK-Datenschutz Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Hide personal data (nachname, vorname, geburtsdatum) from dak_mitarbeiter users at the API level, with a time-limited disclosure request mechanism for KVNR error resolution. **Architecture:** Backend-enforced field masking via a helper function that strips sensitive fields from Case responses for non-admin users. New DisclosureRequest model with admin approval workflow and 24h expiry. Notifications via existing Notification model. Frontend hides columns/fields based on role and disclosure status. **Tech Stack:** FastAPI, SQLAlchemy 2.0 (mapped_column), Alembic, Pydantic v2, React 19, TypeScript, shadcn/ui --- ### Task 1: Alembic Migration — disclosure_requests table + Notification constraint **Files:** - Create: `backend/alembic/versions/xxxx_add_disclosure_requests.py` (manual migration on Hetzner) **Step 1: Write migration SQL** The migration creates the `disclosure_requests` table and updates the Notification CHECK constraint to include the new `disclosure_request` type. ```sql CREATE TABLE disclosure_requests ( id INT AUTO_INCREMENT PRIMARY KEY, case_id INT NOT NULL, requester_id INT NOT NULL, reason VARCHAR(500) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', reviewed_by INT NULL, reviewed_at DATETIME NULL, expires_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_dr_case FOREIGN KEY (case_id) REFERENCES cases(id), CONSTRAINT fk_dr_requester FOREIGN KEY (requester_id) REFERENCES users(id), CONSTRAINT fk_dr_reviewer FOREIGN KEY (reviewed_by) REFERENCES users(id), CONSTRAINT chk_dr_status CHECK (status IN ('pending', 'approved', 'rejected')), INDEX idx_dr_case_status (case_id, status), INDEX idx_dr_requester (requester_id), INDEX idx_dr_status (status) ); ``` Also update the Notification CHECK constraint: ```sql ALTER TABLE notifications DROP CONSTRAINT chk_notif; ALTER TABLE notifications ADD CONSTRAINT chk_notif CHECK ( notification_type IN ( 'new_cases_uploaded','icd_entered','icd_uploaded', 'report_ready','coding_completed','disclosure_request','disclosure_resolved' ) ); ``` **Step 2: Run migration on Hetzner 1** ```bash ssh hetzner1 "mysql -u dak_user -p dak_portal < /tmp/disclosure_migration.sql" ``` **Step 3: Commit migration file** ```bash git add backend/alembic/versions/ git commit -m "feat: add disclosure_requests table and update notification types" ``` --- ### Task 2: Backend Model — DisclosureRequest **Files:** - Modify: `backend/app/models/audit.py` (add DisclosureRequest class after Notification) **Step 1: Add DisclosureRequest model** Add after the Notification class in `backend/app/models/audit.py`: ```python class DisclosureRequest(Base): __tablename__ = "disclosure_requests" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) case_id: Mapped[int] = mapped_column( Integer, ForeignKey("cases.id"), nullable=False ) requester_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) reason: Mapped[str] = mapped_column(String(500), nullable=False) status: Mapped[str] = mapped_column( String(20), nullable=False, server_default="pending" ) reviewed_by: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("users.id"), nullable=True ) reviewed_at: Mapped[Optional[dt.datetime]] = mapped_column( DateTime, nullable=True ) expires_at: Mapped[Optional[dt.datetime]] = mapped_column( DateTime, nullable=True ) created_at: Mapped[dt.datetime] = mapped_column( DateTime, nullable=False, server_default=func.now() ) # Relationships case: Mapped["Case"] = relationship(foreign_keys=[case_id]) requester: Mapped["User"] = relationship(foreign_keys=[requester_id]) reviewer: Mapped[Optional["User"]] = relationship(foreign_keys=[reviewed_by]) __table_args__ = ( CheckConstraint( "status IN ('pending', 'approved', 'rejected')", name="chk_dr_status", ), Index("idx_dr_case_status", "case_id", "status"), Index("idx_dr_requester", "requester_id"), Index("idx_dr_status", "status"), ) ``` Also update the Notification CHECK constraint string to include the new types: ```python # In Notification.__table_args__, change: "'new_cases_uploaded','icd_entered','icd_uploaded'," "'report_ready','coding_completed')" # to: "'new_cases_uploaded','icd_entered','icd_uploaded'," "'report_ready','coding_completed','disclosure_request','disclosure_resolved')" ``` **Step 2: Commit** ```bash git add backend/app/models/audit.py git commit -m "feat: add DisclosureRequest model and update notification types" ``` --- ### Task 3: Backend Schemas — Disclosure + CaseResponse masking **Files:** - Modify: `backend/app/schemas/case.py` (add disclosure_granted field, masking helper) - Create: `backend/app/schemas/disclosure.py` (request/response schemas) **Step 1: Add disclosure schemas** Create `backend/app/schemas/disclosure.py`: ```python """Pydantic schemas for disclosure request endpoints.""" from datetime import datetime from typing import Optional from pydantic import BaseModel class DisclosureRequestCreate(BaseModel): """Payload for creating a disclosure request.""" reason: str class DisclosureRequestResponse(BaseModel): """Response for a single disclosure request.""" id: int case_id: int requester_id: int requester_username: Optional[str] = None fall_id: Optional[str] = None reason: str status: str reviewed_by: Optional[int] = None reviewed_at: Optional[datetime] = None expires_at: Optional[datetime] = None created_at: datetime model_config = {"from_attributes": True} class DisclosureRequestUpdate(BaseModel): """Payload for approving/rejecting a disclosure request.""" status: str # "approved" or "rejected" class DisclosureCountResponse(BaseModel): """Count of pending disclosure requests.""" pending_count: int ``` **Step 2: Add disclosure fields to CaseResponse** In `backend/app/schemas/case.py`, add two optional fields to `CaseResponse`: ```python # Add after updated_at: disclosure_granted: bool = False disclosure_expires_at: Optional[datetime] = None ``` **Step 3: Create masking helper function** Add a helper function at the bottom of `backend/app/schemas/case.py`: ```python SENSITIVE_FIELDS = ("nachname", "vorname", "geburtsdatum", "anrede") def mask_case_for_mitarbeiter(case_dict: dict, disclosure_granted: bool = False) -> dict: """Remove sensitive personal data fields for dak_mitarbeiter users. If disclosure_granted is True, the fields remain visible. """ if not disclosure_granted: for field in SENSITIVE_FIELDS: case_dict[field] = None case_dict["disclosure_granted"] = disclosure_granted return case_dict ``` **Step 4: Commit** ```bash git add backend/app/schemas/case.py backend/app/schemas/disclosure.py git commit -m "feat: add disclosure schemas and case response masking helper" ``` --- ### Task 4: Backend Service — Disclosure logic **Files:** - Create: `backend/app/services/disclosure_service.py` **Step 1: Create disclosure service** ```python """Disclosure request service — create, review, check active grants.""" from datetime import datetime, timedelta, timezone from typing import Optional from sqlalchemy.orm import Session from app.models.audit import DisclosureRequest, Notification from app.models.case import Case from app.models.user import User def has_active_disclosure(db: Session, case_id: int, user_id: int) -> tuple[bool, Optional[datetime]]: """Check if user has an active (approved, non-expired) disclosure for a case. Returns (granted: bool, expires_at: datetime | None). """ dr = ( db.query(DisclosureRequest) .filter( DisclosureRequest.case_id == case_id, DisclosureRequest.requester_id == user_id, DisclosureRequest.status == "approved", DisclosureRequest.expires_at > datetime.now(timezone.utc), ) .first() ) if dr: return True, dr.expires_at return False, None def has_pending_request(db: Session, case_id: int, user_id: int) -> bool: """Check if user already has a pending disclosure request for a case.""" return ( db.query(DisclosureRequest) .filter( DisclosureRequest.case_id == case_id, DisclosureRequest.requester_id == user_id, DisclosureRequest.status == "pending", ) .first() ) is not None def create_disclosure_request( db: Session, case_id: int, user_id: int, reason: str ) -> DisclosureRequest: """Create a new disclosure request and notify admins.""" dr = DisclosureRequest( case_id=case_id, requester_id=user_id, reason=reason, ) db.add(dr) db.flush() # get dr.id # Notify all active admins case = db.query(Case).filter(Case.id == case_id).first() fall_id = case.fall_id if case else str(case_id) requester = db.query(User).filter(User.id == user_id).first() requester_name = requester.username if requester else str(user_id) admins = db.query(User).filter(User.role == "admin", User.is_active == True).all() # noqa: E712 for admin in admins: db.add(Notification( recipient_id=admin.id, notification_type="disclosure_request", title=f"Freigabe-Anfrage für Fall {fall_id}", message=f"{requester_name} bittet um Einsicht in Personendaten. Begründung: {reason}", related_entity_type="disclosure_request", related_entity_id=dr.id, )) db.commit() db.refresh(dr) return dr def review_disclosure_request( db: Session, request_id: int, admin_id: int, new_status: str ) -> DisclosureRequest: """Approve or reject a disclosure request.""" dr = db.query(DisclosureRequest).filter(DisclosureRequest.id == request_id).first() if not dr: raise ValueError("Disclosure request not found") if dr.status != "pending": raise ValueError("Request already reviewed") dr.status = new_status dr.reviewed_by = admin_id dr.reviewed_at = datetime.now(timezone.utc) if new_status == "approved": dr.expires_at = datetime.now(timezone.utc) + timedelta(hours=24) # Notify requester case = db.query(Case).filter(Case.id == dr.case_id).first() fall_id = case.fall_id if case else str(dr.case_id) status_text = "genehmigt" if new_status == "approved" else "abgelehnt" db.add(Notification( recipient_id=dr.requester_id, notification_type="disclosure_resolved", title=f"Freigabe-Anfrage {status_text}", message=f"Ihre Anfrage für Fall {fall_id} wurde {status_text}.", related_entity_type="disclosure_request", related_entity_id=dr.id, )) db.commit() db.refresh(dr) return dr def get_pending_requests(db: Session) -> list[DisclosureRequest]: """Return all pending disclosure requests with related data.""" return ( db.query(DisclosureRequest) .filter(DisclosureRequest.status == "pending") .order_by(DisclosureRequest.created_at.desc()) .all() ) def get_pending_count(db: Session) -> int: """Return count of pending disclosure requests.""" return ( db.query(DisclosureRequest) .filter(DisclosureRequest.status == "pending") .count() ) ``` **Step 2: Commit** ```bash git add backend/app/services/disclosure_service.py git commit -m "feat: add disclosure service with create, review, and check logic" ``` --- ### Task 5: Backend Endpoints — Case masking + Disclosure API **Files:** - Modify: `backend/app/api/cases.py` (mask sensitive fields, restrict search) - Modify: `backend/app/api/admin.py` (add disclosure endpoints) - Modify: `backend/app/main.py` (no change needed — admin router already mounted) **Step 1: Add masking to case endpoints in `backend/app/api/cases.py`** Import the masking helper and disclosure service: ```python from app.schemas.case import SENSITIVE_FIELDS, mask_case_for_mitarbeiter from app.services.disclosure_service import has_active_disclosure ``` Create a helper function to serialize a list of cases with masking: ```python def _serialize_cases(cases: list[Case], user: User, db: Session) -> list[dict]: """Serialize cases, masking sensitive fields for dak_mitarbeiter.""" if user.role == "admin": return [CaseResponse.model_validate(c).model_dump() for c in cases] result = [] for c in cases: case_dict = CaseResponse.model_validate(c).model_dump() granted, expires_at = has_active_disclosure(db, c.id, user.id) case_dict = mask_case_for_mitarbeiter(case_dict, disclosure_granted=granted) if expires_at: case_dict["disclosure_expires_at"] = expires_at result.append(case_dict) return result ``` Modify `list_cases` endpoint: - For `dak_mitarbeiter`: remove `nachname` and `vorname` from the search OR clause - Use `_serialize_cases` instead of returning raw cases Modify `get_case` endpoint: - Use masking for single case response Modify `list_pending_icd` endpoint: - Use masking for case list response **Step 2: Restrict search for dak_mitarbeiter** In `list_cases`, change the search filter: ```python if search: like_pattern = f"%{search}%" if user.role == "admin": query = query.filter( or_( Case.nachname.ilike(like_pattern), Case.vorname.ilike(like_pattern), Case.fall_id.ilike(like_pattern), Case.kvnr.ilike(like_pattern), ) ) else: # dak_mitarbeiter can only search by fall_id and kvnr query = query.filter( or_( Case.fall_id.ilike(like_pattern), Case.kvnr.ilike(like_pattern), ) ) ``` **Step 3: Add disclosure request endpoint to cases.py** ```python from app.schemas.disclosure import DisclosureRequestCreate, DisclosureRequestResponse from app.services.disclosure_service import ( create_disclosure_request, has_active_disclosure, has_pending_request, ) @router.post("/{case_id}/disclosure-request", response_model=DisclosureRequestResponse) def request_disclosure( case_id: int, payload: DisclosureRequestCreate, request: Request, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """Request disclosure of personal data for a case (dak_mitarbeiter).""" # Verify case exists case = db.query(Case).filter(Case.id == case_id).first() if not case: raise HTTPException(status.HTTP_404_NOT_FOUND, "Case not found") # Check for existing pending request if has_pending_request(db, case_id, user.id): raise HTTPException(status.HTTP_409_CONFLICT, "Anfrage bereits ausstehend") # Check for active disclosure granted, _ = has_active_disclosure(db, case_id, user.id) if granted: raise HTTPException(status.HTTP_409_CONFLICT, "Freigabe bereits aktiv") dr = create_disclosure_request(db, case_id, user.id, payload.reason) log_action( db, user_id=user.id, action="disclosure_requested", entity_type="case", entity_id=case_id, new_values={"reason": payload.reason}, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), ) return DisclosureRequestResponse( id=dr.id, case_id=dr.case_id, requester_id=dr.requester_id, reason=dr.reason, status=dr.status, created_at=dr.created_at, ) ``` **Step 4: Add admin disclosure endpoints to `backend/app/api/admin.py`** ```python from app.models.audit import DisclosureRequest from app.schemas.disclosure import ( DisclosureCountResponse, DisclosureRequestResponse, DisclosureRequestUpdate, ) from app.services.disclosure_service import ( get_pending_count, get_pending_requests, review_disclosure_request, ) @router.get("/disclosure-requests", response_model=list[DisclosureRequestResponse]) def list_disclosure_requests( status_filter: Optional[str] = Query(None, alias="status"), admin: User = Depends(require_admin), db: Session = Depends(get_db), ): """List disclosure requests (default: pending).""" query = db.query(DisclosureRequest) if status_filter: query = query.filter(DisclosureRequest.status == status_filter) else: query = query.filter(DisclosureRequest.status == "pending") requests = query.order_by(DisclosureRequest.created_at.desc()).all() result = [] for dr in requests: result.append(DisclosureRequestResponse( id=dr.id, case_id=dr.case_id, requester_id=dr.requester_id, requester_username=dr.requester.username if dr.requester else None, fall_id=dr.case.fall_id if dr.case else None, reason=dr.reason, status=dr.status, reviewed_by=dr.reviewed_by, reviewed_at=dr.reviewed_at, expires_at=dr.expires_at, created_at=dr.created_at, )) return result @router.get("/disclosure-requests/count", response_model=DisclosureCountResponse) def disclosure_request_count( admin: User = Depends(require_admin), db: Session = Depends(get_db), ): """Return count of pending disclosure requests.""" return DisclosureCountResponse(pending_count=get_pending_count(db)) @router.put("/disclosure-requests/{request_id}", response_model=DisclosureRequestResponse) def review_disclosure( request_id: int, payload: DisclosureRequestUpdate, request: Request, admin: User = Depends(require_admin), db: Session = Depends(get_db), ): """Approve or reject a disclosure request.""" if payload.status not in ("approved", "rejected"): raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "Status must be 'approved' or 'rejected'") try: dr = review_disclosure_request(db, request_id, admin.id, payload.status) except ValueError as e: raise HTTPException(status.HTTP_400_BAD_REQUEST, str(e)) log_action( db, user_id=admin.id, action=f"disclosure_{payload.status}", entity_type="disclosure_request", entity_id=request_id, new_values={"status": payload.status, "case_id": dr.case_id}, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), ) return DisclosureRequestResponse( id=dr.id, case_id=dr.case_id, requester_id=dr.requester_id, requester_username=dr.requester.username if dr.requester else None, fall_id=dr.case.fall_id if dr.case else None, reason=dr.reason, status=dr.status, reviewed_by=dr.reviewed_by, reviewed_at=dr.reviewed_at, expires_at=dr.expires_at, created_at=dr.created_at, ) ``` **Step 5: Commit** ```bash git add backend/app/api/cases.py backend/app/api/admin.py git commit -m "feat: add case masking for dak_mitarbeiter and disclosure endpoints" ``` --- ### Task 6: Frontend Types & Services **Files:** - Modify: `frontend/src/types/index.ts` (add disclosure types, update Case) - Create: `frontend/src/services/disclosureService.ts` **Step 1: Update Case interface** In `frontend/src/types/index.ts`, add to the Case interface: ```typescript // Add after updated_at in Case interface: disclosure_granted?: boolean disclosure_expires_at?: string | null ``` Add new types: ```typescript // Disclosure Requests export interface DisclosureRequest { id: number case_id: number requester_id: number requester_username: string | null fall_id: string | null reason: string status: 'pending' | 'approved' | 'rejected' reviewed_by: number | null reviewed_at: string | null expires_at: string | null created_at: string } export interface DisclosureRequestCreate { reason: string } export interface DisclosureCountResponse { pending_count: number } ``` **Step 2: Create disclosure service** Create `frontend/src/services/disclosureService.ts`: ```typescript import api from './api' import type { DisclosureRequest, DisclosureCountResponse } from '@/types' export async function requestDisclosure(caseId: number, reason: string): Promise { const res = await api.post(`/cases/${caseId}/disclosure-request`, { reason }) return res.data } export async function getDisclosureRequests(status?: string): Promise { const params = status ? { status } : {} const res = await api.get('/admin/disclosure-requests', { params }) return res.data } export async function getDisclosureCount(): Promise { const res = await api.get('/admin/disclosure-requests/count') return res.data.pending_count } export async function reviewDisclosure(requestId: number, status: 'approved' | 'rejected'): Promise { const res = await api.put(`/admin/disclosure-requests/${requestId}`, { status }) return res.data } ``` **Step 3: Commit** ```bash git add frontend/src/types/index.ts frontend/src/services/disclosureService.ts git commit -m "feat: add disclosure types and service functions" ``` --- ### Task 7: Frontend — CasesPage masking **Files:** - Modify: `frontend/src/pages/CasesPage.tsx` (hide columns, adjust search) **Step 1: Hide Nachname/Vorname columns for dak_mitarbeiter** In CasesPage, use the existing `useAuth()` hook to get `isAdmin`. Conditionally render the table headers and cells: ```tsx // In the TableHeader, wrap Nachname/Vorname heads: {isAdmin && Nachname} {isAdmin && Vorname} // In each TableRow, wrap corresponding cells: {isAdmin && {c.nachname}} {isAdmin && {c.vorname || '-'}} ``` **Step 2: Adjust search placeholder** ```tsx ``` **Step 3: Commit** ```bash git add frontend/src/pages/CasesPage.tsx git commit -m "feat: hide personal data columns for dak_mitarbeiter in case list" ``` --- ### Task 8: Frontend — CaseDetail disclosure request **Files:** - Modify: `frontend/src/pages/CasesPage.tsx` (CaseDetail component) - Modify: `frontend/src/pages/cases/fieldConfig.ts` (add visibility property) **Step 1: Add `visibleTo` property to FieldConfig** In `frontend/src/pages/cases/fieldConfig.ts`, add to FieldConfig interface: ```typescript visibleTo?: 'admin' | 'all' // defaults to 'all' if not set ``` Mark sensitive fields: ```typescript { key: 'anrede', label: 'Anrede', type: 'select', editableBy: 'admin', visibleTo: 'admin', options: ANREDE_OPTIONS }, { key: 'vorname', label: 'Vorname', type: 'text', editableBy: 'admin', visibleTo: 'admin', placeholder: 'Vorname' }, { key: 'nachname', label: 'Nachname', type: 'text', editableBy: 'admin', visibleTo: 'admin', placeholder: 'Nachname' }, { key: 'geburtsdatum', label: 'Geburtsdatum', type: 'date', editableBy: 'admin', visibleTo: 'admin' }, ``` **Step 2: Filter fields in CaseDetail rendering** In the CaseDetail component, filter section fields by visibility: ```tsx {CASE_SECTIONS.map((section) => { const visibleFields = section.fields.filter((field) => { if (field.visibleTo === 'admin' && !isAdmin && !caseData.disclosure_granted) return false return true }) if (visibleFields.length === 0) return null return (
... {visibleFields.map((field) => (...))}
) })} ``` **Step 3: Add disclosure request button in CaseDetail** Add after the status badges section in CaseDetail, when user is not admin: ```tsx {!isAdmin && !caseData.disclosure_granted && ( )} {caseData.disclosure_granted && caseData.disclosure_expires_at && ( Personendaten sichtbar bis {formatDateTime(caseData.disclosure_expires_at)} )} ``` Create the DisclosureRequestButton component within CasesPage.tsx: ```tsx function DisclosureRequestButton({ caseId }: { caseId: number }) { const [open, setOpen] = useState(false) const [reason, setReason] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [success, setSuccess] = useState(false) const submit = async () => { if (!reason.trim()) return setLoading(true) setError('') try { await requestDisclosure(caseId, reason.trim()) setSuccess(true) setOpen(false) } catch (err: any) { setError(err.response?.data?.detail || 'Fehler beim Senden der Anfrage') } finally { setLoading(false) } } if (success) { return

Freigabe-Anfrage gesendet.

} return ( <> {open && (

Begründung für die Einsicht in Personendaten:

setReason(e.target.value)} placeholder="z.B. KVNR-Fehler, Identifikation nötig" /> {error &&

{error}

}
)} ) } ``` **Step 4: Commit** ```bash git add frontend/src/pages/CasesPage.tsx frontend/src/pages/cases/fieldConfig.ts git commit -m "feat: add disclosure request UI and field visibility for dak_mitarbeiter" ``` --- ### Task 9: Frontend — Admin Disclosures Page **Files:** - Create: `frontend/src/pages/DisclosuresPage.tsx` **Step 1: Create the admin disclosures page** ```tsx import { useState, useEffect } from 'react' import { Check, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import type { DisclosureRequest } from '@/types' import { getDisclosureRequests, reviewDisclosure } from '@/services/disclosureService' export function DisclosuresPage() { const [requests, setRequests] = useState([]) const [loading, setLoading] = useState(true) const load = () => { setLoading(true) getDisclosureRequests('pending') .then(setRequests) .catch(() => setRequests([])) .finally(() => setLoading(false)) } useEffect(() => { load() }, []) const handleReview = async (id: number, status: 'approved' | 'rejected') => { await reviewDisclosure(id, status) load() } return (

Freigabe-Anfragen

{loading ? (
{[1,2,3].map(i => )}
) : requests.length === 0 ? (

Keine offenen Anfragen.

) : (
{requests.map((dr) => (
Fall {dr.fall_id || dr.case_id} {dr.status}

Angefragt von:{' '} {dr.requester_username || `User #${dr.requester_id}`}

Begründung:{' '} {dr.reason}

{new Date(dr.created_at).toLocaleString('de-DE')}

))}
)}
) } ``` **Step 2: Commit** ```bash git add frontend/src/pages/DisclosuresPage.tsx git commit -m "feat: add admin disclosures page for reviewing data access requests" ``` --- ### Task 10: Frontend — Routing & Sidebar **Files:** - Modify: `frontend/src/App.tsx` (add route) - Modify: `frontend/src/components/layout/Sidebar.tsx` (add admin nav item) **Step 1: Add route in App.tsx** Import DisclosuresPage and add the route alongside other admin routes: ```tsx import { DisclosuresPage } from './pages/DisclosuresPage' // Add route: } /> ``` **Step 2: Add sidebar entry** In `frontend/src/components/layout/Sidebar.tsx`, import `ShieldCheck` from lucide-react and add to adminNavItems: ```typescript import { ShieldCheck } from 'lucide-react' const adminNavItems: NavItem[] = [ { label: 'Freigabe-Anfragen', to: '/admin/disclosures', icon: ShieldCheck }, { label: 'Benutzer', to: '/admin/users', icon: Users }, { label: 'Einladungen', to: '/admin/invitations', icon: Mail }, { label: 'Audit-Log', to: '/admin/audit', icon: History }, ] ``` **Step 3: Commit** ```bash git add frontend/src/App.tsx frontend/src/components/layout/Sidebar.tsx git commit -m "feat: add disclosure admin route and sidebar entry" ``` --- ### Task 11: Build & Deploy **Step 1: Build frontend** ```bash cd frontend && pnpm build ``` Expected: Build succeeds without errors. **Step 2: Run migration on Hetzner 1** Execute the SQL migration on the production database. **Step 3: Deploy to Hetzner 1** ```bash # Push and merge git push origin develop git checkout main && git merge develop && git push origin main # Deploy ssh hetzner1 "cd /opt/dak-portal && git pull origin main" ssh hetzner1 "cd /opt/dak-portal/frontend && pnpm install && pnpm build && cp -r dist/* /var/www/vhosts/complexcaresolutions.de/dak.complexcaresolutions.de/dist/" ssh hetzner1 "systemctl restart dak-backend" ``` **Step 4: Verify** - Login as admin: all data visible, disclosure admin page accessible - Login as dak_mitarbeiter: Nachname/Vorname/Geburtsdatum hidden in list and detail - Search only works for Fall-ID/KVNR as dak_mitarbeiter - Disclosure request flow works end-to-end