ALVA deployment config
Guidad Felsökning / test-och-bygg (push) Has been cancelled
Guidad Felsökning / tjanster (push) Has been cancelled
Guidad Felsökning / terraform (push) Has been cancelled
Guidad Felsökning / leverantorskedja (push) Has been cancelled
Guidad Felsökning / aterstallning (push) Has been cancelled
Guidad Felsökning / publicera (push) Has been cancelled
Guidad Felsökning / driftsatt (push) Has been cancelled

This commit is contained in:
Bernt
2026-08-08 13:12:13 +00:00
parent 9872f3bf47
commit 04e0a1e8ec
66 changed files with 16749 additions and 4 deletions
@@ -0,0 +1,208 @@
// ALVA-REP-0100 · Operational metrics.
//
// Vilka siffror som visas avgör vad organisationen optimerar. Det är
// därför det här är ett produktbeslut och inte ett rapportjobb.
//
// Uteslutna med avsikt:
//
// Ärenden per tekniker Belönar snabbhet över noggrannhet. Den som
// hoppar över kontroller vinner.
// Genomsnittlig ledtid Samma sak, en gång till. Ett svårt fel
// SKA ta längre tid.
// Inloggningar, aktivitet Mäter användning, inte nytta.
//
// Medtagna, och varför var och en är handlingsbar:
//
// VERIFIERINGSGRAD Andel avslut med fastställd orsak. Sjunker den
// gissar organisationen mer. Det här är det enda
// måttet ett försäkringsbolag egentligen bryr sig
// om.
// REPRODUKTIONSGRAD Andel symptom som återskapats. Låg siffra
// förutsäger återkommande fordon — man kan inte
// åtgärda det man inte sett.
// OMARBETNING Samma fordon tillbaka med samma orsakskategori.
// Det dyraste felet i en verkstad, och det enda
// ingen mäter.
// UNDANTAGSFREKVENS Vilka kontroller hoppas över, per steg. Ett
// steg med hög frekvens är antingen fel skrivet
// eller kräver utrustning som saknas — bägge är
// åtgärdbara, ingetdera syns i dag.
// EVIDENSPROFIL Fördelning E0E6. Visar om underlaget vilar på
// mätningar eller på observationer.
// FASFÖRDELNING Var tiden går i ALVA-termer.
//
// Alla funktioner är rena över (ärenden). Ingen databasåtkomst, ingen
// klocka — samma underlag ger samma siffra, vilket är förutsättningen
// för att en kvartalsrapport ska gå att granska i efterhand.
import { fasFor } from "./faser.mjs";
const handelserI = (arende) => (arende.handelser ?? []).map((p) => p.handelse ?? p);
const av = (handelser, typ) => handelser.filter((h) => h?.typ === typ);
const arAvslutat = (arende) => handelserI(arende).some((h) => h?.typ === "arende_avslutat");
/** Andel med en decimal, eller null när underlaget saknas. */
function andel(traffar, av_) {
return av_ === 0 ? null : Math.round((traffar / av_) * 1000) / 10;
}
/**
* Verifieringsgrad — andel avslutade ärenden där orsaken fastställdes.
*
* Att den inte är 100 % är friskt. En organisation som aldrig skriver
* "orsaken kunde inte fastställas" ljuger antingen, eller får bara enkla
* fel. Talet är intressant över tid, inte som absolut nivå.
*/
export function verifieringsgrad(arenden) {
const avslutade = arenden.filter(arAvslutat);
const fastställda = avslutade.filter((a) => {
const s = av(handelserI(a), "slutsats").at(-1);
return s && s.orsakFastställd !== false;
});
return { antal: avslutade.length, fastställda: fastställda.length, andel: andel(fastställda.length, avslutade.length) };
}
/** Reproduktionsgrad — andel ärenden där symptomet faktiskt återskapades. */
export function reproduktionsgrad(arenden) {
const medSvar = arenden.filter((a) => av(handelserI(a), "reproducering").length > 0);
const fördelning = { ja: 0, delvis: 0, nej: 0 };
for (const a of medSvar) {
const r = av(handelserI(a), "reproducering").at(-1);
if (r?.status in fördelning) fördelning[r.status] += 1;
}
return { antal: medSvar.length, ...fördelning, andel: andel(fördelning.ja, medSvar.length) };
}
/**
* Omarbetning — samma fordon tillbaka med samma orsakskategori.
*
* Det dyraste felet i en verkstad och det enda ingen mäter, eftersom det
* kräver att man kopplar ihop två ärenden som ser fristående ut. Här är
* kopplingen gratis: loggen bär fordonets identifierare och orsakens
* kategori.
*
* @param inomDagar fönster; 90 dagar är en rimlig utgångspunkt men
* varje verkstad har sin egen uppfattning.
*/
export function omarbetning(arenden, inomDagar = 90) {
const perFordon = new Map();
for (const a of arenden) {
if (!arAvslutat(a)) continue;
const ident = av(handelserI(a), "objekt_identifierat").at(-1)?.objekt?.identifierare;
if (!ident) continue;
const orsak = av(handelserI(a), "felorsak").at(-1);
const avslut = (a.handelser ?? []).find((p) => (p.handelse ?? p)?.typ === "arende_avslutat")?.tidpunkt;
if (!orsak || !avslut) continue;
if (!perFordon.has(ident)) perFordon.set(ident, []);
perFordon.get(ident).push({ tid: Date.parse(avslut), kategorier: orsak.orsaker ?? [], arende: a.id });
}
const fall = [];
const fönster = inomDagar * 86400000;
for (const [ident, poster] of perFordon) {
poster.sort((a, b) => a.tid - b.tid);
for (let i = 1; i < poster.length; i += 1) {
const föregående = poster[i - 1];
const nu = poster[i];
const delad = nu.kategorier.filter((k) => föregående.kategorier.includes(k));
if (delad.length > 0 && nu.tid - föregående.tid <= fönster) {
fall.push({ identifierare: ident, kategorier: delad, dagar: Math.round((nu.tid - föregående.tid) / 86400000) });
}
}
}
const avslutade = arenden.filter(arAvslutat).length;
return { antal: fall.length, andel: andel(fall.length, avslutade), fall: fall.slice(0, 20) };
}
/**
* Undantagsfrekvens per metodiksteg.
*
* Ett steg som ofta hoppas över är antingen felskrivet eller kräver
* utrustning verkstaden inte har. Bägge går att åtgärda — men bara om
* man vet vilket steg det gäller, vilket ingen gör i dag.
*/
export function undantagsfrekvens(arenden) {
const per = new Map();
for (const a of arenden) {
for (const h of av(handelserI(a), "kontroll_utford")) {
const nyckel = `${h.stegId}/${h.kontrollId}`;
if (!per.has(nyckel)) per.set(nyckel, { steg: h.stegId, kontroll: h.kontrollId, text: h.text, utforda: 0, undantag: 0 });
const rad = per.get(nyckel);
rad.utforda += 1;
if ((h.undantag ?? "").trim()) rad.undantag += 1;
}
}
return [...per.values()]
.map((r) => ({ ...r, fas: fasFor(r.steg), andel: andel(r.undantag, r.utforda) }))
.filter((r) => r.undantag > 0)
.sort((a, b) => b.andel - a.andel || b.undantag - a.undantag);
}
/** Evidensprofil — fördelning av starkaste evidenstyp per ärende. */
export function evidensprofil(arenden) {
const nivå = { E0: 0, E1: 0, E2: 0, E3: 0, E4: 0, E5: 0, E6: 0 };
for (const a of arenden) {
const h = handelserI(a);
const har = {
E5: av(h, "arbetsorder_skannad").length > 0,
// Ett mätvärde räknas som E4 bara med spårbart mätdon; utan det är
// det teknikerns observation av en siffra (QUALITY-AUDIT M-1).
E4: av(h, "matvarde").some((m) => m.matdonId),
E3: av(h, "video").length > 0,
E2: av(h, "foto").length > 0,
E1: av(h, "observation").length + av(h, "kontroll_utford").length > 0,
};
const källor = ["E2", "E3", "E4", "E5"].filter((n) => har[n]).length;
const högsta = källor >= 2 ? "E6" : (["E5", "E4", "E3", "E2", "E1"].find((n) => har[n]) ?? "E0");
nivå[högsta] += 1;
}
return nivå;
}
/** Fasfördelning — var tiden går, i ALVA-termer. */
export function fasfordelning(arenden) {
const per = { analysis: 0, localization: 0, verification: 0, action: 0 };
for (const a of arenden) {
for (const h of handelserI(a)) {
if (h?.stegId) per[fasFor(h.stegId)] += 1;
}
}
const summa = Object.values(per).reduce((s, v) => s + v, 0);
return Object.fromEntries(
Object.entries(per).map(([fas, antal]) => [fas, { antal, andel: andel(antal, summa) }]),
);
}
/** Orsakskategorier över flottan. */
export function orsakskategorier(arenden) {
const per = new Map();
for (const a of arenden) {
for (const h of av(handelserI(a), "felorsak")) {
for (const k of h.orsaker ?? []) per.set(k, (per.get(k) ?? 0) + 1);
}
}
return [...per.entries()]
.map(([kategori, antal]) => ({ kategori, antal }))
.sort((a, b) => b.antal - a.antal);
}
/**
* Hela underlaget för portalens analysvy och kvartalsrapporten.
*
* En enda funktion, så att skärmen och rapporten aldrig kan visa olika
* siffror för samma period — det är den vanligaste orsaken till att
* ingen litar på rapporten.
*/
export function oversikt(arenden, { omarbetningsfonster = 90 } = {}) {
return {
version: "ALVA-REP-0100",
antal: { totalt: arenden.length, avslutade: arenden.filter(arAvslutat).length },
verifiering: verifieringsgrad(arenden),
reproduktion: reproduktionsgrad(arenden),
omarbetning: omarbetning(arenden, omarbetningsfonster),
undantag: undantagsfrekvens(arenden).slice(0, 10),
evidens: evidensprofil(arenden),
faser: fasfordelning(arenden),
orsaker: orsakskategorier(arenden).slice(0, 12),
};
}