# Startsidan – devportalen > Utvecklarportal för startsidans ekosystem i Sundsvalls kommun. > Samma innehåll används av människor och agenter. Läs detta innan du > bygger något i repot. > In English: this document describes a Swedish municipal ecosystem. All > UI text, documentation and commit subjects are intentionally in Swedish, > and code you write for this repo must be too. The Swedish is the source > of truth, not a translation artifact. innehallsversion: c8sumfjmzue (hash av innehållet, samma värde som ETag – ändras när och bara när portalen ändras) ## Börja här - /dev: den här portalen, människovyn - /dev/spec.json: samma portal maskinläsbart - /dev/ui/llms.txt: designsystemet – tokens, primitiver, mönster, felindex - /dev/ui/spec.json: designsystemet maskinläsbart - CLAUDE.md i reporoten: reglerna som gäller varje session - docs/: planen, ADR:erna och prototyperna som är designfacit ## Så är det här dokumentet uppbyggt Varje sida står som ett manifest: id, path, summering, nyckelord och innehållet i avsnitt. Efter sidorna kommer katalogen över allt som finns att bygga vidare på, och sist beslutsregistret. ## Innehåll - /dev: Startsidans ekosystem — Vad ekosystemet är, vilka regler som gäller och var du börjar – för människa och agent. - /dev/kom-igang: Kom igång — Miljön, kommandona, repokartan och vägen genom din första ändring. - /dev/arkitektur: Arkitekturen — Skal och moduler, de tre ytformerna, dataflödet och rollfiltreringen. - /dev/moduler: Modulkontraktet — ModuleManifest fält för fält, sloten de fyller och hur en modul registreras. - /dev/recept: Recept — Steg för de fem uppgifter som återkommer: kort, nyckeltal, handlingsrad, fokusflöde och mockdata. - /dev/ai: AI-lagret — Var AI får bo, hur turkontraktet garanteras och varför servern alltid fattar beslutet. - /dev/stack: Teknisk stack — Valen, versionerna och skälen – plus container, deploy och miljövariabler. - /dev/beslut: Beslutsregistret — ADR:erna: vad som är bestämt, varför, och vad som ännu inte är avgjort. - /dev/katalog: katalogen över ekosystemet - /dev/docs: dokumentationen, en sida per del - /dev/ui: designsystemet --- # Startsidans ekosystem id: start path: /dev titel: Startsidans ekosystem summering: Vad ekosystemet är, vilka regler som gäller och var du börjar – för människa och agent. nyckelord: start, översikt, ekosystem, grundregler, portal ## Vad detta är Startsidan är ett skal som komponerar moduler. Användaren ser en sammanhängande sida; varje funktion är ett fristående paket med ett tydligt kontrakt mot skalet. Det är det som gör att nya funktioner kan byggas löpande utan att kärnan rörs. - Skalet: apps/portal/src/shell. Layout, roller, modulregister, rutnät och AI-fältet. Vet vilka slots som finns, aldrig vad en widget innehåller. - Modulerna: packages/modules/*. Äger sitt innehåll och sin data, och beskriver sig för skalet via ett manifest. En ny funktion = ett nytt paket + en rad i registret. - Designgrunden: packages/tokens och packages/ui. Tokens definierar färg, form och typografi; primitiverna är de enda byggstenar en modul behöver. - Den här portalen: apps/portal/src/app/dev. Ingången för den som ska bygga. Samma innehåll i två vyer: en för människa, en för agent. Peka en agent hit och den ska kunna bygga rätt utan att gissa: /dev/llms.txt är hela portalen som text, /dev/ui/llms.txt är designsystemet. ## Grundregler Det som gäller oavsett vad du bygger. Bryter något mot en av dem är det ändringen som är fel, inte regeln. - Nya funktioner byggs som moduler under packages/modules/. Skalet rörs bara när kontraktet faktiskt saknar något. - Designen kommer ur packages/tokens och packages/ui. Inga hex-värden, inga godtyckliga px-mått. - Grönt är status och betyder aldrig AI. AI-ytor tonas med --color-ai-* i blått, och allt AI-genererat märks synligt med AiChip (ADR-026). - Servern fattar besluten. Att modellen sätter create: true räcker aldrig – route handlern validerar själv att allt obligatoriskt finns. - Inga hemligheter i repo eller klientkod. AI-anrop går via en route handler, aldrig direkt från webbläsaren. - TypeScript strict, inga any. Varianter är unionstyper, inte fria strängar. - Server components som default. "use client" bara på det som faktiskt behöver interaktion. - Mockdata har samma form som kommunens mikrotjänst-API:er skulle ge – bytet till riktiga källor ska bli en adapterfråga. - Svenska i all UI-text och alla commit-ämnen. - Ändras en token, en primitiv eller ett kontrakt uppdateras posten i portalen i samma commit. - En funktion är inte klar förrän den är dokumenterad. Ny funktion = ny katalogpost + ett dok på /dev/docs som uppfyller dokumentationskontraktet. Kod utan dok räknas som påbörjad, inte levererad. ## Antimönster Grundreglerna ovan säger vad du ska göra. Det här säger vad du är på väg att göra i stället – de misstag som faktiskt går att göra i den här kodbasen, med rättelsen bredvid. Kolumnen längst till höger är hederlig: står det "ingen" fångas felet av ingen grind, och då är den här raden det enda som står i vägen. Gör inte | Gör i stället | Varför | Fångas av En egen sidmodell för en redaktionell sida – egna typer för sektionsingång eller artikel. | Använd PageManifest och ContentBlock ur module-sdk (ADR-016). Saknas något, föreslå det där. | Ett parallellt sidbegrepp blir omöjligt att slå ihop med det beslutade när det väl bär innehåll. (ADR-016) | ingen Anropa modellen från en klientkomponent, eller lägga en API-nyckel i något klienten läser. | Gå via en route handler. Nyckeln bor på servern och når aldrig webbläsaren. | En nyckel i klientkoden är läckt i samma sekund som sidan levereras, oavsett hur den är obfuskerad. | ingen – granskas i kodgranskning Lägga funktionslogik i skalet därför att det går snabbare än att skapa en modul. | Bygg modulen under packages/modules/. Skalet rörs först när kontraktet bevisligen saknar något. | Skalet är gemensamt för alla moduler. Logik som hamnar där kan ingen modul äga, och ingen vågar ta bort. | ingen Skapa något därför att modellen svarade create: true. | Route handlern kontrollerar själv att allt obligatoriskt finns innan något skapas. | Modellens flagga är ett förslag, inte ett beslut. Servern är den enda part som kan hållas ansvarig. | ingen – täcks av modulens egna tester Hårdkoda färg eller mått: className="text-[#005595]" eller className="text-[14px]". | Tokens och primitiver: className="text-blue", className="text-sm". | Ett hårdkodat värde överlever inte nästa temaändring och syns inte i designsystemets specifikation (R4). | yarn lint Be modellen svara med enbart JSON och sedan parsa svaret. | Structured outputs via output_config.format, eller strict: true på verktyget. Formen är då modellens kontrakt, inte en förhoppning. | En modell som ombeds svara med JSON gör det nästan alltid – och felen kommer när det räknas, inte när man testar. | ingen Läsa åtkomstkakan för att veta vem användaren är. | Vänta på SAML-sessionen i fas 4. Behöver modulen identitet nu: flagga behovet, bygg inte runt det. | Koden delas av alla som provar demon och säger ingenting om vem någon är. Ett bygge ovanpå den blir fel den dag riktig inloggning kommer. (ADR-012) | ingen Skriva en ny CSS-regel i shell.css från en modul. | Tailwind-utilities i modulen. shell.css ägs av skalet och är porterad 1:1 från prototypen. | Ändras skalets krom från en modul går måtten isär mot designfacit, och ingen ser vilken modul som gjorde det (R6). | ingen Lägga till en blocktyp i Blockvy.tsx utan att lägga till den i text.ts. | Två renderare, alltid. En blocktyp utan textrenderare är ett fel, inte en förenkling. | Innehållet försvinner tyst ur agentvyn och ur llms.txt. Portalen ser hel ut för människan och är trasig för agenten. (ADR-010) | ingen Skicka tillbaka fältens tidigare värden till modellen utan serverns signatur. | Förankra fälten med serverns egen HMAC och verifiera den vid varje tur. | Osignerade fält är en inmatningsväg förbi samtalet: klienten kan skriva vad som helst direkt in i det som ska skapas. (ADR-013) | yarn test Lämna över en ny funktion utan katalogpost och dok, med tanken att dokumentera sedan. | Katalogpost i katalog.ts och dok i dok.ts, i samma commit som koden. | Kod utan dok är påbörjad, inte levererad. Det som inte står i portalen finns inte för nästa agent. (ADR-011) | yarn test Rendera en källas HTML med dangerouslySetInnerHTML, eller sanera den och rendera ändå. | Översätt källan till ContentBlock i en adapter och låt skalet rendera blocken som JSX. | Markup ur en källa tar med sig källans skript, stilar och länkar. Sanering är en lista över det man kom ihåg att förbjuda; block är en lista över det man tillåter. (ADR-016) | yarn lint (react/no-danger) ## Stanna och fråga Ekosystemet är byggt i faser, och några kontrakt är beslutade men inte implementerade. Möter du något av det här ska du inte improvisera fram en lösning – det är ett arkitekturbeslut, och det skrivs som en ADR i docs/beslut/ innan koden. Statuskolumnen finns som fält i /dev/spec.json under stanna, så att svaret på "finns det redan?" går att läsa av utan att tolka tabellen. Om kravet leder hit | Läget i dag | Status | Gör detta En redaktionell sida (sektionsingång eller artikel) | PageManifest finns i module-sdk (ADR-016). En modul bidrar med sidor via manifestets pages; skalet renderar dem under /sidor. | begransad, implementerad, ADR-016 | Använd PageManifest och ContentBlock. Bygg inget eget sidbegrepp och ingen egen renderare – saknas en blocktyp, föreslå den i module-sdk. Assistenten ska förstå fritext och utlösa något | AI-fältet (POST /api/assistant) matchar ai.intents med enkla mönster före modellen. Bara navigate har en hanterare. | begransad, fas 3, implementerad | Deklarera intenten i modulen med mönster och ett navigate-mål. Behövs answer eller LLM-matchning: stanna och fråga. Ett nytt fält i ModuleManifest | Kontraktet ska hållas tunt. Nya fält ska bäras av minst två moduler innan de blir permanenta. | begransad, implementerad | Visa att två moduler behöver det, skriv en ADR, ändra sedan. En ändring i skalet eller i shell.css | Skalets krom är porterad 1:1 från prototypen och ägs inte av moduler. | begransad, implementerad, ADR-004 | Kontrollera först att slots eller routes inte redan räcker. En riktig extern källa, eller en ny hemlighet | Fas 4 (BFF) är inte byggd. Nycklar bor på servern, aldrig i repo eller klient. Undantaget är öppna, anonyma källor: webbsidor läser sundsvall.se utan nyckel (ADR-016). | planerad, fas 4, ej implementerad | Kräver källan inloggning: håll dig till modulens datafunktion med mockform, och flagga behovet. Är den öppen: följ webbsidor eller nyheter – server-only, force-cache, timeout, zod och inspelad reserv. En ny flödestyp utöver felanmalan | Två flöden finns: felanmalan med platta fält och utvecklingsarende med typade avsnitt (ADR-015). Motorn lyfts först i M4, och ADR-005 är inte skriven. | planerad, fas M4, ej implementerad, ADR-005 | Bygg flödet som data i modulen, så att M4 blir en flytt och inte en omskrivning. Läs båda formerna först – ADR-005 ska skrivas mot dem, inte mot den ena. Saknas ett svar helt är det portalen som är ofullständig, inte du som ska gissa. Säg vad du sökte och var det tog slut – posten läggs till i apps/portal/src/app/dev/portal/ i samma commit som ändringen. ## Snabbstart Kör och verifiera: ``` yarn dev # utveckling på http://localhost:3000 yarn lint && yarn test # grindarna – ska vara gröna före varje commit yarn build && yarn start # produktionsbygge lokalt docker compose up --build # verifiera containern före push ``` Hämta hela sammanhanget som agent: ``` curl -s http://localhost:3000/dev/llms.txt # portalen: arkitektur, kontrakt, recept curl -s http://localhost:3000/dev/ui/llms.txt # designsystemet: tokens, primitiver, felindex curl -s http://localhost:3000/dev/spec.json # samma portal, maskinläsbart ``` Samma sak från produktion, utan att klona repot: ``` curl -s https://start.sundsvall.dev/llms.txt # kartan: var kontexten finns curl -s https://start.sundsvall.dev/dev/llms.txt # portalen curl -s https://start.sundsvall.dev/dev/ui/llms.txt # designsystemet curl -s https://start.sundsvall.dev/dev/spec.json # portalen som JSON # Adresserna ovan ligger utanför åtkomstkoden (ADR-012) och går att peka en # extern AI-tjänst på direkt. Demon på /start gör det inte. ``` ## Vidare härifrån - /dev/kom-igang — Kom igång (miljö, kommandon, din första ändring) - /dev/arkitektur — Arkitekturen (skal, moduler, ytformer, dataflöde) - /dev/moduler — Modulkontraktet (ModuleManifest fält för fält) - /dev/recept — Recept (steg för de fem vanligaste uppgifterna) - /dev/ui — Designsystemet (tokens, primitiver, mönster, felindex) - /dev/katalog — Katalogen (allt som finns att bygga vidare på) --- # Kom igång id: kom-igang path: /dev/kom-igang titel: Kom igång summering: Miljön, kommandona, repokartan och vägen genom din första ändring. nyckelord: onboarding, miljö, yarn, node, kommandon, repo, docker ## Förutsättningar Krav | Version | Kommentar Node | >= 24 | Som kommunens starter. Installeras med nvm. Yarn | 4.9.1 via corepack | corepack enable. Inte npm, inte pnpm. WSL | WSL2 med Ubuntu | Arbeta i WSL-filsystemet, inte under /mnt/c – I/O-prestandan skiljer dramatiskt. Docker | Desktop med WSL-integration, eller Engine i WSL | Behövs för att verifiera containern före push. ## Kommandon Kommando | Gör yarn dev | Båda apparna: projektportalen på 3000 och startsideportalen på 3010, som nås via http://localhost:3000/portal. yarn lint | ESLint över hela repot från eslint.config.mjs i roten. Ett nytt paket lintas utan att något läggs till. yarn test | Vitest över hela repot från vitest.config.mts. Tester plockas upp på filnamn: packages/**/test/**/*.test.ts. yarn typecheck | tsc --noEmit i varje paket, topologiskt. yarn build && yarn start | Produktionsbygge lokalt. docker compose up --build | Samma bygge som Dokploy gör. yarn lint och yarn test ska vara gröna före varje commit. Docker-bygget körs innan push. ## Var saker ligger ``` startsidan/ ├── CLAUDE.md regler som gäller varje session ├── docs/ │ ├── plan.md planen, modulkontraktet, beslutsregistret │ ├── plan-modul-felanmalan.md │ ├── beslut/ ADR:er │ └── *.html DESIGNFACIT: prototyperna ├── apps/ │ ├── portal/ startsideportalen, basePath /portal (ADR-040) │ │ └── src/ │ │ ├── app/ App Router: sidor, route handlers, /dev │ │ ├── shell/ skalet: sidopanel, AI-fält, registry, session │ │ └── ai/ flödenas AI-adapter och takgräns │ └── projekt/ projektportalen på roten – inte startsidan └── packages/ ├── tokens/ designtokens (@theme) och basregler ├── ui/ primitiverna (medBas för basvägen) ├── ai/ den strukturerade AI-adaptern, gemensam ├── gemensamt/ beslutsregistret, varumärket, spärren mot gissningar ├── dokumentation/ katalogpost, dok, kontraktet och Blockvy ├── module-sdk/ kontraktet mellan skal och modul └── modules// en modul: manifest.ts, components/, data/ ``` Varje modul har samma inre form: manifest.ts, components/, data/ (mock plus adapterinterface), index.ts. Det gör modulerna kopierbara som mall när nästa behov dyker upp. ## Din första ändring 1. Läs sammanhanget CLAUDE.md för reglerna, /dev/arkitektur för hur delarna hänger ihop, /dev/ui/llms.txt för designsystemet. Prototyperna i docs/ är designfacit – utseendet ska matcha dem. 2. Sök i katalogen först Finns primitiven, modulen eller endpointen redan? Katalogen finns just för att svara på det innan något nytt skapas. 3. Välj rätt form Kort på arbetsytan → widget i slots.main eller slots.side. Rad som kräver handling → slots.actions. Helskärmsflöde → routes med mode: "focus". Arbetsplats över hela ytan med sidopanelen kvar → mode: "full". Knapp bredvid hälsningen → slots.genvag. ``` // se /dev/recept för steg per fall ``` 4. Bygg modulen, inte skalet Skalet ändras bara när kontraktet faktiskt saknar något – och då som ADR i docs/beslut/. 5. Stäng med grindarna yarn lint && yarn test, sedan docker compose up --build, sedan commit med Conventional Commits på svenska. ``` yarn lint && yarn test docker compose up --build git commit -m "feat(kalender): veckovy i högerspalten" ``` ## Verifiera att det funkar Grindarna säger att koden håller, inte att funktionen gör rätt. De tre stegen nedan är skillnaden. 1. Kör grindarna Alla tre täcker hela repot, inklusive nya paket. Typecheck är den som ser modulkoden hårdast, eftersom paketen kör noUncheckedIndexedAccess. ``` yarn lint && yarn test && yarn typecheck ``` 2. Växla roll och kontrollera båda vyerna Rollfiltreringen är det som oftast blir fel: en widget utan roles ärver modulens, och en modul utan rätt roles syns inte alls. Växla demoanvändare i användarmenyn och kontrollera att både medarbetare och chef ser det de ska – och inte det de inte ska. 3. Kontrollera formen mot facit Prototyperna i docs/ är designfacit. Gå igenom felindexet på /dev/ui mot din diff: tokens i stället för hex, headingId på varje Card, statusfärg i kant eller chip, AiChip på allt AI-genererat. ``` curl -s http://localhost:3000/api/health # svarar { ok: true } ``` Playwright-röktest kommer i fas 5. Tills dess är rollväxlingen den manuella kontroll som inte får hoppas över. ## När är det klart? Klar betyder körbar, granskad och dokumenterad. Saknas något av de tre är funktionen påbörjad, inte levererad – och den ska inte lämnas över för test. - yarn lint, yarn test och yarn typecheck är gröna. - Båda rollerna är kontrollerade i webbläsaren: medarbetare och chef ser det de ska, och inte det de inte ska. - Diffen är genomgången mot felindexet på /dev/ui. - Katalogposten finns i apps/portal/src/app/dev/portal/katalog.ts – identitet, applikationen den hör till (projekt, portal eller gemensam, ADR-040), status, källa, relationer, vad den levererar. - Doket finns i apps/portal/src/app/dev/portal/dok.ts och uppfyller dokumentationskontraktet, inklusive kvarstar och uppdaterad. - Täckningen på /dev/docs visar inga luckor. - Berörda dok för andra delar är uppdaterade – en ny modul som bidrar med actions ändrar också doket för agera. - Rör ändringen ett arkitekturbeslut finns en ADR i docs/beslut/, i samma commit. Ordningen spelar roll: dokumentationen skrivs som en del av bygget, inte efteråt. Skrivs den efteråt beskriver den vad som råkade bli, inte vad som var avsikten – och då är fältet kvarstar alltid tomt. - /dev/docs#kontrakt — Dokumentationskontraktet (fälten och vad som krävs) - /dev/docs#tackning — Täckningen (vilka delar som saknar dok) --- # Arkitekturen id: arkitektur path: /dev/arkitektur titel: Arkitekturen summering: Skal och moduler, de tre ytformerna, dataflödet och rollfiltreringen. nyckelord: arkitektur, skal, moduler, slots, ytformer, roller, dataflöde ## Bärande idé Skalet äger kromet och kompositionen. Modulen äger innehållet och sin data. Kontraktet däremellan är ModuleManifest – och det är avsiktligt tunt. Kompositionen: ``` ┌──────────────────────────────────────────────┐ │ Skalet (apps/projekt/src/shell) │ │ sidopanel · AI-fält · rollväxling · rutnät │ └───────────────┬──────────────────────────────┘ │ läser registry, filtrerar på roll ┌───────────────▼──────────────────────────────┐ │ ModuleManifest (packages/module-sdk) │ │ nav · routes · slots · ai │ └───────────────┬──────────────────────────────┘ │ ett paket per modul ┌────────┬───────────┼───────────┬──────────┬───────────┐ oversikt agera arenden kalender ledighet utveckling … │ │ │ │ │ └─────────┴──────────┴───────────┴──────────┘ hämtar sin egen data på servern (ADR-006) ``` Ansvar | Skalet | Modulen Layout och krom | Ja – sidopanel, rutnät, sidfot, fokusläge | Nej Vad som visas | Nej – vet bara vilken slot en widget hör till | Ja Data | Nej | Ja – hämtar själv på servern Roll och identitet | Ja – cookie, demoanvändare, UserContext | Nej – tar emot ctx Designtokens och primitiver | Ja – tillhandahåller | Nej – konsumerar Beslut om att skapa något | Ja – route handlern validerar | Nej – föreslår ## Tre ytformer, ett skal Alla tre delar sidopanel, tokens och AI-märkning. De skiljer sig i vad skalet renderar runt innehållet. Ytform | Facit i docs/ | Kännetecken | Deklareras med Verktygsyta | startsidan-prototyp-v3.html | Rollfiltrerad, datadriven, komponerad av modulernas slots | slots.kpi / main / side / actions Sida | it-support-prototyp.html, innehallssida-prototyp.html | Redaktionellt innehåll, brödsmulor, sidkontext i AI-fältet | pages: PageManifest[] (ADR-016) – layout "hub", "article" eller "embed" (ram, ADR-017), innehåll som ContentBlock Fokusflöde | felanmalan-poc.html | Helskärm utan sidopanel, smal topprad med Avbryt | routes med mode: "focus" (ADR-004) Modulvy | – (kort och artikel ur /dev/ui) | Modulens egen vy med sidopanel och menyrad, t.ex. personalakten | routes med mode: "standard" under /verktyg/, path med :parametrar (ADR-019) Modulvy över hela ytan | – (Min AI) | Sidopanelen kvar, vyn tar hela arbetsytan i skärmens höjd – som en chattjänst | routes med mode: "full" under /verktyg/ (ADR-025) En modul kan bidra med alla tre. felanmalan äger ett fokusflöde och snabbvägskortet som leder dit från IT-supportsidan. ## Dataflödet - Rollen bor i en cookie och läses på servern med getCurrentUser() (ADR-006). - Skalet gör om användaren till UserContext och skickar den till varje widget. - Widgeten är en async server component som anropar modulens egen datafunktion i data/ – inget nätverksanrop, ingen fetch. I fas 4 byts funktionens insida mot BFF:en. - /api/mock/ är inte modulens datakälla utan en tunn spegel: route handlern anropar samma funktion och visar vilken form ett riktigt mikrotjänst-API skulle ge. - Ingenting om användaren skickas upp från klienten. En profil som klienten skickar vore en injektionsväg rakt in i ärendet. Så ser en widget ut: ``` export async function Idag({ ctx }: { ctx: UserContext }) { const { poster } = await hamtaDagen(ctx); // modulens egen data return …; } ``` ## Rollfiltrering Allt filtreras mot två roller: medarbetare och chef. Filtret finns på tre nivåer och den snävaste vinner – modulen, widgeten, nav-posten. ``` export const manifest = defineModule({ id: "kalender", roles: ["medarbetare", "chef"], // modulens roller slots: { side: [ { id: "kalender-idag", order: 10, Component: Idag }, { id: "kalender-aterkommande", order: 40, roles: ["chef"], // snävare än modulen Component: AterkommandeHandelser }, ], }, }); ``` Rollskillnader i text väljs i komponenten, inte genom två manifestposter: title={ctx.role === "chef" ? … : …}. --- # Modulkontraktet id: moduler path: /dev/moduler titel: Modulkontraktet summering: ModuleManifest fält för fält, sloten de fyller och hur en modul registreras. nyckelord: modul, manifest, slots, widget, action, routes, intents, registry ## Manifestet Kontraktet mellan skalet och modulen. Ett nytt fält gör kontraktet tyngre för alla moduler – nya fält ska bäras av minst två moduler innan de blir permanenta. Vill du se kontraktet i bruk i stället för i sammandrag: packages/modules/arenden är den kanoniska förlagan, utpekad på /dev/recept. packages/module-sdk/src/types.ts: ``` export interface ModuleManifest { id: string; // "ledighet" name: string; // "Ledighetsansökningar" roles: Role[]; // ["medarbetare"] | ["chef"] | båda nav?: NavEntry[]; // punkter under "Verktyg" i sidopanelen routes?: ModuleRoute[]; // egna vyer: focus under /flode, standard och full under /verktyg (ADR-004, ADR-019, ADR-025) pages?: PageManifest[]; // redaktionella sidor under "Sidor" (ADR-016) slots?: { fokus?: FokusProvider; // fokusnyheten under AI-fältet, tills den stängs (ADR-021) genvag?: Genvag[]; // knappar bredvid hälsningen; ai: true ger ✦ (ADR-025) kpi?: Widget[]; // nyckeltalsraden, helbredd main?: Widget[]; // vänsterspalten side?: Widget[]; // högerspalten actions?: ActionsProvider; // rader i "Att agera på" }; notifications?: NotificationsProvider; // notiser i klockan (ADR-020) ai?: { suggestions?: (ctx: UserContext) => Suggestion[]; // renderas i AI-fältet intents?: IntentHandler[]; // AI-fältet matchar dem // före modellen }; } ``` ## Sloten Slot | Plats | Form | Använd när fokus | Under AI-fältet på startsidan, tills den stängs | FokusProvider | Något alla ska se innan de börjar – en viktig nyhet, en störning. Data, inte komponent (ADR-021). genvag | Bredvid hälsningen på startsidan | Genvag[] | En arbetsplats man ofta går till, som Min AI. NavEntry plus ai?: boolean; skalet ritar knappen (ADR-025). kpi | Fyra spår över hela arbetsytan | Widget[] | Ett mätvärde som tål att stå ensamt. main | Vänsterspalten, minmax(0, 1fr) | Widget[] | Kortet är sidans huvudinnehåll eller behöver bredd. side | Högerspalten, 36 rem | Widget[] | Stödinformation: idag, saldon, korta listor. actions | Kortet "Att agera på" | ActionsProvider | Något kräver en handling av användaren. order styr ordningen inom en slot – lägre först. Utan order gäller registreringsordningen i registry.ts. Widget och ActionItem: ``` interface Widget { id: string; roles?: Role[]; // snävare än modulens order?: number; // lägre först Component: (props: { ctx: UserContext }) => ReactNode | Promise; } interface ActionItem { id: string; status: RowStatus; // krit | warn | info | ok | neutral title: string; meta?: string; aiNote?: string; // "✦ Utkast klart" – märker AI-underlag chip?: { tone: ChipTone; label: string }; action: { label: string; href: string; variant?: "primary" | "ghost" }; } ``` actions är en funktion, inte en lista: raderna hämtas per användare och kan kräva ett anrop. Modulen äger raden, kortet agera äger ordningen (ADR-007). ## Registrera modulen 1. Skapa paketet packages/modules/ med samma inre form som de befintliga: manifest.ts, components/, data/, index.ts. 2. Lägg till i workspaces Roten har redan packages/modules/* som workspace. Lägg beroendet i apps/projekt/package.json och paketet i transpilePackages i next.config.ts. ``` // apps/projekt/next.config.ts transpilePackages: ["@startsidan/ui", …, "@startsidan/modul-"], ``` 3. En rad i registret Ordningen i registry.ts styr nav-ordningen och fallordningen i sloten när order saknas. ``` // apps/portal/src/shell/registry.ts import { manifest as nyModul } from "@startsidan/modul-"; export const registry: ModuleManifest[] = [oversikt, agera, …, nyModul]; ``` 4. Verifiera rollfiltreringen Växla demoanvändare i användarmenyn. Ser båda rollerna rätt saker är kontraktet rätt använt. ## Namngivning Två språk, med en gräns som går vid kontraktet. Blandas de går koden att läsa men inte att känna igen. Var | Språk | Exempel Modulens domänkod: data, typer, fält | Svenska | hamtaLedighet, poster, rubrik, etikett, ton, klaraForAttest Modulens komponenter | Svenska | Ledighetskort, AttAgeraPa, Nyckeltal module-sdk och skalet | Engelska | ModuleManifest, UserContext, widgetsFor, ActionItem, roles, order Filnamn i moduler | Som komponenten | components/Ledighetskort.tsx, data/index.ts All text användaren ser | Svenska | "Väntar på chef", "Visa alla →" Commit-ämnen | Svenska | feat(kalender): veckovy i högerspalten - Gränsen går vid kontraktet: allt som module-sdk namnger är engelskt, allt modulen själv namnger är svenskt. - Översätt aldrig ett SDK-fält. En modul skriver roles och order, inte roller och ordning. - Svenska identifierare skrivs utan diakriter i kod: hamtaArenden, inte hämtaÄrenden. - Kommentarer är svenska och förklarar varför, inte vad. ## Regler för moduler - En modul importerar aldrig en annan modul. Behöver två moduler samma sak går det via module-sdk eller via slots. - En modul skriver aldrig i shell.css. Skalets krom är porterad 1:1 från prototypen. - Widgeten är en server component om den inte behöver interaktion. - Mockdata ligger i modulens data/ och har mikrotjänsternas form, bakom ett adapterinterface. - Kommer innehållet från en modell märks det: aiNote på raden, AiChip i vyn. --- # Recept id: recept path: /dev/recept titel: Recept summering: Steg för de fem uppgifter som återkommer: kort, nyckeltal, handlingsrad, fokusflöde och mockdata. nyckelord: recept, hur, guide, kort, kpi, action, flöde, mock ## Den kanoniska förlagan Recepten nedan säger hur. packages/modules/arenden visar hur det ser ut när de är följda – en riktig modul i drift, inte ett exempel byggt för att visas upp. Visar mest av modulkontraktet på minst kod: nav, en main-slot och en actions-provider i samma manifest, med data och typer åtskilda från komponenten. Läs den i ordningen nedan och diffa ditt eget arbete mot den. Fil | Vad den visar packages/modules/arenden/src/manifest.ts | Manifestet. defineModule med nav, slots.main och slots.actions – actions är en funktion eftersom innehållet kommer per användare. packages/modules/arenden/src/data/types.ts | Datakontraktet. Formen ett riktigt mikrotjänst-API skulle ge, inte formen komponenten råkar vilja ha. packages/modules/arenden/src/data/mock.ts | Mockdatan, skild från hämtningen. Bytet till en riktig källa ska bli en adapterfråga. packages/modules/arenden/src/data/index.ts | hamtaArenden(ctx): den enda vägen in till datan. Speglas av /api/mock/arenden. packages/modules/arenden/src/components/Arendetabell.tsx | Server component byggd enbart på primitiver ur @startsidan/ui. Ingen egen CSS, inga hårdkodade värden. apps/portal/src/app/dev/portal/katalog.ts | Katalogposten arenden: identiteten, källan, relationerna. Skriven i samma commit som koden. apps/portal/src/app/dev/portal/dok.ts | Doket arenden: ansvar, filkarta, API, kontrakt, kvarstår. Utan det räknas modulen som påbörjad. De två sista raderna är inte efterarbete. En modul utan katalogpost och dok räknas som påbörjad, inte levererad, och drifttesterna fäller bygget – också för förlagan själv. ## Skapa modulen från noll Fyra filer och en rad i registret. Kopiera dem rakt av – formen är densamma i alla moduler, och det är den likheten som gör dem läsbara. packages/modules//package.json: ``` { "name": "@startsidan/modul-", "version": "0.1.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts", "./data": "./src/data/index.ts" }, "files": ["src"], "scripts": { "typecheck": "tsc --noEmit" }, "dependencies": { "@startsidan/module-sdk": "workspace:^", "@startsidan/ui": "workspace:^" }, "peerDependencies": { "react": "^19.0.0" }, "devDependencies": { "@types/react": "^19.2.0", "typescript": "~6.0.3" } } ``` - Paketet publiceras aldrig – private och workspace:^ hela vägen. - exports-mappen har två ingångar: "." för manifestet och "./data" så att route handlern kan spegla datafunktionen. - react är peerDependency, inte dependency: appen äger versionen. - Inga lint- eller test-skript behövs. Roten lintar och testar hela repot på filnamn, så en ny modul täcks utan att någon lägger till något. packages/modules//tsconfig.json: ``` { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "noUncheckedIndexedAccess": true, "noEmit": true, "skipLibCheck": true, "isolatedModules": true, "verbatimModuleSyntax": true }, "include": ["src"] } ``` - noUncheckedIndexedAccess: en uppslagning i en array eller ett Record ger T | undefined. Räkna med det i stället för att lägga till ett utropstecken. - verbatimModuleSyntax: typimporter måste skrivas import type { … }, annars faller bygget. packages/modules//src/index.ts: ``` export { manifest } from "./manifest"; export { hamtaNagot } from "./data"; export type { Nagot, NagotSvar } from "./data"; ``` packages/modules//src/data/index.ts: ``` import type { Role, UserContext } from "@startsidan/module-sdk"; /** Formen är den ett mikrotjänst-API skulle svara med. */ export interface Nagot { id: string; rubrik: string; kommentar: string; } export interface NagotSvar { hamtad: string; poster: Nagot[]; } const mock: Record = { chef: { hamtad: "2026-08-20T07:00:00+02:00", poster: [] }, medarbetare: { hamtad: "2026-08-20T07:00:00+02:00", poster: [] }, }; /** Den enda ingången till modulens data, och det enda som ändras i fas 4. */ export async function hamtaNagot(ctx: UserContext): Promise { return mock[ctx.role]; } ``` packages/modules//src/manifest.ts: ``` import { defineModule } from "@startsidan/module-sdk"; import { NagotKort } from "./components/NagotKort"; export const manifest = defineModule({ id: "", name: "Visningsnamn", roles: ["medarbetare", "chef"], nav: [{ label: "Visningsnamn", href: "#", order: 50 }], slots: { main: [{ id: "-kort", order: 40, Component: NagotKort }], }, }); ``` Komponenten skrivs enligt nästa recept. Därefter registreras modulen – tre rader, se Modulkontraktet. 1. Sist: dokumentera modulen Modulen är inte klar förrän den har en katalogpost och ett dok. Båda är data, båda ligger i portalen, och båda skrivs i samma commit som koden. ``` // apps/portal/src/app/dev/portal/katalog.ts – identiteten { id: "", app: "portal", slag: "Modul", namn: "", status: "Byggd", beskrivning: "…", kalla: "packages/modules/", relationer: ["ui", "module-sdk"], levererar: ["slots.main: -kort"], roller: ["medarbetare", "chef"] } // apps/portal/src/app/dev/portal/dok.ts – djupet { id: "", sammanfattning: "…", ansvar: [...], filer: [...], api: [...], beroenden: [...], kvarstar: [...], uppdaterad: "ÅÅÅÅ-MM-DD" } ``` - /dev/moduler#registrera — Registrera modulen (beroende, transpilePackages, registry.ts) - /dev/ui — Designsystemet (primitiverna komponenten ska byggas av) ## Lägg till ett kort på arbetsytan 1. Skriv komponenten En async server component som hämtar sin data och renderar ett Card. Rubriken sätts av Card – skriv aldrig en egen h2 inuti. ``` // packages/modules//src/components/NagotKort.tsx import { Card, Chip, ListRow, MoreLink } from "@startsidan/ui"; import type { UserContext } from "@startsidan/module-sdk"; import { hamtaNagot } from "../data"; export async function NagotKort({ ctx }: { ctx: UserContext }) { const { poster } = await hamtaNagot(ctx); return ( Visa alla →}>
    {poster.map((p) => ( {p.chip.etikett}} /> ))}
); } ``` 2. Anmäl den i manifestet main för huvudinnehåll, side för stödinformation. order är lägre först. ``` slots: { main: [{ id: "nagot-kort", order: 30, Component: NagotKort }] } ``` 3. Kontrollera mot designsystemet headingId satt, tokens i stället för hex, statusfärg i chip eller kant – inte på hela kortet. Felindexet på /dev/ui listar resten. ## Lägg till ett nyckeltal Nyckeltalsraden har fyra spår över hela arbetsytans bredd. Värdet bär tonen, aldrig hela rutan. ``` // datat: en post per ruta interface NyckeltalPost { id: string; etikett: string; // versal, kort varde: string; enhet?: string; kommentar: string; ton?: "neutral" | "positiv" | "varning"; lank: string; } // manifestet slots: { kpi: [{ id: "-nyckeltal", order: 0, Component: Nyckeltal }] } ``` Rutans markup finns som mönstret monster.nyckeltal på /dev/ui – kopiera därifrån i stället för att skriva om den. ## Lägg till en rad i "Att agera på" Modulen bidrar med data, inte med ett kort. Kortet agera renderar raderna åt alla (ADR-007). ``` // packages/modules//src/manifest.ts async function actions(ctx: UserContext): Promise { if (ctx.role !== "chef") return []; const { antal } = await hamtaNagot(ctx); if (!antal) return []; return [ { id: "-attest", order: 20, status: "warn", // krit när tidsgränsen passerats title: "Attest: " + antal + " ansökningar", meta: "Kontrollerad mot schema och saldo", chip: { tone: "warn", label: "Väntar" }, action: { label: "Godkänn", href: "#", variant: "primary" }, }, ]; } export const manifest = defineModule({ …, slots: { actions } }); ``` - Returnera tom lista när det inte finns något att göra – raden ska aldrig visas tom. - status följer av data: passerad tidsgräns → krit, väntar på någon → warn, klart att utföra → ok. - Är underlaget AI-genererat sätts aiNote, t.ex. "✦ Utkast finns". ## Bygg ett fokusflöde 1. Deklarera rutten Skalet renderar vilken modulrutt som helst. mode: "focus" ger topprad med Avbryt och inget annat krom. ``` routes: [{ path: "/flode/", title: "Rubrik i toppraden", mode: "focus", Component: MittFlode, }] ``` 2. Skriv flödet som data FlowDefinition med autoFields och askFields. Skrivs flödet som data redan nu blir M4 en flytt i stället för en omskrivning. 3. Lägg serverändan i en route handler Prompt, profil och beslutet att skapa något bor på servern. Klienten skickar text och renderar svaret. ``` // apps/portal/src/app/api/flows//route.ts const user = await getCurrentUser(); // identiteten ur sessionen if (!inomTakgrans(user.userId)) return 429; const rad = Body.safeParse(await request.json()); // zod // … kör turen, validera, besluta ``` 4. Visa kvittot Samtalet är inmatningen, formuläret är kvittot. Användaren ska se i realtid vad assistenten uppfattat – AI-tonad kant och ✦ per fält. 5. Skapa aldrig utan bekräftelse Att modellen sätter create: true räcker aldrig. Servern kontrollerar själv att allt obligatoriskt finns, och användaren bekräftar uttryckligen. ## Lägg till mockdata Datat bor i modulen, inte bakom ett nätverksanrop. Komponenten är en server component och anropar funktionen direkt – en fetch mot den egna appen vore ett onödigt hopp ut och tillbaka. Formen på datat ska ändå vara den ett mikrotjänst-API skulle ge, så att fas 4 blir ett byte inuti funktionen. ``` packages/modules//src/data/ ├── types.ts formen: samma som mikrotjänsten skulle svara med ├── mock.ts datat (större moduler; små håller allt i index.ts) └── index.ts hamtaNagot(ctx) – den enda ingången, och det enda som ändras när källan blir riktig ``` Route handlern speglar samma funktion: ``` // apps/portal/src/app/api/mock//route.ts import { hamtaNagot } from "@startsidan/modul-"; import { getCurrentUser, toContext } from "../../../../shell"; export async function GET() { const user = await getCurrentUser(); return Response.json(await hamtaNagot(toContext(user))); } ``` - Route handlern är dokumentation, inte datakälla: den visar vilken form API:et skulle ha. Modulen anropar aldrig sin egen endpoint. - Modulen exporterar datafunktionen i sin index.ts så att route handlern kommer åt den. - Rollskillnader hör hemma i datat eller i funktionen, inte i komponenten. - Klientkomponenter som ska skriva något anropar en riktig route handler – se felanmalan/src/store.ts. --- # AI-lagret id: ai path: /dev/ai titel: AI-lagret summering: Var AI får bo, hur turkontraktet garanteras och varför servern alltid fattar beslutet. nyckelord: ai, anthropic, structured outputs, turkontrakt, prompt, rate limit, eneo, wso2 ## Var AI får bo - AI-anrop går alltid via en route handler på servern. Nyckeln finns aldrig i klientkod – POC:en anropade api.anthropic.com direkt från webbläsaren, och det får inte följa med in i modulen. - Prompten, profilen och beslutet att skapa något bor i route handlern, inte i komponenten. - Klienten skickar text och renderar svaret. Inget mer. - AI_PROVIDER=mock ger en regelmotor för offline-demo. Kontraktet är detsamma oavsett provider. - Svar som blir data hämtas med structured outputs. Fri chatt som bara visas – Min AI – strömmas som text med strommaChatt och renderas som typade block, aldrig som HTML (ADR-023). - Verktyg ges till modellen av servern, efter användarens sparade val – aldrig efter något i klientens anrop. Egna verktyg har strict-schema mot modellen och zod på servern innan något skapas; webbsidor och MCP-svar är data, inte instruktioner (ADR-024). ## Turkontraktet Ett samtalsflöde är en serie turer med samma form. Formen garanteras av API:et, inte av en instruktion i prompten. apps/portal/src/ai/anthropic.ts: ``` const schema = buildTurnSchema(flow); // zod ur flödesdefinitionen const system = byggSystemprompt(flow, profil); const svar = await client.messages.parse({ model: process.env.AI_MODELL ?? "claude-sonnet-5", max_tokens: 4000, system, output_config: { format: zodOutputFormat(schema), // structured outputs effort: "low", }, messages: historik, }); ``` - Structured outputs (output_config.format) eller strict: true på verktyget – aldrig "svara bara med JSON". - Schemat byggs ur flödet och validerar dessutom svaret på servern. - Uteblir ett giltigt svar görs ett omtag, inte en loop: latensen hålls nere och kostnaden förutsägbar. - Modellen styrs av AI_MODELL så den kan bytas utan kodändring. ## Servern fattar besluten Att modellen sätter create: true räcker aldrig. Route handlern validerar själv att allt obligatoriskt finns innan något skapas – valideringen är inte till för formatets skull utan för behörighetsbeslutet. ``` const user = await getCurrentUser(); // identiteten ur sessionen, // aldrig ur klientens payload if (!inomTakgrans(user.userId)) return 429; // takgräns: 40 anrop / 5 min const rad = Body.safeParse(await request.json()); // zod på inkommande const varden = sanera(tur.varden); // styrtecken bort, 500 tecken if (!allaObligatoriskaIfyllda(flow, varden)) { /* fråga vidare, skapa inte */ } ``` ## Märkning och färg - Allt AI-genererat märks synligt: AiChip i vyn, aiNote på en action-rad. - Grönt (--color-ai-*) är reserverat för AI-ytor och får aldrig betyda status. - En AI-yta ska kunna peka på sin källa – lämna plats för en källrad eller en länk. ## Framåt Steg | Innebär M3 | Ärendeadapter: ärendet dyker upp i modulen arenden och som notis. M4 | Motorn lyfts till packages/flow-engine. Acceptanskriteriet är att ett andra flöde kan definieras på under en timme utan ny UI-kod. Fas 4 | Express-BFF: sessioner, nycklar, rate limit i Redis, adaptrar mot riktiga källor. Eneo | Kommunens AI-plattform bakom WSO2 som ett tredje värde på AI_PROVIDER. --- # Teknisk stack id: stack path: /dev/stack titel: Teknisk stack summering: Valen, versionerna och skälen – plus container, deploy och miljövariabler. nyckelord: stack, next, react, typescript, tailwind, yarn, docker, dokploy, env ## Valen Kommunens stack, utan deras byggblock. Avvikelserna är medvetna och dokumenterade som ADR:er. Lager | Val | Version | Varför Ramverk | Next.js, App Router | 16.3.1 | Kommunens starter kör Pages Router; App Router är Next:s huvudspår och passar modulkompositionen (ADR-001). UI | React | 19.2.8 | Server components som default. Språk | TypeScript strict | ~6.0.3 | Inga any. Varianter som unionstyper. Stil | Tailwind v4 med egna tokens | 4.3.3 | Tokens som @theme i packages/tokens, porterade 1:1 ur prototypen. Komponentgrund | shadcn/ui i repot | new-york | Radix bär tillgängligheten, färgen är vår (ADR-008). State | Zustand | 5.0.15 | Som kommunens starter. Roll och dockningsläge. Validering | Zod | 4.4.3 | Inkommande data och schemat bakom structured outputs. Dra och släpp | @dnd-kit (core, sortable) | 6.3.1 / 10.0.0 | Användarens ordning av korten (ADR-018). Tangentbord och skärmläsare ingår; ett eget dragsystem skulle inte göra det. AI | @anthropic-ai/sdk | 0.120.0 | Provider-agnostisk adapter bakom AI_PROVIDER. Monorepo | yarn workspaces | yarn 4.9.1 | Kommunen kör yarn. Turborepo först om byggtiderna kräver. Test | Vitest | 4.1.11 | Kommunens grindar i lättviktig prototypversion. Runtime | Node | >= 24 | Som kommunens starter. ## Container och deploy - next.config.ts har output: "standalone" och basePath: "/portal" – containern kör node apps/portal/server.js. - outputFileTracingRoot pekar två nivåer upp, annars gissar Next fel monorepo-rot. - Varje nytt modulpaket måste läggas i transpilePackages. - Dokploy pekar på docker-compose.yml. Git-push ger deploy. - En Compose-tjänst får inga Traefik-etiketter automatiskt – bara Application-typen gör det. Etiketterna och dokploy-network står därför i compose-filen. Utan dem svarar Traefik 404 med sitt standardcertifikat, och Let's Encrypt får inget att utfärda för. - Healthchecken använder 127.0.0.1, inte localhost: busybox wget provar ::1 först och Next lyssnar bara på IPv4. - Tjänsterna heter start-web, och framåt start-bff, start-redis, start-db. Prefixet skiljer stacken från andra på samma värd. - Vid isolerad deploy ligger start-web i två nätverk: stackens eget och dokploy-network. traefik.docker.network avgör vilket Traefik ska använda – utan den kan den välja fel IP. Framtida tjänster ska bara ligga i det egna nätverket. - Hemligheter läggs som Environment i Dokploy, aldrig i repot. Men de måste också listas under environment i docker-compose.yml – Dokploys Environment fyller bara compose-interpolationen, och en variabel som saknas i listan når aldrig containern. - /api/health svarar { ok: true } och används både som healthcheck och rökkontroll efter deploy. Verifiera som Dokploy gör: ``` docker network create dokploy-network # en gång; på servern gör Dokploy det docker compose up -d --build docker inspect $(docker compose ps -q start-web) \ --format '{{.State.Health.Status}}' # ska bli healthy ``` ## Miljövariabler Variabel | Värden | Betydelse DOMAIN | värdnamn | Domänen Traefik ska svara på. Standard start.sundsvall.dev; sätts i Dokploy om den ändras. ACCESS_CODE | valfri sträng | Låser demon bakom en kod (ADR-012). Projektets yta på roten (utom ritningen och ändringsloggen), prototypens ingång /portal, /vision och /dev är alltid öppna. Osatt eller tom = ingen spärr alls, vilket betyder att en deploy utan variabeln är helt öppen. AI_PROVIDER | mock | anthropic | mock ger regelmotorn för offline-demo. Standard är mock. AI_MODELL | modell-id | Standard claude-sonnet-5. Bytbar utan kodändring. ANTHROPIC_API_KEY | hemlighet | Bara på servern. Saknas den faller adaptern tillbaka på mock – och varnar en gång i loggen, så att tystnaden inte döljer det. MIN_AI_MCP | JSON-lista | MCP-tjänster som Min AI får ansluta till: [{ id, namn, beskrivning, url, token? }], bara https (ADR-024). Tom = inga. Listas i docker-compose.yml. ROSTNING_ADMIN_KOD | hemlighet | Slår på workshoprösten och låser /rosta/admin (ADR-028). Tom = röstningen är avstängd. STATUS_SKRIVNYCKEL | hemlighet | Krävs för att skriva statusrapporter, som Bearer-token mot /api/status eller i formuläret på /status/ny (ADR-032). Skild från ACCESS_CODE. Tom = ingen kan skriva; läsningen är öppen. ## Kvalitetsgrindar - yarn lint och yarn test gröna före varje commit. Båda körs från roten – eslint.config.mjs och vitest.config.mts – och täcker alla paket, så ett nytt modulpaket omfattas utan att något läggs till. - yarn typecheck kör tsc i varje paket. Modulpaketen har noUncheckedIndexedAccess, så en uppslagning ger T | undefined. - Drifttesterna i dev/portal/portal.test.ts fäller bygget när portalen beskriver något som inte finns: en modul utan katalogpost eller dok, ett dok som saknar ett obligatoriskt fält, eller en manifestkopia som glidit från module-sdk. - docker compose up --build före push. - Katalogpost och dok skrivna innan funktionen lämnas över för test. Täckningen syns på /dev/docs och kontrolleras av yarn test. - Conventional Commits, ämnesraden på svenska. - Ett arkitekturbeslut skrivs som en kort ADR i docs/beslut/ i samma commit som ändringen. --- # Beslutsregistret id: beslut path: /dev/beslut titel: Beslutsregistret summering: ADR:erna: vad som är bestämt, varför, och vad som ännu inte är avgjort. nyckelord: adr, beslut, arkitektur, register ## Antagna beslut Ett arkitekturbeslut skrivs som en kort ADR i docs/beslut/ i samma commit som ändringen. Ändras ett beslut ersätts ADR:en, den skrivs inte om. ADR | Beslut | Status | Innebörd ADR-001 | App Router istället för Pages Router | Antagen | Kommunens starter kör Pages Router; ett nybygge väljer App Router. Server components passar modulkompositionen och det är Next:s huvudspår framåt. ADR-002 | Egen designgrund istället för @sk-web-gui | Ersatt | Formspråket i V3 skulle inte tvingas in i ett bibliotek som bär sin egen design. Ersatt av ADR-008, som behåller slutsatsen men byter grund. ADR-003 | "Sidor" är en egen sidtyp | Antagen | Redaktionellt innehåll beskrivs av ett tunnare PageManifest vid sidan av ModuleManifest. Två varianter av samma sidtyp: sektionsingång och artikel. ADR-004 | routes + fokusläge i ModuleManifest | Antagen | En modul kan äga en helskärmsvy. mode: "focus" ger topprad med Avbryt och inget annat krom. ADR-005 | Flödesmotorns kontrakt | Skrivs | Skrivs i M4, när motorn lyfts ur felanmalan till packages/flow-engine. ADR-006 | Rollen i cookie; moduler hämtar egen data | Antagen | Demoanvändaren bor i en cookie och läses på servern. Modulerna är server components som hämtar sin egen data i stället för att få den i props. ADR-007 | Aggregerande moduler | Antagen | Modulen agera äger ingen egen data utan renderar andras actions. Registret läses via provideRegistry. ADR-008 | shadcn/ui som komponentgrund | Antagen | Komponenterna kopieras in i repot och all färg går genom våra egna CSS-variabler. Radix bär tillgängligheten; primitivernas API är oförändrat. ADR-009 | Designsystemet som data | Antagen | Systemet skrivs som typad data och renderas som sida, JSON och plattext ur samma källa. Stabila id, uttömmande varianter, felindex i formen fel → rätt → varför. ADR-010 | Devportalen som ingång för människa och agent | Antagen | Allt en byggare behöver samlas under /dev, i två vyer ur samma data. Designsystemet blir en del av portalen och nås på /dev/ui. ADR-011 | Dokumentation som leveransvillkor | Antagen | Varje del har ett dok i portalen enligt ett dokumentationskontrakt. Klar betyder körbar, granskad och dokumenterad; täckningen räknas fram på /dev/docs. ADR-012 | Åtkomstkod som demospärr | Antagen | ACCESS_CODE låser demon i middleware. Projektsidan, prototypens ingång, visionssidan och devportalen är öppna, så att dokumentationen går att läsa och hämta av verktyg. Uttryckligen inte autentisering: koden delas av alla och säger ingenting om vem användaren är. ADR-014 | Portalens påståenden verifieras | Antagen | Det portalen påstår om koden kontrolleras av yarn test och yarn lint: drifttester, uppräkningsbara fält där det stod prosa, lintregel för R4 och namngivna antimönster. Går ett påstående inte att kontrollera ska räckvidden stå utskriven. ADR-013 | Signerad fältförankring mellan turer | Antagen | Fältens tidigare värden skickas tillbaka så att assistenten inte formulerar om dem, men bara med serverns egen HMAC. Utan signaturen vore de en inmatningsväg förbi samtalet. ADR-015 | Dokumentflöden definieras som data i modulen | Antagen | Ett dokument med typade avsnitt passar inte i FlowDefinition, som beskriver platta fält. Definitionen bor i modulen med samma bärande idéer, och ADR-005 skrivs i M4 mot två former i stället för en. Utan fältförankring: varje avsnitt går att skriva i direkt, så klientens värden är inte en väg förbi samtalet utan själva inmatningen. ADR-013 gäller flöden där modellens extraktion är enda källan. ADR-016 | PageManifest med källoberoende innehåll | Antagen | PageManifest skrivs i module-sdk med parent (hierarkin) och content (statiskt eller en källa). Innehållet är typade block, aldrig HTML. Första källan är Sitevisions headless-API på sundsvall.se, med timeout, cache och inspelad reserv; adaptern bor i modulen webbsidor tills en andra konsument finns. ADR-017 | En Sitevision-webbplats inramad i skalet | Antagen | Simulerar en väg tillbaka till startsidan från Sitevision: webbplatsens meny som rad överst, sidorna i en sandlådad ram med skalet runt. PageLayout får "embed"; skalet äger listan webbplatser och läser källans ramskydd i förväg. En sida visas via REST som jämförelse. Tillägg: exemplen samlas i sektionen Sitevision, och menyraden visas bara inne på webbplatsen. ADR-018 | Användaren ordnar korten på startsidan | Antagen | Läget "Anpassa startsidan" med dragning (@dnd-kit) och knappar som alternativ, tillgängligt enligt WCAG 2.2: fokus stannar, flyttar annonseras med plats, reducerad rörelse respekteras, Ångra finns. Ordningen sparas per roll i en kaka som servern läser. Inga kort låses till en spalt: varje kort ligger i en @container och anpassar sig efter sin bredd (R11). Widget får label. ADR-019 | Modulvyer med skalets krom och vägparametrar | Antagen | mode "standard" från ADR-004 byggs: modulvyer under /verktyg/ med sidopanel och menyrad. path får parametrar (/verktyg/personal/:id) som vyn får som params. Fokusflöden ligger kvar under /flode/. ADR-020 | Notiserna kommer från modulerna och leder till funktioner | Antagen | ModuleManifest.notifications och notificationsFor, som actions i ADR-007. Varje notis har href till funktionen. Klockan är en panel i skalet; läst sparas per roll i en kaka. Notiser räknas fram ur datat, t.ex. ett utvecklingssamtal som väntar på chefens godkännande. ADR-021 | Fokusytan under AI-fältet på startsidan | Antagen | slots.fokus lämnar en FokusItem som data; skalet visar den nyaste som inte stängts under AI-fältet, formad som en nyhet med bild, tills användaren stänger den. Stängt sparas per roll i en kaka. Demoknapp i användarmenyn. ADR-022 | Ingångens egna kort och hopfällbara block – IT-support som första sida | Antagen | IT-support byggs som första sida enligt ADR-003. Prövningen ger PageManifest.slots (modulens egna kort på en ingång), PageContent.cards (fler redaktionella kort), blocket details (hopfällbar fråga med valfritt chip) och QuickLink.cta. Felanmälans nav-post flyttar till sidans snabbväg. ADR-023 | Min AI – en personlig assistent med Eneos sessionskontrakt | Antagen | Min AI är en modul med modulvy, navpost och kort på startsidan. Kontraktet är Eneos ({ question, session_id }), så att motorn kan bytas mot Eneo i route handlern. Samtalen och historiken bor på servern och nås bara av ägaren. Svaret strömmas som text och renderas som typade block, aldrig HTML. ADR-024 | Verktyg, skills och MCP-tjänster i Min AI | Antagen | Min AI får webbsök, skapa bild (SVG), skapa dokument (.docx) och skapa graf, plus MCP-tjänster via Anthropics connector. Användaren slår på och av var och en i skrivfältet; valen sparas på servern, som ensam avgör vilka verktyg modellen får. Egna verktyg har strict-schema mot modellen och zod-validering på servern. Nya diagramtokens --color-graf-1…4. ADR-025 | Modulvyer över hela arbetsytan, och genvägar bredvid hälsningen | Antagen | mode: "full" ger en modulvy under /verktyg/ med sidopanelen kvar men hela arbetsytan i skärmens höjd – ingen marginal, sidfot eller AI-knapp. slots.genvag ger knappar bredvid hälsningen på startsidan, AI-tonade med ai: true. Min AI använder båda: knappen ersätter kortet, och samtalet tar ytan som i de stora chattjänsterna, med plats för ett dokument till höger. ADR-026 | AI tonas i blått, inte grönt – grönt är bara status | Antagen | Grönt är status – klart, godkänt, positivt – och betyder aldrig AI. AI-tokens behåller namnen men byter till blå toner, ljusare än identitetsblått (--color-ai #1f64b8). Signalen för AI är märkningen ✦/AiChip, inte färgen. Ersätter regeln att grönt är reserverat för AI. ADR-027 | Målgruppsingång – Verktygslådan under Digitalisering, IT och AI | Antagen | En målgruppsingång är en sektion med en ingång – inget nytt i kontraktet. Sektionen Digitalisering, IT och AI får ingången Verktygslådan, porterad från den fristående verktygslådan, med katalog, verksamheter, omvärld och kontakt som egna kort och en artikel per verktyg. Det databasburna (digitala arvet, PIOS, nyttoportföljen) stannar i verktygslådan. ADR-028 | Workshoprösten – röstkoder utanför spärren, prickar i en fil | Antagen | Prioriteringen efter workshopen görs med prickröstning på /rosta. En röstkod per deltagare, framräknad ur adminkoden med HMAC och aldrig sparad, ger tre prickar. Sidan och API:t ligger utanför demospärren: röstkoden är nyckeln, och lapparna visas i publik form. Prickarna sparas i en JSON-fil på en volym – ingen databas för en workshop. Röstkoden är en biljett, inte en identitet; ingenting byggs ovanpå åtkomstkakan. ADR-029 | Projektets sida på roten, prototypen på /portal | Antagen | Roten blir projektets sida: vad projektet är, var det står, vad som kommer och allt material samlat, med "Testa prototypen" som huvudärende. Den tidigare introsidan med rollvalet flyttar till /portal, och Logga ut leder dit. Båda är öppna och skrivs publikt; projektboken, tidslinjen, samtalen och workshopen ligger kvar bakom spärren och får bara en rubrik på roten. ADR-030 | Kompetens är ett register, inte ett LMS | Antagen | Kommunen köper eller bygger inget nytt LMS. Lärplattformen (Moodle) förblir kursmotorn; det som saknas – vad en person har, vad rollen kräver och när det går ut – blir ett eget register bakom WSO2 med kontraktet kompetens@1, matat från Moodle, ID06, Skolverket och chefens intyg. Startsidan visar det som modulen kompetens, som ersätter utveckling. Planen föds ur utvecklingssamtalet; servern prövar varje aktivitet innan något sparas. ADR-031 | Projektboken ordnas efter typ, inte efter tillfälle | Antagen | Projektbokens meny har fasta flikar – Läget, Tidslinjen, Spåren, Beslut och frågor, Material – som inte blir fler när projektet växer. Allt är typade poster med en fast adress. Varje möte och workshop får en sida under /projekt/handelser/-, och tidslinjen är ingången till dem, senast först med filter på slag och spår. Läget skrivs över; historiken läggs till. Toppraden bär kommunens logotyp enligt kommunikationsguidens mall. ADR-032 | Projektet är roten, med statusrapporter och funktionerna | Antagen | Projektbokens innehåll flyttar från /projekt till roten och den gamla projektsidan slås ihop med Läget. Prototypen står överst men lågt. En topprad med fasta flikar gäller alla projektets sidor. Statusrapporter på /status skrivs i ett formulär eller via /api/status med skrivnyckeln STATUS_SKRIVNYCKEL, och servern prövar varje fält. Funktionerna samlas på /funktioner i tre lager. Allt är öppet utom mötenas anteckningar och ritningen; gamla adresser leds permanent vidare. ADR-033 | Projektets meny är en klassisk huvudmeny enligt WCAG 2.2 AA | Antagen | Flikraden ersätts av en klassisk huvudmeny efter header A: Start, Status, Tidslinje, Beslut och Funktioner, och grupperna Dokument och Teknik som fälls ut, i en rad dockad mot toppradens underkant under avsändaren. Prototypen länkas inte från projektets yta tills vidare; den nås på /portal. Grå text, blått och ett streck för sidan man är på. Under md en Meny-knapp med samma lista, en post per rad; toppraden klistrar bara från lg. W3C:s disclosure-mönster med aria-expanded och aria-current, Escape stänger. Hopplänk till
på varje sida med toppraden. Menyn är data i projektyta/meny.ts. ADR-034 | Projektstödet svarar bara utifrån projektets öppna sidor | Antagen | Fråga om projektet är ett stöd för att förstå projektet, inte en chattassistent: en frågeruta på startsidan och en knapp på varje öppen undersida. Underlaget är de öppna sidorna som en besökare ser dem – servern läser sina egna sidor utan kaka – och ett test fäller bygget om en sida i underlaget är låst. Varje sida är ett dokument med källhänvisningar, så att svaret visar var det står. Servern äger samtalet; /api/fraga är öppen med egna takgränser. Claude Opus 5.5 som standard, utbytbar med PROJEKTSTOD_MODELL. ADR-035 | Notiser till teamschatten via ett flöde i Power Automate, avstängda tills de slås på | Antagen | Nya händelser i tidslinjen blir kort i teamets gruppchatt. Ett flöde i Power Automate tar emot kortet och publicerar det som Flödesrobot; Teams gamla Incoming Webhooks är avvecklade. Servern skickar när en ny version startar, så att sidan finns, och minns vad som skickats i datavolymen. TEAMS_NOTISER är av som standard; första gången och i läget logg markeras allt utan att skickas. Varje inlägg i tidslinjen godkänns av Jari Koponen innan det läggs till, och varje godkänt inlägg skickas oavsett slag, högst fem kort per start. ADR-036 | Att göra-listan redigeras på sidan och bor i en SQLite-fil | Antagen | Uppgifterna på /att-gora läggs till, ändras och byter läge på sidan av den som loggat in med samma skrivnyckel som statusrapporterna; åtkomstkoden räcker inte. De bor i en SQLite-fil med Nodes inbyggda node:sqlite i datavolymen, ATT_GORA_FIL. uppgifter.ts är startdata: det som saknas läggs in vid start, det som finns rörs inte. Servern prövar varje skrivning – ett möte med egen sida eller löpande, länkar från roten – och en uppgift tas aldrig bort. ADR-037 | En ändringslogg i startsidans databas | Antagen | Att göra-listans SQLite-fil blir startsidans databas, DATABAS_FIL, med ett schema som växer i numrerade steg; den gamla filen kopieras in en gång. En tabell andringar får en rad för varje ändring – statusrapporter, uppgifter och nya byggen – skriven av den som ändrar. Uppgifternas rader är inte publika: till chatten går bara statusrapporterna. Den tekniska tidslinjen är /andringar bakom åtkomstkoden, och varje uppgift visar sina rader. Notiserna till teamschatten ska läsa loggen i ett senare steg, och då bara statusrapporterna. ADR-038 | Funktionerna läggs till på sidan och via API, och bor i databasen | Antagen | Funktionerna på /funktioner bor i startsidans databas; funktioner.ts är startdata. Ett nytt läge, Önskemål, för det som kommit in men inte prövats. Den som har skrivnyckeln lägger till och ändrar på sidan, och API:t /api/funktioner tar emot POST, PUT och PATCH med skrivnyckeln som Bearer-token – vägen för en agent att lägga upp något i produktion. GET är öppet. Servern prövar varje skrivning, ger id:t ur namnet och tar aldrig bort en funktion; varje ändring loggas. ADR-039 | Konton för projektgruppen – användarnamn och lösenord, först för att göra-listan | Antagen | Projektgruppen får egna konton med användarnamn och lösenord, som en mellanlösning tills SAML-sessionen i fas 4; åtkomstkoden är fortfarande ingen identitet. Två roller, medlem och admin. Admin skapar konton på /konton med ett engångslösenord som byts vid första inloggningen; det första kontot skapas med skrivnyckeln. Engångslösenordet gäller i sju dagar. Lösenorden sparas som scrypt med OWASP:s parametrar och prövas mot vanliga lösenord; tio fel i rad spärrar kontot i en kvart. Sessionen gäller i 90 dagar och förnyas när den används (ändrat 20 oktober 2026), med bara kakans fingeravtryck i databasen. Ett konto stängs men tas aldrig bort. Ändringsloggen får kolumnen av, som visas bara för inloggade. Först för att göra-listan; statusrapporterna, funktionerna och API:erna behåller skrivnyckeln. ADR-040 | Projektportalen och startsideportalen är två applikationer på en domän | Antagen | start.sundsvall.dev rymmer två applikationer med olika syften: projektportalen, projektet om startsidan, och startsideportalen, prototypen av själva produkten. De delar domän för användarnas skull, inte för att de hör ihop. Allt som hör till prototypen ska ligga under /portal, också devportalen som blir /portal/dev; allt annat är projektportalen. CLAUDE.md delas i gemensamt, projektportalen och startsideportalen, och ingen identitet korsar gränsen. Koden delas i steg enligt docs/plan-uppdelning.md – fas 6 i planen – när projektet är redo. ## Öppna frågor - Flödesmotorns kontrakt (ADR-005) skrivs i M4. Acceptanskriteriet är att ett andra flöde kan definieras på under en timme utan ny UI-kod. - Största risken är fortfarande att modulkontraktet blir fel dimensionerat – för tunt och skalet måste ändras för varje modul, för tjockt och det blir byråkrati. - Nya fält i ModuleManifest ska bäras av minst två moduler innan de blir permanenta. - Intranätets Sitevision som källa för Sidor: samma headless-API som sundsvall.se, men bakom inloggning. Kräver servicekonto via BFF eller WSO2 i fas 4 (ADR-016). - Vilket innehåll hör hemma på intranätet? Spiken visar skolavtalet från sundsvall.se för att pröva kontraktet, inte för att det är rätt innehåll. - /dev/beslut — ADR-001 – App Router istället för Pages Router (docs/beslut/ADR-001-app-router.md) - /dev/beslut — ADR-002 – Egen designgrund istället för @sk-web-gui (docs/beslut/ADR-002-egen-designgrund.md) - /dev/beslut — ADR-003 – "Sidor" är en egen sidtyp (docs/beslut/ADR-003-sidor-som-sidtyp.md) - /dev/beslut — ADR-004 – routes + fokusläge i ModuleManifest (docs/beslut/ADR-004-routes-och-fokuslage.md) - /dev/beslut — ADR-005 – Flödesmotorns kontrakt (docs/beslut/ (ännu inte skriven)) - /dev/beslut — ADR-006 – Rollen i cookie; moduler hämtar egen data (docs/beslut/ADR-006-roll-i-cookie.md) - /dev/beslut — ADR-007 – Aggregerande moduler (docs/beslut/ADR-007-aggregerande-moduler.md) - /dev/beslut — ADR-008 – shadcn/ui som komponentgrund (docs/beslut/ADR-008-shadcn-som-komponentgrund.md) - /dev/beslut — ADR-009 – Designsystemet som data (docs/beslut/ADR-009-designsystem-som-data.md) - /dev/beslut — ADR-010 – Devportalen som ingång för människa och agent (docs/beslut/ADR-010-devportal.md) - /dev/beslut — ADR-011 – Dokumentation som leveransvillkor (docs/beslut/ADR-011-dokumentation-som-leveransvillkor.md) - /dev/beslut — ADR-012 – Åtkomstkod som demospärr (docs/beslut/ADR-012-atkomstkod.md) - /dev/beslut — ADR-014 – Portalens påståenden verifieras (docs/beslut/ADR-014-portalen-verifieras.md) - /dev/beslut — ADR-013 – Signerad fältförankring mellan turer (docs/beslut/ADR-013-signerad-faltforankring.md) - /dev/beslut — ADR-015 – Dokumentflöden definieras som data i modulen (docs/beslut/ADR-015-dokumentflode.md) - /dev/beslut — ADR-016 – PageManifest med källoberoende innehåll (docs/beslut/ADR-016-sidor-med-extern-kalla.md) - /dev/beslut — ADR-017 – En Sitevision-webbplats inramad i skalet (docs/beslut/ADR-017-inramad-webbplats.md) - /dev/beslut — ADR-018 – Användaren ordnar korten på startsidan (docs/beslut/ADR-018-anpassningsbar-startsida.md) - /dev/beslut — ADR-019 – Modulvyer med skalets krom och vägparametrar (docs/beslut/ADR-019-modulvyer-med-skalets-krom.md) - /dev/beslut — ADR-020 – Notiserna kommer från modulerna och leder till funktioner (docs/beslut/ADR-020-notiser-fran-modulerna.md) - /dev/beslut — ADR-021 – Fokusytan under AI-fältet på startsidan (docs/beslut/ADR-021-fokusyta.md) - /dev/beslut — ADR-022 – Ingångens egna kort och hopfällbara block – IT-support som första sida (docs/beslut/ADR-022-ingangens-kort-och-block.md) - /dev/beslut — ADR-023 – Min AI – en personlig assistent med Eneos sessionskontrakt (docs/beslut/ADR-023-min-ai.md) - /dev/beslut — ADR-024 – Verktyg, skills och MCP-tjänster i Min AI (docs/beslut/ADR-024-min-ai-verktyg.md) - /dev/beslut — ADR-025 – Modulvyer över hela arbetsytan, och genvägar bredvid hälsningen (docs/beslut/ADR-025-hel-arbetsyta.md) - /dev/beslut — ADR-026 – AI tonas i blått, inte grönt – grönt är bara status (docs/beslut/ADR-026-ai-i-blatt.md) - /dev/beslut — ADR-027 – Målgruppsingång – Verktygslådan under Digitalisering, IT och AI (docs/beslut/ADR-027-malgruppsingang.md) - /dev/beslut — ADR-028 – Workshoprösten – röstkoder utanför spärren, prickar i en fil (docs/beslut/ADR-028-workshoprost.md) - /dev/beslut — ADR-029 – Projektets sida på roten, prototypen på /portal (docs/beslut/ADR-029-projektsidan-pa-roten.md) - /dev/beslut — ADR-030 – Kompetens är ett register, inte ett LMS (docs/beslut/ADR-030-kompetens-som-register.md) - /dev/beslut — ADR-031 – Projektboken ordnas efter typ, inte efter tillfälle (docs/beslut/ADR-031-projektboken-ordnas-efter-typ.md) - /dev/beslut — ADR-032 – Projektet är roten, med statusrapporter och funktionerna (docs/beslut/ADR-032-projektet-ar-roten.md) - /dev/beslut — ADR-033 – Projektets meny är en klassisk huvudmeny enligt WCAG 2.2 AA (docs/beslut/ADR-033-huvudmeny-enligt-wcag.md) - /dev/beslut — ADR-034 – Projektstödet svarar bara utifrån projektets öppna sidor (docs/beslut/ADR-034-projektstodet.md) - /dev/beslut — ADR-035 – Notiser till teamschatten via ett flöde i Power Automate, avstängda tills de slås på (docs/beslut/ADR-035-teamsnotiser.md) - /dev/beslut — ADR-036 – Att göra-listan redigeras på sidan och bor i en SQLite-fil (docs/beslut/ADR-036-att-gora-i-databas.md) - /dev/beslut — ADR-037 – En ändringslogg i startsidans databas (docs/beslut/ADR-037-andringslogg.md) - /dev/beslut — ADR-038 – Funktionerna läggs till på sidan och via API, och bor i databasen (docs/beslut/ADR-038-funktioner-i-databas.md) - /dev/beslut — ADR-039 – Konton för projektgruppen – användarnamn och lösenord, först för att göra-listan (docs/beslut/ADR-039-konton-for-projektgruppen.md) - /dev/beslut — ADR-040 – Projektportalen och startsideportalen är två applikationer på en domän (docs/beslut/ADR-040-tva-applikationer-en-doman.md) --- # Katalog Sök här innan du skapar något nytt. Varje post har sin källa i repot och sina relationer till övriga poster. id: skal app: portal slag: Skal namn: Skalet (apps/portal) status: Byggt – fas 1–2 klara lage: byggd beskrivning: Next-appen som komponerar allt: sidopanel, AI-fält, rollväxling, modulregister och rutnätet modulerna fyller. Skalet vet vilka slots som finns, aldrig vad en widget innehåller. kalla: apps/portal/src/shell relationer: module-sdk, tokens, ui, api-mock levererar: ShellFrame, AskBand, AskField, Board, FlowBar, registry, getCurrentUser, /sidor/[...vag], shell/sida, Webbmeny, webbplatser, AnpassaBoard, sparaLayout, /verktyg/[...vag], Notiser (klockan), markeraLasta, FokusYta, stangFokus / visaFokusIgen id: tokens app: gemensam slag: Paket namn: @startsidan/tokens status: Byggt lage: byggd beskrivning: Designtokens som Tailwind-tema (@theme) plus basregler. Porterade 1:1 ur prototypens :root. Enda stället där en färg får definieras. kalla: packages/tokens/src relationer: ui, skal levererar: index.css, theme.css, base.css, .wrap, .visually-hidden id: ui app: gemensam slag: Paket namn: @startsidan/ui status: Byggt – shadcn som grund (ADR-008) lage: byggd beskrivning: Primitiverna. Egna komponenter med V3:s mått, byggda ovanpå shadcn/ui så att Radix bär tillgängligheten. Modulerna känner bara till det här API:et. kalla: packages/ui/src relationer: tokens, designsystem levererar: Card, Chip, AiChip, Btn, BtnLink, StatusRow, ListRow, Progress, Avatar, MoreLink, Table, cn id: dokumentation app: gemensam slag: Paket namn: @startsidan/dokumentation status: Byggt lage: byggd beskrivning: Dokumentationsmodellen för båda applikationerna (ADR-011, ADR-040): Katalogpost, Dok, Block och dokumentationskontraktet, textvyerna för agenter och Blockvy för människor. Devportalen och projektportalens /teknik dokumenterar sina delar i samma form och efter samma kontrakt. kalla: packages/dokumentation/src relationer: devportal, ui levererar: Katalogpost, Dok, Block, dokkontrakt, APPAR / appNamn, blockTillText / katalogpostTillText / dokTillText, @startsidan/dokumentation/Blockvy id: gemensamt app: gemensam slag: Paket namn: @startsidan/gemensamt status: Byggt lage: byggd beskrivning: Det som båda applikationerna behöver och som inte är design eller AI (ADR-040): beslutsregistret, kommunens avsändare i toppraden, spärren mot gissningar, kontrollen av ?vidare= och innehållsversionen för agentfilerna. Varje del har en egen exportväg, så att en app bara drar in det den använder. kalla: packages/gemensamt/src relationer: devportal, atkomst levererar: @startsidan/gemensamt/beslut, @startsidan/gemensamt/Varumarke, @startsidan/gemensamt/felforsok, @startsidan/gemensamt/vidare, @startsidan/gemensamt/innehallsversion id: module-sdk app: portal slag: Paket namn: @startsidan/module-sdk status: Byggt lage: byggd beskrivning: Kontraktet mellan skalet och modulerna: ModuleManifest, UserContext, slots, routes, sidor (PageManifest) och flödestyperna. Ett nytt fält här måste bäras av minst två moduler innan det blir permanent. kalla: packages/module-sdk/src relationer: skal, ui levererar: defineModule, ModuleManifest, UserContext, widgetsFor, actionsFor, uppgifterFor, intentsFor, matchaAvsikt, Uppgift, ActionItem.lage/kalla/frist, navFor, routeFor, suggestionsFor, Widget.label, PageManifest.slots, ContentBlock details, Notification, notificationsFor, FokusItem, fokusFor, Genvag, genvagarFor, mode full, RouteComponent, matchaVag, PageManifest, ContentBlock, findPage, pageTrail, resolveContent, FlowDefinition, buildTurnSchema id: flow-engine app: portal slag: Paket namn: @startsidan/flow-engine status: Planerad – två flöden finns nu, underlaget för M4 är komplett lage: planerad beskrivning: Samtalsflödesmotorn. Lyfts ur modulen felanmalan när ett andra flöde kan definieras deklarativt på under en timme utan ny UI-kod. Klarar den inte det testet är den inte redo att lyftas. kalla: packages/flow-engine (finns ännu inte) relationer: felanmalan, utvecklingsarende, module-sdk levererar: FlowDefinition som data, turmotor, formulärspegel id: designsystem app: gemensam slag: Yta namn: Designsystemet status: Byggt (ADR-009) lage: byggd beskrivning: Tokens, primitiver, mönster, regler och felindex som data. Renderas som sida, JSON och plattext ur samma källa. kalla: apps/portal/src/app/dev/ui relationer: ui, tokens, devportal levererar: /dev/ui, /dev/ui/spec.json, /dev/ui/llms.txt id: atkomst app: portal slag: Yta namn: Åtkomstspärren status: Byggd lage: byggd beskrivning: Middleware som låser demon bakom ACCESS_CODE. Projektets yta på roten (utom ritningen och ändringsloggen), prototypens ingång /portal, visionssidan, /dev, healthchecken och upplåsningssidan är öppna; /start, /flode, /verktyg, /sidor och API:erna bakom dem är låsta. En demospärr, inte autentisering – den säger ingenting om vem användaren är. kalla: apps/portal/src/middleware.ts, apps/projekt/src/atkomst/ relationer: skal, intro, bygge levererar: /las-upp, ACCESS_CODE, kakan startsidan-atkomst, /robots.txt id: intro app: portal slag: Yta namn: Prototypens ingång status: Byggd lage: byggd beskrivning: Demons ingång på /portal (ADR-029). Rollvalet står först och startar demon; introduktionen till startsidan och ekosystemet ligger under. Startsidan själv ligger på /start. kalla: apps/portal/src/app/page.tsx relationer: skal, ui, devportal levererar: /portal, inloggning via loggaInSom, ingångar till /vision, /dev, /dev/docs, /dev/ui id: devportal app: portal slag: Yta namn: Devportalen status: Byggd lage: byggd beskrivning: Den här ytan. En ingång för den som ska bygga i ekosystemet – människa eller agent. Miniatyr av dev.sundsvall.se, med startsidans ekosystem som innehåll. kalla: apps/portal/src/app/dev relationer: designsystem, skal levererar: /llms.txt, /dev, /dev/spec.json, /dev/llms.txt, katalogen, recepten, antimönsterindexet id: bygge app: gemensam slag: Yta namn: Grindar och bygge status: Byggt lage: byggd beskrivning: Lint, test och typecheck över hela repot från roten, plus standalone-imagen Dokploy bygger. Ett nytt paket täcks utan att något skript läggs till. kalla: eslint.config.mjs, vitest.config.mts, apps/projekt/next.config.ts, apps/projekt/Dockerfile relationer: skal, dokploy levererar: yarn lint, yarn test, yarn typecheck, docker compose up --build id: oversikt app: portal slag: Modul namn: oversikt status: Byggd lage: byggd beskrivning: Nyckeltalsraden: rekryteringar och ledighetsansökningar för chef – chefens budget och sjukfrånvaro kommer ur dialogstod – och saldo och flex för medarbetare. kalla: packages/modules/oversikt roller: medarbetare, chef relationer: api-mock, ui levererar: slots.kpi: oversikt-nyckeltal, nav: Min översikt id: agera app: portal slag: Modul namn: agera status: Byggd lage: byggd beskrivning: Uppgifterna från alla tjänster: kortet "Att agera på" med det som väntar, och sidan Mina uppgifter med allt – öppet och hanterat, med källa och frist. Äger ingen egen data utan läser det modulerna bidrar med (ADR-007), i samma stund, så listan växer av sig själv när fler moduler byggs och visar alltid tjänsternas läge. Samma lista läser Min AI när användaren frågar om sina uppgifter. kalla: packages/modules/agera roller: medarbetare, chef relationer: arenden, ledighet, kompetens, utvecklingsarende, dialogstod, min-ai, module-sdk levererar: slots.main: agera-lista, routes: /verktyg/uppgifter (standard), nav: Mina uppgifter id: arenden app: portal slag: Modul namn: arenden status: Byggd lage: byggd beskrivning: Ärendetabellen, plus actions för de ärenden som kräver något av användaren. kalla: packages/modules/arenden roller: medarbetare, chef relationer: api-mock, agera, ui levererar: slots.main: arenden-tabell, slots.actions, nav: Mina ärenden id: kalender app: portal slag: Modul namn: kalender status: Byggd lage: byggd beskrivning: "Idag"-kortet och återkommande händelser i högerspalten. kalla: packages/modules/kalender roller: medarbetare, chef relationer: api-mock, ui levererar: slots.side: kalender-idag, slots.side: kalender-aterkommande (chef) id: ledighet app: portal slag: Modul namn: ledighet status: Byggd lage: byggd beskrivning: Ledighetskortet och attestraden för chef när något är klart att godkänna. kalla: packages/modules/ledighet roller: medarbetare, chef relationer: api-mock, agera levererar: slots.side: ledighet-kort, slots.actions id: kompetens app: portal slag: Modul namn: kompetens status: Prototyp med fiktiv data – inför mötet 4 november lage: delvis beskrivning: Kompetensregistret (ADR-030): vad en person har, vad rollen kräver och när det går ut. Kurserna bor kvar i Lärplattformen (Moodle); registret är eget och delar id med personalakten. Chefen ser det som behöver åtgärdas, täckning per område, andelsmål, professionsprogrammet och planer att bekräfta, och en matris för ett urval av område eller roll; medarbetaren sin egen kompetens och plan. Planen tas fram ur utvecklingssamtalet med ett AI-utkast som chefen justerar och servern prövar. Ersätter modulen utveckling. kalla: packages/modules/kompetens roller: medarbetare, chef relationer: api-mock, agera, personal, anthropic, module-sdk, ui levererar: routes: /verktyg/kompetens, /verktyg/kompetens/enhet/:enhet, /verktyg/kompetens/enhet/:enhet/matris, /verktyg/kompetens/person/:id, /verktyg/kompetens/person/:id/plan, /verktyg/kompetens/professionsprogrammet, /verktyg/kompetens/om (standard), slots.main: utveckling-kort (id behållet från utveckling), slots.actions: planer att bekräfta, behörigheter som går ut, egna krav och planaktiviteter, notifications: behörigheter som går ut eller gått ut, planer att bekräfta, nav: Min kompetens (medarbetare), Kompetens (chef), POST /api/kompetens/plan/utkast id: felanmalan app: portal slag: Modul namn: felanmalan status: Byggd t.o.m. M2 – M3 och M4 återstår lage: delvis beskrivning: Det första samtalsflödet: chatt plus formulärspegel i fokusläge. Samtalet är inmatningen, formuläret är kvittot, och ärendet skapas aldrig utan uttrycklig bekräftelse. kalla: packages/modules/felanmalan roller: medarbetare, chef relationer: api-felanmalan, module-sdk, flow-engine levererar: routes: /flode/felanmalan (focus), nås från IT-supportsidan, ai.intents: felanmal-dator, flow.ts id: utvecklingsarende app: portal slag: Modul namn: utvecklingsarende status: Byggd – M3, ärendeadaptern, återstår lage: delvis beskrivning: Beslutsunderlag för utvecklingsinitiativ enligt kommunens mall: fjorton avsnitt där fyra är typade radlistor med summeringar. Assistenten skriver utkastet, användaren granskar rubrik för rubrik, och servern avgör att allt obligatoriskt finns innan ärendet lämnas för beslut. kalla: packages/modules/utvecklingsarende roller: medarbetare, chef relationer: api-utvecklingsarende, api-mock, module-sdk, ui, agera, flow-engine levererar: routes: /flode/utvecklingsarende (focus), nav: Nytt utvecklingsärende, slots.actions, ai.intents: nytt-utvecklingsarende, flow.ts: MALL, KARNAVSNITT, farLamnas, berakning.ts, regelmotorn bakom AI_PROVIDER=mock id: webbsidor app: portal slag: Modul namn: webbsidor status: Spik – sundsvall.se och utveckling.sundsvall.se lage: delvis beskrivning: Sektionen Sitevision under "Sidor": en ingång som beskriver integrationssätten, och ett levande exempel för vart och ett. Kureringen bestämmer placering, snabbvägar och AI-förslag; Sitevisions headless-API levererar texten, som översätts till block. Första prövningen av PageManifest mot verkligt innehåll (ADR-016). kalla: packages/modules/webbsidor roller: medarbetare, chef relationer: module-sdk, sitevision, skal levererar: pages: integrera-med-sitevision – sätten med fördelar, nackdelar och exempel, först i sektionen Sitevision, pages: skolavtal (ingång) + tre artiklar under Sitevision, pages: sundsvall-se – webbkarta i tre nivåer under Sitevision, pages: utveckling.sundsvall.se:s meny – en sida via REST, sju i ram (ADR-017), Sitevision-adaptern: schema, översättning, hämtning med reserv, fixturer/: inspelade headless-svar id: nyheter app: portal slag: Modul namn: nyheter status: Byggd – läser sundsvall.se:s nyhetsflöde lage: byggd beskrivning: De fem senaste nyheterna från sundsvall.se i högerspalten på startsidan. Flödet är Sitevisions RSS; varje nyhet öppnas på sundsvall.se. Samma källa, cache och reservordning som webbsidor (ADR-016). kalla: packages/modules/nyheter roller: medarbetare, chef relationer: module-sdk, ui, sitevision levererar: slots.side: nyheter-flode, notifications: de två senaste nyheterna, hamtaNyheter, tolkaRss id: dialogstod app: portal slag: Modul namn: dialogstod status: Byggd med fiktiv data lage: byggd beskrivning: Dialogstöd för chef, ombyggt ur Dialogstödet (bbb): nyckeltalen för nivån under chefens egen – kommundirektören ser förvaltningarna, förvaltningschefen avdelningarna, avdelningschefen sektionerna – och uppföljningsdialogen om varje enhet i sex områden: ekonomi, HME och sjukfrånvaro med diagram, och verksamhet, digital transformation och kommunikativt ledarskap med frågor och manuell status. Aktiviteter, status och anteckningar sparas, och en aktivitet hamnar hos den ansvariga chefen i Att agera på och som en notis. Startsidans budget och sjukfrånvaro kommer härifrån, för chefens egen enhet. Förvaltningarna har sina riktiga namn; allt annat är fiktivt. kalla: packages/modules/dialogstod relationer: module-sdk, ui, api-mock, skal, agera levererar: routes: /verktyg/dialogstod, /verktyg/dialogstod/:enhet, /verktyg/dialogstod/:enhet/:omrade (standard), nav: Dialogstöd (chef), slots.kpi: ekonomi och sjukfrånvaro för chefens egen enhet, som leder in i dialogen, slots.actions: aktiviteterna för chefens egen enhet – öppna i Att agera på, klarrapporterade som hanterade i Mina uppgifter, notifications: ny aktivitet från uppföljningsdialogen, hamtaOversikt, hamtaDialog, hamtaStartnyckeltal, laggTillAktivitet, andraAktivitet, sattStatus, sparaAnteckning id: personal app: portal slag: Modul namn: personal status: Byggd med fiktiv data lage: byggd beskrivning: Chefens överblick över personalen, med sök och filter, och en personalakt per medarbetare: notiser, ärenden, anställning och vägen in till senaste utvecklingssamtalets dokumentation. Datat är fiktivt men har personalsystemets form. kalla: packages/modules/personal roller: chef relationer: module-sdk, ui, api-mock, skal levererar: routes: /verktyg/personal, /verktyg/personal/:id, /verktyg/personal/:id/utvecklingssamtal (standard), nav: Personal (chef), hamtaPersonal, hamtaMedarbetare, sammanstall, godkannSamtal, notifications: personalaktens notiser, bl.a. utvecklingssamtal att godkänna id: fokusnyhet app: portal slag: Modul namn: fokusnyhet status: Byggd med mockad nyhet lage: byggd beskrivning: Intranätets fokusnyhet: en nyhet om nya startsidan, med bild och en artikel bakom där rollens avsnitt kommer först. Skalet visar den under AI-fältet tills användaren stänger den (ADR-021). kalla: packages/modules/fokusnyhet roller: medarbetare, chef relationer: module-sdk, skal levererar: slots.fokus: nyheten om nya startsidan, routes: /verktyg/nyheter/ny-startsida (standard), hamtaFokusnyhet, hamtaArtikel id: itsupport app: portal slag: Modul namn: itsupport status: Byggd med mockad driftstatus och mockade ärenden lage: byggd beskrivning: Den första sidan under Sidor: IT-supportens ingång enligt it-support-prototyp.html, med snabbvägar, vanliga frågor, driftstatus, kontakt och användarens IT-ärenden, och artikeln Prioriteringsordning för IT-ärenden enligt innehallssida-prototyp.html (ADR-003, ADR-022). kalla: packages/modules/itsupport roller: medarbetare, chef relationer: module-sdk, ui, skal, felanmalan levererar: pages: it-support (ingång) och prioriteringsordning (artikel) under Service, IT och lokaler, slots på ingången: Nytt från IT, Driftstatus, Kontakta supporten, Mina IT-ärenden, hamtaDriftstatus, hamtaSupportkontakt, hamtaItArenden, hamtaItNyheter, oppetLage id: verktygslada app: portal slag: Modul namn: verktygslada status: Byggd med mockad verktygskatalog och omvärld lage: byggd beskrivning: Målgruppsingången för Digitalisering, IT och AI: Verktygslådan, porterad från den fristående verktygslådan, med snabbvägar, vägen från idé till nytta, vanliga frågor, verktygskatalogen, verksamhetsområdena, omvärlden och kontakt – och en artikel per tillgängligt verktyg (ADR-027). kalla: packages/modules/verktygslada roller: medarbetare, chef relationer: module-sdk, ui, skal, utvecklingsarende, itsupport levererar: pages: verktygsladan (ingång) och en artikel per tillgängligt verktyg under Digitalisering, IT och AI, slots på ingången: Verktygen, Din verksamhet, Kontakta oss, Omvärlden, hamtaVerktyg, hamtaVerksamheter, hamtaOmvarld, hamtaKontakt, omradeEtiketter, verktygFor id: api-felanmalan app: portal slag: API namn: POST /api/flows/felanmalan status: Byggd (M2) lage: delvis beskrivning: Flödets enda serverända. Prompt, profil och beslutet att skapa ärendet bor här. Klienten skickar bara text; servern kontrollerar själv att allt obligatoriskt finns. kalla: apps/portal/src/app/api/flows/felanmalan/route.ts relationer: felanmalan, ai-adapter levererar: turkontraktet, zod-validering, takgräns 40 anrop / 5 min id: api-utvecklingsarende app: portal slag: API namn: POST /api/flows/utvecklingsarende/* status: Byggd lage: delvis beskrivning: Dokumentflödets två serverändar: /utkast skriver hela underlaget ur uppladdat material, /tur ändrar ett avsnitt i samtalet. Samma beslutsordning som felanmälans handler, och samma regel: att modellen säger lamna räcker aldrig. kalla: apps/portal/src/app/api/flows/utvecklingsarende/ relationer: utvecklingsarende, ai-adapter levererar: POST /utkast och POST /tur, structured outputs ur MALL, arendeSchema: hela dokumentet valideras innan det når prompten, farLamnas på servern, textuttag ur docx, pptx och xlsx utan beroenden, takgräns 40 anrop / 5 min id: min-ai app: portal slag: Modul namn: min-ai status: Byggd – demo mot Anthropic API, Eneo planerad lage: delvis beskrivning: Medarbetarens personliga AI-assistent – en egen ChatGPT där samtalen i drift hanteras i Eneo. Skiljer sig från AI-fältet, som är startsidans assistent för startsidans funktioner. Kontraktet är Eneos: { question, session_id } in, svaret strömmat och samtalets id i ett svarshuvud (ADR-023). kalla: packages/modules/min-ai roller: medarbetare, chef relationer: module-sdk, ui, api-min-ai, eneo, skal levererar: routes: /verktyg/min-ai, /verktyg/min-ai/:samtal (full, ADR-025), nav: Min AI, slots.genvag: ✦ Min AI bredvid hälsningen på startsidan (ADR-025), data: listaSamtal, hamtaSamtal, nyttSamtal, sparaUtbyte, raderaSamtal, motor: systemprompt, historik, mockSvar, bitar, Verktyg: Mina uppgifter – användarens samlade uppgifter, lästa vid frågan och visade i samtalet, tolkaMarkdown – modellens svar som typade block, Funktionsmeny: verktyg, skills och MCP-tjänster med reglage per användare (ADR-024), Verktyg: Webbsök, Skapa bild (SVG). Skills: Skapa dokument (.docx), Skapa graf, Graf – stapel- och linjediagram i diagramtokens, med tabellvy, byggDocx – Word-dokument utan beroenden id: api-min-ai app: portal slag: API namn: POST /api/min-ai status: Byggd lage: byggd beskrivning: Min AI:s serverända. Tar { question, session_id? } som Eneos sessions-API, strömmar svaret som text och lämnar samtalets id i X-Session-Id. Prompten, historiken och samtalen bor här; klienten skickar bara frågan. DELETE /api/min-ai/samtal/:id raderar ett av användarens samtal. kalla: apps/portal/src/app/api/min-ai relationer: min-ai, ai-adapter levererar: POST /api/min-ai (NDJSON-händelser, X-Session-Id), DELETE /api/min-ai/samtal/:id, GET/PUT /api/min-ai/funktioner – användarens val, GET /api/min-ai/bilagor/:id – Word-dokument id: api-assistant app: portal slag: API namn: POST /api/assistant status: Byggd lage: byggd beskrivning: AI-fältets serverände: frågan in, ett kort svar med knappar ut – inget samtal. En moduls kända fråga (ai.intents) leder till sitt flöde utan modell. Annars svarar modellen med structured outputs och användarens uppgifter, lästa i samma stund, och knapparna väljs ur de mål användaren når. En allmän fråga lämnas över till Min AI med frågan ifylld. kalla: apps/portal/src/app/api/assistant, apps/portal/src/ai/assistent.ts relationer: skal, ai-adapter, agera, min-ai, module-sdk levererar: POST /api/assistant { question } → AssistentSvar, malFor, svaraAvsikt, svarsschema, byggSvar, regelmotorSvar, AskField: svaret under fältet, märkt ✦, förslagen ställer sin fråga id: ai-adapter app: gemensam slag: API namn: AI-adaptern status: Byggd lage: byggd beskrivning: Anthropic-anropet bakom AI_PROVIDER. Turkontraktet garanteras med structured outputs, inte med en instruktion om att svara med JSON. Nyckeln bor på servern. Den strukturerade kärnan är paketet @startsidan/ai, som båda applikationerna använder – projektstödet också (ADR-040); chatten och flödenas adapter bor i startsideportalen. kalla: packages/ai/src/strukturerat.ts, apps/portal/src/ai/anthropic.ts, apps/portal/src/ai/chatt.ts relationer: api-felanmalan, api-utvecklingsarende, api-min-ai, module-sdk levererar: @startsidan/ai: koraStrukturerat med dokumentblock för PDF, strommaChatt – fri chatt med verktygsloop, webbsök och MCP-connector, avbrytbar, koraAnthropicTur, anvandAnthropic, AI_PROVIDER=mock|anthropic, AI_MODELL id: api-mock app: portal slag: API namn: GET /api/mock/* status: Byggd – fas 1–3 lage: byggd beskrivning: Mockdata per modul med samma form som kommunens mikrotjänst-API:er skulle ge. Byts mot riktiga källor i fas 4 – en adapterfråga, inte en omskrivning. kalla: apps/portal/src/app/api/mock relationer: oversikt, arenden, kalender, ledighet, kompetens, personal, dialogstod levererar: /api/mock/oversikt, /api/mock/arenden, /api/mock/kalender, /api/mock/ledighet, /api/mock/kompetens, POST /api/mock/kompetens/:id/plan/bekrafta, POST /api/mock/kompetens/:id/innehav, /api/mock/personal, POST /api/mock/personal/:id/utvecklingssamtal/godkann, /api/mock/dialogstod, POST /api/mock/dialogstod/:enhet/:omrade/aktiviteter, PATCH /api/mock/dialogstod/aktiviteter/:id, POST /api/mock/dialogstod/:enhet/:omrade/status, PUT /api/mock/dialogstod/:enhet/:omrade/anteckningar, /api/health id: anthropic app: gemensam slag: Extern namn: Anthropic API status: I bruk bakom AI_PROVIDER=anthropic lage: byggd beskrivning: Modellen bakom samtalsflödena. Anropas med @anthropic-ai/sdk och structured outputs. Modellvalet styrs av AI_MODELL så det kan bytas utan kodändring. kalla: ANTHROPIC_API_KEY som miljövariabel i Dokploy relationer: ai-adapter levererar: messages.parse, output_config.format, web_search_20260209, MCP-connector (mcp-client-2025-11-20) id: eneo app: portal slag: Extern namn: Eneo via WSO2 status: Framtida alternativ lage: planerad beskrivning: Kommunens egen AI-plattform bakom Sundsvalls API-gateway. Ett tredje värde på AI_PROVIDER när prototypen ska köra på kommunens plattform i stället för direkt mot Anthropic. kalla: api-prod.sundsvall.se (WSO2-abonnemang krävs) relationer: ai-adapter, min-ai levererar: assistent-API bakom WSO2-token + Eneo api-key id: sitevision app: portal slag: Extern namn: Sitevision (sundsvall.se) status: sundsvall.se och utveckling.sundsvall.se läses anonymt; intranätet kräver fas 4 lage: delvis beskrivning: Kommunens CMS, både för sundsvall.se och intranätet. Model REST API:s headless-endpoint ger sidans egenskaper, undersidor och innehållsmoduler som JSON. Publicerat innehåll på sundsvall.se svarar anonymt; offline-modellen (/1/0/) och utveckling.sundsvall.se svarar 401/403. Inga CORS-headers – allt hämtas på servern. kalla: https://sundsvall.se/rest-api/1/1/{nodeId}/headless (SITEVISION_BAS_URL) relationer: webbsidor, nyheter levererar: headless?textPortletFormat=JSON: properties, nodes, contentNodes, RSS 2.0 ur nyhetsportleten (/rss/nyheter) id: dokploy app: gemensam slag: Extern namn: Dokploy status: I bruk lage: byggd beskrivning: Deployplattformen. Pekar på docker-compose.yml i repot; Git-push ger deploy. Hemligheter läggs som Environment i Dokploy, aldrig i repot. kalla: docker-compose.yml + apps/projekt/Dockerfile relationer: skal levererar: publik URL, healthcheck mot /api/health --- # Dokumentation En sida per del av ekosystemet. Identiteten står i katalogen ovan och upprepas inte här; det som följer är ansvar, filkarta, API, kontrakt och det som återstår. ## Dokumentationskontraktet En funktion är inte klar förrän den har ett dok som uppfyller detta. Saknas ett obligatoriskt fält är dokumentationen inte färdig, och då är inte funktionen det heller. - id (obligatoriskt): Samma id som katalogposten. Finns ingen katalogpost skrivs den först – identiteten bor där, inte här. - sammanfattning (obligatoriskt): En mening om vad delen ansvarar för. Inte vad den innehåller, inte hur den är byggd. - ansvar (obligatoriskt): Tre till sex punkter. Varje punkt är något delen svarar för – och därmed något ingen annan ska göra. - filer (obligatoriskt): Varje fil som bär funktionen, med en rad om vad den gör. Filkartan är det som gör doket användbart för någon som ska ändra. - api (villkorat): Exporterade funktioner och typer med signatur. Obligatoriskt så snart delen exporterar något som någon annan importerar. - kontrakt (villkorat): Datakontrakt, manifestform eller turkontrakt – det andra måste följa. Obligatoriskt när delen ställer krav på sin omgivning. - beroenden (obligatoriskt): Katalog-id på det delen bygger på. Tom lista är ett giltigt svar och betyder fristående. - beslut (villkorat): ADR-nummer som styr delen. Utelämnas bara när inget beslut rör den. - kvarstar (obligatoriskt): Känt som inte är gjort. Tom lista betyder färdig; ett utelämnat fält betyder att ingen har tänkt efter. Det är skillnaden. - uppdaterad (obligatoriskt): ISO-datum. Sätts om varje gång delen ändras, i samma commit som ändringen. ## @startsidan/dokumentation id: dokumentation namn: @startsidan/dokumentation sammanfattning: Ger båda applikationerna samma sätt att dokumentera sina delar, så att en del är klar på samma villkor oavsett var den bor (ADR-011, ADR-040). uppdaterad: 2026-10-19 ansvar: - Äga typerna för en katalogpost, ett dok och ett innehållsblock. - Äga dokumentationskontraktet – fälten, vilka som är obligatoriska och kraven på dem. - Rendera block för människor (Blockvy, Avsnittvy) och som text för agenter. filer: - packages/dokumentation/src/typer.ts — Block, Avsnitt, App, APPAR, appNamn, Katalogpost, Dok och dokkontrakt. - packages/dokumentation/src/text.ts — blockTillText, katalogpostTillText och dokTillText. - packages/dokumentation/src/Blockvy.tsx — Blockvy och Avsnittvy. Länkar med next/link, så att de hamnar i rätt app. - packages/dokumentation/src/index.ts — Paketets ingång; Blockvy har en egen exportväg. api: - dokkontrakt: { falt, obligatoriskt, krav }[] Kontraktet. Båda apparnas tester prövar sina dok mot det. - Blockvy: ({ block }) => JSX Ett block för människor: text, punkter, kod, tabell, rutor, steg eller länkar. - dokTillText: (d: Dok, namn: string) => string Ett dok som text, för agentvyn. beroenden: ui beslut: ADR-011, ADR-040 kvarstår: - inget känt ## @startsidan/gemensamt id: gemensamt namn: @startsidan/gemensamt sammanfattning: Håller det som båda applikationerna behöver på ett ställe, så att ingen av dem importerar från den andra (ADR-040). uppdaterad: 2026-10-19 ansvar: - Äga beslutsregistret, som devportalen visar och projektportalens tidslinje och register läser. - Ge båda apparnas topprad kommunens avsändare enligt kommunikationsguidens mall. - Spärra gissningar på koder och lösenord: tio fel per adress och tio minuter, med adressen Traefik lägger sist. - Släppa igenom bara interna vägar i ?vidare=. - Ge agentfilerna en innehållsversion som ändras när och bara när innehållet gör det. filer: - packages/gemensamt/src/beslut.ts — Beslut och beslut[] – registret som speglar docs/beslut/. En ny ADR läggs sist här och får en rad i adrDatum i projektportalens tidslinje. - packages/gemensamt/src/Varumarke.tsx — Logotyp | linje | produktnamn. Länken går med next/link, så den hamnar i rätt app. Logotypen är /brand/… i projektportalen. - packages/gemensamt/src/felforsok.ts — forManganFel och noteraFel. I minnet, per process – varje app räknar för sig. - packages/gemensamt/src/vidare.ts — sakerVidare. - packages/gemensamt/src/innehallsversion.ts — innehallsversion och svarMedVersion: cyrb53 över texten, som ETag. - packages/gemensamt/src/innehallsversion.test.ts — Samma text ger samma version; en ändring ger en ny. api: - beslut: Beslut[] Registret, i ordning. Importeras från @startsidan/gemensamt/beslut. - Varumarke: ({ produkt, href?, etikett? }) => JSX Avsändaren i toppraden. - forManganFel / noteraFel: () => Promise / () => Promise Spärren mot gissningar. Anroparen bestämmer vad som räknas som fel. - sakerVidare: (vidare: unknown) => string Bara interna vägar; annars /. - innehallsversion: (text: string) => string En kort hash av innehållet. beroenden: tokens, ui beslut: ADR-040, ADR-012, ADR-039 kvarstår: - Spärren mot gissningar nollställs vid omstart och räknas per app, inte för hela domänen. ## Åtkomstspärren id: atkomst namn: Åtkomstspärren sammanfattning: Låser startsideportalens demo bakom en delad kod så att den kan ligga på en publik adress utan att vara öppen. Ingången /portal och devportalen /portal/dev ligger utanför spärren; projektportalen på roten har ingen spärr alls (ADR-040). uppdaterad: 2026-10-19 ansvar: - Spärra allt i startsideportalen utom en kort, stängd lista öppna vägar, före routningen, så att en ny yta inte kan glömma spärren. - Hålla ingången /portal och devportalen /portal/dev öppna, så att beskrivningen av systemet går att läsa och hämta – också av verktyg som inte kan mata in en kod. - Vara avstängd när ACCESS_CODE är osatt, så att lokal utveckling inte kräver en kod. - Hålla koden borta ur kakan – den bär ett SHA-256-avtryck, inte klartext. - Validera ?vidare= mot öppen vidarebefordran innan den följs. - Aldrig utge sig för att vara autentisering, och aldrig vara behörighet i projektportalen. Spärren säger ingenting om vem användaren är. filer: - apps/portal/src/middleware.ts — Spärren för startsideportalen. Kör i edge-runtime, före all routning. Vägarna är appens egna – Next tar bort basvägen /portal före middleware och lägger på den i omdirigeringen. - apps/portal/src/atkomst/kod.ts — arOppenVag, atkomstkod, fingeravtryck, likaSakert. Fri från Node-API:er så middleware kan använda den. sakerVidare kommer ur @startsidan/gemensamt. - apps/portal/src/atkomst/actions.ts — lasUpp: jämför koden, sätter kakan, skickar vidare. - apps/portal/src/atkomst/kod.test.ts — Tester på avtryck, jämförelse, vidarebefordran och de öppna vägarna. - apps/portal/src/app/las-upp/page.tsx — Upplåsningssidan, /portal/las-upp. Satt till noindex. - packages/gemensamt/src/felforsok.ts — Spärren mot gissningar: tio fel per adress och tio minuter, med den adress Traefik lägger sist i X-Forwarded-For. Delas med projektportalens konton och skrivnyckel, men räknas per app. api: - arOppenVag: (pathname: string) => boolean Ligger vägen utanför spärren? Vägen är appens egen, utan /portal. Listan är stängd: / (ingången), /dev, /api/health och /las-upp – var och en med allt under sig, utom roten. - atkomstkod: () => string | undefined Koden ur miljön. Osatt eller tom betyder att spärren är avstängd. - fingeravtryck: (kod: string) => Promise SHA-256 i hex med prefix, så avtrycket bara betyder något här. - likaSakert: (a: string, b: string) => boolean Jämförelse som tar lika lång tid oavsett var strängarna skiljer sig. - sakerVidare: (vidare: unknown) => string Ur @startsidan/gemensamt/vidare. Släpper bara igenom interna vägar. Faller tillbaka på /. - lasUpp: (formData: FormData) => Promise Server action bakom upplåsningsformuläret. kontrakt: Detta är inte autentisering. Alla som kommer in delar samma kod, och rollen väljs fortfarande fritt på /portal. En modul som behöver veta vem användaren är ska vänta på SAML-sessionen i fas 4 – läs aldrig åtkomstkakan som en identitet. Projektportalens konton (ADR-039) är aldrig identiteten här. Väg | Läge | Varför /portal | Öppen | Prototypens ingång berättar vad prototypen är. Den visar inte demon, bara vägen in i den. /portal/dev och allt under | Öppen | Portalen, designsystemet, dokumentationen, llms.txt och spec.json ska gå att läsa av både människor och verktyg. /portal/api/health | Öppen | Dokploys healthcheck för start-portal markerar annars containern som sjuk. /portal/las-upp | Öppen | Det är sidan man låser upp på. /portal/start och allt under | Låst | Själva demon. /portal/verktyg/*, /portal/sidor/* | Låst | Modulvyerna och sidorna i skalet. /portal/flode/ | Låst | Fokuslägena är demon i helskärm. /portal/api/flows/*, /portal/api/min-ai | Låst | AI-anropen kostar pengar. /portal/api/mock/*, /portal/api/kompetens/* | Låst | Demons data om påhittade personer. Allt utanför /portal | Ingen spärr | Projektportalen (ADR-040). Öppen med flit; ändringsloggen, ritningen, organisationen och pitchen kräver ett konto (ADR-039). /start, /dev, /verktyg … (gamla) | Omledning | Projektportalens next.config leder varje gammal adress permanent (308) till sin nya under /portal; utskickade länkar ska alltid fungera. - ACCESS_CODE måste sättas i Dokploy innan domänen delas. Osatt = öppet, utan varning. Variabeln listas bara under start-portal i docker-compose.yml. - /portal och /portal/dev är publika. Skriv dem därefter: namn på kommunens medarbetare och projektets deltagare får stå där, känsliga personuppgifter aldrig. - Allt får hämtas men inget indexeras: robots.txt är öppen, så att AI-tjänster kan läsa en länk, och varje sida sätter noindex. noindex verkar bara på en sida som får hämtas (ADR-012, ändrat 20 oktober 2026). - Byts koden loggas alla ut – avtrycket i kakan matchar inte längre. Det är den enda vägen att återkalla åtkomst. - Middleware kör i edge-runtime: atkomst/kod.ts får bara använda Web Crypto, aldrig node:crypto. beroenden: skal, bygge beslut: ADR-012, ADR-029, ADR-040 kvarstår: - Ingen utloggning ur spärren – bara kakans 30 dagar eller ett kodbyte. - Kakan har sökvägen / och syns därför också för projektportalen, som inte läser den. - Formulärets POST är verifierad i webbläsare, inte i test. Middleware och den delade logiken är verifierade i container respektive vitest. ## Prototypens ingång id: intro namn: Prototypens ingång sammanfattning: Startar demon på /portal genom ett rollval, och förklarar under det vad startsidan och ekosystemet är. uppdaterad: 2026-10-08 ansvar: - Låta besökaren komma igång direkt: rollvalet står först på sidan, inte efter en genomgång. - Göra valet konkret genom att säga vad man faktiskt ser som medarbetare respektive chef. - Sätta rollcookien och skicka vidare till /start, med samma cookie som rollväxlaren i sidopanelen använder. - Introducera grundidén – ett skal som komponerar moduler – och peka vidare till devportalen, dokumentationen och designsystemet. - Hålla isär sidans två ärenden visuellt: rollvalet ligger på ljus botten, genomgången i ett mörkare helbreddsband under. - Aldrig innehålla produktlogik. Prototypens ingång är en ingång, inte en modul. filer: - app/portal/page.tsx — Hela ingången: topprad, rollval, grundidé, anatomiskiss, ytformer, byggarlänkar. Låg på / fram till ADR-029. - app/(skal)/start/page.tsx — Startsidan själv, flyttad hit när / blev introsida. - shell/actions.ts — loggaInSom sätter cookien och redirectar till /start. loggaUt rensar den och redirectar till /portal. - shell/UserMenu.tsx — Rollväxlingen efter start, plus Logga ut som leder tillbaka hit. api: - loggaInSom: (role: Role) => Promise Server action. Sätter rollcookien i 30 dagar och redirectar till /start. - loggaUt: () => Promise Server action. Rensar rollcookien och redirectar till /portal. kontrakt: - Rutterna: / är projektsidan, /portal är prototypens ingång, /vision är visionen, /start är startsidan, /flode/ är fokusflöden, /dev är portalen, /las-upp är åtkomstspärren. - Demoanvändarna läses ur shell/demo-users.ts. En ny demoanvändare dyker upp i rollvalet utan att sidan ändras – men texten om vad rollen ser måste skrivas för hand. - Knapparna är server actions i en form – inget klientlager, och valet fungerar utan JavaScript. - Ingången bär inte skalets krom och ligger därför utanför (skal)-gruppen. - Sidan är två band: rollvalet på --color-page, genomgången på --color-page-deep. Bandet går kant i kant medan innehållet håller sig innanför max-w-[104rem]. beroenden: skal, ui, devportal beslut: ADR-006, ADR-029 kvarstår: - Texten om vad varje roll ser är handskriven, inte härledd ur modulernas manifest. Den kan bli inaktuell när en modul ändras. - Ingen bild av produkten. En skärmbild eller en liten animering skulle förklara mer än anatomiskissen gör – och den saknas nu när rollvalet står överst och avgör intrycket. - "Min profil" och "Inställningar" i användarmenyn är fortfarande platshållare. ## Devportalen id: devportal namn: Devportalen sammanfattning: Samlar allt en byggare behöver om ekosystemet och renderar det i en människovy och en agentvy ur samma data. uppdaterad: 2026-09-02 ansvar: - Äga portalens innehåll som typad data – sidor, katalog, dokumentation, beslutsregister – så att ingen uppgift finns bara i JSX. - Rendera samma innehåll för människa och agent. En blocktyp utan textrenderare är ett fel, inte en förenkling. - Vara den enda adress man behöver peka en agent på: /dev/llms.txt. - Visa täckningen: vilka katalogposter som saknar dokumentation. - Fälla grindarna när portalen beskriver något som inte finns – drifttesterna kontrollerar det som går att kontrollera mekaniskt. - Aldrig innehålla en egen sanning. Portalen beskriver koden; skiljer de sig är det portalen som ska rättas. filer: - dev/layout.tsx — Portalens krom: topprad med vyväxel, sidonav, sidfot. - dev/page.tsx — Startsidan. Renderar sidan med id "start". - dev/[sida]/page.tsx — Alla datadrivna sidor. Statiska segment vinner över den här rutten. - dev/portal/typer.ts — Datamodellen: Block, Avsnitt, Sida, Katalogpost, Beslut. - dev/portal/sidor.ts — Sidornas innehåll. Ordningen är navordningen. - dev/portal/katalog.ts — Katalogposterna och beslutsregistret. - dev/portal/antimonster.ts — Antimönsterindexet: fel → rätt för arkitekturen, med grinden som fångar var och en. - dev/portal/stanna.ts — "Stanna och fråga" som fält: status, fas, implementerad. Tabellen på /dev renderas ur den. - dev/portal/referens.ts — Den kanoniska förlagan: vilken riktig modul man diffar sitt arbete mot, och vilka filer man läser i vilken ordning. - dev/portal/dok.ts — Dokumentationen och dokumentationskontraktet. - dev/portal/nav.ts — Sidomenyns grupper. - dev/portal/text.ts — Textrenderaren. Agentvyn och llms.txt går genom den. - dev/komponenter/Blockvy.tsx — Visuell renderare, en gren per blocktyp. - dev/komponenter/Sidvy.tsx — Väljer människovy eller agentvy. - dev/komponenter/Nav.tsx — Sidomenyn. Klientkomponent enbart för aria-current. - dev/komponenter/Vyvaxel.tsx — Människa/Agent. Lägger vyn i frågesträngen. - dev/komponenter/Kopiera.tsx — Kopierar agentvyns text. - dev/docs/ — Dokumentationsindex, täckning och en sida per dok. - app/llms.txt/route.ts — Rotens karta: var kontexten finns. Kort med flit – inte en kopia av portalen. - innehallsversion.ts — Hash av innehållet som version och ETag. Villkorat anrop svarar 304 i stället för att skicka om allt. - dev/llms.txt/route.ts — Hela portalen som plattext. - dev/spec.json/route.ts — Hela portalen som JSON. Tidsstämplas inte – svaret ska gå att diffa. Bär innehallsversion och svarar 304 på oförändrat innehåll. - dev/portal/portal.test.ts — Drifttesterna: katalogen mot packages/modules, applikationen på varje post (ADR-040), doken mot dokumentationskontraktet, manifestkopian mot module-sdk. kontrakt: En ny blocktyp kräver två renderare. Läggs den bara till i Blockvy.tsx försvinner innehållet ur agentvyn och ur llms.txt, tyst. Agentfilerna bär innehallsversion: en hash av innehållet, samma värde som ETag. Den ändras när och bara när innehållet gör det, så ett villkorat anrop med If-None-Match svarar 304. En agent kan därmed avgöra om dess cachade kontext är inaktuell utan att hämta om hundratals rader – och svaret går fortfarande att diffa, vilket en tidsstämpel hade omöjliggjort. Blocktyperna: ``` type Block = | { typ: "text"; text: string } | { typ: "punkter"; punkter: string[] } | { typ: "kod"; rubrik?: string; kod: string } | { typ: "tabell"; kolumner: string[]; rader: string[][] } | { typ: "rutor"; rutor: { rubrik: string; text: string }[] } | { typ: "steg"; steg: { rubrik: string; text: string; kod?: string }[] } | { typ: "lankar"; lankar: { href: string; text: string; nota?: string }[] }; ``` 1. Lägg till en sida Ett objekt i sidor.ts och en rad i nav.ts. Rutten finns redan – [sida] plockar upp den på id. 2. Lägg till en katalogpost Ett objekt i katalog.ts. Posten blir genast synlig på /dev/katalog och i täckningen på /dev/docs. 3. Lägg till dokumentation Ett objekt i dok.ts med samma id som katalogposten, enligt dokumentationskontraktet. beroenden: skal, ui, tokens, designsystem beslut: ADR-010 kvarstår: - Ingen sökning. Med tio sidor gör navigationen mer nytta; över tjugo bör det omprövas. - Katalogen är fortfarande handskriven, inte genererad ur registry.ts. Medvetet – status och skäl finns inte i koden – men nu kontrollerar ett test att modullistan stämmer mot packages/modules. - Prosan i sidorna kontrolleras inte. Drifttesterna når fältnamn, id och täckning; att en beskrivning fortfarande är sann går inte att testa. ## Designsystemet id: designsystem namn: Designsystemet sammanfattning: Beskriver tokens, primitiver, mönster och regler som data, och renderar dem som sida, JSON och plattext. uppdaterad: 2026-10-05 ansvar: - Vara facit för hur något ska se ut och bete sig, med uttömmande varianter i stället för beskrivningar. - Rendera levande exempel med samma primitiver som modulerna använder – går ett exempel sönder är det systemet som gått sönder. - Hålla felindexet: de misstag som faktiskt uppstår, i formen fel → rätt → varför. - Ge en maskinform som får plats i ett sammanhangsfönster. filer: - dev/ui/page.tsx — Sidan: regler, tokens, poster, felindex, checklista. - dev/ui/system/typer.ts — Datamodellen: Token, Prop, Fel, Post, Regel. - dev/ui/system/tokens.ts — Färg, typografi, form och rörelse, läst ur theme.css. - dev/ui/system/poster.ts — Primitiver, mönster och ytor. Ingen JSX – filen läses också av route handlers. - dev/ui/system/regler.ts — De tio bärande reglerna, felindexet och checklistan. R4 fångas numera av eslint-rules/inga-hardkodade-designvarden.mjs. - dev/ui/exempel.tsx — Levande exempel, ett per post-id. - dev/ui/komponenter.tsx — Sidans egna byggstenar: Postkort, Fargruta, Proptabell. - dev/ui/llms.txt/route.ts — Systemet som plattext. - dev/ui/spec.json/route.ts — Systemet som JSON. kontrakt: - Stabila id: ui.card, monster.aiyta. Ett id ändras aldrig – nya former får nya poster. - Varianter räknas upp uttömmande. Finns värdet inte i listan finns det inte i systemet. - Villkor i stället för smak: "Använd när" och "Använd inte" ska räcka för att välja mellan två poster. - Ändras en token i packages/tokens eller en primitiv i packages/ui ändras posten här i samma commit. beroenden: ui, tokens beslut: ADR-009, ADR-010 kvarstår: - Ingen kontroll av att posterna stämmer med koden. En primitiv som får en ny variant kan glömmas bort här. ## Skalet (apps/portal) id: skal namn: Skalet (apps/portal) sammanfattning: Komponerar modulerna till en sammanhängande sida och äger allt krom, all identitet och all layout. uppdaterad: 2026-10-07 ansvar: - Läsa modulregistret, filtrera på roll och rendera varje slot i ordning. - Äga identiteten: rollen ur cookien, DemoUser, och UserContext som är allt modulerna får veta om användaren. - Äga kromet: sidopanel, AI-fält, användarmeny, sidfot, fokuslägets topprad. - Lägga ut rutnätet – nyckeltalsraden i fyra spår, arbetsytan i två spalter – och låta modulerna fylla det. - Aldrig veta vad en widget innehåller. Skalet känner slotens namn, inte innehållet. filer: - shell/registry.ts — Modulregistret. En ny modul = en rad. Anropar provideRegistry för de aggregerande modulerna. - shell/session.ts — getCurrentUser ur cookien, toContext som gör om DemoUser till UserContext. - shell/demo-users.ts — Demoanvändarna – medarbetaren och fyra chefer på olika nivåer, för dialogstödet – sektionerna under "Sidor" och de verktygspunkter som ännu saknar modul. Kakan bär en roll eller en användares id. - shell/actions.ts — switchRole, loggaInSom och loggaUt: server actions som sätter eller rensar rollcookien. sparaLayout för kortens ordning, även standardordningen (ADR-018). markeraLasta för notiserna (ADR-020). stangFokus och visaFokusIgen (demo) för fokusytan (ADR-021). - shell/ShellFrame.tsx — Ramen: sidopanel plus arbetsyta. Klientkomponent för dockningsläget; children renderas på servern. - shell/Sidebar.tsx — Sidopanelen: Verktyg, Sidor, notiser, användarmeny. Sektionerna kommer som prop från layouten. - app/(skal)/verktyg/[...vag]/page.tsx — Modulvyer med mode "standard": slår upp vägen med routeFor och skickar vidare params (ADR-019). - shell/fokus/FokusYta.tsx — Fokusytan under AI-fältet, formad som en nyhet: bild, datum, rubrik, ingress, avsändare. Ljusblå botten och blå ram. Stäng flyttar fokus till AI-fältet (ADR-021). - shell/fokus/kaka.ts — Kakan med stängda fokus-id per roll. Samma logik och tak som de lästa notiserna. - shell/notiser/Notiser.tsx — Klockan och panelen: modulernas notiser, olästa först i räknaren, kantfärg efter status. Intern notis navigerar, extern öppnas i ny flik (ADR-020). - shell/notiser/lasta.ts — Kakan med lästa notis-id per roll: lasLasta, markera. Testad i lasta.test.ts. - shell/Board.tsx — Rutnätet. Läser widgetsFor för kpi, main och side, och användarens ordning ur kakan (ADR-018). - shell/AnpassaBoard.tsx — Spalterna i användarens ordning och läget "Anpassa startsidan": @dnd-kit, flyttknappar, svenska utrop. Ordningen ändras först när kortet släpps; målet är kortet under pekaren. Fokus stannar efter flytt, utropen anger ny plats, reducerad rörelse respekteras, Ångra och Återställ standardordning (ADR-018, WCAG 2.2). Varje kort i en @container. - shell/layout/ordning.ts — Ren logik: layoutSchema, lasLayout, tillampa (sparad över standard), flytta, hitta. Testad i ordning.test.ts. - shell/AskBand.tsx — AI-fältet på startsidan: hälsningen runt AskField. - shell/AskField.tsx — Själva AI-fältet: frågan, förslagen och det korta svaret från POST /api/assistant. Används av AskBand och av en sidas ingång. - app/(skal)/sidor/[...vag]/page.tsx — Sidtypen: slår upp sektion, ingång eller artikel, löser innehållet och sätter ctx.page. - shell/sida/SidaIngang.tsx — Ingången enligt it-support-prototyp.html: sidhuvud, AI-fält, snabbvägar, redaktionella kort (cards), modulens egna kort (slots), undersidor, källa (ADR-022). - shell/sida/SidaArtikel.tsx — Artikeln enligt innehallssida-prototyp.html: kicker, ingress, brödtext i kort, sidansvarig-fot. - shell/sida/Innehall.tsx — Den enda renderaren för ContentBlock, rekursiv för details (native
med chip). Intern länk via next/link, extern med ↗. - shell/Webbmeny.tsx — En webbplats meny som rad överst i arbetsytan (ADR-017). Fast höjd 5.2rem, som ramen räknar med. Klientkomponent för aktivt val. - shell/sida/SidaInbaddad.tsx — Sidan i en sandlådad ram som fyller arbetsytan under menyraden (100dvh − 5.2rem), eller förklaringen när källan blockerar inbäddning. - shell/sida/inbaddning.ts — tillaterInbaddning (ren) och kontrolleraInbaddning: läser X-Frame-Options och frame-ancestors mot vår origin. - shell/sida/Lanklista.tsx — PageLink som lista; en länk med children blir en hopfälld
med sidan själv först. - shell/sida/Kallmarkning.tsx — Källa, datum och länk till originalet; gult chip för sparad version. - shell/sida/ — Även Brodsmulor, Snabbvagar, SektionSida, Aterkoppling och datum. - shell/sektioner.test.ts — Varje sidas sektion finns i skalets lista; registrets invarianter håller. - shell/FlowBar.tsx — Fokuslägets topprad med Avbryt. - shell/store.ts — Zustand. Enda äkta klientstate: dockad sidopanel. - shell/shell.css — Kromets CSS, porterad 1:1 ur prototypen. Ägs av skalet – moduler skriver inte här. api: - getCurrentUser: () => Promise Den inloggade användaren. Läser rollcookien; i skarp drift SAML-sessionen. Anropsstället är detsamma. - toContext: (user: DemoUser, page?: string) => UserContext Det modulerna får se om användaren. Fler uppgifter hämtar modulen själv. - switchRole: (role: Role) => Promise Server action bakom rollväxlaren i sidopanelen. Sätter httpOnly-cookie i 30 dagar och revaliderar layouten. - loggaInSom / loggaUt: (role: Role) => Promise / () => Promise Prototypingångens start och utloggning. Samma cookie, men med redirect till /start respektive /portal. - registry: ModuleManifest[] Modulregistret. Ordningen styr nav och fallordning när order saknas. kontrakt: - Identiteten kommer alltid från servern. En profil som klienten skickar vore en injektionsväg rakt in i ärendet. - Rollcookien heter startsidan-roll, är httpOnly och faller tillbaka på chef. - Skalet renderar vilken modulrutt som helst: modulen äger vyn, skalet äger kromet. - Rutterna: / är projektsidan och /portal prototypens ingång (båda utan krom), /start är startsidan, /flode/ är fokusflöden, /verktyg/ är modulvyer med skalets krom (ADR-019) – med mode "full" över hela arbetsytan i skärmens höjd, utan sidfot och AI-knapp (ADR-025), /sidor/[/[/]] är sidor, /las-upp är åtkomstspärren. - Skalet äger sektionerna under "Sidor". En sektion länkar först när någon modul har en ingång där. - Modulernas genvägar (genvagarFor) står bredvid hälsningen i AI-fältet; en AI-genväg har AI-tonen och märks ✦ (ADR-025, ADR-026). - Startsidan visar den nyaste fokusytan från fokusFor som användaren inte stängt, under AI-fältet (ADR-021). Användarmenyn har "Visa fokusnyheten igen (demo)". - Klockans notiser kommer från modulerna via notificationsFor; layouten lägger på läst-status ur kakan startsidan-notiser-lasta- (ADR-020). - Användaren ordnar korten i main och side (ADR-018). Ordningen sparas per roll i kakan startsidan-layout-, valideras på servern och läggs över modulernas order. Nya kort hamnar sist i sin standardspalt. - Skalet äger också webbplatserna (ADR-017): samma form som en sektion, men visade som menyrad överst – bara inne på webbplatsen. Med under blir webbplatsen ett kort på en sektions sida. Sidopanelen står kvar runt en inramad sida. - En sida får ctx.page = sidans slug, så att modulernas ai.suggestions kan svara på sidans ämne. beroenden: module-sdk, tokens, ui beslut: ADR-001, ADR-003, ADR-004, ADR-006, ADR-007, ADR-016, ADR-017, ADR-018, ADR-019, ADR-020, ADR-021, ADR-022 kvarstår: - pendingTools och pendingSuggestions i demo-users.ts är platshållare tills modulerna tar över. - Fyra av sex sektioner under "Sidor" saknar sidor och står kvar som "#". - Återkopplingen på en artikel sparas inte – det finns ingen mottagare än. - Kortens ordning följer webbläsaren, inte personen, tills SAML-sessionen finns i fas 4. ## @startsidan/tokens id: tokens namn: @startsidan/tokens sammanfattning: Definierar varje färg, mått och rörelse i systemet som Tailwind-tema, och är det enda stället där ett värde får uppstå. uppdaterad: 2026-08-31 ansvar: - Bära prototypens värden 1:1 som CSS-variabler i @theme. - Mappa shadcns semantiska tokens mot kommunens palett, så att komponenterna inte bär egen färg. - Hålla basreglerna: rotens 62,5 %, typsnitt, fokusmarkering, .wrap och prefers-reduced-motion. - Skilja betydelserna åt: blått är identitet, grönt med --ai-prefix är AI, statusfärgerna är sina egna. filer: - packages/tokens/src/theme.css — @theme med alla tokens, plus shadcn-mappningen och keyframes. - packages/tokens/src/base.css — Basregler: html 62,5 %, body, rubriker, fokus, .wrap, reduced-motion. - packages/tokens/src/index.css — Importerar båda. Det appen importerar. kontrakt: - Rotens font-size är 62,5 %: 1rem = 10px, och --spacing: 0.4rem ger Tailwinds vanliga px-semantik (p-4 = 16px). - --color-ai-* är reserverat för AI-ytor och får aldrig betyda status. - --sidebar är shadcns token för sidopanelens färg. Breddmåttet heter därför --sidebar-w. - --color-page-deep är ett steg mörkare än --color-page och används för helbreddsband som ska läsas som en egen del av sidan. Ljusare än --color-border, så att kortens ramar inte försvinner mot botten. - Nya utilities som ligger utanför Tailwinds skala måste registreras i cn() – annars plockas de tyst bort av tailwind-merge. beroenden: inga beslut: ADR-002, ADR-008 kvarstår: - Inget mörkt läge. Tokens är enfärgade och prototypen har ingen mörk variant. - Bara två bottennivåer: page och page-deep. En tredje bör inte läggas till utan att någon bestämt vad nivåerna betyder. ## @startsidan/ui id: ui namn: @startsidan/ui sammanfattning: Primitiverna modulerna bygger av: V3:s form och mått ovanpå shadcn/ui, så att Radix bär tillgängligheten. uppdaterad: 2026-08-28 ansvar: - Erbjuda ett litet, stabilt API som modulerna kan klara sig på. - Hålla V3:s mått – shadcns default skrivs medvetet över av tailwind-merge i cn. - Bära tillgängligheten: Card är en section med aria-labelledby, Progress har korrekta aria-värden. - Skilja identitet från status i sina varianter: Avatar har ingen färgprop, och ska inte få en. filer: - packages/ui/src/Card.tsx — Kortet. Rubrik, antal, åtgärdslänk, ai-ton. - packages/ui/src/Chip.tsx — Chip med statustoner, och AiChip för AI-märkning. - packages/ui/src/Btn.tsx — Btn och BtnLink: primary, ghost, ai i två storlekar. - packages/ui/src/StatusRow.tsx — Rad med färgad vänsterkant för "Att agera på". - packages/ui/src/ListRow.tsx — Rad i en lista, plus Progress. - packages/ui/src/Avatar.tsx — Identitetsikon med initialer. Alltid blå. - packages/ui/src/MoreLink.tsx — "Visa alla →" i korthuvudet. - packages/ui/src/cn.ts — clsx plus tailwind-merge, utökad med våra egna skalsteg. - packages/ui/src/components/ui/ — shadcn-komponenterna. Våra filer, med avvikelser kommenterade. api: - Card: { title?, count?, action?, headingId?, tone?, bodyClassName?, className?, children } Grundformen för innehåll på arbetsytan. - Chip / AiChip: { tone?: ChipTone, children } ok | warn | info | muted | avv. AiChip märker AI-genererat. - Btn / BtnLink: { variant?: BtnVariant, size?: BtnSize } primary | ghost | ai, i md eller sm. - StatusRow: { status?: RowStatus, title, meta?, end? } krit | warn | info | ok | neutral. Renderas som li. - ListRow / Progress: { lead?, title, meta?, end?, children } / { value, done? } Listrad med valfritt förlopp. - cn: (...inputs: ClassValue[]) => string Slår ihop klasser och låter senare utilities vinna. Känner till text-h2, text-kpi, text-eyebrow, shadow-card och shadow-pop. beroenden: tokens beslut: ADR-002, ADR-008 kvarstår: - Ingen Storybook och inga komponenttester. /dev/ui:s levande exempel är den enda visuella kontrollen. - Ingen primitiv för formulärfält – felanmalans formulärspegel bygger sina egna. ## @startsidan/module-sdk id: module-sdk namn: @startsidan/module-sdk sammanfattning: Kontraktet mellan skalet och modulerna, plus urvalsfunktionerna skalet använder för att komponera dem. uppdaterad: 2026-10-07 ansvar: - Definiera ModuleManifest, UserContext och de typer modulerna fyller. - Göra urvalet: filtrera på roll, sortera på order, aggregera actions. - Äga den samlade uppgiftslistan: uppgifterFor läser alla modulers uppgifter, öppna och hanterade, med källan ifylld – vid varje anrop, utan lager. - Låta aggregerande moduler nå registret utan att skalet skickar det som prop. - Beskriva redaktionella sidor och deras innehåll som block, oberoende av källa. - Bära flödestyperna tills motorn lyfts till packages/flow-engine i M4. filer: - packages/module-sdk/src/types.ts — ModuleManifest, UserContext, Widget (med label för flyttknapparna, ADR-018), ActionItem, ModuleRoute (focus, standard, full) och RouteComponent (ADR-019, ADR-025), Genvag (ADR-025), Notification och NotificationsProvider (ADR-020), FokusItem och FokusProvider (ADR-021), IntentHandler. - packages/module-sdk/src/select.ts — widgetsFor, actionsFor, uppgifterFor, navFor, genvagarFor, mergeNav, routeFor (med parametrar), matchaVag, suggestionsFor. - packages/module-sdk/src/define.ts — defineModule: typkontroll utan annotering. - packages/module-sdk/src/provide.ts — provideRegistry och currentRegistry (ADR-007). - packages/module-sdk/src/page.ts — PageManifest, PageContent, ContentBlock och Inline (ADR-016). - packages/module-sdk/src/pages.ts — findPage, pageTrail, pageHref, hubsInSection, articlesUnder, resolveContent, pageProblems. - packages/module-sdk/src/flow.ts — FlowDefinition, turkontraktets zod-schema, farSkapas. - packages/module-sdk/src/prompt.ts — byggSystemprompt: systemprompten ur flödesdefinitionen, inklusive blocket med redan fastställda fält (ADR-013). api: - defineModule: (manifest: ModuleManifest) => ModuleManifest Identitetsfunktion som ger typkontroll på manifestet. - widgetsFor: (modules, slot, ctx) => Widget[] Widgets i en slot som rollen ska se, i ordning. Widgetens roles är snävare än modulens. - actionsFor: (modules, ctx) => Promise Aggregatet bakom "Att agera på": de öppna uppgifterna. En modul som fallerar tar inte ned kortet. - uppgifterFor: (modules, ctx) => Promise Den samlade uppgiftslistan bakom Mina uppgifter och AI-assistentens verktyg: öppna i ordning, sedan hanterade, senast hanterade först. Varje uppgift får modul, kalla (modulens namn om modulen inte satt en) och lage (öppen om inget sattes). - routeFor: (modules, path, ctx) => { route, module, params } | undefined Slår upp en modulrutt, med parametrar ur mönster som /verktyg/personal/:id. En exakt väg går före ett mönster (ADR-019). - fokusFor: (modules, ctx) => Promise Fokusytornas innehåll, nyast först. Skalet visar den första som inte är stängd (ADR-021). - genvagarFor: (modules, ctx) => Genvag[] Knapparna bredvid hälsningen på startsidan, rollfiltrerade och sorterade på order (ADR-025). - notificationsFor: (modules, ctx) => Promise Klockans notiser från alla moduler, nyast först. En modul som fallerar tar inte ned de andras (ADR-020). - matchaVag: (monster: string, path: string) => Record | undefined Mönstermatchningen bakom routeFor. ":namn" fångar ett segment. - intentsFor / matchaAvsikt: (modules, ctx) => IntentHandler[] / (intents, fraga) => IntentHandler | undefined Avsikterna rollen når, och den första vars mönster står i frågan – gemener, delsträng. AI-fältet använder dem före modellen. - provideRegistry / currentRegistry: (all: ModuleManifest[]) => void / () => ModuleManifest[] Registret för aggregerande moduler (ADR-007). - findPage: (modules, segments: string[]) => PageManifest | undefined Slår upp en sida på URL-segmenten under /sidor: [sektion, ingång] eller [sektion, ingång, artikel]. - pageTrail / pageHref: (modules, page) => PageManifest[] / (page) => string Brödsmulorna från ingången ned till sidan, och sidans väg. - resolveContent: (page, ctx) => Promise Statiskt innehåll eller källa – skalet behöver inte veta vilket. - topPagesInSection: (modules, section) => PageManifest[] Sektionens sidor utan parent – ingångar och inramade sidor. En webbplats meny. - pageProblems: (modules) => string[] Registrets invarianter: unika slugar, artikelns parent är en ingång i samma sektion. Körs i test. - byggSystemprompt: (flow, profil, faltHittills?) => string Prompten ur flödet. faltHittills förankrar värdena så att modellen återger dem ordagrant i stället för att formulera om sig varje tur. - buildTurnSchema: (flow: FlowDefinition) => ZodObject Turkontraktet byggt ur flödet: reply, fields, complete, create. - farSkapas: (flow, turCreate, varden, antalAnvandarsvar) => boolean Tre villkor samtidigt: modellen har begärt det, allt obligatoriskt finns, och användaren har sett en sammanfattning. kontrakt: Kontraktet ska hållas tunt. Ett nytt fält gör det tyngre för alla moduler och ska bäras av minst två innan det blir permanent. Rollfiltreringens tre nivåer – den snävaste vinner: ``` roles på modulen gäller allt modulen bidrar med roles på widgeten snävare än modulens roles på nav-posten snävare än modulens ``` PageManifest och innehållsblocken (page.ts): ``` interface PageManifest { slug: string; // unik i registret, blir ctx.page section: string; // sektions-id ur skalets "Sidor" parent?: string; // ingångens slug – bara för en artikel layout: "hub" | "article"; title: string; lede?: string; kicker?: string; quickLinks?: QuickLink[]; // bara ingång article?: ArticleMeta; // bara artikel ai?: { suggestions?: (ctx) => Suggestion[] }; content?: PageContent | ((ctx) => Promise); } type ContentBlock = | { type: "heading"; level: 2 | 3 | 4; text: string } | { type: "paragraph"; inlines: Inline[] } | { type: "list"; ordered: boolean; items: Inline[][] } | { type: "image"; src: string; alt: string; caption?: string }; // Aldrig HTML: en adapter översätter källan till block (ADR-016). | { type: "details"; summary: string; chip?: { label; tone }; blocks: ContentBlock[] } // ADR-022 PageManifest.slots?: { main?: Widget[]; side?: Widget[] } // bara ingång: modulens egna kort PageContent.cards?: { title: string; blocks: ContentBlock[] }[] // fler redaktionella kort QuickLink.cta?: string // knapptext på snabbvägskortet interface PageLink { title: string; href: string; external: boolean; children?: PageLink[] } // layout "embed" + embed: { src } = en extern sida i ram (ADR-017). https, ingen parent. // children = en hopfälld nivå; så blir en webbkarta en länklista, inte en sidtyp. ``` beroenden: ui beslut: ADR-004, ADR-006, ADR-007, ADR-016, ADR-017, ADR-018, ADR-019, ADR-020, ADR-021, ADR-022 kvarstår: - IntentHandler matchas med enkla mönster (matchaAvsikt) av AI-fältet; LLM-matchning av avsikter finns inte. - Sidor rollfiltreras inte – de är gemensamma enligt ADR-003. Behövs det, är det ett nytt fält och en ny ADR. - Flödestyperna ligger här i väntan på M4. ADR-005 skrivs när motorn lyfts. ## oversikt id: oversikt namn: oversikt sammanfattning: Levererar nyckeltalsraden: mätvärden som skiljer sig åt mellan chef och medarbetare. uppdaterad: 2026-08-28 ansvar: - Fylla sloten kpi med rollens nyckeltal: fyra för medarbetaren, två för chefen – chefens budget och sjukfrånvaro kommer ur dialogstod. - Äga formen på ett nyckeltal: versal etikett, stort värde, kommentar, länk vidare. - Låta värdet bära tonen – aldrig hela rutan. filer: - modules/oversikt/src/manifest.ts — slots.kpi och navposten "Min översikt". - modules/oversikt/src/components/Nyckeltal.tsx — Server component som hämtar datat och mappar till rutor. - modules/oversikt/src/components/KpiSquare.tsx — En ruta. Hela rutan är en länk. - modules/oversikt/src/data/types.ts — NyckeltalPost och NyckeltalSvar. - modules/oversikt/src/data/mock.ts — Datat per roll, hämtat ur prototypen. - modules/oversikt/src/data/index.ts — hamtaNyckeltal(ctx). api: - hamtaNyckeltal: (ctx: UserContext) => Promise Rollens nyckeltal. Speglas av /api/mock/oversikt. kontrakt: ``` interface NyckeltalPost { id: string; etikett: string; // versal, kort varde: string; enhet?: string; kommentar: string; ton?: "neutral" | "positiv" | "varning"; // färgar värdet lank: string; } ``` beroenden: ui, module-sdk, api-mock beslut: ADR-006 kvarstår: - Länkarna pekar på platshållare tills målsidorna finns. ## agera id: agera namn: agera sammanfattning: Renderar kortet "Att agera på" ur de rader andra moduler bidrar med, och äger ingen egen data. uppdaterad: 2026-10-20 ansvar: - Läsa registret via currentRegistry och aggregera alla modulers uppgifter. - Visa de öppna i kortet "Att agera på" och alla – öppna och hanterade, med källa och frist – på sidan Mina uppgifter. - Äga radens form och ordning: AiChip, chip, knapp – i den ordningen. - Dölja kortet helt när ingen modul har något att bidra med. - Aldrig veta vilka moduler som bidrar – källan står på uppgiften, inte i agera. filer: - modules/agera/src/manifest.ts — slots.main, navposten Mina uppgifter och rutten /verktyg/uppgifter (standard). Båda rollerna. - modules/agera/src/components/AttAgeraPa.tsx — Kortet. Anropar actionsFor(currentRegistry(), ctx); Visa alla leder till Mina uppgifter. - modules/agera/src/components/MinaUppgifter.tsx — Sidan: antal öppna, passerade frister och hanterade, sedan Att göra och Hanterade. Anropar uppgifterFor. - modules/agera/src/datum.ts — datum och passerad – en frist gäller hela sin dag i svensk tid. - module-sdk/test/uppgifter.test.ts — Källa och läge ifyllda, ordningen, rollfiltreringen, en trasig modul, och att Att agera på bara får de öppna. kontrakt: - En modul bidrar med data via slots.actions – aldrig med ett eget kort eller en egen lista. - Tjänsten som äger uppgiften äger läget. En klarrapporterad uppgift lämnas med lage: "hanterad" och tiden i hanterad; den försvinner ur kortet och står kvar under Hanterade. - Fälten för listan: lage, kalla (t.ex. "Uppföljningsdialog" när modulens namn inte räcker), frist (ISO-datum), skapad och hanterad (ISO-tid). Allt är frivilligt. - Listan lagrar ingenting. Den läses ur modulerna varje gång, så den visar alltid samma läge som tjänsterna – för sidan, kortet och AI-assistenten. - En passerad frist gör raden röd och ger chipet Frist passerad, oavsett vilken status modulen satt. - actionsFor fångar fel per modul: en modul som fallerar tar inte ned kortet. - Statusen ska följa av data, inte väljas för utseendet. - Modulerna som bidrar i dag står i varje moduls dok, under slots.actions – bland dem dialogstod, med de öppna aktiviteterna från uppföljningsdialogen. Kortet vet inte vilka de är. beroenden: ui, module-sdk beslut: ADR-007 kvarstår: - Bara dialogstod lämnar hanterade uppgifter än. De andra modulerna har inget klart-läge i sina mockdata. - Flera moduler leder till "#" tills deras målvyer finns. - Listan går inte att filtrera på källa eller sortera på frist. - Raderna från utvecklingsarende kommer ur mockade underlag. Ett utkast användaren just skrivit syns inte förrän lagringen finns (fas 4). ## arenden id: arenden namn: arenden sammanfattning: Visar användarens ärenden som tabell och lyfter de ärenden som kräver något till "Att agera på". uppdaterad: 2026-08-28 ansvar: - Fylla sloten main med ärendetabellen. - Översätta ärenden med en åtgärd till ActionItem för modulen agera. - Äga ärendets datakontrakt: status, motpart, uppdateringstid. filer: - modules/arenden/src/manifest.ts — slots.main, slots.actions, navposten (medarbetare). - modules/arenden/src/components/Arendetabell.tsx — Tabellen när kortet är brett, en lista med status på egen rad när det är smalt (@3xl, R11). Kortet får bodyClassName så raderna går kant i kant. - modules/arenden/src/data/types.ts — Arende, ArendeStatus, ArendePerson, ArendeSvar. - modules/arenden/src/data/mock.ts — Ärendena per roll. - modules/arenden/src/data/format.ts — formateraUppdaterad: relativ tid mot svarets hamtad-fält. - modules/arenden/src/data/index.ts — hamtaArenden(ctx). api: - hamtaArenden: (ctx: UserContext) => Promise Rollens ärenden. Speglas av /api/mock/arenden. - formateraUppdaterad: (uppdaterad: string, hamtad: string) => string Relativ tid räknad mot svarets tidpunkt, inte mot klockan – så att mockdata inte åldras. beroenden: ui, module-sdk, agera, api-mock beslut: ADR-006, ADR-007 kvarstår: - Ärenden skapade i felanmalan landar inte här ännu – det är M3. - Tabellen har ingen sortering eller filtrering. ## kalender id: kalender namn: kalender sammanfattning: Visar dagens händelser, och för chef även årshjulets återkommande åtaganden. uppdaterad: 2026-08-28 ansvar: - Fylla sloten side med "Idag", och för chef med "Återkommande". - Formatera klockslag i vyn – datat bär alltid full ISO-tid. - Låta händelsens ton färga kantlinjen, inte raden. filer: - modules/kalender/src/manifest.ts — Två widgets i side; den andra bara för chef. - modules/kalender/src/components/Idag.tsx — Dagens händelser med tid och plats. - modules/kalender/src/components/Aterkommande.tsx — Årshjulet. - modules/kalender/src/data/index.ts — Typer, mockdata, hamtaDagen, hamtaAterkommande, klockslag. api: - hamtaDagen: (ctx: UserContext) => Promise Dagens händelser för rollen. Speglas av /api/mock/kalender. - hamtaAterkommande: () => Promise Årshjulet. Rollfiltreras i manifestet, inte i datat. - klockslag: (iso: string) => string "09:30" ur en ISO-tidpunkt. beroenden: ui, module-sdk, api-mock beslut: ADR-006 kvarstår: - Ingen riktig kalenderkälla. Datat är statiskt kring ett fixerat datum. - Modulen är den enda som visar en widget för bara en roll – mönstret bör bekräftas av en till innan det generaliseras. ## ledighet id: ledighet namn: ledighet sammanfattning: Visar ledighetssaldon för medarbetare och attesthögen för chef, och lyfter färdiga attester till "Att agera på". uppdaterad: 2026-08-28 ansvar: - Fylla sloten side med ledighetskortet, med olika rubrik och innehåll per roll. - Bidra med en attestrad först när något faktiskt är klart att godkänna. - Äga bedömningen av vad "klart att attestera" betyder. filer: - modules/ledighet/src/manifest.ts — slots.side och slots.actions. Actions returnerar tom lista för medarbetare. - modules/ledighet/src/components/Ledighetskort.tsx — Kortet. Rollen väljs i komponenten, inte i två manifestposter. - modules/ledighet/src/data/index.ts — LedighetPost, LedighetSvar, hamtaLedighet. api: - hamtaLedighet: (ctx: UserContext) => Promise Poster, totalt och klaraForAttest. Speglas av /api/mock/ledighet. beroenden: ui, module-sdk, agera, api-mock beslut: ADR-006, ADR-007 kvarstår: - Knapparna är platshållare: ingen ansökan går att skapa eller attestera. ## kompetens id: kompetens namn: kompetens sammanfattning: Håller ordning på vad varje medarbetare kan, vad rollen kräver och när det går ut – och gör utvecklingssamtalet till en plan som chefen bekräftar. uppdaterad: 2026-10-09 ansvar: - Räkna fram status per krav när datat läses – giltig, går ut inom 90 dagar, utgången eller saknas – utan schemaläggare. - Ge chefen det som behöver åtgärdas, täckningen per krav och andelsmål – vyer som växer i höjdled och klarar trettio krav – och en matris för ett urval; medarbetaren sin egen kompetens och plan. - Räkna fram vilken nivå i professionsprogrammet underlaget räcker till enligt förordning (2025:280) 6–9 §§, och säga att Skolverket beslutar. - Ta fram ett utkast till kompetensplan ur samtalets önskemål och rollens krav, med modellen eller regelmotorn, och låta servern pröva varje aktivitet innan planen bekräftas. - Mata Att agera på och klockan med det som kräver handling, härlett ur registret. - Fylla kortet på startsidan som tidigare hette Utveckling och utbildningar, med samma widget-id. filer: - modules/kompetens/src/manifest.ts — Fem rutter (standard), navposter per roll, slots.main, actions och notifications. - modules/kompetens/src/data/types.ts — Kontraktet kompetens@1: Kompetenstyp (med Omrade), Innehav, Rollkrav, Andelsmal, Lararuppgifter, Person, Plan, Aktivitet, Tillfalle, Enhet. - modules/kompetens/src/data/mock.ts — 49 kompetenstyper i sex områden och fyra enheter: IT-avdelningen (samma id som personal) och tre exempel ur förstudien – gata/park i full skala med 31 krav på tre roller och tolv personer, förskola och myndighet äldre. fyllEnhet ger gata/park allt giltigt utom de undantag demon visar. Alla personer är påhittade. - modules/kompetens/src/data/index.ts — Lagret på globalThis, statusFor, kravFor, professionsprogram, hamta-funktionerna (Enhetsvy med avvikelser, roller och kolumner med roller), valideraAktivitet, bekraftaPlan och registreraInnehav. - modules/kompetens/src/plan.ts — Utkastets zod-schema, prompt och regelmotor. Importeras av route handlern, aldrig av klienten. - modules/kompetens/src/components/KompetensKort.tsx — Startsidans två kort per roll, och rutten /verktyg/kompetens som väljer vy efter roll. - modules/kompetens/src/components/Enhetsvy.tsx — Chefens översikt: enhetsflikar, fyra tal, Att åtgärda, lärare, planer, andelsmål och täckning per område med luckorna först. Matrisvy är matrisens egen sida. - modules/kompetens/src/components/Avvikelser.tsx — Klientkomponent: det som gått ut, saknas eller går ut, grupperat per krav eller per person, med filter på status och område. - modules/kompetens/src/components/Matris.tsx — Klientkomponent: person × krav för ett urval av område och roll. Över MAX_KOLUMNER (10) visas ett val i stället för tabellen. Namnkolumnen står kvar vid sidledsscroll. - modules/kompetens/src/components/Personsida.tsx — En persons kompetens i tre perspektiv: chefens, den egna och exemplet med en förskollärare. - modules/kompetens/src/components/Delar.tsx — Kravkort (brister först, giltiga hopfällda per område), Ovrigtkort, Aktiviteter och Professionskort – delas av vyerna. - modules/kompetens/src/components/OmModulen.tsx — Beskrivningen för verksamheten: bakgrund, syfte, vad modulen gör per roll, fyra flöden att prova, vad som fungerar och vad som är simulerat, källor, AI, dataskydd, faser och frågor till mötet. Skriven för chefer och HR, inte för utvecklare. - modules/kompetens/src/components/Planvy.tsx — Underlaget från samtalet bredvid utkastet eller den bekräftade planen. - modules/kompetens/src/components/PlanUtkast.tsx — Klientkomponent: hämtar utkastet, låter chefen ändra, ta bort och lägga till, och skickar bekräftelsen. - modules/kompetens/src/components/RegistreraKnapp.tsx — Klientkomponent: chefen registrerar ett intyg med datum. Servern räknar giltigheten. - modules/kompetens/src/components/format.ts — Datum, vägar, statusmeta (chip och kantlinje), områdesnamn, källor och aktivitetsstatus. - modules/kompetens/test/data.test.ts — Status på gränserna, behörighet per roll, summering, enheten med trettio krav, områden, andelsmål, nivåberäkningen, regelmotorn, bekräftelsen och registreringen. - app/api/kompetens/plan/utkast/route.ts — Utkastet: identitet, takgräns, zod, underlag ur registret, modellen via structured outputs, validering per rad, regelmotorn som reserv. api: - hamtaEnhet: (ctx, enhetId = "it", nu?) => Promise En enhet för chef: personer med krav, kolumner, täckning, andelar, lärare och summa. undefined för andra roller. - hamtaPerson / hamtaMinKompetens / hamtaExempel: (ctx, …) => Promise En person: krav, övrig kompetens, plan och professionsprogram. Chefen ser sina enheter, medarbetaren sig själv via användar-id. - professionsprogram: (person, nu) => Professionsstatus | undefined Högskolepoäng per kategori och nivå, beräknad nivå och kraven till nästa. Bara lärare. - bekraftaPlan: (ctx, personId, aktiviteter, nu?) => Promise> Prövar roll, person, att planen väntar och varje rad. 404/409/400. Speglas av POST /api/mock/kompetens/:id/plan/bekrafta. - registreraInnehav: (ctx, personId, typId, utfardad, nu?) => Promise> Ett intyg registrerat av chef. Giltigheten räknas ur typen. Speglas av POST /api/mock/kompetens/:id/innehav. - regelutkast / byggPlanprompt / planutkastSchema: @startsidan/modul-kompetens/plan Utkastets tre delar: regelmotorn, prompten och schemat för structured outputs. kontrakt: Rutterna: ``` /verktyg/kompetens chef: egna enheten · medarbetare: min kompetens /verktyg/kompetens/enhet/:enhet chef: exempel ur förstudien (gata-park, forskola, myndighet) /verktyg/kompetens/enhet/:enhet/matris chef: matrisen för ett urval (it, gata-park …) /verktyg/kompetens/person/:id chef: en persons kompetens /verktyg/kompetens/person/:id/plan chef: samtalet blir en plan /verktyg/kompetens/professionsprogrammet båda: en förskollärares vy, som exempel /verktyg/kompetens/om båda: modulen beskriven för verksamheten ``` - Kompetensregistret äger innehav, krav, planer och meritering. Kurserna bor i Lärplattformen; registret läser märken och genomföranden därifrån (ADR-030). - Personer på den egna enheten har samma id som i personal (m-101 …). Modulerna importerar inte varandra – kompetensvyn länkar till personalakten med href och tvärtom. - Status räknas när datat läses. En notis eller rad i Att agera på är aldrig inskriven, och försvinner när ett intyg registreras. - Översikten skalar med problemen, inte med kraven: startvyn är avvikelser och täckning i höjdled. Matrisen visar högst tio kolumner åt gången – fler kräver ett urval av område eller roll. - Att modellen föreslår en aktivitet räcker aldrig. valideraAktivitet gäller för modellens rader, regelmotorns och chefens, både i utkastet och vid bekräftelsen. - Professionsprogrammets nivå är en beräkning för samtalet och tjänstgöringsintyget. Skolverket beslutar; registrerad meritering är det läraren rapporterar. beroenden: module-sdk, ui, agera, api-mock, personal, anthropic beslut: ADR-030, ADR-019, ADR-020, ADR-007, ADR-026 kvarstår: - Ingen källa är ansluten: Moodle-pluginen, ID06 och Skolverkets beslut är beskrivna i docs/plan-modul-kompetens.md men mockade. - xAPI-ingången och käll-id per kompetenstyp är utredda men inte byggda – se docs/plan-modul-kompetens.md, avsnitt 7 (Standarder för inflöde och intyg). - Chefen ser exempelenheterna för att demon ska visa förstudiens behov. I skarp drift styr chefFor i person@1. - Samtalsraderna i Att agera på (obokade samtal, förbered samtalet) kom med från utveckling och hör till samtalsflödet. De flyttar när personal får en medarbetarsida. - Bekräftelser och intyg lever i serverns minne tills omstart. En riktig tjänst kräver egen lagring – den första modulen som skriver data (plan.md, fas 4). - Dataklassning per fält, gallring enligt dokumenthanteringsplanen och en DPIA återstår före drift. ## felanmalan id: felanmalan namn: felanmalan sammanfattning: Det första samtalsflödet: användaren beskriver felet i fri text och ser i realtid vad assistenten uppfattat. uppdaterad: 2026-08-30 ansvar: - Äga fokusrutten /flode/felanmalan och hela vyn bakom den. - Beskriva flödet som data i flow.ts, så att motorn kan lyftas i M4 utan omskrivning. - Hålla samtalstillståndet i klienten och skicka historiken till serverändan. - Hålla regelmotorn som kör flödet när AI_PROVIDER=mock, så att demon fungerar utan nyckel. - Aldrig skapa ett ärende själv – servern avgör. filer: - modules/felanmalan/src/manifest.ts — routes med mode focus och ai.intents. Ingen nav-post: ingången är IT-supportsidans snabbväg Felanmäl (ADR-022). - modules/felanmalan/src/flow.ts — felanmalanFlow: grupper, autoFalt, fragaFalt, mål. KARNFALT. - modules/felanmalan/src/components/FelanmalanFlode.tsx — Vyn: chatt till vänster, formulärspegel till höger. - modules/felanmalan/src/components/ChatPane.tsx — Samtalet. Nya bubblor animeras med animate-msg-in. - modules/felanmalan/src/components/FormMirror.tsx — Kvittot: AI-tonad kant och ✦ per fält assistenten fyllt. - modules/felanmalan/src/store.ts — Zustand: bubblor, fältvärden, signaturen och anropet mot route handlern. - modules/felanmalan/src/motor/index.ts — koraTur och spelaUpp: regelmotorn bakom AI_PROVIDER=mock. - modules/felanmalan/test/motor.test.ts — 18 tester på regelmotorn. Plockas upp av vitest.config.mts i roten. api: - felanmalanFlow: FlowDefinition Flödet som data. Systemprompten, formulärspegeln och turkontraktet genereras alla ur den. - koraTur / spelaUpp: regelmotorn Deterministisk tur för offline-demo. Samma kontrakt som modellen. - KARNFALT: readonly ["problem", "plats", "datornamn"] De fält som måste finnas innan ett ärende ens kan övervägas. kontrakt: - Samtalet är inmatningen, formuläret är kvittot. Användaren ska se vad som uppfattats innan något skapas. - Ärendet skapas aldrig utan uttrycklig bekräftelse, och farSkapas kräver tre villkor samtidigt. - Klienten skickar meddelanden plus fältvärdena och serverns signatur över dem. Prompt, profil och beslut bor i route handlern. - Fälten skickas tillbaka för att assistenten ska återge dem ordagrant. Ett fält som byter lydelse medan användaren tittar på formulärspegeln läses som ett fel (ADR-013). beroenden: module-sdk, ui, api-felanmalan, flow-engine beslut: ADR-004 kvarstår: - M3: ärendeadaptern. Ett skapat ärende landar ingenstans ännu. - M4: motorn ska lyftas till packages/flow-engine, och ett andra flöde definieras deklarativt. - Flödet är det enda som finns – generaliseringen är obevisad tills ett andra flöde byggts. ## webbsidor id: webbsidor namn: webbsidor sammanfattning: Samlar sektionen Sitevision under "Sidor": ingången Integrera med Sitevision som beskriver sätten med fördelar och nackdelar, kuraterade sidor från sundsvall.se via headless REST, sundsvall.se:s sidträd som webbkarta och utveckling.sundsvall.se i ram – översatta och märkta med källan. uppdaterad: 2026-10-06 ansvar: - Äga kureringen: vilka Sitevision-noder som visas, var, och med vilka snabbvägar och AI-förslag. - Beskriva integrationssätten på ingången Integrera med Sitevision, med ett levande exempel för vart och ett. - Översätta Sitevisions headless-svar till ContentBlock – aldrig släppa igenom markup. - Hämta på servern med timeout och cache, och falla till sparad version när källan inte svarar. - Hålla länkar mellan kuraterade sidor kvar i startsidan – och webbkartans länkar på den riktiga sidan. filer: - modules/webbsidor/src/manifest.ts — pages: guiden först, sedan kureringen; content hämtas per nod. Länkkartan byggs här. - modules/webbsidor/src/integrera.ts — Guidens statiska innehåll: sju sätt som utfällbara block med läge, fördelar, nackdelar och exempel. EXEMPEL håller exemplens adresser; integrera.test.ts kontrollerar att de finns. - modules/webbsidor/src/kurering.ts — SEKTION = "sitevision". kurering: sidor med nodeId och uri. kartor: webbkartor med rotId och djup. utvecklingsmeny: utveckling.sundsvall.se:s meny, visas via rest eller ram (ADR-017). - modules/webbsidor/src/kalla/sitevision/schema.ts — zod-schema för headless-svaret. Lös där Sitevision får växa, hård där vi läser. - modules/webbsidor/src/kalla/sitevision/oversatt.ts — oversatt och losLank: ren funktion från svar till PageContent. - modules/webbsidor/src/kalla/sitevision/hamta.ts — skapaHamtare → hamtaSvar och hamtaSida: fetch med force-cache, revalidate och timeout, reservordningen, källmärkningen. server-only. - modules/webbsidor/src/kalla/sitevision/trad.ts — hamtaTrad och synligaSidor: webbkartans sidträd som PageLink med children, alltid externa länkar. - modules/webbsidor/src/kalla/sitevision/fixturer/ — Inspelade svar per nod-id, webbkarta.json med trädets noder, och utveckling/ med menyn och Målbild-sidan. index.ts exporterar fixturer, tradfixturer och utvecklingsfixturer. - modules/webbsidor/skript/spela-in.mjs — Spelar in fixturerna; skalar ned egenskaperna och maskerar personliga e-postadresser. --webbplats utveckling för utveckling.sundsvall.se. api: - oversatt: (svar: HeadlessSvar, { bas, lankkarta, varna? }) => PageContent Rubrik- och Ingressmodulerna blir titel och ingress, övriga textmoduler block, Mediaflow-bilder bildblock. Okänt blir text eller utelämnas, med varning. - losLank: (href, { bas, lankkarta }) => { href, external } | null Bara http, https, mailto och tel. En kuraterad sundsvall.se-sida blir intern väg. - skapaHamtare: (h: Hamtare) => { hamtaSvar, hamtaSida } Bas, länkkarta och fixturer in; hämtare med eget minne ut. hamtaSvar ger det validerade svaret och dess härkomst, hamtaSida den översatta sidan. Bas läses vid varje anrop. - hamtaTrad: (hamtaSvar, rotId, djup, varna?) => Promise<{ children, source }> Sidträdet under roten. En gren utan svar visas som länk utan undersidor och märker källan som sparad. kontrakt: Sektionen Sitevision: ``` /sidor/sitevision sektionen: guiden, exemplen, webbplatsen /sidor/sitevision/integrera-med-sitevision sätten, fördelar och nackdelar /sidor/sitevision/skolavtal[/] headless REST /sidor/sitevision/sundsvall-se sidträdet som webbkarta /sidor/utveckling-sundsvall/ ram med menyrad (+ en sida via REST) /start, kortet Nyheter (modulen nyheter) RSS ``` Källan och reservordningen: ``` GET {SITEVISION_BAS_URL}/rest-api/1/1/{nodeId}/headless?textPortletFormat=JSON cache force-cache · timeout 4 s · next.revalidate 300 · tags ["sitevision", "sitevision:{nodeId}"] (force-cache krävs: efter cookies() cachar Next 16 inget fetch utan uttryckligt val) svaret är text/plain men kroppen JSON → JSON.parse + zod Reserv när källan inte svarar, inte validerar eller bas är tom: 1. Nexts datacache (inom 300 s märks inget) 2. senast lyckade svar i processens minne → source.stale = true 3. fixturer/{nodeId}.json → source.stale = true ``` Sitevision | Blir contentNode "Rubrik" | title (annars displayName) contentNode "Ingress" | lede (annars ett inledande stycke utan länkar) p · h1–h6 · ul/ol > li | paragraph · heading 2–4 · list strong/b · em/i · a · lineBreak | text med flagga · link · break custommodule Mediaflow, del image · bildmodul (type image) | image med absolut src andra Rubrik/Ingress längre ned | vanlig brödtext – bara den första är titel och ingress allt annat | texten som stycke, eller utelämnat – med varning nodes av typ sv:page | children: intern om kuraterad, annars extern webbkarta: nodes sv:page med visibleInMenus | PageLink med children, djup enligt kartan, alltid extern Lägga till en sida: ``` # 1. Hitta nod-id: sv.PageContext.pageId i sidans HTML # 2. Spela in svaret yarn workspace @startsidan/modul-webbsidor spela-in 4.xxxxxxxx # 3. Rad i fixturer/index.ts och post i kurering.ts (uri = properties.URI) # 4. yarn test – kurering.test.ts kontrollerar uri, fixtur och invarianter # Webbkartan spelas in i ett svep (rot och djup står i skriptet och i kurering.ts) yarn workspace @startsidan/modul-webbsidor spela-in --trad # utveckling.sundsvall.se: menyn och sidorna som visas via REST yarn workspace @startsidan/modul-webbsidor spela-in --webbplats utveckling --trad yarn workspace @startsidan/modul-webbsidor spela-in --webbplats utveckling 4.44e3ea617a090538136b03e ``` beroenden: module-sdk, sitevision beslut: ADR-016, ADR-017, ADR-003 kvarstår: - Ramsidorna kräver att utveckling.sundsvall.se:s CSP frame-ancestors listar startsidans origin – i dag start.sundsvall.dev och localhost:3000. Se ADR-017. - Sajtens kakbanner visas i ramen och kan återkomma i webbläsare som blockerar tredjepartskakor. - Länkar i Målbild-sidan till andra sidor än menyns öppnas på utveckling.sundsvall.se i ny flik, inte i ramen. - Webbkartan visar tre nivåer – ett anrop per förälder. Djupare sidor nås på sundsvall.se. Hela trädet (~9 800 adresser i sitemap) kräver sitemap eller en egen RESTApp. - Ingen push från Sitevision: innehållet kan vara fem minuter gammalt. En RESTApp på sv:publishing:publish som anropar revalidateTag är nästa steg. - Intranätets Sitevision kräver servicekonto via BFF eller WSO2 – fas 4. - Bara text och Mediaflow-bilder översätts. Tabeller, formulär och andra moduler blir text eller utelämnas. - Bilderna laddas direkt från sundsvall.se, ett anrop till tredje part från webbläsaren. - Innehållet är publikt kommuninnehåll. Det prövar kontraktet, inte frågan om vad som hör hemma på intranätet. ## nyheter id: nyheter namn: nyheter sammanfattning: Visar de senaste nyheterna från sundsvall.se på startsidan, med länk till varje nyhet på källan. uppdaterad: 2026-10-05 ansvar: - Fylla sloten side med nyhetskortet, sist i spalten. - Tolka Sitevisions RSS till validerade nyheter – bara http(s)-länkar, alltid ett datum. - Hämta på servern med cache och timeout, och visa sparat flöde när källan inte svarar. - Märka källan, och märka en sparad version med ett gult chip. filer: - modules/nyheter/src/manifest.ts — slots.side: nyheter-flode, order 50. notifications: de två senaste nyheterna, som leder till nyheten på källan (ADR-020). - modules/nyheter/src/flode.ts — FLODE: RSS-portletens sökväg, nyhetslistan och antal. - modules/nyheter/src/components/Nyhetsflode.tsx — Kortet: ListRow per nyhet med datum och ingress, bild när kortet är brett, källrad. - modules/nyheter/src/kalla/rss.ts — tolkaRss och nyhetSchema. Ingen XML-tolk som beroende; zod efteråt. - modules/nyheter/src/kalla/hamta.ts — skapaNyhetshamtare: force-cache, revalidate 300, timeout, reservordning. server-only. - modules/nyheter/src/data/index.ts — hamtaNyheter, inkopplad mot SITEVISION_BAS_URL. - modules/nyheter/src/kalla/fixturer/nyheter.ts — Inspelat flöde som sträng. Testdata och sista reserv. - modules/nyheter/skript/spela-in.mjs — Spelar in flödet och maskerar personliga e-postadresser. api: - hamtaNyheter: () => Promise Nyheterna och källan: { nyheter, kalla: { namn, hamtad, stale } }. - tolkaRss: (xml: string) => Nyhet[] RSS 2.0 till Nyhet { titel, lank, ingress?, publicerad, bild? }. Kastar om ett fält inte validerar. kontrakt: Källan och reservordningen: ``` GET {SITEVISION_BAS_URL}{FLODE.sokvag} (RSS 2.0, Sitevisions nyhetsportlet) cache force-cache · timeout 4 s · next.revalidate 300 · tags ["nyheter"] Reserv: Nexts datacache → senaste flöde i minnet → fixturer/nyheter.ts Tom SITEVISION_BAS_URL = bara det inspelade flödet. # Spela in på nytt yarn workspace @startsidan/modul-nyheter spela-in ``` - Sidhuvudets rel="alternate"-länk på sundsvall.se ger 404. Flödet som svarar ligger på portleten bakom sidan /rss/nyheter. - Escapad HTML i description avkodas och rensas till text. Inget renderas som HTML (AM12). - Nyhetskortet är inte en AI-yta och får därför inte AI-tonen. beroenden: module-sdk, ui, sitevision beslut: ADR-016, ADR-020 kvarstår: - Bilderna visas bara när kortet ligger i den breda spalten (@3xl, R11). - Inga intranätsnyheter: intranätets flöde kräver inloggning och väntar på fas 4. - Ingen push: en ny nyhet syns inom fem minuter. ## dialogstod id: dialogstod namn: dialogstod sammanfattning: Ger chefen en samlad bild av nyckeltalen för nivån under sin egen, och stöd för uppföljningsdialogen med varje underställd chef – med det som bestäms i samtalet kvar på den ansvariga chefens startsida. uppdaterad: 2026-10-20 ansvar: - Visa nyckeltalen för nivån under chefens egen, sida vid sida: kommundirektören ser förvaltningarna, förvaltningschefen avdelningarna, avdelningschefen sektionerna. Samma områden och trösklar på alla nivåer; den som inte har någon nivå under sig ser sin egen enhet. - Visa dialogen om en enhet i sex områden: ekonomi, HME och sjukfrånvaro med värde, mål, trend och diagram, och verksamhet, digital transformation och kommunikativt ledarskap med dialogfrågor och manuell status. - Spara aktiviteter och åtgärder (lägg till, ändra, klarrapportera, öppna igen), manuell status med historik som aldrig skrivs över, och ett anteckningsblock per enhet och område. - Lägga varje aktivitet hos chefen för enheten den gäller: en rad i Att agera på och en notis tills den är klarrapporterad, och sedan som hanterad i Mina uppgifter. - Fylla startsidans KPI-rad med ekonomin och sjukfrånvaron för chefens egen enhet – samma värden som i dialogen – och leda vidare till området i dialogen. - Pröva varje läsning och skrivning på servern: bara chefer, och bara den egna enheten och nivån under. filer: - modules/dialogstod/src/manifest.ts — Tre rutter med mode "standard", navposten Dialogstöd, slots.kpi, slots.actions och notifications. Bara chef. - modules/dialogstod/src/data/organisation.ts — Organisationsträdet: kommunen, de nio förvaltningarna med riktiga namn, påhittade avdelningar under Kommunstyrelsekontoret och sektioner under IT-avdelningen. synliga och harTillgang avgör vad en chef ser. - modules/dialogstod/src/data/omraden.ts — De sex områdena med frågor och faktarutor ur Dialogstödet, och trösklarna: ekonomi i balans, HME ≥ 75, sjukfrånvaro ≤ 6,0 % / 7,5 % och kvartalsökning > 1,5 p.e. - modules/dialogstod/src/data/generera.ts — Fiktiva, stabila värden per enhet ur enhetens id. En enhet med enheter under sig får sina värden ur dem: ekonomin summeras, sjukfrånvaron vägs med anställda och HME med svar. - modules/dialogstod/src/data/index.ts — Lagret på globalThis, korten, hamtaOversikt och hamtaDialog, skrivningarna, och raderna och notiserna till startsidan. - modules/dialogstod/src/components/Oversikt.tsx — Översikten: en tabellrad per enhet med de sex områdena och de öppna aktiviteterna. - modules/dialogstod/src/components/Startnyckeltal.tsx — Startsidans KPI-rutor för ekonomi och sjukfrånvaro, i nyckeltalsrutans form. Leder till området i dialogen. - modules/dialogstod/src/components/Dialog.tsx — Dialogen: korten som länkar, det valda området med diagram eller manuell status, frågorna, aktiviteterna och anteckningarna. - modules/dialogstod/src/components/Diagram.tsx — Stapel- och linjediagram i SVG med tokens: talen vid punkterna, färgbyte exakt vid mållinjen, tabellvy. - modules/dialogstod/src/components/Aktiviteter.tsx — Klient: lägga till, ändra, klarrapportera och öppna igen. - modules/dialogstod/src/components/StatusForm.tsx — Klient: välj status, motivera, spara i historiken. - modules/dialogstod/src/components/Anteckningsblock.tsx — Klient: anteckningar som sparas av sig själva. - modules/dialogstod/test/data.test.ts — Nivåerna, behörigheten, trösklarna, sammanvägningen, skrivningarna, startsidans nyckeltal och vägen till startsidan. api: - hamtaOversikt: (ctx: UserContext) => Promise<{ egen?: Enhet; rader: Oversiktsrad[] }> Nivån under chefens egen. Tomt för andra roller. Speglas av /api/mock/dialogstod. - hamtaDialog: (ctx, enhetId) => Promise Dialogen om den egna enheten eller en enhet på nivån under. - laggTillAktivitet: (ctx, enhetId, omrade, text, nu?) => Resultat POST /api/mock/dialogstod/:enhet/:omrade/aktiviteter med { text }. - andraAktivitet: (ctx, id, { text?, klar?, notering? }, nu?) => Resultat PATCH /api/mock/dialogstod/aktiviteter/:id. klar: false öppnar igen. - sattStatus: (ctx, enhetId, omrade, { status, kommentar, dimension? }, nu?) => Resultat POST …/status. Bara områden utan mätdata; Verksamhet kräver dimensionen grunduppdrag eller fullmaktigemal. - sparaAnteckning: (ctx, enhetId, omrade, text, nu?) => Resultat PUT …/anteckningar med { text }. Den senaste sparningen gäller. - hamtaStartnyckeltal: (ctx) => Promise<{ enhet: Enhet; kort: Kortvarde[] } | undefined> Ekonomin och sjukfrånvaron för chefens egen enhet, till slots.kpi. Ingenting för den som inte leder någon enhet. - dialogstodActions / dialogstodNotiser: (ctx) => Promise / Promise Aktiviteterna för chefens egen enhet som uppgifter med kalla "Uppföljningsdialog", lage och tider – de öppna i Att agera på – och en notis för en öppen aktivitet som någon annan lagt till. kontrakt: Rutterna: ``` /verktyg/dialogstod nivån under chefens egen /verktyg/dialogstod/:enhet dialogen, första området /verktyg/dialogstod/:enhet/:omrade dialogen, ett visst område ``` - Chefens plats i organisationen kommer ur trädet, nyckat på userId – inte ur UserContext, som saknar enhet. I skarp drift ger organisationen via SCIM samma sak. - Demons chefer: Lena Forsberg (kommundirektör), Johan Ek (Kommunstyrelsekontoret), Sara Nyström (IT-avdelningen) och Kim Andersson (Drift och support). Byt i användarmenyn. - Statusens färg står alltid med text: Över mål, Bevaka, Åtgärd krävs. I översikten och på startsidan bärs den av prick och värde (R5). Dialogens områdeskort fylls helt i statusens ljusa ton, utan ram – ett medvetet undantag från R5, så att lägena syns på avstånd när vyn visas i ett möte. Ett område utan status har streckad ram. - Allt är fiktivt utom förvaltningarnas namn. Dialogstödets riktiga data kopieras inte: sjukfrånvaron per kön och ålder i små enheter går att härleda till personer. beroenden: module-sdk, ui, api-mock, skal, agera beslut: ADR-019, ADR-020, ADR-006 kvarstår: - Allt är fiktiv data. Ekonomi, sjukfrånvaro och HME per avdelning och sektion kräver kommunens organisationsträd och källorna på de nivåerna – i Dialogstödet finns de bara per förvaltning. - Aktiviteter, status och anteckningar ligger i serverns minne: de gäller för alla som tittar på demon och nollställs när servern startas om. - Anteckningarna har inga bifogade filer, eftersom prototypen inte sparar på disk. - HME på en nivå med enheter under sig är vägt med antalet svar, inte den officiella viktningen. - De andra modulerna visar samma chefsdata för alla chefer – de vet inte var i organisationen chefen sitter. ## personal id: personal namn: personal sammanfattning: Ger chefen överblick över sin personal och en väg in i varje medarbetares personalakt och senaste utvecklingssamtal. uppdaterad: 2026-10-05 ansvar: - Lista enhetens medarbetare med närvaro, notiser, ärenden och nästa utvecklingssamtal – sökbart och filtrerbart. - Visa personalakten: notiser att agera på, kopplade ärenden, anställning och kontakt. - Visa dokumentationen från senaste utvecklingssamtalet: mål, kompetensutveckling, arbetsmiljö och överenskommelser. - Lämna bara ut personal till chef – datafunktionen ger tomt svar för andra roller. - Göra personalaktens notiser till notiser i klockan, och låta chefen godkänna ett signerat utvecklingssamtal. filer: - modules/personal/src/manifest.ts — Tre rutter med mode "standard" (ADR-019) och navposten Personal. Bara chef. - modules/personal/src/data/types.ts — Medarbetare, Narvaro, Notis, PersonArende, Samtalsdokumentation, PersonalSvar. - modules/personal/src/data/mock.ts — Tio fiktiva medarbetare. Knyter an till startsidans rehabärende, samtal utan tid och ärenden A-156772–A-156802. - modules/personal/src/data/index.ts — hamtaPersonal, hamtaMedarbetare, sammanstall, godkannSamtal och personalnotiser. Lagret på globalThis, så att route handlers och sidor ser samma skrivningar. - modules/personal/src/components/GodkannKnapp.tsx — Klientknappen som skickar godkännandet och laddar om sidan. - modules/personal/src/components/Oversikt.tsx — Översikten: sammanfattning i fyra tal och listan. - modules/personal/src/components/Personallista.tsx — Klientkomponent: sök, filter med aria-pressed och tabell där hela raden leder till akten. - modules/personal/src/components/Personalakt.tsx — Personalakten, och läget "finns inte" för en okänd person. - modules/personal/src/components/Samtal.tsx — Samtalsdokumentationen i artikelns form. - modules/personal/src/components/format.ts — Datum, vägar (AKT, SAMTAL, OVERSIKT, KOMPETENS) och målstatus. KOMPETENS leder till personens vy i modulen kompetens – samma id, ingen import. api: - hamtaPersonal: (ctx: UserContext) => Promise Chefens personal; tomt för andra roller. Speglas av /api/mock/personal. - hamtaMedarbetare: (ctx, id: string) => Promise En medarbetare ur chefens personal. - godkannSamtal: (ctx, id, nu?) => Promise Chefen godkänner senaste samtalets dokumentation. Falskt för fel roll, okänd person eller redan godkänt. Speglas av POST /api/mock/personal/:id/utvecklingssamtal/godkann. - personalnotiser: (ctx) => Promise Varje notis i personalakterna som notis i klockan, med personen som meta. Manifestets notifications. - sammanstall: (poster: Medarbetare[]) => { totalt, iTjanst, franvarande, attAgera, samtalUtanTid } Talen överst i översikten. kontrakt: Rutterna: ``` /verktyg/personal översikten /verktyg/personal/:id personalakten /verktyg/personal/:id/utvecklingssamtal senaste samtalets dokumentation ``` - Namn, e-post och telefonnummer är påhittade. E-postdomänen är exempel.invalid, som inte kan finnas. - Notisernas allvar följer kantlinjens färger: krit = tidsgräns, warn = väntar, info = att känna till. - Ett samtal med signerad men utan godkand är en notis "Utvecklingssamtal att godkänna" tills chefen godkänt – notisen räknas fram, den är inte inskriven. beroenden: module-sdk, ui, api-mock beslut: ADR-019, ADR-020, ADR-004, ADR-006 kvarstår: - Allt är fiktiv data. Personalsystemet nås via BFF och WSO2 i fas 4. - Godkännanden ligger i serverns minne: de gäller för alla som tittar på demon och nollställs när servern startas om. - Knapparna Boka, Attestera och Öppna rehabärendet leder ingenstans än. - En okänd person ger sidan "finns inte" med status 200, inte 404 – modulen har inget sätt att säga 404 (ADR-019). - Notiserna i akten blir inte rader i "Att agera på"; där finns i stället startsidans egna demorader. ## fokusnyhet id: fokusnyhet namn: fokusnyhet sammanfattning: Lämnar intranätets fokusnyhet – nyheten om nya startsidan – till fokusytan under AI-fältet, och visar hela nyheten. uppdaterad: 2026-10-05 ansvar: - Äga nyhetens innehåll: rubrik, ingress, bild, avsändare och artikeln. - Lägga rollens avsnitt först i artikeln: chefen läser först om det som rör personalen. - Ge nyheten ett stabilt id, så att en stängd nyhet förblir stängd och en ny visas. filer: - modules/fokusnyhet/src/manifest.ts — slots.fokus och artikelns rutt (mode standard), båda rollerna. - modules/fokusnyhet/src/data/index.ts — hamtaArtikel(ctx): hela nyheten. hamtaFokusnyhet(ctx): fokusytans form av den, med bild och länk till artikeln. - modules/fokusnyhet/src/components/Artikel.tsx — Hela nyheten i artikelns form: kicker, rubrik, ingress, bild och avsnitt. api: - hamtaFokusnyhet: (ctx: UserContext) => Promise Nyheten om nya startsidan för fokusytan. Samma id för båda rollerna. - hamtaArtikel: (ctx: UserContext) => Nyhetsartikel Hela nyheten, med rollens avsnitt först. beroenden: module-sdk, ui beslut: ADR-021, ADR-019 kvarstår: - Mockad: i skarp drift kommer nyheten från intranätets publicering, troligen Sitevision (ADR-016). - Bilden hämtas från utveckling.sundsvall.se; utan nät visas en blå platta i dess ställe. - "Lämna synpunkter" leder ingenstans än. ## itsupport id: itsupport namn: itsupport sammanfattning: Ger medarbetare och chefer en ingång till IT-supporten: snabbvägar, svar på vanliga frågor, driftstatus, kontakt och de egna IT-ärendena – och prioriteringsordningen för IT-ärenden. uppdaterad: 2026-10-06 ansvar: - Äga IT-supportens ingång och artikeln om prioriteringsordningen, med innehåll ur facit. - Visa det levande på ingången som egna kort: driftstatus, kontakt med öppettider och användarens IT-ärenden. - Vara felanmälans ingång: snabbvägen Felanmäl och kontaktkortets Skapa ärende leder till flödet. - Ge AI-fältet sidans egna förslag; felanmalan lägger till sitt när ctx.page är it-support. filer: - modules/itsupport/src/manifest.ts — Två sidor: ingången it-support med snabbvägar, förslag, innehåll och slots, och artikeln prioriteringsordning. - modules/itsupport/src/innehall.ts — Ingångens kort (Så fungerar IT-supporten, Vanliga frågor med details) och artikelns block med P1–P4 som details med chip. - modules/itsupport/src/data/ — Typer, mock och hämtfunktioner för driftstatus, kontakt, IT-ärenden och nyheter. oppetLage räknar öppettider i svensk tid. - modules/itsupport/src/components/ — Korten Driftstatus, Kontakt, MinaItArenden och NyttFranIt. api: - oppetLage: (nu: Date, oppnar: string, stanger: string) => { oppet, text } Öppet nu eller stängt, vardagar i svensk tid. Ren funktion, testad. - hamtaItArenden: (ctx: UserContext) => Promise<{ hamtad, poster }> Användarens egna IT-ärenden. - hamtaDriftstatus / hamtaSupportkontakt / hamtaItNyheter: () => Promise<…> Driftstatus, supportens kontaktuppgifter och Nytt från IT. kontrakt: Sidorna: ``` /sidor/service-it-lokaler/it-support ingången /sidor/service-it-lokaler/it-support/prioriteringsordning artikeln ``` - Kontaktuppgifterna är påhittade – facits nummer och adress kan vara riktiga. - Snabbvägen Mina IT-ärenden är ett ankare till kortet på samma sida (#it-arenden-h). beroenden: module-sdk, ui, felanmalan beslut: ADR-022, ADR-003 kvarstår: - Driftstatus, ärenden och nyheter är mockade; i skarp drift nås ärendesystemet och driftövervakningen via BFF i fas 4. - Beställningsportalen, lösenordsåterställningen och Boka IT-torget leder ingenstans än. ## verktygslada id: verktygslada namn: verktygslada sammanfattning: Ger chefer, verksamhetsutvecklare och beslutsfattare en målgruppsingång till digitalisering, IT och AI: verktygen koncernen redan har, vad de kan göra inom varje verksamhetsområde, vad omvärlden lärt sig och vägen från idé till nytta. uppdaterad: 2026-10-07 ansvar: - Äga ingången Verktygslådan i sektionen Digitalisering, IT och AI, och en artikel per tillgängligt verktyg. - Visa verktygskatalogen, verksamhetsområdena, omvärldsträffarna och kontakten som egna kort på ingången. - Leda den som har en idé till flödet Testa ditt utvecklingsärende, och den som har ett fel till IT-support. - Ge AI-fältet sidans egna förslag om verktyg och utvecklingsärenden. filer: - modules/verktygslada/src/manifest.ts — Ingången verktygsladan med snabbvägar, förslag, innehåll och slots, och en artikel per tillgängligt verktyg ur katalogen. - modules/verktygslada/src/innehall.ts — Ingångens kort (Om, Från idé till nytta, Vanliga frågor med details), verktygsartikelns block och sökvägarna. - modules/verktygslada/src/data/ — Typer, mock och hämtfunktioner för verktyg, verksamhetsområden, omvärld och kontakt. omradeEtiketter och verktygFor är rena funktioner. - modules/verktygslada/src/components/ — Korten Verktygen, Verksamheterna, Omvarlden och Kontakt. api: - hamtaVerktyg: () => Promise<{ hamtad, poster: Verktyg[] }> Verktygskatalogen, digitala verktyg och metodstöd, tillgängliga och kommande. - hamtaVerksamheter / hamtaOmvarld / hamtaKontakt: () => Promise<…> De åtta verksamhetsområdena, granskade omvärldsträffar nyast först och digitaliseringsenhetens kontakt. - omradeEtiketter: (omraden: OmradeKey[], verksamheter: Verksamhet[]) => string[] Områdesetiketter; alla åtta blir "Alla verksamheter". Ren funktion, testad. - verktygFor: (omrade: OmradeKey, verktyg: Verktyg[]) => Verktyg[] Tillgängliga verktyg som rör ett område. Ren funktion, testad. kontrakt: Sidorna: ``` /sidor/digitalisering-it-ai sektionen /sidor/digitalisering-it-ai/verktygsladan ingången /sidor/digitalisering-it-ai/verktygsladan/ artikeln, t.ex. eneo ``` - Verksamhetsområdenas nycklar är desamma som i den fristående verktygslådan: vard, skola, social, bygg, admin, kultur, tech, lead. - Kommande verktyg syns i katalogen med chippet Kommande men får ingen artikel. - Kontaktuppgifterna är påhittade. beroenden: module-sdk, ui, utvecklingsarende, itsupport beslut: ADR-027, ADR-022, ADR-003 kvarstår: - Verktyg, verksamheter, omvärld och kontakt är mockade; i skarp drift läses verktygslådans data via BFF i fas 4. - Digitala arvet, projektportföljen, nyttoportföljen och omvärldsbevakningens redaktion finns kvar i den fristående verktygslådan och är inte porterade. - Verksamhetskortet visar bara vilka verktyg som rör området – verktygslådans fördjupade förvaltningsöversikt finns inte här. ## GET /api/mock/* id: api-mock namn: GET /api/mock/* sammanfattning: Speglar varje moduls datafunktion som en HTTP-endpoint, för att visa vilken form ett riktigt mikrotjänst-API skulle ha. uppdaterad: 2026-08-28 ansvar: - Visa API-formen utan att vara modulernas datakälla – modulerna anropar sin funktion direkt. - Hämta identiteten på servern och skicka rollen vidare som UserContext. - Ge /api/health för healthcheck i compose och rökkontroll efter deploy. filer: - app/api/mock/oversikt/route.ts — Speglar hamtaNyckeltal. - app/api/mock/arenden/route.ts — Speglar hamtaArenden. - app/api/mock/kalender/route.ts — Speglar hamtaDagen. - app/api/mock/ledighet/route.ts — Speglar hamtaLedighet. - app/api/mock/kompetens/route.ts — Speglar hamtaRegister: hela registret för chef, den egna kompetensen för medarbetare. - app/api/mock/kompetens/[id]/plan/bekrafta/route.ts — POST: chefen bekräftar en plan. 400/404/409 från bekraftaPlan. - app/api/mock/kompetens/[id]/innehav/route.ts — POST: chefen registrerar ett intyg. Giltigheten räknas på servern. - app/api/health/route.ts — { ok: true }. kontrakt: Formen är alltid densamma: ``` import { hamtaNagot } from "@startsidan/modul-"; import { getCurrentUser, toContext } from "../../../../shell"; export async function GET() { const user = await getCurrentUser(); return Response.json(await hamtaNagot(toContext(user))); } ``` Att modulen exporterar sin datafunktion i index.ts är förutsättningen för spegeln. Utan den exporten finns ingen endpoint att skriva. beroenden: skal, oversikt, arenden, kalender, ledighet, kompetens beslut: ADR-006 kvarstår: - Ingen av endpointerna konsumeras av appen. De är dokumentation i körbar form. - Ingen felhantering eller statuskod utöver 200 – de speglar bara den lyckade formen. ## POST /api/flows/felanmalan id: api-felanmalan namn: POST /api/flows/felanmalan sammanfattning: Samtalsflödets enda serverända: äger prompten, validerar turen och fattar beslutet om ärendet får skapas. uppdaterad: 2026-08-30 ansvar: - Hämta identiteten ur sessionen – aldrig ur klientens payload. - Hålla takgränsen: 40 anrop per användare och femminutersfönster. - Validera inkommande med zod och sanera fältvärden innan de kan bli ett ärende. - Välja regelmotor eller modell utifrån AI_PROVIDER, med samma kontrakt åt båda hållen. - Avgöra själv om allt obligatoriskt finns. Att modellen sätter create räcker aldrig. filer: - app/api/flows/felanmalan/route.ts — POST-handlern: takgräns, validering, förankring, turkörning, beslut. - ai/faltsignatur.ts — HMAC över fältvärdena, så att klientens förankring bevisligen är serverns egen (ADR-013). - ai/faltsignatur.test.ts — Tester på signaturen, inklusive att ett tillagt fält avvisas. kontrakt: Ordningen är inte utbytbar: ``` const user = await getCurrentUser(); // identitet från servern if (!inomTakgrans(user.userId)) return 429; // takgräns före arbete const rad = Body.safeParse(await request.json()); // zod på inkommande const varden = sanera(tur.varden); // styrtecken bort, 500 tecken farSkapas(flow, tur.create, varden, antalSvar); // serverns beslut ``` - Meddelandehistoriken är begränsad till 60 meddelanden och 2000 tecken per meddelande. - Turkontraktet garanteras av structured outputs, inte av en instruktion i prompten. - Ett omtag vid ogiltigt svar, inte en loop – latensen hålls nere och kostnaden förutsägbar. - Klienten skickar tillbaka fälten så att assistenten kan återge dem ordagrant, men de når prompten bara om serverns egen signatur stämmer (ADR-013). Utan den kontrollen vore fältvärden en väg att mata in uppgifter förbi samtalet. - Förankringen påverkar aldrig beslutet: allaObligatoriskaIfyllda och farSkapas läser modellens svar, inte klientens. beroenden: felanmalan, ai-adapter, module-sdk, skal beslut: ADR-013 kvarstår: - Takgränsen ligger i minnet och överlever inte omstart eller flera instanser. Flyttar till Redis i fas 4. - Ingen ärendeadapter: beslutet att skapa fattas, men ingenting skapas (M3). - Signaturnyckeln lever i processen. Vid omstart eller flera instanser faller förankringen och fälten kan formuleras om igen – M3:s sessionsdata gör den överflödig. ## min-ai id: min-ai namn: min-ai sammanfattning: Ger varje medarbetare en personlig AI-assistent – en egen ChatGPT – med samtal som bara syns för ägaren, och verktyg, skills och MCP-tjänster som användaren slår på och av. I demon svarar Anthropic API eller regelmotorn; i drift Eneo. uppdaterad: 2026-10-07 ansvar: - Visa samtalslistan grupperad efter tid och samtalet över hela arbetsytan, strömma svaret medan det skrivs, med stopp – och ge en ingång på startsidan: knappen ✦ Min AI bredvid hälsningen. - Hålla samtalen och användarens val på servern per användare – aldrig lämna ut något till någon annan än ägaren. - Tolka modellens Markdown till typade block, så att inget svar kan bli HTML. - Hålla kontraktet likt Eneos sessions-API, så att bytet till Eneo blir en adapterfråga. - Äga funktionskatalogen – verktyg, skills, MCP-tjänster – och de egna verktygen med strict-schema mot modellen och zod på servern. - Visa det verktygen skapat: grafer i diagramtokens med tabellvy, bilder som SVG i , dokument att ladda ner som Word – och användarens uppgifter, som verktyget Mina uppgifter läser ur den samlade listan i samma stund som frågan ställs, som rader med länk. filer: - modules/min-ai/src/manifest.ts — Navposten Min AI, genvägen bredvid hälsningen och två rutter med mode "full" (ADR-025). Båda rollerna. - modules/min-ai/src/typer.ts — MinAiFraga, Utbyte, Samtal och Samtalspost i Eneos form, plus ENDPOINT, VAG, SESSION_HUVUD, MAX_FRAGA och namnFran. - modules/min-ai/src/data/index.ts — Samtalslagret och valen per användare, server-only, på globalThis så att route handlers och sidor ser samma. mcpTjanster ur MIN_AI_MCP, funktionskatalog, aktivaFunktioner, sattVal, funktionslage, hittaBilaga. - modules/min-ai/src/motor/index.ts — systemprompt, historik (tidigare utbyten plus frågan, högst 20), mockSvar och bitar för regelmotorns strömning, och arUppgiftsfraga, urvalUrFraga, URVALSNAMN och uppgiftssvar. - modules/min-ai/src/funktioner.ts — Katalogen: GRUPPER, INBYGGDA (webbsök, skapa bild, skapa dokument, skapa graf), PLANERADE_MCP, arPa och FunktionsLage – det klienten ser. - modules/min-ai/src/verktyg.ts — VERKTYG med strict JSON-scheman, GrafSchema, DokumentSchema, BildSchema, UppgiftsfragaSchema och rensaSvg. - app/../ai/uppgifter.ts — lasUppgifter och uppgiftsresultat: listan för den inloggade ur uppgifterFor, och det modellen får tillbaka. Delas med AI-fältet. - modules/min-ai/test/uppgifter.test.ts — Verktyget, prompten, urvalet ur frågan och regelmotorns svar. - app/../ai/uppgifter.test.ts — Att en ny aktivitet syns direkt och blir hanterad, och att bara den inloggades uppgifter läses. - modules/min-ai/src/dokument/docx.ts — byggDocx, zip, crc32, xmlText och filnamn – Word utan beroenden. - modules/min-ai/src/components/Funktionsmeny.tsx — Knappen Verktyg, panelen med role=switch per funktion och chipen för det som är på. - modules/min-ai/src/components/Bilagor.tsx — Bilagor och Kallor: graf, bild som ur data-adress, dokument med nedladdning när svaret är sparat. - modules/min-ai/src/components/Graf.tsx — Stapel- och linjediagram i --color-graf-1…4: legend, direktetiketter, hovring, tabellvy. skala och slutlagen. - modules/min-ai/src/markdown.ts — tolkaMarkdown och tolkaSpann: rubrik, stycke, listor, citat, kodblock, fetstil, kursiv, kod och http(s)-länkar. - modules/min-ai/src/datum.ts — kortDatum, tidsgrupp och grupperaEfterTid – i svensk tid, så att servern och webbläsaren ger samma. - modules/min-ai/src/components/MinAiVy.tsx — Modulvyn: chatten, utan rubrik – vyn är ytan. Läser listan och samtalet på servern och ger chatten serverns klocka. - modules/min-ai/src/components/MinAiChatt.tsx — Klientkomponent över hela ytan: samtalslistan längs vänsterkanten (en låda på smal skärm), samtalet i en läsbar spalt, strömningen, stopp, kopiera och radera. Skickar ?fraga= ur adressen en gång. - modules/min-ai/src/components/Markdown.tsx — Blocken som JSX. Länkar öppnas i ny flik med ↗. - app/api/min-ai/route.ts — POST: takgräns, zod, ägarkontroll, motor efter AI_PROVIDER, strömning och sparande. Stopp avbryter modellanropet. - app/api/min-ai/samtal/[id]/route.ts — DELETE: raderar ett av användarens samtal, 404 annars. - app/api/min-ai/funktioner/route.ts — GET: katalogen med användarens läge. PUT { id, pa }: sparar ett val, 409 för det som inte går. - app/api/min-ai/bilagor/[id]/route.ts — GET: ett dokument som Word-fil, bara för samtalets ägare. api: - POST /api/min-ai: { question: string (1–8000), session_id?: string } → text/plain-ström + X-Session-Id Ny fråga i ett nytt eller befintligt samtal. 400 vid ogiltig fråga, 404 för ett samtal som inte är användarens, 429 över takgränsen. - DELETE /api/min-ai/samtal/:id: → 204 | 404 Raderar samtalet om det är användarens. - GET /api/min-ai/funktioner: → { funktioner: FunktionsLage[] } Katalogen med användarens läge. - PUT /api/min-ai/funktioner: { id, pa: boolean } → { funktioner } | 400 | 409 Slår på eller av. 409 för en funktion som inte finns eller inte är tillgänglig. - GET /api/min-ai/bilagor/:id: → .docx | 404 Bygger Word-dokumentet ur bilagans data. 404 för någon annans. - listaSamtal: (userId: string) => Samtalspost[] Användarens samtal utan innehåll, senast ändrade först. - hamtaSamtal: (userId, id) => Samtal | undefined Ett samtal, bara om det tillhör användaren. - aktivaFunktioner: (userId: string) => Funktion[] Det som är på och tillgängligt för användaren. Route handlern ger modellen verktyg ur den och inget annat. - sattVal: (userId, id, pa) => boolean Sparar ett val. Falskt för en funktion som inte finns, eller som inte är tillgänglig och ska slås på. - byggDocx: (d: DokumentData) => Uint8Array Ett Word-dokument med formatmallarna Title, Ingress, Heading1 och Punkt. - rensaSvg: (svg: string) => string | null En fristående SVG utan skript, händelseattribut, objekt eller länkar ut – annars null. Lägger till xmlns. - tolkaMarkdown: (text: string) => MdBlock[] Markdown-urvalet till block. Ett oavslutat kodblock tar resten, så att ett svar som strömmar ritas rätt. kontrakt: Kontraktet, och Eneos motsvarighet: ``` startsidan POST /api/min-ai { question, session_id? } ← text/plain-ström, X-Session-Id: Eneo POST /assistants/{id}/sessions/ { question, session_id?, stream } ← { session_id, answer, references } (via WSO2: Authorization: Bearer , api-key: ) Samtal = { id, name, created_at, updated_at, messages: Utbyte[] } Utbyte = { id, question, answer, created_at, avbrutet?, bilagor?, kallor? } Händelser i strömmen, en JSON per rad: { typ: "text", text } svaret, i bitar { typ: "status", text } "Söker på webben …", "Ritar grafen …" { typ: "bilaga", bilaga } { typ: "graf" | "bild" | "dokument" | "uppgifter", … } { typ: "kallor", kallor: [{ titel, url }] } ``` Funktion | Grupp | Hur | Standard Webbsök | Verktyg | Anthropics web_search_20260209, källorna under svaret | på Mina uppgifter | Verktyg | Eget verktyg mina_uppgifter { urval: oppen | hanterad | alla }: läser uppgifterFor för den inloggade och visar listan i samtalet | på Skapa bild | Verktyg | Eget verktyg skapa_bild: SVG, kontrollerad av rensaSvg, visad som | på Skapa dokument | Skills | Eget verktyg skapa_dokument: DokumentData → .docx vid nedladdning | på Skapa graf | Skills | Eget verktyg skapa_graf: GrafData, högst fyra serier, ritad i klienten | på MCP-tjänst ur MIN_AI_MCP | MCP-tjänster | Anthropics MCP-connector, adress och nyckel på servern | av Intranätet, Mina ärenden, Kalender och e-post | MCP-tjänster | Planerade – kräver användarens identitet (fas 4) | går inte att slå på - Klienten skickar bara frågan och samtalets id. Historiken byggs på servern ur samtalet. - Svaret är fri text och renderas som block ur tolkaMarkdown – aldrig som HTML (AM12). Svaret märks med AiChip. - Min AI är inte AI-fältet: den känner inte startsidans funktioner och säger det om frågan kräver det. Undantaget är användarens uppgifter, som verktyget Mina uppgifter läser. - Uppgifterna läses på nytt vid varje fråga om dem – prompten förbjuder svar ur en tidigare lista i samtalet, och en lista i historiken är märkt som möjligen inaktuell. Vems uppgifter det gäller kommer ur sessionen; verktygsanropet väljer bara urvalet. - Uppgifternas text kan vara skriven av andra – en aktivitet i dialogstödet skrivs av chefens chef – och går till modellen som JSON märkt som data, inte instruktioner. - Demonotisen längst ned i samtalslistan säger att svaren kommer från en språkmodell via servern och ber användaren att inte skriva personuppgifter. - Vyn tar hela arbetsytan (mode "full", ADR-025). Svaret är löptext märkt med AiChip, frågan en ljusblå bubbla. Spalten är högst 78rem bred, så att ytan till höger kan bära ett dokument som skrivs. - Samtalslistan grupperas i kalenderdagar i svensk tid: Idag, Igår, Senaste 7 dagarna, Senaste 30 dagarna och sedan per månad, med år om det inte är i år – samma indelning som de stora chattjänsterna. Grupperingen utgår från serverns klocka, så att hydreringen inte skiljer sig. Varje grupp är en role=group med rubrik. - Verktygen följer användarens sparade val. Klienten kan inte ge modellen ett verktyg genom att be om det i anropet. - Ett avvisat verktygsanrop går tillbaka till modellen som is_error med zods fel, så att den kan rätta sig. beroenden: module-sdk, ui, ai-adapter beslut: ADR-023, ADR-024, ADR-025, ADR-019, ADR-006 kvarstår: - Motorn är Anthropic API eller regelmotorn. Eneo-adaptern är inte byggd: det är inte utrett om en användares personliga assistent i Eneo går att nå via API med användarens identitet, eller om det behöver byggas i Eneo. - Ett personligt utrymme i Eneo kräver användarens riktiga identitet – SAML-sessionen i fas 4. I demon är identiteten rollkakans demoanvändare. - Samtalen ligger i serverns minne och tappas vid omstart. Med flera instanser ser varje instans bara sina egna. - Användaren kan inte ladda upp filer. Eneos references visas inte än – webbsökets källor gör. - Skapa bild ger vektorgrafik, inte fotografier: Anthropic har ingen bildmodell. En bildmodell i Eneo behövs för riktiga bilder. - MCP-connectorn är inte prövad mot en riktig tjänst – ingen är ansluten i demon. Tjänsten nås från Anthropics servrar, så en tjänst på kommunens interna nät kan inte anslutas den vägen. - Valen ligger i serverns minne och tappas vid omstart, som samtalen. - Dokumentet är enkelt: rubriker, stycken och punktlistor. Inga tabeller, bilder eller kommunens mall. - Ingen återkoppling på svaren. Eneo tar emot den på /assistants/{id}/sessions/{session_id}/feedback/. - Dokument som modellen skriver visas än så länge som bilaga i svaret. Nästa steg: en panel till höger om samtalet där dokumentet växer medan det skrivs. ## POST /api/min-ai id: api-min-ai namn: POST /api/min-ai sammanfattning: Min AI:s serverända: tar frågan, bygger historiken ur användarens samtal, låter motorn svara och strömmar svaret medan det sparas. uppdaterad: 2026-10-06 ansvar: - Hämta identiteten ur sessionen – aldrig ur klientens payload – och hålla takgränsen: 40 anrop per användare och femminutersfönster. - Validera frågan med zod och bara fortsätta ett samtal som tillhör användaren. - Välja regelmotor eller modell utifrån AI_PROVIDER, med samma kontrakt åt båda hållen. - Avbryta modellanropet när användaren stoppar och spara det som hann skrivas som stoppat; ta bort ett nytt samtal som inte fick något svar. - Ge modellen verktyg efter användarens sparade val, köra verktygsloopen och validera varje verktygsanrop innan något skapas. - Spara och lämna ut användarens val av funktioner, och dokumenten verktygen skapat – bara till ägaren. filer: - app/api/min-ai/route.ts — POST: takgräns, validering, ägarkontroll, motor, ReadableStream, sparande. - app/api/min-ai/samtal/[id]/route.ts — DELETE: radera ett av användarens samtal. kontrakt: Ordningen: ``` const user = await getCurrentUser(); // identitet från servern if (!inomTakgrans(user.userId)) return 429; // takgräns före arbete const rad = Body.safeParse(await request.json()); // { question 1–8000, session_id? } const samtal = session_id ? hamtaSamtal(user.userId, session_id) // bara ägarens – annars 404 : nyttSamtal(user.userId, question); const meddelanden = historik(samtal.messages, question); // byggs här, aldrig av klienten return new Response(strom, { headers: { "X-Session-Id": samtal.id } }); ``` - Svaret är application/x-ndjson – en händelse per rad – med Cache-Control: no-store. Ett fel före första raden ger JSON med fel och statuskod; ett fel mitt i bryter strömmen. - Formen följer Eneos POST /assistants/{id}/sessions/, så att Eneo-adaptern blir en ny gren här och inte en ny klient. - Bakom åtkomstkoden (ADR-012) som allt annat under /api utom /api/health. beroenden: min-ai, ai-adapter, module-sdk, skal beslut: ADR-023, ADR-024 kvarstår: - Takgränsen och samtalen ligger i minnet och överlever inte omstart eller flera instanser. Flyttar till Redis eller Eneo i fas 4. - Ingen Eneo-gren ännu: AI_PROVIDER har bara mock och anthropic. - Verktygsloopen har ett tak på åtta varv. Når den taket slutar svaret där, utan förklaring. ## POST /api/assistant id: api-assistant namn: POST /api/assistant sammanfattning: AI-fältets serverände: ger ett kort svar med knappar till rätt ställe på startsidan, och lämnar det som kräver ett samtal till Min AI. uppdaterad: 2026-10-20 ansvar: - Hämta identiteten ur sessionen och hålla takgränsen, som Min AI. - Låta en moduls kända fråga (ai.intents) leda till sitt flöde utan modell – snabbt, gratis och förutsägbart. - Ge modellen målen användaren når och användarens uppgifter, lästa ur tjänsterna i samma stund, som data. - Hämta svaret med structured outputs där knapparnas mål är ett enum, och pröva svaret igen innan det lämnas ut: texten kortad, högst tre knappar, uppgifterna ur serverns egen läsning. - Lämna en allmän fråga till Min AI med frågan ifylld (?fraga=), och svara med regelmotorn när AI_PROVIDER inte är anthropic. filer: - app/api/assistant/route.ts — POST: takgräns, zod, avsikt, regelmotor eller modell. - app/../ai/assistent.ts — malFor, svaraAvsikt, svarsschema, systemprompt, byggSvar och regelmotorSvar. - app/../ai/assistent-typer.ts — AssistentSvar och Knapp – kontraktet som AskField läser. - shell/AskField.tsx — Klienten: frågan, förslagen som ställer sin fråga, och svaret under fältet med märke, källrad, text, upp till fem uppgifter och knappar. - app/../ai/assistent.test.ts — Målen, avsikten, schemat som bara tar målen, serverns prövning och regelmotorn. api: - POST /api/assistant: { question: string (1–500) } → AssistentSvar | { fel } Ett kort svar. 400 vid ogiltig fråga, 429 över takgränsen, 502 om modellen inte svarar. - malFor: (modules, ctx) => Mal[] Menyposter, sidornas ingångar och flöden med en riktig väg, rollfiltrerade. Min AI ingår inte – den har en egen knapp. - svarsschema: (mal: Mal[]) => ZodObject { text, knappar: [{ mal: enum, etikett }], uppgifter: urval | null, minAi }. kontrakt: Svaret: ``` interface AssistentSvar { kalla: "avsikt" | "modell" | "regelmotor"; // märket: ✦ Genväg, ✦ AI-genererat, ✦ Regelmotor text: string; // högst tre meningar kallrad: string; // vad svaret bygger på knappar: { label; href; variant }[]; // ur malFor, plus "Fortsätt i Min AI" uppgifter?: UppgiftsData; // när frågan gällde uppgifterna } ``` - Bara modellens svar märks ✦ AI-genererat. En känd fråga är en genväg och regelmotorn säger att den är en regelmotor. - Svaret läses upp för skärmläsare (aria-live) och stängs med en knapp. Rutan är porterad ur V3:s .ai-answer. - AI-fältet är inte en chatt: det minns inget mellan frågorna. Ett samtal fortsätter i Min AI. beroenden: skal, ai-adapter, agera, min-ai, module-sdk beslut: ADR-026, ADR-023 kvarstår: - Modellen ser målens namn, inte vad de innehåller – en fråga om semester kan få en knapp till närmaste namn. - Felanmälan och utvecklingsärende läser inte frågan som följer med (?fraga=) än. - Avsikter av typen answer har ingen hanterare; bara navigate används. - Svaret visas inte i en webbläsare i testerna – serverändan och kontraktet är testade. ## AI-adaptern id: ai-adapter namn: AI-adaptern sammanfattning: Kapslar in modellanropet så att turkontraktet garanteras av API:et och providern kan bytas utan kodändring. uppdaterad: 2026-08-28 ansvar: - Anropa modellen med structured outputs byggda ur flödesdefinitionen. - Validera svaret mot samma zod-schema och göra ett omtag om formatet brister. - Hålla nyckeln på servern – server-only är importerad överst av det skälet. - Falla tillbaka på regelmotorn när nyckel eller provider saknas. filer: - app/../ai/anthropic.ts — koraAnthropicTur och anthropicArKonfigurerad. Importerar server-only. - app/../ai/ratelimit.ts — inomTakgrans: 40 anrop per användare per 5 minuter, i minnet. - app/../ai/chatt.ts — strommaChatt: fri chatt med verktygsloop, webbsök och MCP-connector via beta-API:t, effort low, avbrytbar via signal. Lägger till en rad vid refusal eller max_tokens. api: - koraAnthropicTur: ({ flow, meddelanden, profil, faltHittills }) => Promise En tur enligt kontraktet. Kastar om modellen inte svarar enligt formatet efter ett omtag. - anthropicArKonfigurerad: () => boolean Om ANTHROPIC_API_KEY finns. Avgör om providern kan användas. - inomTakgrans: (userId: string, nu?: number) => boolean Takgränsen. Nollställs per fönster. - strommaChatt: ({ system, meddelanden, signal, webbsok?, egna?, mcp?, utfor? }) => AsyncGenerator Svaret som händelser medan det skrivs: text, status och källor. Kör verktygsloopen (högst åtta varv, fortsätter efter pause_turn), ger webbsök, egna verktyg med strict och MCP-connectorn, och låter utfor validera och utföra egna verktyg. kontrakt: Miljövariabel | Värden | Betydelse AI_PROVIDER | mock | anthropic | Standard mock. anthropic kräver dessutom att nyckeln finns. AI_MODELL | modell-id | Standard claude-sonnet-5. ANTHROPIC_API_KEY | hemlighet | Bara på servern, satt som Environment i Dokploy. beroenden: module-sdk, anthropic beslut: inga kvarstår: - Bara en provider. Eneo via WSO2 blir ett tredje värde när det är dags. - Ingen loggning av turer, och ingen kostnadsmätning. ## @startsidan/flow-engine id: flow-engine namn: @startsidan/flow-engine sammanfattning: Planerad: samtalsflödesmotorn som lyfts ur felanmalan när ett andra flöde kan definieras som data. uppdaterad: 2026-08-28 ansvar: - Ta emot en FlowDefinition och köra den – tur för tur, med formulärspegel och turkontrakt genererade ur definitionen. - Vara oberoende av vilket flöde som körs. Felanmälans särdrag får inte koda in sig i motorn. - Behålla serverns beslutsrätt: motorn föreslår, route handlern avgör. filer: - packages/flow-engine/ — Finns inte ännu. Koden bor tills vidare i modules/felanmalan och module-sdk. - packages/module-sdk/src/flow.ts — Flödestyperna som ska flytta hit: FlowDefinition, buildTurnSchema, farSkapas. - packages/modules/felanmalan/src/motor/ — Regelmotorn som ska generaliseras. kontrakt: Acceptanskriteriet för M4 är kontraktet: ett andra flöde – "Beställ behörighet" – ska kunna definieras på under en timme utan ny UI-kod. Klarar motorn inte det är den inte redo att lyftas, och då ska den stanna i felanmalan. - Skriv nya flöden som FlowDefinition redan nu. Då blir M4 en flytt i stället för en omskrivning. - ADR-005 skrivs när motorn lyfts, inte innan – kontraktet ska bevisas av två flöden först. beroenden: module-sdk, felanmalan beslut: ADR-005 kvarstår: - Hela paketet. Ingenting är byggt – doket finns för att beskriva vad som ska bli, och vad som avgör om det får bli. - Bara ett flöde existerar, så generaliseringen är obevisad. - ADR-005 är inte skriven. ## Grindar och bygge id: bygge namn: Grindar och bygge sammanfattning: Grindarna och paketeringen: lint, test och typecheck över hela repot, och en container per applikation som Dokploy kan bygga (ADR-040). uppdaterad: 2026-09-02 ansvar: - Linta och testa hela monorepot från roten, så att ett nytt paket täcks utan att någon lägger till ett skript. - Typkontrollera varje paket topologiskt. - Bygga en standalone-image per app – projektportalen och startsideportalen – och köra dem rotlöst. - Ge Dokploy en compose-fil med två tjänster, var sin healthcheck och Traefik-regler som delar trafiken på /portal. - Hålla apparna isär: ingen fil i den ena importerar från den andra, och det gemensamma bor i packages/. filer: - eslint.config.mjs — Lintkonfiguration för hela repot. no-html-link-for-pages avstängd: App Router har ingen pages-katalog. - eslint-rules/inga-hardkodade-designvarden.mjs — Egen lintregel: R4 som grind. Hex-färger och px i hakparentes i className. shadcn-grunden undantagen. - vitest.config.mts — Tester plockas upp på filnamn: packages/**/test/**/*.test.ts. passWithNoTests. - package.json — Rotskripten: dev (båda apparna), build (båda), start, lint, test, typecheck. - apps/projekt/next.config.ts — Projektportalen: standalone, permanenta omledningar från startsideportalens gamla vägar till /portal, och i utveckling en omskrivning av /portal till startsideportalens server på 3010. - apps/portal/next.config.ts — Startsideportalen: basePath /portal, NEXT_PUBLIC_BASE_PATH för medBas, standalone och modulerna i transpilePackages. - apps/projekt/Dockerfile — Multi-stage mot Next standalone, med datavolymens ENV. Kör som icke-root. - apps/portal/Dockerfile — Samma bygge för startsideportalen, utan datavolym. - docker-compose.yml — Det Dokploy pekar på: start-web (projektportalen, med volymen) och start-portal (Host && PathPrefix(/portal)), Traefik-etiketter, dokploy-network och var sin healthcheck. - apps/projekt/src/miljo.test.ts — Vaktar att varje process.env.X i projektportalen står under start-web. - apps/portal/src/miljo.test.ts — Vaktar att varje process.env.X i startsideportalen och modulerna står under start-portal. kontrakt: - Ett nytt modulpaket måste läggas i transpilePackages i apps/portal/next.config.ts, annars faller bygget. - En import mellan apparna fäller lint (no-restricted-imports). Det som båda behöver läggs i packages/ai eller packages/gemensamt. - Modulpaket behöver varken lint- eller test-skript. Roten täcker dem. - Modulpaketen kör noUncheckedIndexedAccess och verbatimModuleSyntax – typecheck är den grind som ser det. - Grön logg betyder granskad kod först sedan grindarna flyttades till roten. Tidigare lintades bara appen (dåvarande apps/web). Två fällor i Dokploy-uppsättningen, båda upptäckta vid första deployen och båda rättade. De ser likadana ut utifrån – sidan svarar inte – men har olika orsak. Symptom | Orsak | Rättning 404 från Traefik på domänen, och TRAEFIK DEFAULT CERT i stället för Let's Encrypt | En Compose-tjänst får inga Traefik-etiketter automatiskt – det gör bara Dokploys Application-typ. Utan router finns inget att utfärda certifikat för. | Etiketter och dokploy-network deklareras i docker-compose.yml. Containern blir aldrig frisk: wget: can't connect to remote host | Healthchecken pekade på localhost. Busybox wget provar ::1 först och Next lyssnar bara på IPv4. | 127.0.0.1 i healthchecken, plus start_period så uppstarten inte räknas som fel. Allt fungerar, men AI-svaren är regelmotorns: svar på under en sekund och fält som ekar användarens ord ordagrant | En variabel som sätts i Dokploy men inte listas under environment i compose-filen når aldrig containern. Dokploys Environment fyller bara compose-interpolationen. ANTHROPIC_API_KEY saknades i listan, och adaptern föll tyst tillbaka på mock. | Varje process.env.X i koden har en rad under environment. Route handlern varnar dessutom en gång i loggen när providern är vald men nyckeln saknas. COPY failed: stat app/apps/web/public | Tom katalog följer inte med i en git-klon. | apps/web/public/.gitkeep (i dag apps/portal/public/.gitkeep). - Domänen sätts med DOMAIN i miljön; standard är start.sundsvall.dev. - Lokalt kräver docker compose up att nätverket finns: docker network create dokploy-network. På servern skapar Dokploy det. - Kontrollera alltid hälsan, inte bara att containern kör: docker inspect $(docker compose ps -q start-web) --format '{{.State.Health.Status}}'. - Lägger du till en miljövariabel i koden måste den läggas till på tre ställen: .env.example, environment i docker-compose.yml, och Dokploys Environment. Glöms den mittersta faller appen tillbaka på sitt standardvärde utan att något syns. - Tjänsten heter start-web, inte web. Prefixet skiljer stacken från andra på samma värd, och gäller även framtida start-bff, start-redis och start-db. - Vid isolerad deploy ligger start-web i två nätverk. traefik.docker.network=dokploy-network talar om vilket Traefik ska ansluta över; utan den kan den välja stackens interna IP och få timeout. Övriga tjänster ska aldrig läggas i dokploy-network. beroenden: skal, dokploy beslut: ADR-010 kvarstår: - Ingen CI. Grindarna körs manuellt före commit. - Inget Playwright-röktest – det är fas 5. - Ingen kontroll av att katalog och dokumentation är kompletta. Täckningen syns bara på /dev/docs. ## utvecklingsarende id: utvecklingsarende namn: utvecklingsarende sammanfattning: Beslutsunderlag för utvecklingsinitiativ: fjorton avsnitt enligt kommunens mall, där fyra är typade radlistor med summeringar i stället för fritext. uppdaterad: 2026-09-04 ansvar: - Äga datakontraktet för ett utvecklingsärende: fjorton avsnitt, sju avsnittstyper, inga fria strängar där en union räcker. - Hålla beräkningarna som rena funktioner, så att vyn och servern får samma summor. - Härleda kostnadsraderna ur resursbehovet, så att dokumentet inte säger två olika saker om samma sak. - Äga fokusrutten /flode/utvecklingsarende och hela vyn bakom den. - Bidra med rader i "Att agera på" för underlag som väntar på granskning eller beslut. - Aldrig lämna ett ärende för beslut själv – servern avgör. filer: - modules/utvecklingsarende/src/data/types.ts — Datakontraktet: avsnittstyper, radtyper, Material, Utvecklingsarende. - modules/utvecklingsarende/src/data/mock.ts — Exempelärendet ur designfacit. Siffrorna är facit för berakning.test.ts. - modules/utvecklingsarende/src/data/index.ts — hamtaUtvecklingsarenden(ctx) – enda ingången till datat. - modules/utvecklingsarende/src/berakning.ts — summorKostnader, summorNyttor, summorResurser, synkaHarledda. - modules/utvecklingsarende/src/manifest.ts — routes med mode focus, nav, slots.actions och ai.intents. - modules/utvecklingsarende/src/components/UtvecklingsarendeFlode.tsx — Fyra steg i en vy. Stegraden ritas här, inte i skalets FlowBar. - modules/utvecklingsarende/src/components/Underlag.tsx — Steg 1: fälten och uppladdningen med sina gränser. - modules/utvecklingsarende/src/components/Avsnittskort.tsx — Steg 3: ett avsnitt som kort, med rätt editor för sin typ. - modules/utvecklingsarende/src/components/Assistent.tsx — Samtalet om det avsnitt användaren står i. Flödets enda AI-yta. - modules/utvecklingsarende/src/components/Forhandsvisning.tsx — Steg 4: mallens form. Ren presentationskomponent utan state. - modules/utvecklingsarende/src/components/Kontrollista.tsx — Steg 4: status per rubrik, bekräftelsedialog och inlämning. - modules/utvecklingsarende/src/store.ts — Zustand: steg, ärende, samtal och anropen mot serverändarna. - modules/utvecklingsarende/src/flow.ts — MALL som DokumentflodeDefinition, KARNAVSNITT, luckor och farLamnas. - modules/utvecklingsarende/src/schema.ts — Utkast- och turschema byggda ur MALL, plus byggAvsnitt. - modules/utvecklingsarende/src/prompt.ts — Systemprompten ur definitionen. Serverns summor följer med turprompten. - modules/utvecklingsarende/src/motor/mock.ts — Regelmotorn bakom AI_PROVIDER=mock. Samma kontrakt som modellen. - modules/utvecklingsarende/src/flode.ts — Serversidans ingång: definition, prompt, schema och motor utan vyn. - modules/utvecklingsarende/test/berakning.test.ts — 11 tester på summorna och härledningen. - modules/utvecklingsarende/test/flow.test.ts — 28 tester på mallen, luckorna, farLamnas och schemat. - modules/utvecklingsarende/test/motor.test.ts — 18 tester på regelmotorn. - modules/utvecklingsarende/test/store.test.ts — 7 tester på redigeringarna, bland annat att en ändrad timkostnad räknar om Kostnader direkt. api: - hamtaUtvecklingsarenden: (ctx: UserContext) => Promise Rollfiltrerade underlag. Mockade – det finns ingen lagring förrän fas 4. - summorKostnader / summorNyttor / summorResurser: (avsnitt) => summor Rena funktioner över datakontraktet. Treårsfönstret är mallens. - synkaHarledda: (kostnader, resurser) => Kostnadsrad[] Räknar om de kostnadsrader som bär harleddFran ur resursbehovet. - MALL: DokumentflodeDefinition Flödet som data. Prompt, schema och förhandsvisning genereras ur den. - farLamnas: (arende, modellenForeslar, anvandarenHarBekraftat) => boolean Serverns beslut. Tre villkor samtidigt; modellens förslag räcker aldrig. - utkastSchema / turSchema: () => ZodObject Structured outputs-kontraktet, byggt ur MALL i stället för skrivet för hand. - parsaArende: (varde: unknown) => Utvecklingsarende | null Validerar dokumentet klienten skickar. Enda stället formen konverteras. - koraTur / spelaUppUtkast: regelmotorn Deterministisk tur för offline-demo. Samma kontrakt som modellen. - useUtvecklingsarende: zustand store Flödets tillstånd i klienten. Ingen prompt och ingen nyckel bor här. kontrakt: - Ett avsnittsvärde är ett objekt eller en radlista, aldrig en sträng. Därför passar det inte i FlowDefinition (ADR-015). - Avsnittsstatus är tom, utkast, granskad eller last. Allt som står som utkast bär AiChip tills användaren granskat det. - Nettonyttan är tre års nytta minus tre års kostnad: engångskostnaden en gång, den löpande tre. - Kostnadsrader med harleddFran ägs av resursbehovet och skrivs aldrig för hand. - Actions-raden kommer ur datafunktionen och visar mockade underlag. Utkastet som står öppet i en flik känner servern inte till. - Allt modellen skrivit bär AiChip tills användaren markerat avsnittet som granskat, och märket följer med in i förhandsvisningen och inlämningen. beroenden: module-sdk, ui, api-mock, api-utvecklingsarende, agera beslut: ADR-015, ADR-006 kvarstår: - Ingen lagring: ärendet lever i klientens store under sessionen och nollställs när flödet öppnas. Spara utkast finns inte, och verkliga utkast i actions-raden kräver fas 4. - Förslagschipet ger AI-fältets knapp till flödet, men flödet läser inte frågan som följer med (?fraga=) än. - Word-export hör hemma i BFF:en. Knappen finns men är avstängd. - Beslutsavsnittet fylls av beslutsfattaren – den vyn finns inte. - Utkastet tar drygt en minut mot modellen och skrivs i ett anrop. Steg 2 visar väntetiden, inte ett påhittat förlopp – och avbryts av sig självt efter tre minuter. ## POST /api/flows/utvecklingsarende/* id: api-utvecklingsarende namn: POST /api/flows/utvecklingsarende/* sammanfattning: Dokumentflödets serverändar: en som skriver hela utkastet ur uppladdat material, en som ändrar ett avsnitt i samtalet. uppdaterad: 2026-09-04 ansvar: - Äga prompten, profilen och beslutet att lämna ärendet för beslut. - Läsa uppladdat underlag på servern: docx, pptx och xlsx blir text, PDF skickas som dokumentblock, och båda vägarna går i samma anrop. - Garantera svarets form med structured outputs ur ett zod-schema byggt ur flödesdefinitionen. - Validera hela dokumentet mot arendeSchema innan något av det når prompten. filer: - app/api/flows/utvecklingsarende/utkast/route.ts — POST: underlag och filer in, ett komplett utkast ut. - app/api/flows/utvecklingsarende/tur/route.ts — POST: en samtalstur mot ett avsnitt. Dokumentet valideras först. - app/../ai/strukturerat.ts — koraStrukturerat: det generiska modellanropet båda flödena delar. - app/../ai/underlag.ts — Uppladdat material till text eller dokumentblock. Filgränser och styrteckensanering. - app/../ai/ooxml.ts — Textuttag ur docx, pptx och xlsx: zip-läsare och XML-plockning, utan beroenden. - app/../ai/ooxml.test.ts — 10 tester mot zip-filer som byggs i testet – både lagrade och deflaterade poster. - modules/utvecklingsarende/src/schema.ts — arendeSchema och parsaArende: dokumentets form, kontrollerad på servern. kontrakt: - Ordningen i handlern: identitet ur sessionen, takgräns, zod, underlag till text, modellanrop, sanering. - Att modellen sätter lamna räcker aldrig. farLamnas kräver alla kravs-avsnitt ifyllda, inget kärnavsnitt tomt och användarens bekräftelse. - Modellen säger lamna, servern svarar lamnad – samma namnbyte som create och created i felanmälan. - Ingen fältförankring. Varje avsnitt går att skriva i direkt, så klientens värden är inmatningen, inte en väg förbi den – ADR-013 gäller flöden där modellens extraktion är enda källan (ADR-015). beroenden: utvecklingsarende, ai-adapter, skal beslut: ADR-015, ADR-013 kvarstår: - Ingen OCR: en inskannad PDF utan textlager läses av modellen som bild, en skannad docx inte alls. Filer som inte gick att läsa räknas upp i olasta. - Zip64 stöds inte i ooxml.ts. Ett Office-dokument måste vara mycket stort för att skrivas så, och 10 MB-taket träffar först. - Takgränsen delas med felanmälan och lever i minnet – den nollställs vid omstart och delas inte mellan instanser. - Filerna lagras inte. Bilagelistan är metadata. - Ingen sessionsdata: att användaren bekräftat inlämning kommer från klienten. Servern kontrollerar allt annat själv, men just den flaggan väntar på M3. --- # Beslutsregister ADR-001 (Antagen) — App Router istället för Pages Router Kommunens starter kör Pages Router; ett nybygge väljer App Router. Server components passar modulkompositionen och det är Next:s huvudspår framåt. fil: docs/beslut/ADR-001-app-router.md ADR-002 (Ersatt) — Egen designgrund istället för @sk-web-gui Formspråket i V3 skulle inte tvingas in i ett bibliotek som bär sin egen design. Ersatt av ADR-008, som behåller slutsatsen men byter grund. fil: docs/beslut/ADR-002-egen-designgrund.md ADR-003 (Antagen) — "Sidor" är en egen sidtyp Redaktionellt innehåll beskrivs av ett tunnare PageManifest vid sidan av ModuleManifest. Två varianter av samma sidtyp: sektionsingång och artikel. fil: docs/beslut/ADR-003-sidor-som-sidtyp.md ADR-004 (Antagen) — routes + fokusläge i ModuleManifest En modul kan äga en helskärmsvy. mode: "focus" ger topprad med Avbryt och inget annat krom. fil: docs/beslut/ADR-004-routes-och-fokuslage.md ADR-005 (Skrivs) — Flödesmotorns kontrakt Skrivs i M4, när motorn lyfts ur felanmalan till packages/flow-engine. fil: docs/beslut/ (ännu inte skriven) ADR-006 (Antagen) — Rollen i cookie; moduler hämtar egen data Demoanvändaren bor i en cookie och läses på servern. Modulerna är server components som hämtar sin egen data i stället för att få den i props. fil: docs/beslut/ADR-006-roll-i-cookie.md ADR-007 (Antagen) — Aggregerande moduler Modulen agera äger ingen egen data utan renderar andras actions. Registret läses via provideRegistry. fil: docs/beslut/ADR-007-aggregerande-moduler.md ADR-008 (Antagen) — shadcn/ui som komponentgrund Komponenterna kopieras in i repot och all färg går genom våra egna CSS-variabler. Radix bär tillgängligheten; primitivernas API är oförändrat. fil: docs/beslut/ADR-008-shadcn-som-komponentgrund.md ADR-009 (Antagen) — Designsystemet som data Systemet skrivs som typad data och renderas som sida, JSON och plattext ur samma källa. Stabila id, uttömmande varianter, felindex i formen fel → rätt → varför. fil: docs/beslut/ADR-009-designsystem-som-data.md ADR-010 (Antagen) — Devportalen som ingång för människa och agent Allt en byggare behöver samlas under /dev, i två vyer ur samma data. Designsystemet blir en del av portalen och nås på /dev/ui. fil: docs/beslut/ADR-010-devportal.md ADR-011 (Antagen) — Dokumentation som leveransvillkor Varje del har ett dok i portalen enligt ett dokumentationskontrakt. Klar betyder körbar, granskad och dokumenterad; täckningen räknas fram på /dev/docs. fil: docs/beslut/ADR-011-dokumentation-som-leveransvillkor.md ADR-012 (Antagen) — Åtkomstkod som demospärr ACCESS_CODE låser demon i middleware. Projektsidan, prototypens ingång, visionssidan och devportalen är öppna, så att dokumentationen går att läsa och hämta av verktyg. Uttryckligen inte autentisering: koden delas av alla och säger ingenting om vem användaren är. fil: docs/beslut/ADR-012-atkomstkod.md ADR-014 (Antagen) — Portalens påståenden verifieras Det portalen påstår om koden kontrolleras av yarn test och yarn lint: drifttester, uppräkningsbara fält där det stod prosa, lintregel för R4 och namngivna antimönster. Går ett påstående inte att kontrollera ska räckvidden stå utskriven. fil: docs/beslut/ADR-014-portalen-verifieras.md ADR-013 (Antagen) — Signerad fältförankring mellan turer Fältens tidigare värden skickas tillbaka så att assistenten inte formulerar om dem, men bara med serverns egen HMAC. Utan signaturen vore de en inmatningsväg förbi samtalet. fil: docs/beslut/ADR-013-signerad-faltforankring.md ADR-015 (Antagen) — Dokumentflöden definieras som data i modulen Ett dokument med typade avsnitt passar inte i FlowDefinition, som beskriver platta fält. Definitionen bor i modulen med samma bärande idéer, och ADR-005 skrivs i M4 mot två former i stället för en. Utan fältförankring: varje avsnitt går att skriva i direkt, så klientens värden är inte en väg förbi samtalet utan själva inmatningen. ADR-013 gäller flöden där modellens extraktion är enda källan. fil: docs/beslut/ADR-015-dokumentflode.md ADR-016 (Antagen) — PageManifest med källoberoende innehåll PageManifest skrivs i module-sdk med parent (hierarkin) och content (statiskt eller en källa). Innehållet är typade block, aldrig HTML. Första källan är Sitevisions headless-API på sundsvall.se, med timeout, cache och inspelad reserv; adaptern bor i modulen webbsidor tills en andra konsument finns. fil: docs/beslut/ADR-016-sidor-med-extern-kalla.md ADR-017 (Antagen) — En Sitevision-webbplats inramad i skalet Simulerar en väg tillbaka till startsidan från Sitevision: webbplatsens meny som rad överst, sidorna i en sandlådad ram med skalet runt. PageLayout får "embed"; skalet äger listan webbplatser och läser källans ramskydd i förväg. En sida visas via REST som jämförelse. Tillägg: exemplen samlas i sektionen Sitevision, och menyraden visas bara inne på webbplatsen. fil: docs/beslut/ADR-017-inramad-webbplats.md ADR-018 (Antagen) — Användaren ordnar korten på startsidan Läget "Anpassa startsidan" med dragning (@dnd-kit) och knappar som alternativ, tillgängligt enligt WCAG 2.2: fokus stannar, flyttar annonseras med plats, reducerad rörelse respekteras, Ångra finns. Ordningen sparas per roll i en kaka som servern läser. Inga kort låses till en spalt: varje kort ligger i en @container och anpassar sig efter sin bredd (R11). Widget får label. fil: docs/beslut/ADR-018-anpassningsbar-startsida.md ADR-019 (Antagen) — Modulvyer med skalets krom och vägparametrar mode "standard" från ADR-004 byggs: modulvyer under /verktyg/ med sidopanel och menyrad. path får parametrar (/verktyg/personal/:id) som vyn får som params. Fokusflöden ligger kvar under /flode/. fil: docs/beslut/ADR-019-modulvyer-med-skalets-krom.md ADR-020 (Antagen) — Notiserna kommer från modulerna och leder till funktioner ModuleManifest.notifications och notificationsFor, som actions i ADR-007. Varje notis har href till funktionen. Klockan är en panel i skalet; läst sparas per roll i en kaka. Notiser räknas fram ur datat, t.ex. ett utvecklingssamtal som väntar på chefens godkännande. fil: docs/beslut/ADR-020-notiser-fran-modulerna.md ADR-021 (Antagen) — Fokusytan under AI-fältet på startsidan slots.fokus lämnar en FokusItem som data; skalet visar den nyaste som inte stängts under AI-fältet, formad som en nyhet med bild, tills användaren stänger den. Stängt sparas per roll i en kaka. Demoknapp i användarmenyn. fil: docs/beslut/ADR-021-fokusyta.md ADR-022 (Antagen) — Ingångens egna kort och hopfällbara block – IT-support som första sida IT-support byggs som första sida enligt ADR-003. Prövningen ger PageManifest.slots (modulens egna kort på en ingång), PageContent.cards (fler redaktionella kort), blocket details (hopfällbar fråga med valfritt chip) och QuickLink.cta. Felanmälans nav-post flyttar till sidans snabbväg. fil: docs/beslut/ADR-022-ingangens-kort-och-block.md ADR-023 (Antagen) — Min AI – en personlig assistent med Eneos sessionskontrakt Min AI är en modul med modulvy, navpost och kort på startsidan. Kontraktet är Eneos ({ question, session_id }), så att motorn kan bytas mot Eneo i route handlern. Samtalen och historiken bor på servern och nås bara av ägaren. Svaret strömmas som text och renderas som typade block, aldrig HTML. fil: docs/beslut/ADR-023-min-ai.md ADR-024 (Antagen) — Verktyg, skills och MCP-tjänster i Min AI Min AI får webbsök, skapa bild (SVG), skapa dokument (.docx) och skapa graf, plus MCP-tjänster via Anthropics connector. Användaren slår på och av var och en i skrivfältet; valen sparas på servern, som ensam avgör vilka verktyg modellen får. Egna verktyg har strict-schema mot modellen och zod-validering på servern. Nya diagramtokens --color-graf-1…4. fil: docs/beslut/ADR-024-min-ai-verktyg.md ADR-025 (Antagen) — Modulvyer över hela arbetsytan, och genvägar bredvid hälsningen mode: "full" ger en modulvy under /verktyg/ med sidopanelen kvar men hela arbetsytan i skärmens höjd – ingen marginal, sidfot eller AI-knapp. slots.genvag ger knappar bredvid hälsningen på startsidan, AI-tonade med ai: true. Min AI använder båda: knappen ersätter kortet, och samtalet tar ytan som i de stora chattjänsterna, med plats för ett dokument till höger. fil: docs/beslut/ADR-025-hel-arbetsyta.md ADR-026 (Antagen) — AI tonas i blått, inte grönt – grönt är bara status Grönt är status – klart, godkänt, positivt – och betyder aldrig AI. AI-tokens behåller namnen men byter till blå toner, ljusare än identitetsblått (--color-ai #1f64b8). Signalen för AI är märkningen ✦/AiChip, inte färgen. Ersätter regeln att grönt är reserverat för AI. fil: docs/beslut/ADR-026-ai-i-blatt.md ADR-027 (Antagen) — Målgruppsingång – Verktygslådan under Digitalisering, IT och AI En målgruppsingång är en sektion med en ingång – inget nytt i kontraktet. Sektionen Digitalisering, IT och AI får ingången Verktygslådan, porterad från den fristående verktygslådan, med katalog, verksamheter, omvärld och kontakt som egna kort och en artikel per verktyg. Det databasburna (digitala arvet, PIOS, nyttoportföljen) stannar i verktygslådan. fil: docs/beslut/ADR-027-malgruppsingang.md ADR-028 (Antagen) — Workshoprösten – röstkoder utanför spärren, prickar i en fil Prioriteringen efter workshopen görs med prickröstning på /rosta. En röstkod per deltagare, framräknad ur adminkoden med HMAC och aldrig sparad, ger tre prickar. Sidan och API:t ligger utanför demospärren: röstkoden är nyckeln, och lapparna visas i publik form. Prickarna sparas i en JSON-fil på en volym – ingen databas för en workshop. Röstkoden är en biljett, inte en identitet; ingenting byggs ovanpå åtkomstkakan. fil: docs/beslut/ADR-028-workshoprost.md ADR-029 (Antagen) — Projektets sida på roten, prototypen på /portal Roten blir projektets sida: vad projektet är, var det står, vad som kommer och allt material samlat, med "Testa prototypen" som huvudärende. Den tidigare introsidan med rollvalet flyttar till /portal, och Logga ut leder dit. Båda är öppna och skrivs publikt; projektboken, tidslinjen, samtalen och workshopen ligger kvar bakom spärren och får bara en rubrik på roten. fil: docs/beslut/ADR-029-projektsidan-pa-roten.md ADR-030 (Antagen) — Kompetens är ett register, inte ett LMS Kommunen köper eller bygger inget nytt LMS. Lärplattformen (Moodle) förblir kursmotorn; det som saknas – vad en person har, vad rollen kräver och när det går ut – blir ett eget register bakom WSO2 med kontraktet kompetens@1, matat från Moodle, ID06, Skolverket och chefens intyg. Startsidan visar det som modulen kompetens, som ersätter utveckling. Planen föds ur utvecklingssamtalet; servern prövar varje aktivitet innan något sparas. fil: docs/beslut/ADR-030-kompetens-som-register.md ADR-031 (Antagen) — Projektboken ordnas efter typ, inte efter tillfälle Projektbokens meny har fasta flikar – Läget, Tidslinjen, Spåren, Beslut och frågor, Material – som inte blir fler när projektet växer. Allt är typade poster med en fast adress. Varje möte och workshop får en sida under /projekt/handelser/-, och tidslinjen är ingången till dem, senast först med filter på slag och spår. Läget skrivs över; historiken läggs till. Toppraden bär kommunens logotyp enligt kommunikationsguidens mall. fil: docs/beslut/ADR-031-projektboken-ordnas-efter-typ.md ADR-032 (Antagen) — Projektet är roten, med statusrapporter och funktionerna Projektbokens innehåll flyttar från /projekt till roten och den gamla projektsidan slås ihop med Läget. Prototypen står överst men lågt. En topprad med fasta flikar gäller alla projektets sidor. Statusrapporter på /status skrivs i ett formulär eller via /api/status med skrivnyckeln STATUS_SKRIVNYCKEL, och servern prövar varje fält. Funktionerna samlas på /funktioner i tre lager. Allt är öppet utom mötenas anteckningar och ritningen; gamla adresser leds permanent vidare. fil: docs/beslut/ADR-032-projektet-ar-roten.md ADR-033 (Antagen) — Projektets meny är en klassisk huvudmeny enligt WCAG 2.2 AA Flikraden ersätts av en klassisk huvudmeny efter header A: Start, Status, Tidslinje, Beslut och Funktioner, och grupperna Dokument och Teknik som fälls ut, i en rad dockad mot toppradens underkant under avsändaren. Prototypen länkas inte från projektets yta tills vidare; den nås på /portal. Grå text, blått och ett streck för sidan man är på. Under md en Meny-knapp med samma lista, en post per rad; toppraden klistrar bara från lg. W3C:s disclosure-mönster med aria-expanded och aria-current, Escape stänger. Hopplänk till
på varje sida med toppraden. Menyn är data i projektyta/meny.ts. fil: docs/beslut/ADR-033-huvudmeny-enligt-wcag.md ADR-034 (Antagen) — Projektstödet svarar bara utifrån projektets öppna sidor Fråga om projektet är ett stöd för att förstå projektet, inte en chattassistent: en frågeruta på startsidan och en knapp på varje öppen undersida. Underlaget är de öppna sidorna som en besökare ser dem – servern läser sina egna sidor utan kaka – och ett test fäller bygget om en sida i underlaget är låst. Varje sida är ett dokument med källhänvisningar, så att svaret visar var det står. Servern äger samtalet; /api/fraga är öppen med egna takgränser. Claude Opus 5.5 som standard, utbytbar med PROJEKTSTOD_MODELL. fil: docs/beslut/ADR-034-projektstodet.md ADR-035 (Antagen) — Notiser till teamschatten via ett flöde i Power Automate, avstängda tills de slås på Nya händelser i tidslinjen blir kort i teamets gruppchatt. Ett flöde i Power Automate tar emot kortet och publicerar det som Flödesrobot; Teams gamla Incoming Webhooks är avvecklade. Servern skickar när en ny version startar, så att sidan finns, och minns vad som skickats i datavolymen. TEAMS_NOTISER är av som standard; första gången och i läget logg markeras allt utan att skickas. Varje inlägg i tidslinjen godkänns av Jari Koponen innan det läggs till, och varje godkänt inlägg skickas oavsett slag, högst fem kort per start. fil: docs/beslut/ADR-035-teamsnotiser.md ADR-036 (Antagen) — Att göra-listan redigeras på sidan och bor i en SQLite-fil Uppgifterna på /att-gora läggs till, ändras och byter läge på sidan av den som loggat in med samma skrivnyckel som statusrapporterna; åtkomstkoden räcker inte. De bor i en SQLite-fil med Nodes inbyggda node:sqlite i datavolymen, ATT_GORA_FIL. uppgifter.ts är startdata: det som saknas läggs in vid start, det som finns rörs inte. Servern prövar varje skrivning – ett möte med egen sida eller löpande, länkar från roten – och en uppgift tas aldrig bort. fil: docs/beslut/ADR-036-att-gora-i-databas.md ADR-037 (Antagen) — En ändringslogg i startsidans databas Att göra-listans SQLite-fil blir startsidans databas, DATABAS_FIL, med ett schema som växer i numrerade steg; den gamla filen kopieras in en gång. En tabell andringar får en rad för varje ändring – statusrapporter, uppgifter och nya byggen – skriven av den som ändrar. Uppgifternas rader är inte publika: till chatten går bara statusrapporterna. Den tekniska tidslinjen är /andringar bakom åtkomstkoden, och varje uppgift visar sina rader. Notiserna till teamschatten ska läsa loggen i ett senare steg, och då bara statusrapporterna. fil: docs/beslut/ADR-037-andringslogg.md ADR-038 (Antagen) — Funktionerna läggs till på sidan och via API, och bor i databasen Funktionerna på /funktioner bor i startsidans databas; funktioner.ts är startdata. Ett nytt läge, Önskemål, för det som kommit in men inte prövats. Den som har skrivnyckeln lägger till och ändrar på sidan, och API:t /api/funktioner tar emot POST, PUT och PATCH med skrivnyckeln som Bearer-token – vägen för en agent att lägga upp något i produktion. GET är öppet. Servern prövar varje skrivning, ger id:t ur namnet och tar aldrig bort en funktion; varje ändring loggas. fil: docs/beslut/ADR-038-funktioner-i-databas.md ADR-039 (Antagen) — Konton för projektgruppen – användarnamn och lösenord, först för att göra-listan Projektgruppen får egna konton med användarnamn och lösenord, som en mellanlösning tills SAML-sessionen i fas 4; åtkomstkoden är fortfarande ingen identitet. Två roller, medlem och admin. Admin skapar konton på /konton med ett engångslösenord som byts vid första inloggningen; det första kontot skapas med skrivnyckeln. Engångslösenordet gäller i sju dagar. Lösenorden sparas som scrypt med OWASP:s parametrar och prövas mot vanliga lösenord; tio fel i rad spärrar kontot i en kvart. Sessionen gäller i 90 dagar och förnyas när den används (ändrat 20 oktober 2026), med bara kakans fingeravtryck i databasen. Ett konto stängs men tas aldrig bort. Ändringsloggen får kolumnen av, som visas bara för inloggade. Först för att göra-listan; statusrapporterna, funktionerna och API:erna behåller skrivnyckeln. fil: docs/beslut/ADR-039-konton-for-projektgruppen.md ADR-040 (Antagen) — Projektportalen och startsideportalen är två applikationer på en domän start.sundsvall.dev rymmer två applikationer med olika syften: projektportalen, projektet om startsidan, och startsideportalen, prototypen av själva produkten. De delar domän för användarnas skull, inte för att de hör ihop. Allt som hör till prototypen ska ligga under /portal, också devportalen som blir /portal/dev; allt annat är projektportalen. CLAUDE.md delas i gemensamt, projektportalen och startsideportalen, och ingen identitet korsar gränsen. Koden delas i steg enligt docs/plan-uppdelning.md – fas 6 i planen – när projektet är redo. fil: docs/beslut/ADR-040-tva-applikationer-en-doman.md --- # Om något saknas Saknas ett svar här är det portalen som är ofullständig, inte du som ska gissa. Lägg till posten i apps/portal/src/app/dev/portal/ i samma commit som ändringen, så finns svaret nästa gång.