Files
alva/services/plattform/openapi.yaml
T
Claude 3a0be7261e Märkesspecifika kopplingar — kunden lägger in sina egna credentials
Verkstaden har redan sina avtal: Volvo-verkstaden har VIDA, VAG-verkstaden
har erWin, den fria verkstaden har en fordonsdataleverantör. Kopplingarna
konfigureras därför av kunden själv under Inställningar, med sina egna
uppgifter — vi tillhandahåller ramen, inte kontot.

Uppgifterna når aldrig webbläsaren. De krypteras med AES-256-GCM
(INTEGRATION_NYCKEL) innan de skrivs till tabellen integrationer, och
API:t returnerar hemliga fält maskerade. Alla uppslag görs av servern.
Saknas krypteringsnyckeln sparas ingenting alls — 503 och en förklaring i
gränssnittet i stället för klartext i databasen. Endast
systemadministratören hanterar uppgifterna; kopplingarna är
organisationsknutna som all annan ärendedata.

Leverantörer är data, inte kod: URL-mall, autentiseringstyp
(bearer/header/basic/query) och svarsmappning beskrivs i
services/plattform/integrationer.json, utbytbar via ConfigMap
(INTEGRATIONER_FIL). Nya märken läggs till utan att appen byggs om.

Varje uppslag skriver senast_testad och senaste_status på kopplingen, så
ett utgånget abonnemang syns i inställningarna i stället för att ge tysta
tomma svar.

Två latenta krascher hittade av klicktestet och åtgärdade: TextFalt och
UNDANTAGSORSAKER användes utan import. vite build typkontrollerar inte,
så de passerade bygget — därav nya npm-skriptet typkontroll, nu del av
verifieringen.

Verifierat: 80 vitest-tester, typkontroll, eslint, OpenAPI-validering,
integrationstest mot riktig Postgres (rollstyrning, kryptering i vila,
maskering, organisationsisolering, fail closed, borttagning) och
klickgenomgång mot en körande plattform.

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

926 lines
31 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: Integrationer
- 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/integrationer/leverantorer:
get:
tags: [Integrationer]
summary: Registret över märkesspecifika kopplingar
description: >
Leverantörer är data, inte kod: registret läses ur
`integrationer.json` (eller filen i `INTEGRATIONER_FIL`) och kan
bytas via ConfigMap utan att applikationen byggs om. Innehåller
endast fältdefinitioner — aldrig någon organisations uppgifter.
responses:
"200":
description: Leverantörsdefinitioner.
content:
application/json:
schema:
type: object
properties:
version: { type: string }
leverantorer:
type: array
items:
type: object
properties:
id: { type: string }
namn: { type: string }
beskrivning: { type: string }
nyckeltyp:
type: string
enum: [vin, regnr]
description: Vad uppslaget sker på. Utelämnat betyder VIN.
falt:
type: array
items:
type: object
properties:
nyckel: { type: string }
etikett: { type: string }
hemlig:
type: boolean
description: >
Hemliga fält maskeras alltid i svar och
visas aldrig igen efter sparande.
"401": { $ref: "#/components/responses/Fel" }
/api/integrationer:
get:
tags: [Integrationer]
summary: Organisationens konfigurerade kopplingar
description: >
Kräver rollen `admin`. Uppgifterna lagras krypterade
(AES-256-GCM) och returneras alltid maskerade — hemliga värden
lämnar aldrig servern i klartext.
responses:
"200":
description: Konfigurerade kopplingar med maskerade uppgifter.
content:
application/json:
schema:
type: object
properties:
krypteringKonfigurerad:
type: boolean
description: Falskt om `INTEGRATION_NYCKEL` saknas — då kan inget sparas.
integrationer:
type: array
items:
type: object
properties:
leverantor: { type: string }
namn: { type: string }
aktiv: { type: boolean }
uppdaterad: { type: string, format: date-time }
senast_testad: { type: string, format: date-time, nullable: true }
senaste_status: { type: string, nullable: true }
uppgifter:
type: object
additionalProperties: { type: string }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
post:
tags: [Integrationer]
summary: Spara eller uppdatera en kopplings uppgifter
description: >
Kräver rollen `admin`. Endast leverantörens definierade fält
sparas och samtliga måste fyllas i. Uppgifterna krypteras innan
de skrivs. Ett sparande nollställer tidigare testresultat.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [leverantor, uppgifter]
properties:
leverantor: { type: string }
aktiv: { type: boolean, default: true }
uppgifter:
type: object
additionalProperties: { type: string }
responses:
"200": { description: Sparad. }
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
"503":
description: Kryptering är inte konfigurerad (`INTEGRATION_NYCKEL` saknas).
content:
application/json:
schema: { $ref: "#/components/schemas/Fel" }
/api/integrationer/{leverantor}:
delete:
tags: [Integrationer]
summary: Ta bort en kopplings uppgifter
description: Kräver rollen `admin`.
parameters:
- name: leverantor
in: path
required: true
schema: { type: string }
responses:
"200": { description: Borttagen. }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
/api/integrationer/{leverantor}/uppslag:
post:
tags: [Integrationer]
summary: Slå upp ett fordon hos leverantören
description: >
Anropet görs alltid av servern — kundens leverantörsnycklar når
aldrig webbläsaren. Svaret mappas till våra fält enligt
registrets `svarsfalt`. Resultatet skrivs som kopplingens
senaste teststatus.
parameters:
- name: leverantor
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [identifierare]
properties:
identifierare:
type: string
description: VIN eller registreringsnummer beroende på leverantörens `nyckeltyp`.
responses:
"200":
description: Fordonsuppgifter från leverantören.
content:
application/json:
schema:
type: object
properties:
fordon:
type: object
additionalProperties: { type: string }
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"404":
description: Kopplingen är inte konfigurerad för organisationen.
content:
application/json:
schema: { $ref: "#/components/schemas/Fel" }
"502":
description: Leverantören svarade med fel eller inga kända fält.
content:
application/json:
schema: { $ref: "#/components/schemas/Fel" }
"503":
description: Kryptering är inte konfigurerad.
content:
application/json:
schema: { $ref: "#/components/schemas/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:
Fel:
type: object
properties:
error: { type: string }
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: []