Arkitektur¶
Levende dokument — opdateres i samme MR som arkitekturændringer (jf. CLAUDE.md). Senest opdateret ved indførelsen af forløbsmodellen (ADR-001).
Overblik¶
Fuld-stack TypeScript-monorepo: backend/ (Express + Bun + Prisma +
PostgreSQL) og frontend/ (React 18 + Vite + MUI + Redux Toolkit).
Realtid via Socket.io; dokumentsamarbejde via OnlyOffice Document Server;
auth via Better Auth med Organization-plugin (multi-tenancy).
Browser ── frontend (nginx) ──┬── /api → backend:3000
├── /api/socket.io → backend (websocket)
└── /ds-vpath/ → documentserver
backend ── Prisma ── PostgreSQL
backend ⇄ documentserver (intern JWT: ONLYOFFICE_JWT_SECRET)
Frontend-containeren er eneste offentlige ingress; SSL/routing håndteres af
platform-proxyen (Coolify i produktion, Traefik via compose.ci.yml på
build-serveren). Fail-fast env-validering ved boot i config/env.ts.
Backend-lagdeling¶
routes/ → controllers/ → services/ → Prisma. Middleware-kæde:
requestId → requestLogger → authenticate (Better Auth-session fra
cookie) → resolveTenant (sætter req.organizationId) →
authorize(roller) → validate(zod) → handler → errorHandler.
Audit-spor: logging.service.ts skriver ActivityLog fra alle centrale
mutationer (klasse/forløb/gruppe/opgave, inkl. BULK_UPDATE) — eksponeret
rolle-scopet via GET /api/activity-logs.
Domænemodel¶
Begreberne fra mødenoterne og deres modeller (ADR-001):
| Begreb | Model | Rolle |
|---|---|---|
| Hold | Class |
Persongruppen: elever (ClassEnrollment), skema, fag (ClassSubject), lærer |
| Fag | Subject |
Organisationens taksonomi ("Matematik") |
| Forløb | Course |
Pædagogisk enhed ("Geometri") for ét hold + ét fag; unik på (classId, subjectId, name) |
| Emne | Assignment |
Én opgaverunde i et forløb: title = emne-navn ("Figurer"), description = opgavetekst, deadline = eneste frist-kilde, isShared = delt/unik opgavetekst |
| Gruppe | Group |
Elevgruppe i et hold; courseId placerer den i forløbet; startDate styrer elevsynlighed |
| Aflevering | GroupAssignment |
Forretningsreglen "én gruppe = én aflevering": unik pr. (assignment, group); bærer status, submissions, descriptionOverride (unik opgavetekst) |
| Dokument | WorkRoomDocument |
OnlyOffice-dokument i gruppens arbejdsrum (WorkRoom, 1:1 med gruppe) |
Organization ─ Member (org-roller) ─ User
└─ Class (HOLD) ── ClassSubject ─> Subject (FAG)
├─ Course (FORLØB, ADR-001) ←──────────────┐
│ ├─ Assignment (EMNE + opgavetekst) │ courseId-invariant:
│ └─ Group.courseId ────────────────────┘ gruppe følger sin
├─ Assignment ── GroupAssignment ─> Group aktive opgaves forløb
│ │ ├─ descriptionOverride (unik tekst pr. gruppe)
│ │ ├─ Submission(s) + currentSubmission
│ │ └─ AssignmentAttachment.groupAssignmentId
│ │ (null = delt fil, sat = kun den gruppe)
│ └─ isShared (delt/unik-toggle)
└─ Group ── GroupMember ─> User
├─ startDate (fremtidig ⇒ usynlig for elever, inkl. sockets)
└─ WorkRoom (1:1) ── WorkRoomDocument ── WorkRoomMessage
Effektiv opgavetekst og filer (ADR-001)¶
Én regel, ét sted (AssignmentService.effectiveDescription):
effectiveDescription = isShared ? assignment.description
: (ga.descriptionOverride ?? assignment.description)
Alle assignment-svar beriges med effectiveDescription pr.
groupAssignment. Filer: elever ser emnets delte filer
(groupAssignmentId IS NULL) plus deres egen gruppes — håndhævet i
listAttachments.
Bulk-redigering ("alle grupper med dette emne")¶
PATCH /api/assignments/:id/bulk opdaterer KUN delte felter
(titel/beskrivelse/frist + gruppernes startDate) — aldrig medlemmer,
overrides eller pr.-gruppe-filer. ?dryRun=1 returnerer preview af berørte
grupper ("Dette ændrer N grupper i …"). Anvendelser logges som
ASSIGNMENT BULK_UPDATE i ActivityLog med berørte gruppe-id'er.
Kontrakt-migration (gennemført)¶
Group.deadline og Group.topic er droppet
(20260611150000_drop_group_deadline_and_topic): frist læses fra
assignment.deadline via GroupAssignment, og emnet bæres af
Assignment.title + Course. Datarapport ved migrering: 0 grupper havde
deadline uden tilknyttet opgave — ingen frist-information gik tabt.
Auth & multi-tenancy¶
Better Auth (cookie-sessions, 24t/1t refresh/5 min cookie-cache) +
Organization-plugin. Platform-roller User.role ∈ {SUPER_ADMIN, ADMIN,
TEACHER, STUDENT}; org-roller mappes til app-roller i authenticate.
Org-scoping i alle queries via class.organizationId-kæden. Se ADR
0001-fjern-legacy-jwt.
OnlyOffice¶
workroom.controller.ts: editor-config signeres med
ONLYOFFICE_JWT_SECRET; dokument-download/callback er offentlige endpoints
beskyttet ALENE af samme JWT (påkrævet ved boot). Dokumentnøgle
document_{id}_{mtime} er stabil på tværs af brugere (live-samarbejde).
Realtid (Socket.io)¶
Rum: user:<id>, class:<id>, class:<id>:staff (kun lærere/admins —
bruges til grupper med fremtidig startDate), group:<id>. Elever joiner
ikke gruppe-rum for fremtidigt planlagte grupper.
Test og drift¶
- 70+ integrationstests (
bun run test) med Better Auth-helper (src/test/helpers/auth.ts); CI blokerer på testfejl. - Migrationer: expand/contract; destruktive ændringer kun i separate
kontrakt-migrationer, og alt testes mod kopi af produktionsdata før
merge. Reset af database sker KUN via
backend/scripts/reset-db.sh. - Se også
docs/operations.md(miljøvariabler, seed, reset) ogdocs/funktioner.md.