Sammanfattning och slutsats i rapport och Live Share; API dokumenterat

Sammanfattningen ligger först i både kundrapporten och Live Share.
Mottagaren är oftast inte tekniker — det är kunden, en
försäkringshandläggare eller en flottansvarig — och de ska få bilden på
fem sekunder och sedan kunna gå djupare, inte tvärtom.

I Live Share härleds den ur det NIVÅFILTRERADE underlaget. Det betyder
att sammanfattningen aldrig kan avslöja något som delningsnivån döljer:
filtret ligger före projektionen, inte efter.

Slutsatsen visas som ett eget avsnitt före underlaget. En handläggare
läser skälet först och kontrollerar det sedan — den ordningen är hela
poängen med att fältet finns.

Sju nya vägar dokumenterade i OpenAPI och låsta av paritetstestet:
sammanfattning, protokollinläsning, statistik, integrationskategorier,
prenumerationer, radering och mätdon. Ett API som inte är dokumenterat
är inte ett API någon kan koppla in sig mot — och specen valideras
maskinellt så dokumentationen inte kan glida från servern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-05 15:55:31 +00:00
parent 3ec25dea9c
commit cd6b2caca5
4 changed files with 307 additions and 0 deletions
@@ -10,6 +10,7 @@ import { KUNDBESLUT_LABEL, handelseRubrik } from "./domain";
import { arAvslutat, arendeidentitet, brief, foton, tidsfordelningsRader, videor } from "./projektioner";
import { metodikForArende } from "./store";
import { Bild, Klipp } from "./Bilagevisning";
import { sammanfatta } from "../../../services/gemensam/sammanfattning.mjs";
import { tidDatum, tidKlockslag } from "./format";
import { FelsokningSkal, Panel, StorKnapp } from "./ui";
import { IkonCheck, IkonKlocka, IkonUppdatera } from "./ikoner";
@@ -157,6 +158,41 @@ export function DelatArendeVy({
ärende vyn avser. Härledd ur det nivåfiltrerade underlaget. */}
<IdentitetsPanel arende={arende} avslutat={avslutat} />
{/* ALVA-PROC-0030 · Sammanfattningen först. Mottagaren är oftast
inte tekniker: kunden, en försäkringshandläggare, en
flottansvarig. De ska få bilden på fem sekunder och sedan kunna
gå djupare — inte tvärtom. Texten härleds ur det nivåfiltrerade
underlaget, så den kan aldrig avslöja något som nivån döljer. */}
<Panel rubrik="Sammanfattning">
<p className="text-[15px] leading-[24px] text-[#1B1E22]">{sammanfatta(arende).text}</p>
</Panel>
{/* ALVA-RULE-200 · Teknikerns varför. Den enda rad en
försäkringsbedömare egentligen behöver, kunddelbar med avsikt. */}
{(() => {
const post = [...arende.handelser].reverse().find((p) => p.handelse.typ === "slutsats");
if (!post || post.handelse.typ !== "slutsats") return null;
const h = post.handelse;
const rader: [string, string][] = [
[h.orsakFastställd === false ? "Skäl till att orsaken inte fastställts" : "Motivering", h.motivering],
["Uteslutna alternativ", h.uteslutet],
...(h.atgardsval ? ([["Val av åtgärd", h.atgardsval]] as [string, string][]) : []),
["Kvarstående osäkerhet", h.kvarstaende],
];
return (
<Panel rubrik="Slutsats och motivering">
<dl className="text-[14px] leading-[22px]">
{rader.map(([etikett, text]) => (
<div key={etikett} className="border-t border-[#D7DCE2] py-2 first:border-t-0">
<dt className="text-[11px] font-semibold uppercase tracking-[0.12em] text-[#4D5662]">{etikett}</dt>
<dd className="mt-1 text-[#1B1E22]">{text}</dd>
</div>
))}
</dl>
</Panel>
);
})()}
<p className="mb-4 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#4A5560] print:hidden">
{notis}
</p>
@@ -101,6 +101,15 @@ describe("AI-orkestern", () => {
["/api/arenden/{arendeId}/bilagor", "laddaUppVag"],
["/api/bilagor/{bilagaId}", "bilagaVag"],
["/api/delad/{delningskod}/bilagor/{bilagaId}", "delatBilaga"],
// ALVA: nya vägar. Ett API som inte är dokumenterat är inte ett API
// någon kan koppla in sig mot.
["/api/arenden/{arendeId}/sammanfattning", "sammanfattningVag"],
["/api/arenden/{arendeId}/protokoll", "protokollVag"],
["/api/statistik/oversikt", "/api/statistik/oversikt"],
["/api/integration/kategorier", "/api/integration/kategorier"],
["/api/integration/prenumerationer", "/api/integration/prenumerationer"],
["/api/radering", "/api/radering"],
["/api/matdon", "/api/matdon"],
];
for (const [iSpec, iServer] of vagar) {
expect(spec).toContain(`${iSpec}:`);
@@ -7,6 +7,7 @@ import { nastaSteg } from "@/felsokning/metodik";
import { fasFor, klaraFaser } from "../../../../services/gemensam/faser.mjs";
import { Fasrad } from "@/alva/komponenter";
import { Slutsatspanel } from "@/felsokning/Slutsats";
import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
import { arendebeteckning, fasDefinition } from "@/alva/system";
import {
arAvslutat,
@@ -2317,6 +2318,33 @@ function RapportFlik({
Delningslänken kräver att ärendet är synkat mot molnet (inloggad användare).
</p>
</Panel>
{/* ALVA-PROC-0030 · Sammanfattningen först. Den som öppnar
rapporten — kund, handläggare, nästa tekniker — ska få bilden
på fem sekunder och sedan kunna gå djupare. Texten är härledd
ur loggen, så den kan inte säga något som inte står där. */}
<Panel rubrik="Sammanfattning">
<p className="text-[15px] leading-[24px] text-[#1B1E22]">{sammanfatta(arende).text}</p>
<p className="mt-2 font-mono text-[11px] text-[#4D5662]">ALVA-PROC-0030 · härledd ur händelseloggen</p>
</Panel>
{/* ALVA-RULE-200 · Teknikerns varför. Placerad före underlaget:
en handläggare läser skälet först och kontrollerar det sedan. */}
{(() => {
const s = [...arende.handelser].reverse().find((p) => p.handelse.typ === "slutsats");
if (!s || s.handelse.typ !== "slutsats") return null;
const h = s.handelse;
return (
<Panel rubrik="Slutsats och motivering">
{identitetsRader([
[h.orsakFastställd === false ? "Skäl till att orsaken inte fastställts" : "Motivering", h.motivering],
["Uteslutna alternativ", h.uteslutet],
["Val av åtgärd", h.atgardsval ?? "—"],
["Kvarstående osäkerhet", h.kvarstaende],
])}
</Panel>
);
})()}
{/* Rapportens första sida: ärendeidentiteten — registrerad en gång,
återanvänd här automatiskt. */}
<Panel rubrik="Ärendeinformation">
+234
View File
@@ -952,6 +952,240 @@ paths:
"502": { $ref: "#/components/responses/Fel" }
"503": { $ref: "#/components/responses/Fel" }
/api/arenden/{arendeId}/sammanfattning:
get:
summary: Härledd sammanfattning av ärendet (ALVA-PROC-0030)
description: >
Några få meningar som ger vem som helst en bild av ärendet.
Sammanfattningen är HÄRLEDD ur händelseloggen, inte genererad:
samma ärende ger samma text i dag och om två år, den innehåller
inget som inte står i loggen, och den säger uttryckligen när något
saknas i stället för att utelämna det. Avsedd för handläggare,
kundtjänst och integrationer.
security: [{ bearerAuth: [] }]
parameters:
- { name: arendeId, in: path, required: true, schema: { type: string } }
responses:
"200":
description: Sammanfattning
content:
application/json:
schema:
type: object
properties:
version: { type: string, example: ALVA-PROC-0030 }
text: { type: string }
meningar: { type: array, items: { type: string } }
enrading: { type: string }
fullstandig:
type: boolean
description: Sant när inget obligatoriskt underlag saknas.
saknas: { type: array, items: { type: string } }
"404": { description: Ärendet är inte tillgängligt }
/api/arenden/{arendeId}/protokoll:
post:
summary: Läs in ett diagnosprotokoll som evidens (ALVA-PROC-0020)
description: >
Avläsningar blir händelser i loggen, inte en bilaga. Härkomsten
bevaras i varje post, så ett värde som kommit utifrån aldrig ser ut
som något teknikern själv mätt. Lämnas instrumentets identitet
behålls den; utan den graderas värdet E1 i stället för E4.
Profilen beskriver var värdena ligger i leverantörens format —
ALVA antar aldrig ett visst leverantörsformat.
security: [{ bearerAuth: [] }]
parameters:
- { name: arendeId, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [kalla, profil, protokoll]
properties:
kalla:
type: string
description: Vad som producerade protokollet. Följer med varje händelse.
example: Diagnosinstrument, plats 3
profil:
type: object
properties:
felkoder:
type: object
properties:
vag: { type: string, example: dtcs }
kod: { type: string, example: code }
text: { type: string, example: description }
matvarden:
type: object
properties:
vag: { type: string, example: liveData }
beskrivning: { type: string, example: name }
varde: { type: string, example: value }
enhet: { type: string, example: unit }
instrumentId: { type: string, example: toolSerial }
protokoll:
type: object
description: Leverantörens nyttolast, oförändrad.
responses:
"200":
description: Antal skrivna händelser
content:
application/json:
schema:
type: object
properties:
handelser: { type: integer }
kalla: { type: string }
"422": { description: Profilen gav inga händelser ur protokollet }
/api/statistik/oversikt:
get:
summary: Driftmått för organisationen (ALVA-REP-0100)
description: >
Samma underlag som portalens analysvy och kvartalsrapporten, så att
skärm och rapport aldrig visar olika siffror för samma period.
Ett mått utan underlag returneras som null, aldrig som noll.
Kräver arbetsledare eller administratör.
security: [{ bearerAuth: [] }]
responses:
"200":
description: Driftmått
content:
application/json:
schema:
type: object
properties:
version: { type: string, example: ALVA-REP-0100 }
verifiering:
type: object
description: Andel avslut med fastställd orsak.
properties:
antal: { type: integer }
fastställda: { type: integer }
andel: { type: number, nullable: true }
reproduktion: { type: object }
omarbetning:
type: object
description: Samma fordon tillbaka med samma orsakskategori.
undantag:
type: array
description: Kontroller som oftast hoppas över, per steg och ALVA-fas.
items: { type: object }
evidens: { type: object }
faser: { type: object }
orsaker: { type: array, items: { type: object } }
"403": { description: Kräver arbetsledare eller administratör }
/api/integration/kategorier:
get:
summary: Systemkategorier och utgående händelser (ALVA-SPEC-020)
description: >
ALVA är leverantörsoberoende. Kategorierna beskriver vilka slags
system som kan kopplas in och åt vilket håll data går; en profil
märks validated först efter att den körts mot leverantörens
faktiska gränssnitt.
security: [{ bearerAuth: [] }]
responses:
"200":
description: Kategorier och händelsetyper
content:
application/json:
schema:
type: object
properties:
kategorier: { type: object }
handelser:
type: object
description: Utgående händelsetyper med beskrivning.
/api/integration/prenumerationer:
get:
summary: Organisationens utgående prenumerationer (ALVA-SPEC-021)
security: [{ bearerAuth: [] }]
responses:
"200": { description: Prenumerationer }
"403": { description: Kräver administratörsbehörighet }
post:
summary: Registrera en mottagare för utgående händelser
description: >
Leveranser signeras med HMAC över tidsstämpel och kropp i huvudet
`alva-signatur` (`t=<unix>,v1=<hex>`). Tidsstämpeln ligger inne i
signaturen, så en fångad leverans inte går att spela upp senare;
toleransen är fem minuter. Adressen kontrolleras mot samma
SSRF-gräns som leverantörsuppslagen.
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [namn, url, handelser]
properties:
namn: { type: string }
url: { type: string, format: uri }
handelser:
type: array
items:
type: string
enum:
- arende.skapat
- arende.fas
- arende.slutsats
- arende.avslutat
- media.tillagt
- atgardsforslag.lamnat
- kundbeslut.registrerat
hemlighet:
type: string
description: Utelämnas för att låta servern generera en.
responses:
"200": { description: Skapad }
"400": { description: Ogiltig adress eller okänd händelsetyp }
/api/radering:
post:
summary: Radera personuppgifter för ett fordon (krypto-shredding)
description: >
Nyckeln förstörs; loggen står kvar. Det som raderas är
identifieringen, inte protokollet över vad som kontrollerades —
den enda konstruktion där bevisvärdet överlever en
raderingsbegäran. Kräver en bekräftelse som upprepar subjektet
exakt; åtgärden går inte att ångra.
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [subjekt, bekraftelse]
properties:
subjekt: { type: string, description: Registreringsnummer eller ärende-id. }
bekraftelse: { type: string, description: Måste vara identisk med subjekt. }
responses:
"200": { description: Verkställd }
"400": { description: Bekräftelsen matchar inte }
"404": { description: Inget skyddat underlag finns }
/api/matdon:
get:
summary: Organisationens mätdon med kalibreringsstatus (ALVA-SPEC-004)
description: >
Ett mätvärde graderas E4 endast med spårbart, kalibrerat
instrument. Utan register är påståendet inte kontrollerbart.
security: [{ bearerAuth: [] }]
responses:
"200": { description: Mätdon }
post:
summary: Registrera eller uppdatera ett mätdon
security: [{ bearerAuth: [] }]
responses:
"200": { description: Sparat }
"403": { description: Kräver arbetsledare eller administratör }
components:
securitySchemes:
bearerAuth: