Hoppa till innehållet

Devportalen /portal/dev

Startsideportalens ekosystem för människa och agent

Agentvy

Recept

Steg för de fem uppgifter som återkommer: kort, nyckeltal, handlingsrad, fokusflöde och mockdata.

page manifest · /dev/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/<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