feat(L+M): configurable dashboard widget system + test framework

Phase L: Dashboard widget system
- Migration 046: dashboard_configs table (user/tenant/role fallback cascade)
- DashboardConfig model + dashboard_service with get/upsert per-user and tenant-default
- API router: GET/PUT /api/dashboard/config, GET/PUT /api/dashboard/tenant-default
- Frontend: 5 widget components (ProductionStats, QueueStatus, RecentRenders, CostOverview, WorkerStatus)
- DashboardGrid with API-backed config, DashboardCustomizeModal (checkbox selection, save/cancel)
- Dashboard.tsx: widget grid + analytics section (privileged users)
- Admin.tsx: restructured with new section order and Maintenance 2-col grid

Phase M: Test framework
- Backend: pytest-asyncio + pytest-cov + factory-boy in pyproject.toml
- conftest.py: excel_dir fixtures + async DB fixtures + mock storage/celery stubs
- Domain tests: billing_service, cache_service, notifications_service, imports_sanity
- Integration: test_api_health.py smoke test (requires running backend)
- Frontend: vitest + @testing-library/react + msw added to package.json
- vite.config.ts: test block (jsdom + globals + setupFiles)
- tsconfig.json: exclude src/__tests__ from main tsc (test runner handles its own types)
- MSW handlers for /api/auth/me, Billing.test.tsx, formatters.test.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 21:50:07 +01:00
parent 19c15adbee
commit bfc0050580
38 changed files with 4210 additions and 13 deletions
+56 -1
View File
@@ -1,7 +1,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { toast } from 'sonner'
import { UserPlus, Trash2, Pencil, ChevronDown, ChevronUp, ChevronRight, Settings, RefreshCw, CheckCircle2, XCircle, Clock, DollarSign, Layers, AlertTriangle, Upload, FileBox, Plus, X } from 'lucide-react'
import { UserPlus, Trash2, Pencil, ChevronDown, ChevronUp, ChevronRight, Settings, RefreshCw, CheckCircle2, XCircle, Clock, DollarSign, Layers, AlertTriangle, Upload, FileBox, Plus, X, LayoutDashboard } from 'lucide-react'
import { Link } from 'react-router-dom'
import api from '../api/client'
import TemplateEditor from '../components/admin/TemplateEditor'
@@ -17,6 +17,9 @@ import {
listAssetLibraries, createAssetLibrary, deleteAssetLibrary, refreshAssetLibraryCatalog,
type AssetLibrary,
} from '../api/assetLibraries'
import { getTenantDefaultDashboard } from '../api/dashboard'
import type { WidgetConfig } from '../api/dashboard'
import DashboardCustomizeModal from '../components/dashboard/DashboardCustomizeModal'
export default function AdminPage() {
const qc = useQueryClient()
@@ -158,6 +161,14 @@ export default function AdminPage() {
const [smtpDraft, setSmtpDraft] = useState<Partial<Settings>>({})
const smtp = { ...settings, ...smtpDraft } as Settings
const [showTenantDashboardModal, setShowTenantDashboardModal] = useState(false)
const { data: tenantDefaultWidgets } = useQuery<WidgetConfig[]>({
queryKey: ['tenant-default-dashboard'],
queryFn: getTenantDefaultDashboard,
enabled: isAdmin,
staleTime: 300_000,
})
return (
<div className="p-8 space-y-8">
<h1 className="text-2xl font-bold text-content">Admin</h1>
@@ -886,6 +897,50 @@ export default function AdminPage() {
</div>
</div>
{/* ------------------------------------------------------------------ */}
{/* Dashboard Widget Configuration (admin only) */}
{/* ------------------------------------------------------------------ */}
{isAdmin && (
<div className="card">
<div className="p-4 border-b border-border-default flex items-center gap-2">
<LayoutDashboard size={16} className="text-content-muted" />
<div>
<h2 className="font-semibold text-content">Dashboard Widget-Konfiguration</h2>
<p className="text-xs text-content-muted mt-0.5">
Legt das Standard-Widget-Layout für alle Nutzer dieses Tenants fest. Nutzer können ihr eigenes Layout individuell anpassen.
</p>
</div>
</div>
<div className="p-4 flex items-center gap-4">
<div className="flex-1">
<p className="text-sm text-content-secondary">
Tenant-Standard:{' '}
<span className="font-medium text-content">
{tenantDefaultWidgets && tenantDefaultWidgets.length > 0
? `${tenantDefaultWidgets.length} Widget${tenantDefaultWidgets.length !== 1 ? 's' : ''} konfiguriert`
: 'Noch kein Standard festgelegt (Systemvorgabe aktiv)'}
</span>
</p>
</div>
<button
onClick={() => setShowTenantDashboardModal(true)}
className="btn-secondary text-sm flex items-center gap-2"
>
<LayoutDashboard size={14} />
Tenant-Standard-Dashboard bearbeiten
</button>
</div>
</div>
)}
{showTenantDashboardModal && (
<DashboardCustomizeModal
currentWidgets={tenantDefaultWidgets ?? []}
onClose={() => setShowTenantDashboardModal(false)}
tenantMode={true}
/>
)}
{/* ------------------------------------------------------------------ */}
{/* Material Library link */}
{/* ------------------------------------------------------------------ */}