Devportalen
Startsidans ekosystem
Vad ekosystemet är, vilka regler som gäller och var du börjar – för människa och agent.
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/<namn>. 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/<namn>. 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 pushHä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äsbartSamma 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.