Merge main: Guidad Felsökning flyttar ur roten till felsokning/

Main är sedan den här grenen skapades en helt annan produkt — Semantika,
en mobilapp med egen CDK-infrastruktur. Den äger nu repots rot: en
npm-workspaces-monorepo med apps/mobile, services/api och infra.

För att båda ska rymmas i samma repo flyttar Guidad Felsökning in i en
egen katalog i stället för att göra anspråk på roten:

  felsokning/app        webbklienten (Vite, egen package.json och
                        eslint-/vitest-konfiguration)
  felsokning/services   plattformstjänsten och AI-orkestern
  felsokning/infra      Terraform och databasschemat
  felsokning/docs       vision, moduler, drift
  felsokning/supabase   edge-funktion och migrationer

Merge:n hade tagit bort 128 filer som Guidad Felsökning bygger på —
värdapplikationens komponenter, Supabase-klienten, tillgångar — eftersom
main raderat dem och den här grenen inte råkat ändra just dem. De är
återställda på sin nya plats. Utan dem gick varken bygget eller
testerna: ai.ts och synk.ts importerar Supabase-klienten.

Semantikas rotfiler är orörda: package.json, eslint.config.js och
.github/workflows/ är deras. Guidad Felsökning har egna motsvarigheter i
sin katalog.

CI flyttar samtidigt från GitHub Actions till .gitea/workflows — samma
syntax, egna runners. .github/workflows/ tillhör Semantika härefter.

Verifierat på den nya platsen: 96 vitest-tester, typkontroll, eslint på
både klient och tjänster, bygge, och integrationstest mot riktig Postgres.

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-04 12:22:49 +00:00
298 changed files with 23491 additions and 6140 deletions
+1111
View File
@@ -0,0 +1,1111 @@
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
description: >
Takt-begränsad i databasen, så spärren håller bakom flera
repliker: 10 misslyckade försök per konto och 30 per källadress
inom 15 minuter ger 429. Spärren gäller kontot även vid rätt
lösenord — annars vore den meningslös. Ett avstängt konto svarar
403.
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" }
"403":
description: Kontot är avstängt.
content:
application/json:
schema: { $ref: "#/components/schemas/Fel" }
"429":
description: För många misslyckade försök.
content:
application/json:
schema: { $ref: "#/components/schemas/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/anvandare/{anvandarId}/avaktivera:
post:
tags: [Användare]
summary: Stäng av ett konto
description: >
Kräver rollen `admin` och att användaren tillhör samma
organisation. Avstängningen höjer kontots token-version, så
**pågående sessioner upphör omedelbart** — annars vore den
verkningslös tills utfärdade tokens gick ut. Ett konto kan inte
stänga av sig självt.
parameters:
- name: anvandarId
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200":
description: Kontot är avstängt.
content:
application/json:
schema:
type: object
properties:
id: { type: string, format: uuid }
namn: { type: string }
aktiv: { type: boolean }
"400":
description: Försök att stänga av sitt eget konto.
content:
application/json:
schema: { $ref: "#/components/schemas/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
/api/anvandare/{anvandarId}/aktivera:
post:
tags: [Användare]
summary: Öppna ett avstängt konto
description: >
Kräver rollen `admin`. Kontot kan logga in igen, men tokens som
återkallades vid avstängningen förblir ogiltiga.
parameters:
- name: anvandarId
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200": { description: Kontot är öppnat. }
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
/api/auth/logga-ut-alla:
post:
tags: [Auth]
summary: Logga ut på alla enheter
description: >
Höjer den egna token-versionen, vilket gör samtliga utfärdade
tokens för kontot ogiltiga direkt — vägen ut när en enhet
tappats bort.
responses:
"200": { description: Alla sessioner är avslutade. }
"401": { $ref: "#/components/responses/Fel" }
/api/arenden/{arendeId}/bilagor:
post:
tags: [Ärenden]
summary: Ladda upp en bilaga
description: >
Foton, videoklipp och instrumentbilder. Kroppen är råa bytes och
`Content-Type` anger mediatypen — endast bild och video tas emot.
Servern beräknar innehållets SHA-256 och returnerar en referens
som ska läggas i händelsen; **hashen hamnar därmed i den
append-only-skyddade loggen**, så en utbytt bild går att upptäcka.
Innehållsadresserat: samma innehåll lagras en gång.
parameters:
- name: arendeId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
image/jpeg: { schema: { type: string, format: binary } }
image/png: { schema: { type: string, format: binary } }
image/webp: { schema: { type: string, format: binary } }
video/mp4: { schema: { type: string, format: binary } }
video/webm: { schema: { type: string, format: binary } }
responses:
"200":
description: Referensen att spara i händelsen.
content:
application/json:
schema:
type: object
properties:
id: { type: string }
hash: { type: string, description: "SHA-256 av innehållet, hex." }
mediatyp: { type: string }
storlek: { type: integer }
"400": { $ref: "#/components/responses/Fel" }
"401": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
"413":
description: Bilagan är större än 32 MB.
content:
application/json:
schema: { $ref: "#/components/schemas/Fel" }
"415":
description: Endast bilder och videoklipp tas emot.
content:
application/json:
schema: { $ref: "#/components/schemas/Fel" }
/api/bilagor/{bilagaId}:
get:
tags: [Ärenden]
summary: Hämta en bilaga
description: >
Organisationsknuten. Innehållet kontrolleras mot hashen innan det
lämnas ut — stämmer det inte svarar tjänsten 409 i stället för att
visa en bild som kan ha bytts ut.
parameters:
- name: bilagaId
in: path
required: true
schema: { type: string }
responses:
"200":
description: Innehållet.
content:
application/octet-stream:
schema: { type: string, format: binary }
"401": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
"409":
description: Innehållet stämmer inte med hashen i loggen.
content:
application/json:
schema: { $ref: "#/components/schemas/Fel" }
/api/delad/{delningskod}/bilagor/{bilagaId}:
get:
tags: [Delning]
summary: Hämta en bilaga via delningslänk
description: >
Samma filtrering som händelserna: bilagan lämnas bara ut om
händelsen den hör till är synlig på delningens nivå. En bild som
hör till en intern händelsetyp — t.ex. den skannade arbetsordern —
nås alltså aldrig via kundlänken.
security: []
parameters:
- name: delningskod
in: path
required: true
schema: { type: string }
- name: bilagaId
in: path
required: true
schema: { type: string }
responses:
"200":
description: Innehållet.
content:
application/octet-stream:
schema: { type: string, format: binary }
"404": { $ref: "#/components/responses/Fel" }
"409": { $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: []