Files
alva/services/plattform/openapi.yaml
T
Claude 64b2124cd8 Publikt kundgodkännande i delningslänken
Kunden kan nu godkänna eller avböja åtgärdsförslaget direkt i sin
Live Share-länk — API:ts enda skrivande publika väg, med sex spärrar
som var och en verifieras i integrationstestet:

1. endast delningar på kundnivå (partner-/internlänkar får aldrig svara
   åt kunden) och aldrig återkallade
2. ärendets ursprungliga delningskod saknar registrerad nivå och kan
   inte heller svara
3. det måste finnas ett åtgärdsförslag att svara på
4. ett besked per ärende — svaret kan inte ändras i efterhand
5. endast godkant/avbojt/delvis plus kommentar på högst 500 tecken;
   inget annat kan skrivas till loggen den vägen
6. takt-begränsning per delningskod (429)

Beskedet loggas som kundbeslut med kanal "Delningslänk" och avsändaren
"Kund via delningslänk"; verkstadens egna registreringar (telefon, på
plats …) fungerar precis som förut, och kvalitetsgrindens krav gäller
oförändrat. Beslutspanelen visas bara på den publika kundlänken —
interna och partnervyer förblir helt skrivskyddade.

73 vitest-tester, integrationstest (åtta nya kontroller för spärrarna)
och OpenAPI-validering gröna. Därmed är även den sista dokumenterade
avgränsningen utöver externa fordonsdatabaser avklarad.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-03 12:25:14 +00:00

742 lines
24 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: Guidad Felsökning plattforms-API
version: 1.0.0
description: |
API-first-ytan för Guidad Felsökning: autentisering, organisationens
användare, ärenden med append-only händelselogg, arbetsledaröversikt,
publik Live Share-delning samt AI-orkestern.
**Händelseloggen är append-only.** Det finns inga update- eller
delete-operationer — historik kan aldrig ändras eller raderas, vilket
även garanteras av databastriggers. En händelse med redan känt `id`
ignoreras tyst (idempotent synk).
**Multi-tenant.** All ärendedata är organisationsknuten. Ett ärende i
en annan organisation ger `404`, som om det inte fanns.
Specen serveras live av tjänsten på `GET /api/openapi.yaml`.
servers:
- url: https://app.exempel.se
description: Klustrets ingress (ersätt med er domän)
tags:
- name: Auth
- name: Användare
- name: Ärenden
- name: Översikt
- name: Delning
- name: AI
- name: Drift
paths:
/halsa:
get:
tags: [Drift]
summary: Hälsokontroll
security: []
responses:
"200":
description: Tjänsten svarar.
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: ok }
/api/openapi.yaml:
get:
tags: [Drift]
summary: Denna specifikation
security: []
responses:
"200":
description: OpenAPI-specen i YAML.
content:
application/yaml:
schema: { type: string }
/api/auth/registrera:
post:
tags: [Auth]
summary: Skapa organisation + systemadministratör
description: >
Registrerar en ny organisation (tenant) och dess första användare,
som blir systemadministratör. Kan stängas av driften
(REGISTRERING_OPPEN=false → 403).
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [epost, losenord, namn, organisation]
properties:
epost: { type: string, format: email }
losenord: { type: string, minLength: 8 }
namn: { type: string }
organisation: { type: string }
responses:
"200": { $ref: "#/components/responses/Inloggad" }
"400": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
"409": { $ref: "#/components/responses/Fel" }
/api/auth/logga-in:
post:
tags: [Auth]
summary: Logga in
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [epost, losenord]
properties:
epost: { type: string, format: email }
losenord: { type: string }
responses:
"200": { $ref: "#/components/responses/Inloggad" }
"401": { $ref: "#/components/responses/Fel" }
/api/anvandare:
get:
tags: [Användare]
summary: Lista organisationens användare
description: Kräver rollen `admin` eller `arbetsledare`.
responses:
"200":
description: Användare i den egna organisationen.
content:
application/json:
schema:
type: object
properties:
anvandare:
type: array
items: { $ref: "#/components/schemas/Anvandare" }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
post:
tags: [Användare]
summary: Skapa användare i den egna organisationen
description: Kräver rollen `admin`.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [epost, losenord, namn, roll]
properties:
epost: { type: string, format: email }
losenord: { type: string, minLength: 8 }
namn: { type: string }
roll: { $ref: "#/components/schemas/Roll" }
responses:
"200":
description: Den skapade användaren.
content:
application/json:
schema: { $ref: "#/components/schemas/Anvandare" }
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
"409": { $ref: "#/components/responses/Fel" }
/api/ecm/regler:
get:
tags: [Drift]
summary: ECM Knowledge Library — aktuellt regelpaket
description: >
Serverdistribuerat, versionerat regelpaket (compliance-regler per
ärendetyp, orsakskategorier, evidenskällor, undantagsorsaker).
Uppdateras i driften utan appändring; klienten cachar och faller
tillbaka till sitt inbyggda standardpaket offline.
responses:
"200":
description: Regelpaketet.
content:
application/json:
schema:
type: object
properties:
version: { type: string }
arendetypRegler: { type: object, additionalProperties: true }
undantagsorsaker: { type: array, items: { type: string } }
orsakskategorier: { type: array, items: { type: string } }
underlagskallor: { type: array, items: { type: string } }
/api/fordon/{identifierare}/historik:
get:
tags: [Ärenden]
summary: Fordonshistorik — tidigare ärenden på samma objekt
description: >
Organisationens tidigare ärenden där objektet (regnr/VIN) matchar,
med dokumenterade felorsaker. Underlag för pre-diagnostikens
historiksteg och orsakskedjan.
parameters:
- name: identifierare
in: path
required: true
schema: { type: string }
responses:
"200":
description: Tidigare ärenden (senaste 20).
content:
application/json:
schema:
type: object
properties:
arenden:
type: array
items:
type: object
properties:
id: { type: string }
nummer: { type: integer }
skapad: { type: string, format: date-time }
avslutat: { type: boolean }
felbeskrivning: { type: string, nullable: true }
felorsaker: { type: array, items: { type: object, additionalProperties: true } }
"401": { $ref: "#/components/responses/Fel" }
/api/statistik/felorsaker:
get:
tags: [Översikt]
summary: Felorsaksstatistik per orsakskategori
description: >
Flottdata ur dokumenterade felorsaksanalyser i organisationen.
Kräver rollen `arbetsledare` eller `admin`.
responses:
"200":
description: Antal per orsakskategori, fallande.
content:
application/json:
schema:
type: object
properties:
orsaker:
type: array
items:
type: object
properties:
orsak: { type: string }
antal: { type: integer }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
/api/delad/{delningskod}/beslut:
post:
tags: [Delning]
summary: Kundens besked på ett åtgärdsförslag
description: >
Den enda skrivande publika vägen. Endast delningar på **kundnivå**
som inte återkallats får svara, det måste finnas ett
åtgärdsförslag, och **ett besked per ärende** — svaret kan inte
ändras i efterhand. Takt-begränsad per delningskod. Beskedet
loggas som `kundbeslut` med kanal `Delningslänk`.
security: []
parameters:
- name: delningskod
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [beslut]
properties:
beslut: { type: string, enum: [godkant, avbojt, delvis] }
kommentar: { type: string, maxLength: 500 }
responses:
"200":
description: Beskedet registrerat.
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
"400": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
"409": { $ref: "#/components/responses/Fel" }
"429": { $ref: "#/components/responses/Fel" }
/api/organisation:
get:
tags: [Organisation]
summary: Hämta organisationens namn och inställningar
description: >
Inställningarna styr vad som visas när ett ärende startas
(objekttyper och identifieringsmetoder). Alla inloggade läser.
responses:
"200":
description: Organisationen.
content:
application/json:
schema:
type: object
properties:
namn: { type: string }
installningar: { $ref: "#/components/schemas/OrganisationsInstallningar" }
"401": { $ref: "#/components/responses/Fel" }
/api/organisation/installningar:
post:
tags: [Organisation]
summary: Uppdatera organisationens inställningar
description: Kräver rollen `admin`. Minst ett alternativ per lista.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/OrganisationsInstallningar" }
responses:
"200":
description: Sparat.
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
/api/arenden:
get:
tags: [Ärenden]
summary: Lista organisationens ärenden
responses:
"200":
description: Ärenden (senaste 200).
content:
application/json:
schema:
type: object
properties:
arenden:
type: array
items: { $ref: "#/components/schemas/Arende" }
"401": { $ref: "#/components/responses/Fel" }
post:
tags: [Ärenden]
summary: Registrera ett ärende (idempotent)
description: >
Skapar ärendet i användarens organisation. Ett redan känt `id`
ignoreras tyst — ärenderaden ändras aldrig.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [id, nummer, skapad]
properties:
id: { type: string }
nummer: { type: integer }
skapad: { type: string, format: date-time }
delningskod: { type: string, nullable: true }
metodikId: { type: string, nullable: true }
responses:
"200":
description: Mottaget.
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
/api/arenden/{arendeId}/handelser:
parameters:
- name: arendeId
in: path
required: true
schema: { type: string }
get:
tags: [Ärenden]
summary: Hämta ärendets händelselogg
responses:
"200":
description: Händelser i tidsordning.
content:
application/json:
schema:
type: object
properties:
handelser:
type: array
items: { $ref: "#/components/schemas/LoggPost" }
"401": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
post:
tags: [Ärenden]
summary: Lägg till händelser (append-only, idempotent)
description: >
Lägger till händelser i loggen. Max 500 per anrop. En händelse med
redan känt `id` ignoreras tyst — befintliga händelser skrivs aldrig
över, och databastriggern stoppar alla ändringsförsök.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [handelser]
properties:
handelser:
type: array
maxItems: 500
items: { $ref: "#/components/schemas/LoggPost" }
responses:
"200":
description: Mottaget.
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
/api/oversikt:
get:
tags: [Översikt]
summary: Organisationsöversikt med status och statistikunderlag
description: >
Kräver rollen `arbetsledare` eller `admin`. Status, deltagande
tekniker och sammanfattning härleds ur händelseloggen.
responses:
"200":
description: Alla ärenden i organisationen med härledd status.
content:
application/json:
schema:
type: object
properties:
arenden:
type: array
items: { $ref: "#/components/schemas/OversiktsRad" }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
/api/arenden/{arendeId}/delningar:
parameters:
- name: arendeId
in: path
required: true
schema: { type: string }
get:
tags: [Delning]
summary: Lista ärendets delningslänkar
responses:
"200":
description: Delningar (inklusive återkallade).
content:
application/json:
schema:
type: object
properties:
delningar:
type: array
items: { $ref: "#/components/schemas/Delning" }
"401": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
post:
tags: [Delning]
summary: Skapa delningslänk med behörighetsnivå
description: >
Verkstaden kontrollerar alltid delningen: kund (det kunddelbara),
partner (även hypoteser, tydligt märkta ej verifierade) eller
intern (full insyn). Nivåfiltreringen sker på serversidan.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [niva]
properties:
niva: { $ref: "#/components/schemas/DelningsNiva" }
responses:
"200":
description: Den skapade länkens kod.
content:
application/json:
schema:
type: object
properties:
kod: { type: string }
niva: { $ref: "#/components/schemas/DelningsNiva" }
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
/api/delningar/{kod}/aterkalla:
post:
tags: [Delning]
summary: Återkalla en delningslänk
description: Efter återkallelse ger länken 404. Kan inte ångras.
parameters:
- name: kod
in: path
required: true
schema: { type: string }
responses:
"200":
description: Återkallad.
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
"401": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
/api/delad/{delningskod}:
get:
tags: [Delning]
summary: Publik Live Share-vy via delningskod
description: >
Kräver ingen inloggning — delningskoden är nyckeln. Filtreringen
styrs av länkens behörighetsnivå: kund utesluter kategoribyten,
hypoteser och AI-dialog; partner utesluter kategoribyten och
AI-dialog; intern visar allt. Ärendets ursprungliga delningskod
fungerar bakåtkompatibelt som kundnivå.
security: []
parameters:
- name: delningskod
in: path
required: true
schema: { type: string, pattern: "^[A-Za-z0-9_-]+$" }
responses:
"200":
description: Ärendet med nivåfiltrerad händelselogg.
content:
application/json:
schema:
type: object
properties:
arende:
type: object
properties:
id: { type: string }
nummer: { type: integer }
skapad: { type: string, format: date-time }
handelser:
type: array
items: { $ref: "#/components/schemas/LoggPost" }
niva: { $ref: "#/components/schemas/DelningsNiva" }
"404": { $ref: "#/components/responses/Fel" }
/api/ai:
post:
tags: [AI]
summary: AI-orkestern (separat tjänst bakom samma ingress)
description: >
Routas av ingressen till ai-orkester-tjänsten. Servern äger
modellval, effort, systemprompt och svarsschema per uppgift:
handledning (Claude Sonnet 5), granskning (Claude Opus 5),
sammanfattning (Claude Sonnet 5), metodikval (Claude Haiku 4.5).
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [uppgift, prompt]
properties:
uppgift:
type: string
enum: [handledning, granskning, sammanfattning, metodikval]
prompt: { type: string, maxLength: 40000 }
responses:
"200":
description: Klassificerat AI-svar med modellen som svarade.
content:
application/json:
schema:
type: object
properties:
modell: { type: string, example: claude-sonnet-5 }
svar:
oneOf:
- $ref: "#/components/schemas/AiSvar"
- type: object
properties:
metodikId:
type: string
enum: [vibration, elsystem, generisk]
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"502": { $ref: "#/components/responses/Fel" }
"503": { $ref: "#/components/responses/Fel" }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: >
HS256-JWT från /api/auth/logga-in eller /api/auth/registrera.
Anspråk: sub (användar-id), namn, org (organisations-id),
roll, iat, exp (12 timmar).
responses:
Inloggad:
description: Inloggad — token + kontouppgifter.
content:
application/json:
schema:
type: object
properties:
token: { type: string }
namn: { type: string }
roll: { $ref: "#/components/schemas/Roll" }
organisation: { type: string }
Fel:
description: Felsvar.
content:
application/json:
schema:
type: object
properties:
error: { type: string }
schemas:
Roll:
type: string
enum: [tekniker, arbetsledare, admin]
OrganisationsInstallningar:
type: object
required: [objekttyper, identifieringsmetoder]
properties:
objekttyper:
type: array
minItems: 1
items: { type: string }
identifieringsmetoder:
type: array
minItems: 1
items: { type: string }
DelningsNiva:
type: string
enum: [kund, partner, intern]
Delning:
type: object
properties:
kod: { type: string }
niva: { $ref: "#/components/schemas/DelningsNiva" }
skapad: { type: string, format: date-time }
aterkallad: { type: string, format: date-time, nullable: true }
Anvandare:
type: object
properties:
id: { type: string, format: uuid }
epost: { type: string, format: email }
namn: { type: string }
roll: { $ref: "#/components/schemas/Roll" }
Arende:
type: object
properties:
id: { type: string }
nummer: { type: integer }
skapad: { type: string, format: date-time }
delningskod: { type: string, nullable: true }
metodik_id: { type: string, nullable: true }
LoggPost:
type: object
required: [id, tidpunkt, anvandare, handelse]
properties:
id: { type: string }
tidpunkt: { type: string, format: date-time }
anvandare: { type: string }
handelse: { $ref: "#/components/schemas/Handelse" }
Handelse:
type: object
description: >
Diskriminerad på `typ`; fullständiga fältdefinitioner i klientens
domänmodell (src/felsokning/domain.ts).
required: [typ]
additionalProperties: true
properties:
typ:
type: string
enum:
- objekt_identifierat
- felbeskrivning
- fraga_besvarad
- kontroll_utford
- observation
- matvarde
- hypotes
- foto
- kommentar
- kategori_byte
- inaktivitet_forklarad
- overlamning
- ansvarig_satt
- arbetsorder_skannad
- arendetyp_satt
- historik_kontrollerad
- matarstallning
- video
- atgardsforslag
- kundbeslut
- atgard_utford
- kvalitetskontroll
- reproducering
- felorsak
- export_skapad
- ai_svar
- arende_avslutat
OversiktsRad:
type: object
properties:
id: { type: string }
nummer: { type: integer }
skapad: { type: string, format: date-time }
delningskod: { type: string, nullable: true }
metodik_id: { type: string, nullable: true }
antal_handelser: { type: integer }
forsta: { type: string, format: date-time, nullable: true }
senaste: { type: string, format: date-time, nullable: true }
avslutat: { type: boolean }
objekt: { type: string, nullable: true }
felbeskrivning: { type: string, nullable: true }
tekniker:
type: array
items: { type: string }
nullable: true
AiSvar:
type: object
required: [rader, nastaSteg]
properties:
rader:
type: array
items:
type: object
required: [typ, text]
properties:
typ:
type: string
enum: [observation, verifierat, hypotes, rekommendation]
text: { type: string }
nastaSteg: { type: string }
security:
- bearerAuth: []