9035dfebc9
Filerna byter namn så att basnamnet är den engelska versionen: SYSTEM-DESCRIPTION.md är dokumentet, övriga språk är suffixade. Bytet gjordes med git mv så historiken följer med. Auktoritetskedjan vänds: engelskan säger att den är gällande version och räknar upp översättningarna; svenska, tyska, danska och norska pekar nu på engelskan i stället för på svenskan. Två dokument som båda påstår sig gälla blir i praktiken två sanningar, så exakt ett måste vara källan. Svenskan blir därmed en översättning bland de andra. Den behåller en egen not: kodidentifierarna är oöversatta även där, men där syns det inte eftersom koden är svensk — värt att veta för den som jämför med en annan språkversion. Engelskans ingress säger uttryckligen att dokumentets språk inte ändrar vad koden heter. Det är den missuppfattning som annars uppstår när ett engelskt dokument beskriver ett svenskt kodbas: läsaren börjar söka på översatta namn och hittar ingenting. Alla 48 numrerade avsnitt finns kvar i varje version, verifierat maskinellt, och inga länkar pekar på de gamla filnamnen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
942 lines
42 KiB
Markdown
942 lines
42 KiB
Markdown
# Guidad Felsökning (Guided Diagnostics) — complete system description
|
||
|
||
> **This is the canonical version.** Translations: [Swedish](SYSTEM-DESCRIPTION.sv.md) · [German](SYSTEM-DESCRIPTION.de.md) · [Danish](SYSTEM-DESCRIPTION.da.md) · [Norwegian](SYSTEM-DESCRIPTION.no.md).
|
||
> Where a translation disagrees with this document, this document is correct.
|
||
>
|
||
> **Code identifiers are Swedish and are not translated.** Event types, function
|
||
> names, field names, file paths and configuration keys are Swedish *in the code
|
||
> itself* — the domain is Swedish and the implementation follows it. Translating
|
||
> them here would make this document useless against the repository, so they
|
||
> appear verbatim, with an English gloss where the meaning is not obvious. This
|
||
> document being English does not change what the code is called.
|
||
>
|
||
> A self-contained reference document. Everything below is taken from the code
|
||
> in `felsokning/` (branch `claude/guidad-felsokning-vision-1mnx7f`), not from
|
||
> plans or intentions. Where something does **not** exist, this is stated
|
||
> explicitly.
|
||
>
|
||
> Last synchronised against code: commit `1bb4031`, 2026-08-04.
|
||
|
||
---
|
||
|
||
## 0. Summary in thirty seconds
|
||
|
||
Guidad Felsökning is a SaaS platform for **vehicle workshops**. It guides a
|
||
technician through a structured diagnostic process, requires evidence for every
|
||
claim, and produces a traceable record that can be shared with the customer, an
|
||
insurer, or the next technician.
|
||
|
||
The load-bearing idea is negative rather than positive: **the system never
|
||
presents a hypothesis as a confirmed fault.** This is not a policy in a document
|
||
— it is coded, tested, and it blocks flows. When evidence is missing the system
|
||
says "Evidens saknas" (evidence missing), not a qualified guess.
|
||
|
||
Technically: an **append-only event log** is the single source of truth.
|
||
Everything else — the case view, the brief, the customer report, the quality
|
||
gate, the statistics — is a pure projection of the log and can always be
|
||
regenerated.
|
||
|
||
| | |
|
||
|---|---|
|
||
| Client | React 18 + TypeScript + Vite + Tailwind + zustand + react-router |
|
||
| Backend | Two Node services (`plattform`, `ai-orkester`), plain `node:http`, minimal dependencies |
|
||
| Database | PostgreSQL (Aurora Serverless v2), append-only enforced by database triggers |
|
||
| Model | Claude, server-owned routing per task |
|
||
| Infrastructure | AWS + EKS, 126 Terraform resources across two layers |
|
||
| Git & CI | **Self-hosted Gitea + Actions runners on our own EKS** — no GitHub in the operational path |
|
||
| Tests | 120 vitest tests + an integration test against real Postgres |
|
||
| Language in code | Swedish (identifiers, comments, commit messages) |
|
||
|
||
---
|
||
|
||
## 1. Product principles
|
||
|
||
These five are invariants, not guidelines. Each has a counterpart in code and in
|
||
a test.
|
||
|
||
### 1.1 No hypothesis is presented as a confirmed fault
|
||
|
||
Hypotheses are their own event type (`hypotes`) with a mandatory confidence
|
||
level, and can **never** take the level `hog` (high) —
|
||
`niva: Exclude<Tillforlitlighet, "hog">`; the type system forbids it. In the
|
||
customer report they are explicitly marked as unverified. The quality gate has a
|
||
dedicated row for this.
|
||
|
||
The wording on failed reproduction is *"kunde inte reproduceras under de
|
||
förhållanden som rådde"* ("could not be reproduced under the conditions that
|
||
prevailed") — never "fault confirmed" or "no fault found". This is coded in both
|
||
the projections and the orchestrator's base prompt.
|
||
|
||
### 1.2 A checkbox is not evidence
|
||
|
||
Every check item in every methodology carries a **minimum requirement**:
|
||
`matvarde` (measured value) | `kommentar` (observation) | `foto` (photo). A
|
||
measurement cannot be marked complete without a value; a photo check cannot be
|
||
marked complete without an image. If the technician wants to skip something, a
|
||
**documented exemption** with a reason from a fixed list is required.
|
||
|
||
Locked by the test *"varje kontroll kräver bevis — en kryssruta är inte
|
||
evidens"*.
|
||
|
||
### 1.3 The log is append-only, all the way down
|
||
|
||
There are no update or delete operations in the API, and the database has
|
||
triggers that reject them even if someone bypasses the application. A test
|
||
actively searches the server code for `update`/`delete` against the event table
|
||
and fails if they appear.
|
||
|
||
Consequence: an incorrect entry is *corrected by a new event*, never by making
|
||
the old one disappear. The history is what gives the record its value in a
|
||
dispute.
|
||
|
||
### 1.4 Terminology
|
||
|
||
In the UI and in customer communication the words used are **the system, the
|
||
analysis, the assessment, the decision support** — not "AI", unless technically
|
||
necessary. The product is described as an *evidence-based diagnostic system* /
|
||
*intelligent decision support*.
|
||
|
||
The reason is both commercial and epistemic: a workshop customer who hears "AI"
|
||
hears "guess". An insurance assessor who reads "AI assessment" in a record
|
||
weights it lower.
|
||
|
||
### 1.5 The sharing boundary is an allowlist
|
||
|
||
What may leave the organisation is enumerated **positively**, per level. A new
|
||
event type is therefore internal until someone actively releases it. A test
|
||
requires every type in the domain model to be classified — if one is forgotten
|
||
the build fails, instead of it leaking.
|
||
|
||
---
|
||
|
||
## 2. The domain model — the event log
|
||
|
||
`app/src/felsokning/domain.ts` (281 lines).
|
||
|
||
A case is: identity + metadata + an **ordered list of log entries**. Each entry
|
||
carries `id`, `tidpunkt` (timestamp), `tekniker` (technician) and a `handelse`
|
||
(event).
|
||
|
||
### 2.1 All event types
|
||
|
||
| Type | Contents | Role |
|
||
|---|---|---|
|
||
| `objekt_identifierat` | `objekt` (plate/VIN, make, model, engine …) | What the case concerns |
|
||
| `arbetsorder_skannad` | `falt[]` + attachment | Interpreted work order (**internal**) |
|
||
| `felbeskrivning` | `text` | The customer's words, verbatim |
|
||
| `arendetyp_satt` | `arendetyp` | Warranty / insurance / customer — selects rule pack |
|
||
| `fraga_besvarad` | `stegId`, `frageId`, `fraga`, `svar` | Methodology symptom questions |
|
||
| `kontroll_utford` | `stegId`, `kontrollId`, `text`, `resultat?`, `undantag?` | Verified checklist item |
|
||
| `observation` | `text` | What the technician saw — not what they believe |
|
||
| `matvarde` | `beskrivning`, `varde`, `enhet?` | Measurement (E4) |
|
||
| `hypotes` | `text`, `niva` (never `hog`) | Working hypothesis (**internal**) |
|
||
| `foto` | `beskrivning` + attachment | Photographic evidence (E2) |
|
||
| `video` | `beskrivning` + attachment | Moving-image evidence (E3) |
|
||
| `matarstallning` | `lage` (in/out), `varde` + attachment | Odometer in/out |
|
||
| `historik_kontrollerad` | `kontrollerad`, `kommentar?` | Service history |
|
||
| `reproducering` | `status` (ja/delvis/nej), `beskrivning` | **Symptom verification** |
|
||
| `felorsak` | structured root-cause analysis | Cause, category, supporting evidence |
|
||
| `atgardsforslag` | proposal with justification | What should be done |
|
||
| `kundbeslut` | approved/declined, channel | The customer's decision |
|
||
| `atgard_utford` | work performed | What was actually done |
|
||
| `kvalitetskontroll` | verification after repair | Is the symptom gone? |
|
||
| `kommentar` | `text` | Free note |
|
||
| `kategori_byte` | `kategori` | Time accounting (**internal**) |
|
||
| `inaktivitet_forklarad` | `text`, `minuter` | Why work stood still |
|
||
| `overlamning` | `fran`, `till?` | Shift handover |
|
||
| `ansvarig_satt` | `ansvarig` | Supervisor reassignment (**internal**) |
|
||
| `ai_svar` | classified `rader[]`, model name | Decision-support response (**internal**) |
|
||
| `export_skapad` | `format`, `version` | The export logs itself |
|
||
| `arende_avslutat` | `signatur?` | Technician's sign-off |
|
||
|
||
### 2.2 Attachments are content-addressed
|
||
|
||
`foto`, `video`, `matarstallning` and `arbetsorder_skannad` are *intersection
|
||
types* with `Bilaga` (attachment):
|
||
|
||
```ts
|
||
export interface Bilaga {
|
||
bilagaId?: string;
|
||
bilagaHash?: string; // SHA-256
|
||
dataUrl?: string; // stays forever — the log is append-only
|
||
}
|
||
```
|
||
|
||
The content lives outside the log (S3 or database), but **the hash lives in the
|
||
log**. On read the hash is verified; if it does not match, `409` is returned.
|
||
The meaning: if someone swaps an image in storage it is detected, and the log
|
||
can prove the original image was a different one.
|
||
|
||
`dataUrl` is kept in the type because older entries have it embedded — and the
|
||
log cannot be rewritten.
|
||
|
||
---
|
||
|
||
## 3. The methodology engine
|
||
|
||
Since the most recent change, **engine and content are separated**:
|
||
|
||
- `metodik.ts` (171 lines) — types, methodology selection, derivation of the
|
||
next step.
|
||
- `metodiker.ts` (899 lines) — the sixteen methodologies.
|
||
|
||
The library can grow without the engine changing.
|
||
|
||
### 3.1 The methodology library
|
||
|
||
Step ids are code and stay Swedish; an English gloss follows each table row
|
||
where useful. `symptom` = symptom, `visuell` = visual, `matningar` =
|
||
measurements, `provkorning` = road test, `sakerhet` = safety, `avlasning` =
|
||
readout.
|
||
|
||
| id | Name (in code) | Area | Steps | Checks |
|
||
|---|---|---|---|---|
|
||
| `vibration` | Vibration under körning | Wheels and balance | symptom → visuell → kontroller → provkorning | 19 |
|
||
| `bromsar` | Bromssystem | Chassis | symptom → visuell → matningar → system | 14 |
|
||
| `styrning_fjadring` | Styrning och fjädring | Chassis | symptom → visuell → glapp → installning | 11 |
|
||
| `elsystem` | Elsystem och strömförsörjning | Electrical | symptom → visuell → matningar → rela → funktionstest | 12 |
|
||
| `start_laddning` | Start- och laddningssystem | Electrical | symptom → batteri → start → laddning → krypstrom | 15 |
|
||
| `motor_drift` | Motorgång och effekt | Engine | symptom → felkoder → mekanik → tandning_bransle → provkorning | 16 |
|
||
| `kylsystem` | Kylsystem och överhettning | Engine | symptom → visuell → matningar → packning | 12 |
|
||
| `drivlina` | Växellåda och drivlina | Drivetrain | symptom → visuell → matningar → provkorning | 10 |
|
||
| `avgas_emission` | Avgassystem och emissioner | Engine | symptom → avlasning → matningar → orsak | 12 |
|
||
| `klimat` | Klimatanläggning | Comfort | symptom → visuell → matningar → styrning | 11 |
|
||
| `hogvolt` | Högvoltsystem — elbil och hybrid | High voltage | **sakerhet** → symptom → avlasning → laddning | 16 |
|
||
| `diagnos_natverk` | Felkoder och kommunikation | Diagnostics | symptom → grund → buss → koder | 10 |
|
||
| `lackage` | Läckage | Other | symptom → visuell → metod | 8 |
|
||
| `missljud` | Missljud | Other | symptom → inspelning → lokalisering | 7 |
|
||
| `adas` | Förarassistans och kalibrering | Diagnostics | symptom → forutsattningar → kalibrering | 9 |
|
||
| `generisk` | Generell strukturerad felsökning | Other | symptom → visuell → grundkontroller → funktionstest | 9 |
|
||
|
||
English names: vibration while driving · braking system · steering and
|
||
suspension · electrical system and power supply · starting and charging ·
|
||
engine running and power · cooling and overheating · gearbox and drivetrain ·
|
||
exhaust and emissions · climate control · high-voltage system (EV/hybrid) ·
|
||
fault codes and communication · leakage · abnormal noise · driver assistance and
|
||
calibration · generic structured diagnosis.
|
||
|
||
### 3.2 Three rules, locked by tests
|
||
|
||
1. **Every check has a minimum requirement.** Measured value, photo or
|
||
observation.
|
||
2. **Every methodology begins by verifying the symptom**, never by repairing.
|
||
The customer's words become a verified symptom only once reproduced.
|
||
3. **Where the work can injure someone, the safety step comes first.** Only
|
||
`sakerhet` may precede `symptom` — the test permits exactly that exception
|
||
and no other.
|
||
|
||
`hogvolt` is the only methodology with a safety step. It requires
|
||
authorisation, a documented removed service disconnect (photo), the
|
||
manufacturer's waiting time, **measured absence of voltage** (a measured value —
|
||
not a yes to a question), and protective equipment. The test checks that the
|
||
step comes first, that `spanningsfrihet` requires a measured value, and that the
|
||
description contains the word "livsfarlig" (potentially lethal).
|
||
|
||
The reason is simple: that work can kill someone. A checkbox will not do there.
|
||
|
||
### 3.3 Methodology selection
|
||
|
||
Previously a regex chain with three outcomes. Now **scored keyword matching**:
|
||
|
||
```ts
|
||
export function metodikPoang(metodik: Metodik, text: string): number
|
||
export function valjMetodik(felbeskrivning: string): Metodik
|
||
```
|
||
|
||
- Score = the sum of the lengths of the keywords that match. A longer — more
|
||
specific — word weighs more. `traktionsbatteri` (16) beats `batteri`.
|
||
- **Short words (≤3 characters) match as whole words, longer ones as stems.**
|
||
Otherwise `"ac"` would have matched *acceleration* and a vibration would have
|
||
ended up in the climate-control methodology.
|
||
- On a tie the one listed first in the library wins → the selection is **stable**
|
||
across runs.
|
||
- No match → `generisk`.
|
||
|
||
**A pitfall that actually bit during development:** the keywords must be
|
||
*stems*, not fully inflected words. Swedish inflection often drops an `e`:
|
||
*filter → filtret*, so `"partikelfilter"` never matches the text a technician
|
||
actually writes. The same applies to *regenerering → regenererar*,
|
||
*misständning → misständer*, *skrammel → skramlar*. The library therefore uses
|
||
`partikelfilt`, `regenerer`, `misständ`, `skram`.
|
||
|
||
*(For readers translating this to another market: this is a property of Swedish
|
||
morphology, and the same class of problem exists in German compounding and
|
||
Danish/Norwegian definite forms. A localised keyword set has to be validated
|
||
against the same test, not translated word-for-word.)*
|
||
|
||
**The selection is a question order, not a diagnosis.** It decides where the
|
||
technician starts looking, not what is wrong. If nothing matches, `generisk` is
|
||
the honest answer — structurally complete, and better than a guess.
|
||
|
||
### 3.4 Next step
|
||
|
||
```ts
|
||
export function nastaSteg(arende: Arende, metodik: Metodik): NastaSteg
|
||
```
|
||
|
||
Purely derived from the log: the first unanswered question, then the first
|
||
un-performed check, in the methodology's order. No hidden state machine — the
|
||
same log always yields the same next step.
|
||
|
||
### 3.5 On "covering everything"
|
||
|
||
That cannot be promised honestly, and the documentation does not claim it. What
|
||
is possible is to cover the vehicle's systems systematically and let `generisk`
|
||
be a structurally complete safety net for what nobody anticipated.
|
||
|
||
---
|
||
|
||
## 4. ECM v2.0 — the evidence and rule engine
|
||
|
||
`app/src/felsokning/ecm.ts` (749 lines). Six engines:
|
||
|
||
### 4.1 Evidence Engine
|
||
|
||
Evidence levels, derived from the log:
|
||
|
||
| Level | Meaning |
|
||
|---|---|
|
||
| E0 | No supporting evidence |
|
||
| E1 | Technician's observation |
|
||
| E2 | Photo |
|
||
| E3 | Video |
|
||
| E4 | Measured value |
|
||
| E5 | Diagnostic data / document |
|
||
| E6 | Multiple independent sources |
|
||
|
||
A case's evidence level is the highest the record supports. It is shown in the
|
||
UI and travels with the export.
|
||
|
||
**Content hash:** `innehallsHash()` is a deterministic FNV-1a over the evidence
|
||
content. The same record ⇒ the same hash, regardless of machine or moment. That
|
||
makes the export verifiable after the fact.
|
||
|
||
### 4.2 Rule Engine
|
||
|
||
- `ORSAKSKATEGORIER` — fixed list of root-cause categories (gives comparable
|
||
statistics across the fleet).
|
||
- `UNDANTAGSORSAKER` — fixed list of reasons for "why this was not done".
|
||
- `UNDERLAGSKALLOR` — what a conclusion rests on.
|
||
- `INGEN_ATGARD_ORSAKER`, `KUNDKANALER` (customer channels).
|
||
- `granskaAvvikelse()` — flags text phrased as a statement of fact without
|
||
cover.
|
||
|
||
Fixed lists instead of free text is a deliberate choice: free text cannot be
|
||
aggregated, and fleet statistics are one of the product's real assets.
|
||
|
||
### 4.3 Compliance Engine
|
||
|
||
`ARENDETYPER` (case types) selects which **rule pack** applies. A warranty case
|
||
requires a claim number and service history; an insurance case requires a claim
|
||
reference and photographic evidence; a customer-paid case requires less. The
|
||
packs are data (`ecm-regler.json`, servable via `/api/ecm/regler`) — new
|
||
requirements need no new release.
|
||
|
||
### 4.4 Validation Engine — pre-diagnostics
|
||
|
||
Before diagnosis may begin: object identification verified, work order read in,
|
||
vehicle history checked **or justified**, incoming odometer documented,
|
||
customer's fault description verified, early observations handled.
|
||
|
||
### 4.5 Completion Engine — the quality gate
|
||
|
||
The largest single function (`kvalitetsgrind`, ~240 lines). The case cannot be
|
||
closed until every row is green or justified:
|
||
|
||
- Vehicle history checked or justified
|
||
- Incoming/outgoing odometer documented
|
||
- Customer's fault description verified
|
||
- **Symptom verification:** reproduced, or documented as non-reproducible
|
||
- Root-cause analysis documented
|
||
- Repair documented or justified
|
||
- Customer's decision on the proposal recorded
|
||
- Work performed despite a declined proposal (where applicable)
|
||
- Quality check performed — symptom verified
|
||
- Methodology checks: evidence or documented exemption
|
||
- Photos present for photo-requiring checks
|
||
- Technician's conclusion signed
|
||
- Hypotheses presented as unverified
|
||
- The case type's rule pack satisfied (claim / insurance reference / odometer /
|
||
history)
|
||
|
||
### 4.6 Traceability Engine
|
||
|
||
`sparbarhetspaket()` — the whole chain of evidence in one structured object:
|
||
what is claimed, what it rests on, who documented it and when.
|
||
|
||
---
|
||
|
||
## 5. Symptom verification (SVP)
|
||
|
||
Its own principle, because it is the product's sharpest edge against reality.
|
||
|
||
**The customer's description ≠ a confirmed fault.**
|
||
|
||
1. The description is documented **verbatim** (`felbeskrivning`).
|
||
2. It is clarified through the methodology's symptom questions — *when, where,
|
||
how*, never "what is wrong".
|
||
3. It is **reproduced**, with three possible outcomes:
|
||
- **Yes** — with documented conditions.
|
||
- **Partly** — what could and could not be recreated.
|
||
- **No** — mandatory justification.
|
||
|
||
The report's chain of evidence separates four things that otherwise get mixed
|
||
together: *the customer's description*, *verified observation*, *root-cause
|
||
analysis* and *recommended action*.
|
||
|
||
---
|
||
|
||
## 6. The client
|
||
|
||
`app/src/felsokning/` + `app/src/pages/felsokning/`.
|
||
|
||
| Module | Lines | Responsibility |
|
||
|---|---|---|
|
||
| `ArendeSida.tsx` | 2433 | The case view. Three-column layout on desktop |
|
||
| `metodiker.ts` | 899 | The methodology library |
|
||
| `ecm.ts` | 749 | Rule and evidence engine |
|
||
| `NyttArende.tsx` | 497 | Case start, work-order scanning |
|
||
| `Arendelista.tsx` | 399 | Dashboard: counters, filters |
|
||
| `projektioner.ts` | 356 | All views as pure functions of the log |
|
||
| `ai.ts` | 305 | Client side of the orchestrator, prompt building, response parsing |
|
||
| `plattform.ts` | 296 | API client against the self-hosted platform |
|
||
| `DelatArendeVy.tsx` | 283 | Shared view (customer/partner/internal) |
|
||
| `domain.ts` | 281 | Event types |
|
||
| `Installningar.tsx` | 281 | Organisation, users, integrations |
|
||
| `Oversikt.tsx` | 238 | Supervisor view |
|
||
| `demo.ts` | 200 | Demo case with 1 h 35 min of history |
|
||
| `ui.tsx` | 174 | Industrial workshop UI |
|
||
| `metodik.ts` | 171 | The methodology engine |
|
||
| `synk.ts` | 141 | Conflict-free merging of events |
|
||
| `ikoner.tsx` | 132 | Own SVG line icons (no emojis) |
|
||
| `streckkod.ts` | 131 | Barcode/VIN reading |
|
||
| `store.ts` | 106 | zustand store |
|
||
| `bilagor.ts` | 96 | Upload + blob-URL cache |
|
||
| `installningar.ts` | 86 | Organisation settings |
|
||
| `Bilagevisning.tsx` | 69 | `<Bild>` / `<Klipp>` |
|
||
| `Mikrofon.tsx` / `rost.ts` | 66 / 65 | Speech recognition |
|
||
| `format.ts` | 48 | Photo scaling etc. |
|
||
|
||
### 6.1 The projections
|
||
|
||
```
|
||
objekt · felbeskrivning · ansvarig · arendeidentitet · arAvslutat
|
||
lokalFordonshistorik · utfordaKontroller · ejKontrollerat
|
||
observationer · hypoteser · foton · videor
|
||
tidsfordelning · formateraTid · tillforlitlighet
|
||
brief · overlamningstext · tidsfordelningsRader · sistaAktivitet
|
||
```
|
||
|
||
All pure functions of `Arende`. `ejKontrollerat` ("not yet checked") is the one
|
||
that saves the most time in practice: *what causes duplicated work at shift
|
||
change is the thing nobody wrote down that nobody did.*
|
||
|
||
### 6.2 UI language
|
||
|
||
An ETKA-inspired workshop UI: flat light-grey surfaces (#ECECEC/#F7F7F7), sharp
|
||
edges, deep navy as the primary colour, dense typography (11–15 px), rectangular
|
||
buttons (max 4 px radius), toolbar ~44 px. Own line icons instead of emojis;
|
||
status as colour dots.
|
||
|
||
The motive: the technician is wearing gloves, standing in a noisy space, and has
|
||
no time for an airy consumer UI.
|
||
|
||
### 6.3 Local mode
|
||
|
||
Without login the app works against `localStorage`. The methodology guides
|
||
alone; the orchestrator is off. Status is shown in the case header. On login,
|
||
local events are merged with the server's — conflict-free per event id, tested.
|
||
|
||
---
|
||
|
||
## 7. Backend
|
||
|
||
### 7.1 `services/plattform` (1210 lines)
|
||
|
||
Plain `node:http`. The only dependency is `pg`.
|
||
|
||
**API routes:**
|
||
|
||
```
|
||
GET /halsa health
|
||
GET /api/openapi.yaml
|
||
POST /api/auth/registrera creates organisation + system administrator
|
||
POST /api/auth/logga-in log in
|
||
POST /api/auth/logga-ut-alla raises token_version → every session dies
|
||
GET /api/anvandare users; admin only
|
||
POST /api/anvandare
|
||
POST /api/anvandare/{id}/avaktivera | /aktivera
|
||
GET /api/organisation
|
||
GET/PUT /api/organisation/installningar
|
||
GET /api/ecm/regler rule packs as data
|
||
GET/POST /api/arenden cases
|
||
POST /api/arenden/{id}/handelser append-only
|
||
POST /api/arenden/{id}/bilagor attachments
|
||
GET /api/bilagor/{id} hash verified on read
|
||
GET /api/fordon/{identifierare}/historik
|
||
GET /api/statistik/felorsaker root-cause statistics
|
||
GET /api/oversikt supervisor view
|
||
GET /api/delad/{kod} shared, filtered by level
|
||
POST /api/delad/{kod}/beslut customer decision without login
|
||
GET /api/delad/{kod}/bilagor/{id} level-filtered
|
||
GET /api/integrationer/leverantorer
|
||
GET/PUT/DELETE /api/integrationer/{leverantor}
|
||
POST /api/integrationer/{leverantor}/uppslag
|
||
```
|
||
|
||
There are no update or delete routes against case data. By design.
|
||
|
||
**Security functions in the service:**
|
||
|
||
```
|
||
ursprungFor · forTataForsok · kallaFor · inloggningSparrad · loggaForsok
|
||
skapaJwt · verifieraJwt · kontoGiltigt · kravAuth · arendeIOrg
|
||
integrationsNyckel · kryptera · dekryptera · maskera
|
||
arPrivatAdress · pekarInat · gorUppslag · skickaBilaga · synligaTyper
|
||
```
|
||
|
||
### 7.2 `services/ai-orkester` (400 lines)
|
||
|
||
Owns the Claude key. The client **never** has it. Routing per task:
|
||
|
||
| Task | Model | Effort | Vision |
|
||
|---|---|---|---|
|
||
| `handledning` (live guidance) | `claude-sonnet-5` | medium | — |
|
||
| `granskning` (deep review) | `claude-opus-5` | **high** | — |
|
||
| `sammanfattning` (handover summary) | `claude-sonnet-5` | low | — |
|
||
| `metodikval` (classification) | `claude-haiku-4-5` | *(none — the model does not take the parameter)* | — |
|
||
| `instrumentavlasning` (instrument reading) | `claude-sonnet-5` | low | ✔ |
|
||
| `dokumenttolkning` (document reading) | `claude-sonnet-5` | low | ✔ |
|
||
|
||
All responses are **schema-bound** (`json_schema`). The base prompt encodes the
|
||
rules: *"Never invent facts"*, *"never a hypothesis as a confirmed fault"*,
|
||
*"REQUIRES verification"*. On a declined request there is automatic fallback to
|
||
a reserve model. **The model that answered is logged in every `ai_svar`
|
||
event** — the record must be auditable after the fact.
|
||
|
||
The methodology catalogue is built from a single list (`METODIK_KATALOG`) that
|
||
generates both the schema's `enum` and the prompt's bullet list. A test compares
|
||
it against the client's library: if the lists drift apart, the classifier
|
||
returns an id the client does not recognise, and the selection would fall back
|
||
*silently* to generic. Now the test fails instead.
|
||
|
||
### 7.3 `services/gemensam/observation.mjs` (166 lines)
|
||
|
||
Tracing and metrics **with no new dependencies**. The services deliberately have
|
||
almost no dependencies; pulling in an OpenTelemetry SDK with thirty packages to
|
||
measure four things would be the wrong trade. Instead, two standards that are
|
||
both just text on stdout:
|
||
|
||
- **W3C Trace Context** — `traceparent` travels through the whole chain
|
||
(client → platform → orchestrator).
|
||
- **CloudWatch EMF** — structured JSON from which CloudWatch itself extracts
|
||
metrics. No agent, no SDK, nothing that can silently stop working.
|
||
|
||
```
|
||
spårFrån(header) · traceparent(trace) · starta(name, trace)
|
||
→ .mät(partName, work) · .ms() · .delar()
|
||
logga(level, message, fields) · mätvärde(name, value, unit, dims, extra)
|
||
avsluta(span, { status, väg, extra })
|
||
```
|
||
|
||
What is measured was chosen from one question: *what do you want to know at
|
||
three in the morning when something is slow?* The answer is **where the time
|
||
went** — not how many calls were made. Hence `delar()` ("parts"): the database,
|
||
the model call, object storage, the customer's vendor, with count and sum per
|
||
part in the same log line.
|
||
|
||
`mät()` measures even when the work throws — otherwise errors look like zero
|
||
time.
|
||
|
||
**Dimensions are deliberately few.** Every unique combination is its own time
|
||
series that costs money, so organisation, case and trace id must never become
|
||
dimensions — they are ordinary fields. Locked by a test that explicitly forbids
|
||
`org`, `organisation`, `arende`, `spårId`, `anvandare` among the dimensions.
|
||
|
||
---
|
||
|
||
## 8. Security
|
||
|
||
| Protection | Implementation |
|
||
|---|---|
|
||
| **Multi-tenant isolation** | All case queries are organisation-scoped (`arendeIOrg`); integration-tested against real Postgres |
|
||
| **Roles** | `tekniker` / `arbetsledare` / `admin` (technician / supervisor / admin), in the JWT and as a database check |
|
||
| **JWT claims** | `{ sub, namn, org, roll, tv }` — `tv` = token_version |
|
||
| **Immediate revocation** | `kontoGiltigt()` checks `aktiv` + `token_version` on *every* authenticated request. A valid signature is not enough |
|
||
| **Global logout** | `/api/auth/logga-ut-alla` raises `token_version` → every issued token dies immediately |
|
||
| **Passwords** | bcrypt via `gen_salt('bf')` in the database |
|
||
| **Login throttling** | 15-minute window; max 10 attempts per account, 30 per source. Cleaned probabilistically (2 % chance per write) to avoid needing a cron job |
|
||
| **Encryption at rest** | AES-256-GCM for customers' integration credentials; secret fields are always masked in API responses |
|
||
| **SSRF defence** | `arPrivatAdress()` + `pekarInat()`: 10/8, 127/8, 169.254/16, 172.16–31, 192.168/16, 100.64/10, ::1, fc/fd, fe80, ::ffff:. **DNS is resolved** before the call; `.local`/`.internal` blocked. Escape hatch `TILLAT_INTERNA_UPPSLAG` for test environments |
|
||
| **CORS** | `TILLATNA_URSPRUNG` allowlist; the origin is set once per request |
|
||
| **Attachment integrity** | SHA-256 in the log, verified on read → `409` on mismatch |
|
||
| **Append-only in the database** | Triggers `before update or delete` on both `felsokning_handelser` and `felsokning_arenden` |
|
||
| **Pod hardening** | IMDSv2 mandatory, hop limit 1 → pods cannot borrow the node's IAM role |
|
||
| **IRSA** | Every service account has its own role; nodes share no permissions |
|
||
| **Split IAM roles** | `bygg` (build) may publish to ECR but not touch the cluster; `drift` (operate) may touch the cluster but not publish images |
|
||
| **Network policy** | Default deny inbound; explicit `_ut` (outbound) rules per service |
|
||
| **Database access** | Only from the cluster's nodes, in a subnet layer **with no route out** |
|
||
|
||
### 8.1 Database schema
|
||
|
||
```
|
||
organisationer · anvandare · inloggningsforsok
|
||
felsokning_arenden · felsokning_handelser
|
||
bilagor · bilage_innehall
|
||
delningar · integrationer
|
||
```
|
||
|
||
(organisations · users · login attempts · cases · events · attachments ·
|
||
attachment content · shares · integrations)
|
||
|
||
---
|
||
|
||
## 9. Live Share — sharing levels
|
||
|
||
Three levels, server-side filtering:
|
||
|
||
| Level | Sees |
|
||
|---|---|
|
||
| **kund** (customer) | 22 event types: object, fault description, questions, checks, observations, measurements, photos, videos, comments, handovers, repairs, quality check … |
|
||
| **partner** | Everything the customer sees **+ `hypotes`** (marked unverified) |
|
||
| **intern** (internal) | Full visibility — no filtering |
|
||
|
||
**Never outside the organisation:**
|
||
`kategori_byte`, `hypotes`, `ai_svar`, `ansvarig_satt`, `arbetsorder_skannad`.
|
||
|
||
```js
|
||
export function synligaTyper(niva) {
|
||
if (niva === "intern") return null; // full visibility
|
||
return niva === "partner" ? DELBART_PARTNER : DELBART_KUND;
|
||
}
|
||
```
|
||
|
||
Links are revocable. The public share page (`/felsokning/delad/:kod`) requires
|
||
no login and polls for live updates. The customer can give their decision
|
||
directly in the view (`POST /api/delad/{kod}/beslut`).
|
||
|
||
**Why an allowlist:** a denylist must be updated whenever a new event type is
|
||
added — and that is exactly what gets forgotten. An allowlist turns "forgotten"
|
||
into "internal", which is the safe outcome.
|
||
|
||
---
|
||
|
||
## 10. Brand-specific integrations
|
||
|
||
Vendors are **data, not code** (`integrationer.json`, mountable as a ConfigMap
|
||
via `INTEGRATIONER_FIL`). New brands require no rebuild.
|
||
|
||
| id | Vendor |
|
||
|---|---|
|
||
| `generisk_vin` | Any VIN service over HTTP |
|
||
| `vag_erwin` | Volkswagen Group erWin (VW, Audi, Škoda, SEAT) |
|
||
| `volvo_vida` | Volvo VIDA |
|
||
| `fordonsregister` | Registration number → vehicle |
|
||
|
||
Each vendor declares its fields, which are secret (encrypted + masked), and how
|
||
the response maps to the domain's fields (`marke`, `modell`, `arsmodell`,
|
||
`motor`, `vaxellada` — make, model, year, engine, transmission).
|
||
|
||
Lookups go through the SSRF protection — a customer therefore cannot point a
|
||
"vendor" at the cluster's internal addresses.
|
||
|
||
---
|
||
|
||
## 11. Visual-first
|
||
|
||
The camera **is** the integration layer. What appears on a screen or an
|
||
instrument is photographed and interpreted, rather than integrated.
|
||
|
||
- **Work-order scanning** is the primary path at case start. Sonnet 5 (vision)
|
||
reads customer, vehicle and workshop details regardless of layout, with a
|
||
confidence per field:
|
||
- 🟢 ≥95 % accepted automatically
|
||
- 🟡 80–95 % flagged for reading
|
||
- 🔴 <80 % requires active confirmation
|
||
|
||
The technician therefore reviews only the uncertain fields. Visual review with
|
||
the document beside the fields; clicking marks the approximate position.
|
||
|
||
- **Instrument reading** — a photo of a diagnostic screen or instrument →
|
||
structured values.
|
||
|
||
The motive is commercial: one integration per workshop system is one sales cycle
|
||
per customer. A camera works against everything, immediately.
|
||
|
||
---
|
||
|
||
## 12. Infrastructure
|
||
|
||
Two Terraform layers. The base runs rarely, the workload layer often.
|
||
|
||
### 12.1 `infra/aws` — the base (91 resources)
|
||
|
||
| Area | Contents |
|
||
|---|---|
|
||
| **Network** | 1 VPC, 3 subnet layers × 3 zones: public (ALB + NAT only), private (nodes, no public addresses), data (Aurora, **no route out at all**). VPC endpoints: S3 (gateway); ECR, logs, Secrets Manager, STS, ELB (interface) → traffic never leaves the network |
|
||
| **Cluster** | EKS, arm64 nodes, IRSA via OIDC provider, IMDSv2 hop limit 1, all five control-plane logs on |
|
||
| **Data** | Aurora PostgreSQL Serverless v2, PITR to the second, KMS with its own key, `sslmode=require` |
|
||
| **Object storage** | S3 for attachments: SSE-KMS, public access blocked, TLS mandatory, versioning on. The platform role may read and write — **but never delete** |
|
||
| **Registry** | ECR with **immutable tags** + vulnerability scanning |
|
||
| **Secrets** | Secrets Manager; readable only by the platform role via IRSA |
|
||
| **Roles** | 9 IAM roles, including the split `bygg` / `drift` |
|
||
| **Domain** | Route 53 + ACM with DNS validation |
|
||
| **Observability** | 7 alarms, 1 dashboard, 3 log groups, SNS topic |
|
||
|
||
**The alarms** — few, but the ones that exist mean something. *An alarm nobody
|
||
acts on teaches people to ignore alarms.*
|
||
|
||
- Aurora CPU > 85 % for three periods (the scaling ceiling may have been hit)
|
||
- Aurora free local storage < 5 GiB
|
||
- **Backup age** — `treat_missing_data = "breaching"`. If the metric is missing,
|
||
there is no backup. *A backup you believe exists is worse than none.*
|
||
- Fewer nodes than the desired minimum
|
||
- Response time **p95** > 3 s for three periods — not the mean, which hides that
|
||
every twentieth technician waits unreasonably long
|
||
- Server errors (sum > 5)
|
||
- The model declines (indicates unexpected input, not an operational fault)
|
||
|
||
### 12.2 `infra/terraform` — the workload (35 resources)
|
||
|
||
Reads the base via `terraform_remote_state`; repeats nothing.
|
||
|
||
```
|
||
kubernetes_namespace_v1 denna, gitea
|
||
kubernetes_service_account_v1 plattform (IRSA), drift, runner
|
||
kubernetes_deployment_v1 plattform, orkester, web
|
||
kubernetes_service_v1 plattform, orkester, web
|
||
kubernetes_horizontal_pod_autoscaler_v2 × 3
|
||
kubernetes_pod_disruption_budget_v1 × 3
|
||
kubernetes_ingress_v1 denna, gitea
|
||
kubernetes_network_policy_v1 neka_allt_in, tjanster_in, dns_ut,
|
||
plattform_ut, orkester_ut, web_ut
|
||
kubernetes_manifest hemlighetskalla, hemligheter (External Secrets)
|
||
helm_release lastbalanserare (ALB), external_secrets,
|
||
cloudwatch, metrics, gitea
|
||
aws_route53_record denna
|
||
```
|
||
|
||
`karta.tf` (117 lines) produces a readable map of the whole operational picture:
|
||
`terraform output karta`.
|
||
|
||
### 12.3 Git and CI — fully self-hosted
|
||
|
||
An explicit product decision: **no GitHub in the operational path.** Gitea +
|
||
Actions runners run on our own EKS. `.gitea/workflows/felsokning.yml`:
|
||
|
||
| Job | Contents |
|
||
|---|---|
|
||
| `test-och-bygg` | `vitest run`, `typkontroll`, eslint, `vite build` |
|
||
| `tjanster` | eslint on the services, **integration test against real Postgres**, `swagger-cli validate` |
|
||
| `terraform` | `fmt -check -recursive`, `init -backend=false`, `validate` |
|
||
| `publicera` | Only on `main`, only if the above passed. Builds three images, tags with the commit SHA, pushes to our own ECR. **OIDC, no static key** |
|
||
| `driftsatt` | **Manual** (`workflow_dispatch`) with an explicit image tag |
|
||
|
||
Deployment is a separate step by design: *an image in the registry is not the
|
||
same thing as an image that is running.* Rollback = run again with the previous
|
||
tag.
|
||
|
||
The client's API address is baked in at build time (Vite), so the image is
|
||
environment-bound. The build context for the services is `felsokning/services`
|
||
so that both reach the shared observability module without duplicating it.
|
||
|
||
---
|
||
|
||
## 13. Testing
|
||
|
||
**120 vitest tests** across 13 files:
|
||
|
||
| File | Count | Locks |
|
||
|---|---|---|
|
||
| `ecm.test.ts` | 32 | Evidence levels, rule packs, quality gate, pre-diagnostics |
|
||
| `metodiker.test.ts` | 14 | Library structure, methodology selection, catalogue parity with the orchestrator |
|
||
| `projektioner.test.ts` | 13 | Views as pure functions, next step |
|
||
| `ai.test.ts` | 11 | Orchestrator parity, OpenAPI ↔ server, append-only, prompt rules |
|
||
| `observation.test.ts` | 10 | Tracing, EMF format, **forbidden dimensions** |
|
||
| `bilagor.test.ts` | 9 | Content hash, SigV4, storage-layer selection |
|
||
| `delning.test.ts` | 7 | The allowlist covers every event type |
|
||
| `integrationer.test.ts` | 7 | Vendor lookups, SSRF guard |
|
||
| `demo.test.ts` | 4 | The demo case is rich enough to show |
|
||
| `installningar.test.ts` | 4 | Organisation settings |
|
||
| `streckkod.test.ts` | 4 | VIN/barcode |
|
||
| `synk.test.ts` | 4 | Conflict-free merging |
|
||
| `example.test.ts` | 1 | — |
|
||
|
||
**Beyond the unit tests:**
|
||
|
||
- `integrationstest.sh` — the whole flow against **real Postgres**:
|
||
organisations, roles, the append-only trigger, isolation, sharing,
|
||
attachments.
|
||
- SigV4 **cross-verified bit-for-bit against botocore**
|
||
(`sigv4-referens.json`).
|
||
- `swagger-cli validate` on the OpenAPI spec.
|
||
- Parity tests comparing spec ↔ server, client ↔ orchestrator (× 2 copies),
|
||
domain model ↔ sharing list.
|
||
|
||
### 13.1 The verification loop before every commit
|
||
|
||
```
|
||
npx vitest run # 120 tests
|
||
npm run typkontroll # tsc --noEmit (vite build does NOT typecheck)
|
||
npx eslint src/felsokning src/pages/felsokning
|
||
cd ../services && npx eslint .
|
||
npm run build
|
||
terraform fmt -check -recursive
|
||
# root CI: lint · format:check · typecheck · test
|
||
```
|
||
|
||
`typkontroll` was added after two latent crashes (`TextFalt` and
|
||
`UNDANTAGSORSAKER` used without import) got past `vite build` — which
|
||
transpiles but does not typecheck.
|
||
|
||
---
|
||
|
||
## 14. Repository structure
|
||
|
||
`main` is an npm-workspaces monorepo called **Semantika** which owns the root.
|
||
When the two products were merged, both were kept, with the toolchains **kept
|
||
separate per tree** — not by weakening anyone's rules.
|
||
|
||
```
|
||
/ Semantika (workspaces root)
|
||
├── apps/mobile/ Semantika
|
||
├── services/api/ Semantika
|
||
├── infra/ Semantika
|
||
├── .github/workflows/ci.yml Semantika — untouched
|
||
│
|
||
├── .gitea/workflows/felsokning.yml Guidad Felsökning (self-hosted CI)
|
||
└── felsokning/
|
||
├── app/ client (own package.json, eslint, vitest, tsconfig)
|
||
├── services/
|
||
│ ├── plattform/
|
||
│ ├── ai-orkester/
|
||
│ └── gemensam/ observation.mjs (shared)
|
||
├── infra/
|
||
│ ├── aws/ the base, 91 resources
|
||
│ ├── terraform/ the workload, 35 resources
|
||
│ └── postgres-init.sql
|
||
├── docs/
|
||
└── supabase/ migrations + edge function (older path)
|
||
```
|
||
|
||
**Two operational paths exist in parallel:** the self-hosted AWS stack (the one
|
||
that counts) and an older Supabase-based one (the edge function
|
||
`felsokning-ai`, migrations). The orchestrator therefore exists in **two
|
||
copies**, kept in sync by tests.
|
||
|
||
---
|
||
|
||
## 15. Documentation in the repository
|
||
|
||
```
|
||
docs/VISION.md the product vision
|
||
docs/MASTER-PROMPT.md the founding instruction
|
||
docs/MVP.md what is built, feature by feature
|
||
docs/DEMO.md demo script for presentations
|
||
docs/DRIFT.md operations
|
||
docs/SYSTEM-DESCRIPTION.md this document — the canonical version
|
||
docs/SYSTEM-DESCRIPTION.sv.md Swedish
|
||
docs/SYSTEM-DESCRIPTION.de.md German
|
||
docs/SYSTEM-DESCRIPTION.da.md Danish
|
||
docs/SYSTEM-DESCRIPTION.no.md Norwegian (Bokmål)
|
||
docs/exempel/vibration-vid-88-km-h.md worked example
|
||
docs/moduler/ eight module documents
|
||
```
|
||
|
||
---
|
||
|
||
## 16. Design decisions and their motives
|
||
|
||
Collected because the motive is often more important than the decision.
|
||
|
||
| Decision | Motive |
|
||
|---|---|
|
||
| Event sourcing | The record has to hold up in a dispute. The history *is* the value |
|
||
| Append-only in the database too | The application layer can be bypassed; the trigger cannot |
|
||
| Allowlist for sharing | A forgotten event type becomes internal, not leaked |
|
||
| Fixed cause categories | Free text cannot be aggregated; fleet statistics are an asset |
|
||
| Server-owned model routing | The client must never hold the key, and routing must change without a release |
|
||
| Model logged per response | The record must be auditable after the fact |
|
||
| Avoid the word "AI" | The customer hears "guess"; the assessor weights it lower |
|
||
| Visual-first | One integration per workshop system = one sales cycle per customer. The camera works immediately |
|
||
| Vendors as data | A new brand should not require a release |
|
||
| Own observability, zero dependencies | 30 packages to measure 4 things is the wrong trade |
|
||
| Few EMF dimensions | Every combination is a paid time series |
|
||
| p95 in the alarm, not the mean | The mean hides that every twentieth technician waits |
|
||
| Alarm on *missing* backup data | A backup you believe exists is worse than none |
|
||
| Split build/operate roles | A compromised build must not be able to touch the cluster |
|
||
| Manual deployment | An image in the registry ≠ an image that is running |
|
||
| Immutable ECR tags | A tag must mean the same thing tomorrow |
|
||
| Content-addressed attachments | A swapped image must be detected, not assumed |
|
||
| Hash verification on read | Hashing on write is not enough |
|
||
| The S3 role may not delete | Append-only must apply to storage too |
|
||
| Engine separated from content | The library grows; the engine should not have to change |
|
||
| Scored methodology selection | Regex chains become opaque at 16 alternatives |
|
||
| Keywords as stems | Swedish inflection drops an `e` — otherwise nothing matches |
|
||
| `generisk` as fallback | An honest "we don't know" beats a guess |
|
||
| Safety step first in high voltage | That work can kill |
|
||
| Swedish in the code | The domain is Swedish; translating back and forth loses precision |
|
||
|
||
---
|
||
|
||
## 17. Known limitations and open items
|
||
|
||
Explicitly not finished:
|
||
|
||
- **Two orchestrator copies** (Supabase edge function + K8s service) are kept in
|
||
sync by tests, not by shared code. The Supabase path is the older one and
|
||
should be retired.
|
||
- **`ArendeSida.tsx` is 2433 lines.** It works, but it is the file that costs
|
||
the most to change.
|
||
- **`terraform validate` cannot be run locally** in the development environment
|
||
(the outbound network policy blocks provider downloads). Replaced by
|
||
`terraform fmt` plus a bespoke static reference check; real validation happens
|
||
in CI.
|
||
- **The Claude key is filled in by hand** after the first `apply` — it is not in
|
||
Terraform state, by design.
|
||
- **`postgres-init.sql` is run manually** against the database after the base
|
||
`apply`.
|
||
- **No automatic restore test of the backup.** The alarm says backup *happens*,
|
||
not that it *can be restored*.
|
||
- **The methodology library does not cover everything** — and does not claim to.
|
||
`generisk` is the safety net.
|
||
- The root eslint has 20 pre-existing errors in Semantika's own pages
|
||
(`no-explicit-any`) unrelated to Guidad Felsökning.
|
||
|
||
---
|
||
|
||
## 18. Glossary
|
||
|
||
The left column is the term as it appears in code and UI.
|
||
|
||
| Swedish | English |
|
||
|---|---|
|
||
| Ärende | Case — one diagnostic job |
|
||
| Händelse / loggpost | Event / log entry — atomic entry in the append-only log |
|
||
| Metodik | Methodology — structured diagnostic flow |
|
||
| Steg | Step — phase in a methodology (symptom, visual, measurements …) |
|
||
| Kontroll | Check — individual checklist item with a minimum requirement |
|
||
| Krav | Requirement — `matvarde` / `kommentar` / `foto` |
|
||
| Undantag | Exemption — documented reason a check was skipped |
|
||
| Brief | Brief — compiled case picture; a projection |
|
||
| Kvalitetsgrind | Quality gate — rule set that must be passed before closing |
|
||
| Evidensnivå | Evidence level — E0–E6, the probative value of the record |
|
||
| Reproducering | Reproduction — symptom verification: yes / partly / no |
|
||
| Felorsak | Root cause — structured analysis with category and supporting evidence |
|
||
| Delning | Share — external link with a permission level |
|
||
| Orkester | Orchestrator — the service that owns model routing |
|
||
| Spann / spår | Span / trace — timing and W3C tracing respectively |
|
||
| Bilaga | Attachment — content-addressed photo/video/document |
|
||
| Tekniker | Technician |
|
||
| Arbetsledare | Supervisor / foreman |
|
||
| Fordon | Vehicle |
|
||
| Mätvärde | Measured value |
|
||
| Felbeskrivning | Fault description (the customer's words) |
|
||
| Arbetsorder | Work order |
|
||
| Mätarställning | Odometer reading |
|
||
| Överlämning | Handover |
|
||
| Åtgärd | Repair / action |
|
||
| Kundbeslut | Customer decision |
|
||
| Säkerhet | Safety |
|
||
| Högvolt | High voltage |
|