01Fejlene, jeg meldte ind — og hvad jeg gjorde ved dem
Jeg fik en gennemgang til at bekræfte hver enkelt, og så rettede jeg dem én for én. Alt herunder er verificeret på den kørende build — ikke bare “burde virke”.
| Punkt | Hvad der var galt | Sådan løste jeg det | Status |
|---|---|---|---|
| Knyt forløb | Invarianten group.courseId === assignment.courseId blev brudt; næste emne-gem nulstillede knytningen; fejltekster nåede aldrig frem; picker viste arkiverede forløb. | Nu flytter emnet med gruppen i én transaktion, jeg validerer faget, filtrerer picker'en til aktive forløb, og lader backend-fejlen boble helt op. | Rettet |
| Slet-knap (lærer) | Lærere havde slet ingen slet-mulighed; tomme forløb kunne aldrig slettes; sletning fejlede stille. | Jeg gav læreren en synlig sletning med bekræftelsesdialog og konkret konsekvens, kaskadesletning, og fik fejlene frem i lyset. | Rettet |
| Lærerchat væk | Et gammelt commit havde hardcodet chatType='GROUP' og fjernet fane-skifteren; backend blokerede eleven med 403 oveni. | Jeg redefinerede lærerchatten som en elev↔lærer-kanal, gendannede fanerne, og sikrede privacy pr. kanal over websocket — lærere kan ikke kigge med i elevernes private rum. | Rettet |
| Opgavemateriale | Arbejdsrummet viste intet materiale og hentede ikke engang dataen; kun et lillebitte ikon-klik, skjult når tomt. | Nu vises opgavens materiale i arbejdsrummet med samme download-flow, hele rækken er klikbar, og fejl vises i stedet for at forsvinde i et tomt catch. | Rettet |
| Elever så alle klasser | Klasse-ruterne havde ingen rolle-gate — enhver elev kunne læse enhver klasse i org'en, inkl. navne og emails. | Det her tog jeg alvorligt: rolle-gates + ejer/enrollment-scoping på klasse-, skema- og homeroom-ruterne, og en negativ sikkerhedstest der beviser det. | Sikkerhed |
| Afleveringer → dropdown | En lodret stak af afleveringskort uden stabil rækkefølge. | Erstattet med en dropdown hvor man vælger gruppe/aflevering, og jeg gav den en deterministisk sortering på gruppenummer. | Rettet |
| Gå til arbejdsrum | Fuldskærms-rute uden nav, stale-flash af det forrige arbejdsrum, og knappen forsvandt hvis arbejdsrummet manglede. | Jeg rydder state ved mount, gav en tydelig vej tilbage til opgaven, og lazy-opretter arbejdsrummet så knappen altid virker. | Rettet |
| Karakterer | Gruppe-karakter lå i koden men var utilgængelig; individuel elev-karakter fandtes slet ikke. | Jeg byggede en rigtig karaktermodel — gruppe og individuel pr. elev, synlig for eleven. Mere i Del 2. | Bygget |
| Lyst tema | Hvide kort på næsten-hvid baggrund (1.05:1), usynlig zebra/hover, og forgrundsfarver der dumpede AA-kontrast. | Jeg hævede flade-adskillelsen, lavede synlig zebra/hover og AA-sikre tekstfarver — det hele styret fra design-tokens, ikke tilfældige hex. | Rettet |
02Karakterer — gruppe & individuel
Det her var den største enkeltting. Jeg ville have det ordentligt: to niveauer, valideret i backenden, og synligt for eleven. Migrationen add_grades er den eneste DB-ændring i hele leverancen — og jeg holdt den bevidst rent additiv, så den ikke kunne gøre skade.
- Datamodel: jeg tilføjede en
student_grades-tabel plusgrade / feedback / gradedAt / gradedByIdpåGroupAssignment. Ingen DROP, ingen DELETE — testet mod en kopi af produktionen først. - To niveauer: gruppe-karakter og individuel karakter pr. elev, sat direkte fra afleveringskortet hvor medlemmerne alligevel er i scope.
- Synlig for eleven: både gruppe- og egen karakter + feedback vises nu på elevens opgaveside. Før var den bogstaveligt talt usynlig — det gav ingen mening.
- Oprydning: jeg fjernede en stale karakter der hang efter af-aflevering, og rettede en forkert gennemsnitsberegning der læste “02” som 2 i stedet for at bruge 7-trins-skalaen.
03Tom instans på eget subdomæne — nu med cert
Jeg ville have, at en super-admin kan trykke på en knap og få en helt tom organisation med sit eget subdomæne <slug>.groopswork.dk og gyldig TLS. App-laget var klar — det var certet, der drillede.
Multi-host var på plads
nginx er catch-all (server_name _), og jeg havde allerede åbnet auth trustedOrigins og socket-CORS for enhver *.groopswork.dk, plus host → org-resolution og bootstrap af første admin i ét trin.
Din melding: “no cert gets added”
Du fangede en rigtig fejl. Rodårsagen: Traefiks Let's Encrypt kører HTTP-01, som kræver et konkret domæne — en regex-router kan aldrig få et cert. Og min gamle domain.service.ts var ærligt talt et no-op (forkerte service-navne, ikke-monteret sti, blev aldrig kaldt).
- Sådan løste jeg det: backenden skriver nu én konkret
Host()-router pr. tenant til Traefiks dynamiske mappe → så udsteder HTTP-01 certet af sig selv. Ruten peger på frontend-servicen, og nginx klarer selv/api+/ds-vpathindenfor. - Hele livscyklussen: den kaldes ved opret / opdatér / slet, og reconciler fra databasen ved boot, så gamle tenants bliver bakket op og alt self-healer hvis mappen ryddes.
- Holdt væk fra dev: Traefik-mappen bind-mountes kun i produktion (
:zfor SELinux); dev og review-apps bruger et isoleret volume, så de aldrig roder i den delte Traefik. - Jeg testede det live: oprettede en tenant → https://qa-cert.groopswork.dk svarede 200 med et gyldigt Let's Encrypt-cert på ~12 sekunder → og sletning fjernede ruten igen. Så virker det.
04Sikkerhed & data-tab — det der ikke måtte gå live
Undervejs faldt jeg over en håndfuld huller, der var værre end de rapporterede punkter. Dem lukkede jeg alle — med en test til hver.
Kritisk Uploads overlevede ikke redeploy
Afleveringsfiler og opgave-vedhæftninger blev skrevet uden for volumen og forsvandt ved hver deploy. Jeg samlede alle upload-stier ét sted (config/uploads.ts) under volumen.
Kritisk Vedhæftninger gav 404
Skrevet ét sted, læst et andet. Nu samme sti begge steder — og jeg tilføjede en autoriseret download-rute til afleveringsfiler, som slet ikke fandtes før.
Kritisk OnlyOffice-token = alle dokumenter
Token tjekkede kun signaturen, ikke hvilket dokument, og udløb aldrig. Nu bundet til dokument-id + exp.
Kritisk Cross-tenant-læk (IDOR)
Aktivitet, elev-opgaver/grupper og admin-brugerstyring manglede org/ejer-scoping. Lukket med org-scope, ejer-tjek og slankede socket-payloads.
“Skift adgangskode” var et no-op
Den skrev til et felt Better Auth slet ikke verificerer imod. Nu opdaterer jeg Account-rækken, så nye koder faktisk virker.
Auth-hærdning
Rate-limit monteret på /api/auth/* (både IP og email), selv-registrering lukket, CORS/localhost strammet, og magic-link uden en hardcoded kode.
05Design-system, tema & driftsmodel
UI-refresh (ADR-002)
Jeg lagde et token-baseret design-system i theme.ts: login-redesign, dashboards med rigtige stat-kort, fag-farver, lift-kort og en pænere admin. CI afviser nu rå hex uden for tokens.
Lyst tema med AA-kontrast
Dybere flader, en reel surface.raised, synlig zebra/hover, AA-sikker sekundær tekst, og TEAL_DEEP som forgrund i stedet for de farver der dumpede kontrast.
Lokal-autoritativ drift
Det her var en bevidst beslutning: prod kører nu lokalt fra docker compose -p groopworks-main og er autoritativt. CI er bare test+build-gate, og deploy_production er manuel — så et push aldrig overskriver den kørende stack.
CI som kvalitetsgate
Blokerende tsc --noEmit, token-gate på frontend, mkdocs --strict og backend-tests. Jeg merger ikke før alt er grønt.
06QA — her gik jeg ikke på kompromis
Jeg kørte hele appen igennem flere lag automatiseret og ekspektations-drevet review, og loopede til rene runder — per rolle, tema, viewport og data-tilstand.
- Coverage-matrix: rute × rolle × tema × viewport × data-tilstand — to rene runder i træk (272 celler, 0 fund, 0 tomme celler).
- Dyb per-element-inspektion: alle 4 roller rene; jeg tjekkede geometri, døde klik, og verificerede effekter via API/DB — ikke bare skærmbilleder.
- Formularer & tastatur: dialoger kørt valid/invalid/tom + annuller; ingen fokus-fælder; effekterne API-verificeret.
- Per-side-verdicts fangede rigtige fejl som de automatiske tripwires missede — bl.a. at “Afslut emne” og “Genåbn” blev vist samtidig, en admin-blindgyde der gav 400, og dansk ental/flertal. Alt rettet.
07Polish & find-barhed — de små ting
Det er de her detaljer, der afgør om det føles færdigt. Jeg tog dem seriøst.
Dansk ental/flertal
En fælles pluralDa-hjælper, jeg brugte 33 steder i 15 komponenter — “1 gruppe”, ikke “1 grupper”. Sådan noget stikker i øjnene på mig.
Lærerchat-farven
Fra en klashende pink (#f093fb) til brand-teal, ændret ét sted i theme.ts så hele kanalen fulgte med. Gruppechatten beholdt sin violette identitet.
Ensret terminologi
Opgave-headeren siger nu konsekvent “emne”, og admin er ensrettet til “Hold” overalt — stat-kort, knapper, tomme tilstande, nav. Ingen “Forløb” hvor det er et hold.
Dansk dato-casing
capitalize gjorde det til “Onsdag D. 11. Juni”. Nu er kun første bogstav stort, og måneder skrives med småt — som dansk skal være.
Find-barhed på lister
Resultat-tælling (“Viser 9 af 10”), “Ryd filtre” og kolonne-sortering på Brugere, Hold, Fag og Grupper.
Dag-separatorer
Én delt formatDaySeparator — “I dag / I går / lørdag 8. august 2026” i både aktivitet og chat.
08Go-live — sådan flyttede jeg produktionen
Jeg flyttede prod over på den verificerede kode i sikre trin — uden datatab, med sessionerne i behold.
Jeg reddede prod-hemmelighederne til en root-only prod.env (uden at printe værdierne), og tog fuld pg_dump (31 tabeller, verificeret) + tar af uploads-volumen.
Jeg byggede images lokalt med prod-secrets uden at røre de kørende containere. Hvis build fejlede, stod prod urørt.
Samme projektnavn og volumener; add_grades kørte ved boot; samme BETTER_AUTH_SECRET → aktive sessioner overlevede.
/api/health/deep = DB connected, ingen console-fejl eller 5xx, data urørt. Jeg oprettede prod-super-admin med koden gemt i en root-only fil.
Hele leverancen merget til main, prod redeployet fra main og verificeret byte-identisk. Tenant-TLS-fixet (!35) fulgte lige efter.
app.groopswork.dk er live og sund på main, prod == main, og arbejdstræet er rent. Rollback (DB-dump + de forrige cda3601a-images) er dokumenteret i qa/go-live.md.
09Det jeg selv skal huske at gøre
1 · Rotér de hardcodede adgangskoder. Nogle rigtige koder har ligget i update*Password.ts i git-historikken på main. Jeg scrubbede dem fremadrettet, men historikken har dem stadig — så jeg bør rotere de konti og overveje at scrubbe historikken.
2 · Super-admin-koden. Læses én gang med sudo cat /srv/groopworks/superadmin-credentials.txt og skiftes ved første login.
3 · Nice-to-have: vise det nye subdomæne-URL til admin i UI'et efter jeg opretter en tom instans — i dag skal man selv vide, at det er <slug>.groopswork.dk.
10Det hele i tal
| Område | Filer | Hvad der ligger i det |
|---|---|---|
| Backend | 41 | services (assignment, class, course, group, tenant, domain, workspace, user), controllers, middleware (auth, class, tenant, upload, security), routes, config (auth, socket, uploads), schema + 1 migration, super-admin-script |
| Frontend | 56 | dashboards, opgaver (lærer/elev), grupper, hold, arbejdsrum, admin (brugere/fag/hold/tenants/overblik), layout (TopBar/SideNav), utils (theme, plural, relativeTime, api), routes |
| Docs & rod | 7 | CHANGELOG, CLAUDE.md, .gitlab-ci.yml, compose.ci.yml, .gitignore, docs/deployment.md, docs/design-system.md |
| QA-artefakter | 742 | coverage-/deep-/forms-/keyboard-harnesses, per-side-verdicts, screenshots, go-live-runbook, decisions- og polish-logs |