API-first: OpenAPI 3.0-spec för plattforms-API:t
- services/plattform/openapi.yaml dokumenterar hela ytan: auth (registrera organisation, logga in), användarhantering (admin), ärenden + append-only händelselogg (idempotent synk), arbetsledar- översikten, publik Live Share-delning och AI-orkestern — inklusive scheman för alla 15 händelsetyper, roller, JWT-anspråken och API:ts bärande principer (append-only, multi-tenant-404). - Specen serveras live av plattformstjänsten på GET /api/openapi.yaml och följer med i containern. - Verifierad i tre lager: maskinell validering (swagger-cli), paritetstest i vitest (varje dokumenterad väg finns i servern, händelsetyperna är kompletta) och integrationsteststeg som hämtar specen från den körande tjänsten. Verifierat: integrationstestets 18 kontroller gröna mot Postgres 16, 29 vitest-tester gröna, produktionsbygge ok. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
+2
-1
@@ -50,7 +50,8 @@ kubectl apply -k infra/k8s
|
|||||||
|
|
||||||
# 4. Verifiera
|
# 4. Verifiera
|
||||||
kubectl -n guidad-felsokning get pods
|
kubectl -n guidad-felsokning get pods
|
||||||
curl https://app.exempel.se/halsa # → {"status":"ok"} (plattformen)
|
curl https://app.exempel.se/halsa # → {"status":"ok"} (plattformen)
|
||||||
|
curl https://app.exempel.se/api/openapi.yaml # API-first: hela API-specen
|
||||||
```
|
```
|
||||||
|
|
||||||
Byt domän och cert-issuer i `infra/k8s/ingress.yaml`. Att skapa nya organisationer är öppet i beta — stäng med `REGISTRERING_OPPEN=false` på plattformens Deployment; användare inom en organisation skapas alltid av dess systemadministratör.
|
Byt domän och cert-issuer i `infra/k8s/ingress.yaml`. Att skapa nya organisationer är öppet i beta — stäng med `REGISTRERING_OPPEN=false` på plattformens Deployment; användare inom en organisation skapas alltid av dess systemadministratör.
|
||||||
|
|||||||
+1
-2
@@ -33,7 +33,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
|
|||||||
| Live Share | ✅ Skrivskyddad livevy per ärende (`/felsokning/dela/:id`): status ✔/🔄/⏳, bilder, mätvärdestabell, tidslinje, rekommenderat nästa steg. Uppdateras automatiskt, interna poster filtreras bort. Publik delningssida (`/felsokning/delad/:kod`) läser via `hamta_delat_arende` utan inloggning och pollar för liveuppdatering; "Kopiera delningslänk" finns i rapportfliken. Behörighetsnivåer (kund/intern/partner) i nästa fas. |
|
| Live Share | ✅ Skrivskyddad livevy per ärende (`/felsokning/dela/:id`): status ✔/🔄/⏳, bilder, mätvärdestabell, tidslinje, rekommenderat nästa steg. Uppdateras automatiskt, interna poster filtreras bort. Publik delningssida (`/felsokning/delad/:kod`) läser via `hamta_delat_arende` utan inloggning och pollar för liveuppdatering; "Kopiera delningslänk" finns i rapportfliken. Behörighetsnivåer (kund/intern/partner) i nästa fas. |
|
||||||
| Dashboard | ✅ Enligt direktivet: räknare och filter för Alla/Pågående/Klara plus Starta nytt ärende. |
|
| Dashboard | ✅ Enligt direktivet: räknare och filter för Alla/Pågående/Klara plus Starta nytt ärende. |
|
||||||
| Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. |
|
| Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. |
|
||||||
| Öppet API | 🔶 Datamodellen är API-klar (händelser som JSON i Postgres). REST/OpenAPI-lager i nästa fas. |
|
| Öppet API | ✅ Plattforms-API:t är dokumenterat med OpenAPI 3.0 (`services/plattform/openapi.yaml`) — auth, användare, ärenden/händelser (append-only), översikt, publik delning och AI-orkestern, med scheman för alla händelsetyper. Specen valideras maskinellt, paritetstestas mot serverns rutter och serveras live på `GET /api/openapi.yaml`. |
|
||||||
|
|
||||||
## Arkitekturprinciper i koden
|
## Arkitekturprinciper i koden
|
||||||
|
|
||||||
@@ -47,4 +47,3 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
|
|||||||
- Omfördelning av ärenden (arbetsledare) är inte implementerad — överlämningshändelsen täcker handover tills en ansvarig-modell införs.
|
- Omfördelning av ärenden (arbetsledare) är inte implementerad — överlämningshändelsen täcker handover tills en ansvarig-modell införs.
|
||||||
- QR/streckkod/OCR för objektidentifiering, bildanalys och tillverkarintegrationer ingår inte ännu.
|
- QR/streckkod/OCR för objektidentifiering, bildanalys och tillverkarintegrationer ingår inte ännu.
|
||||||
- Live Share-behörighetsnivåerna (kund/intern/partner) är specificerade men inte byggda — dagens delningslänk motsvarar kundnivån.
|
- Live Share-behörighetsnivåerna (kund/intern/partner) är specificerade men inte byggda — dagens delningslänk motsvarar kundnivån.
|
||||||
- OpenAPI-dokumentation av plattforms-API:t återstår (API-first-kravet).
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ FROM node:22-alpine
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force
|
RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force
|
||||||
COPY server.mjs ./
|
COPY server.mjs openapi.yaml ./
|
||||||
|
|
||||||
ENV NODE_ENV=production PORT=8080
|
ENV NODE_ENV=production PORT=8080
|
||||||
USER node
|
USER node
|
||||||
|
|||||||
@@ -112,4 +112,11 @@ kontroll "översikten härleder felbeskrivning" "$(echo "$OVERSIKT" | falt '.are
|
|||||||
kontroll "översikten härleder status" "$(echo "$OVERSIKT" | falt '.arenden[0].avslutat')" "false"
|
kontroll "översikten härleder status" "$(echo "$OVERSIKT" | falt '.arenden[0].avslutat')" "false"
|
||||||
kontroll "översikten räknar händelser" "$(echo "$OVERSIKT" | falt '.arenden[0].antal_handelser')" "2"
|
kontroll "översikten räknar händelser" "$(echo "$OVERSIKT" | falt '.arenden[0].antal_handelser')" "2"
|
||||||
|
|
||||||
|
# 10. API-first: OpenAPI-specen serveras live, utan inloggning
|
||||||
|
SPEC=$(curl -s "$BAS/api/openapi.yaml")
|
||||||
|
case "$SPEC" in
|
||||||
|
"openapi: 3.0.3"*) echo "✓ OpenAPI-specen serveras på /api/openapi.yaml" ;;
|
||||||
|
*) echo "✗ OpenAPI-specen saknas"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "Integrationstest: allt grönt"
|
echo "Integrationstest: allt grönt"
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
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`.
|
||||||
|
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/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/delad/{delningskod}:
|
||||||
|
get:
|
||||||
|
tags: [Delning]
|
||||||
|
summary: Publik Live Share-vy via delningskod
|
||||||
|
description: >
|
||||||
|
Kräver ingen inloggning — delningskoden är nyckeln. Interna poster
|
||||||
|
(kategoribyten, hypoteser, AI-dialog) är bortfiltrerade.
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- name: delningskod
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema: { type: string, pattern: "^[a-z0-9-]+$" }
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Ärendet med kunddelbar 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" }
|
||||||
|
"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]
|
||||||
|
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
|
||||||
|
- 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: []
|
||||||
@@ -16,8 +16,14 @@
|
|||||||
|
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
import pg from "pg";
|
import pg from "pg";
|
||||||
|
|
||||||
|
// API-first: OpenAPI-specen är en versionerad artefakt och serveras live.
|
||||||
|
const OPENAPI = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "openapi.yaml"), "utf8");
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT ?? 8080);
|
const PORT = Number(process.env.PORT ?? 8080);
|
||||||
const MAX_KROPP = 4 * 1024 * 1024;
|
const MAX_KROPP = 4 * 1024 * 1024;
|
||||||
const TOKEN_LIVSTID_S = 12 * 60 * 60;
|
const TOKEN_LIVSTID_S = 12 * 60 * 60;
|
||||||
@@ -117,6 +123,13 @@ export function skapaServer() {
|
|||||||
if (req.method === "GET" && vag === "/halsa") {
|
if (req.method === "GET" && vag === "/halsa") {
|
||||||
return svara(res, 200, { status: "ok" });
|
return svara(res, 200, { status: "ok" });
|
||||||
}
|
}
|
||||||
|
if (req.method === "GET" && vag === "/api/openapi.yaml") {
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": "application/yaml; charset=utf-8",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
});
|
||||||
|
return res.end(OPENAPI);
|
||||||
|
}
|
||||||
|
|
||||||
const hemlighet = process.env.JWT_SECRET;
|
const hemlighet = process.env.JWT_SECRET;
|
||||||
if (!hemlighet) return svara(res, 503, { error: "Tjänsten är inte konfigurerad." });
|
if (!hemlighet) return svara(res, 503, { error: "Tjänsten är inte konfigurerad." });
|
||||||
|
|||||||
@@ -61,6 +61,33 @@ describe("AI-orkestern", () => {
|
|||||||
expect(schema).toContain("check (roll in ('tekniker', 'arbetsledare', 'admin'))");
|
expect(schema).toContain("check (roll in ('tekniker', 'arbetsledare', 'admin'))");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("OpenAPI-specen och plattformsservern täcker samma endpoints", () => {
|
||||||
|
const spec = readFileSync("services/plattform/openapi.yaml", "utf8");
|
||||||
|
const server = readFileSync("services/plattform/server.mjs", "utf8");
|
||||||
|
// Varje dokumenterad väg finns i servern …
|
||||||
|
const vagar: [string, string][] = [
|
||||||
|
["/halsa", "/halsa"],
|
||||||
|
["/api/openapi.yaml", "/api/openapi.yaml"],
|
||||||
|
["/api/auth/registrera", "/api/auth/registrera"],
|
||||||
|
["/api/auth/logga-in", "/api/auth/logga-in"],
|
||||||
|
["/api/anvandare", "/api/anvandare"],
|
||||||
|
["/api/arenden", "/api/arenden"],
|
||||||
|
["/api/arenden/{arendeId}/handelser", "handelser"],
|
||||||
|
["/api/oversikt", "/api/oversikt"],
|
||||||
|
["/api/delad/{delningskod}", "delad"],
|
||||||
|
];
|
||||||
|
for (const [iSpec, iServer] of vagar) {
|
||||||
|
expect(spec).toContain(`${iSpec}:`);
|
||||||
|
expect(server).toContain(iServer);
|
||||||
|
}
|
||||||
|
// … och AI-endpointen + händelsetyperna är dokumenterade.
|
||||||
|
expect(spec).toContain("/api/ai:");
|
||||||
|
expect(spec).toContain("append-only");
|
||||||
|
for (const typ of ["objekt_identifierat", "kontroll_utford", "ai_svar", "arende_avslutat"]) {
|
||||||
|
expect(spec).toContain(typ);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("endpointen kodar AI-reglerna i grundprompten", () => {
|
it("endpointen kodar AI-reglerna i grundprompten", () => {
|
||||||
expect(ENDPOINT).toContain("Hitta aldrig på fakta");
|
expect(ENDPOINT).toContain("Hitta aldrig på fakta");
|
||||||
expect(ENDPOINT).toContain("aldrig en hypotes som ett konstaterat fel");
|
expect(ENDPOINT).toContain("aldrig en hypotes som ett konstaterat fel");
|
||||||
|
|||||||
Reference in New Issue
Block a user