Agentvy
Recept
Steg för de fem uppgifter som återkommer: kort, nyckeltal, handlingsrad, fokusflöde och mockdata.
page manifest · /dev/receptid: 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/<namn>/package.json:
```
{
"name": "@startsidan/modul-<namn>",
"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/<namn>/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/<namn>/src/index.ts:
```
export { manifest } from "./manifest";
export { hamtaNagot } from "./data";
export type { Nagot, NagotSvar } from "./data";
```
packages/modules/<namn>/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<Role, NagotSvar> = {
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<NagotSvar> {
return mock[ctx.role];
}
```
packages/modules/<namn>/src/manifest.ts:
```
import { defineModule } from "@startsidan/module-sdk";
import { NagotKort } from "./components/NagotKort";
export const manifest = defineModule({
id: "<namn>",
name: "Visningsnamn",
roles: ["medarbetare", "chef"],
nav: [{ label: "Visningsnamn", href: "#", order: 50 }],
slots: {
main: [{ id: "<namn>-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: "<namn>", app: "portal", slag: "Modul", namn: "<namn>", status: "Byggd",
beskrivning: "…", kalla: "packages/modules/<namn>",
relationer: ["ui", "module-sdk"], levererar: ["slots.main: <namn>-kort"],
roller: ["medarbetare", "chef"] }
// apps/portal/src/app/dev/portal/dok.ts – djupet
{ id: "<namn>", 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/<namn>/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 (
<Card headingId="nagot-h" title="Rubrik" count={poster.length}
action={<MoreLink href="#">Visa alla →</MoreLink>}>
<ul className="grid list-none">
{poster.map((p) => (
<ListRow key={p.id} title={p.rubrik} meta={p.kommentar}
end={<Chip tone={p.chip.ton}>{p.chip.etikett}</Chip>} />
))}
</ul>
</Card>
);
}
```
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: "<namn>-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/<namn>/src/manifest.ts
async function actions(ctx: UserContext): Promise<ActionItem[]> {
if (ctx.role !== "chef") return [];
const { antal } = await hamtaNagot(ctx);
if (!antal) return [];
return [
{
id: "<namn>-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/<namn>",
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/<namn>/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/<namn>/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/<namn>/route.ts
import { hamtaNagot } from "@startsidan/modul-<namn>";
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.Hela portalen i samma form: /dev/llms.txt · /dev/spec.json