ddc11c6ac2
Skillnaden mellan VAD som är fel och VARFÖR felet uppstått är nu kodad:
ett ärende kan aldrig avslutas med enbart "komponent defekt, byt
komponent", och en kundbeskrivning blir aldrig ett konstaterat fel utan
verifiering.
Symptom Verification Protocol:
- Ny händelse reproducering (ja/delvis/nej): ja kräver hur/förhållanden,
delvis vad som kunde respektive inte kunde återskapas, nej kräver
motivering
- Generiska metodiken utökad med SVP-frågorna var/hur
- Rapportens beviskedja skiljer kundens beskrivning, verifierad
observation, felorsaksanalys och rekommenderad åtgärd; "kunde inte
reproduceras under de förhållanden som rådde" i stället för "felet
konstaterat" — kodat även i orkesterns grundprompt
Felorsaksanalys (Root Cause Analysis):
- Ny händelse felorsak: avvikelse + orsakskategorier + underlag +
säkerhetsnivå + rekommenderad åtgärd
- Kvalitetsregeln avvisar generella formuleringar ("trasig", "defekt",
"sliten", "behöver bytas") utan förklaring
- Valda evidenskällor valideras mot loggen — "Foto" godtas bara om ett
foto faktiskt finns
- Okänd orsak kräver motivering; medel/låg säkerhet kräver vilka
ytterligare kontroller som stärker bedömningen
- Avslutsknappen spärrad tills SVP + felorsak dokumenterats;
kvalitetsgrinden gör båda obligatoriska vid stängning
Demoärendet bär hela beviskedjan; 54 vitest-tester, integrationstest
och OpenAPI-validering gröna.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
615 lines
20 KiB
YAML
615 lines
20 KiB
YAML
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/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
|
||
- 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: []
|