Revisionen: C-3, C-4, M-1, M-4, M-5, M-6, m-3, m-5 och m-7 åtgärdade
C-3 · Dataskydd genom krypto-shredding. Identifierande fält krypteras med en nyckel per ärende; radering sker genom att nyckeln förstörs. Loggen förblir intakt och hashverifierbar — det som blir oåtkomligt är identifieringen, inte protokollet över vad som kontrollerades. Ett raderat ärende visar fortfarande att lufttrycket mättes till 2,4 bar klockan 08:42, bara inte längre vems bil det gällde. Vad som inte krypteras är lika viktigt: mätvärden, observationer och kontrollresultat är verksamhetsdata. Krypteras allt raderas beviset tillsammans med personuppgiften. En raderingsbegäran gäller ett fordon, inte ett ärende — men identifieraren är krypterad och går inte att söka på. Därför ett blindat index: HMAC av den normaliserade identifieraren, samma fordon ger alltid samma värde, värdet går inte att vända tillbaka utan nyckeln. Gallringsdatum sätts vid avslut utifrån ärendetypen. Ett ärende utan datum gallras aldrig — för tidig gallring går inte att ångra. C-4 · Modellanropen kan stängas av per organisation. Flaggan bärs i token så orkestern kan neka utan databasåtkomst. Metodikmotorn fungerar ensam; en verkstad som inte kan acceptera överföringen till modelleverantören kan ändå använda produkten. M-1 · Mätvärden bär vilket mätdon som användes och när det var kalibrerat. Utan spårbart instrument nedgraderas värdet från E4 till E1 — det är teknikerns observation av en siffra, inte en mätning. Kalibreringen bedöms vid mättillfället, inte i dag. Demoärendet fick kalibrerade instrument: det ska visa den praxis produkten kräver. M-4 · Läslogg. Varje skrivning loggades redan; ingen läsning gjorde det. M-5 · Återställningstest i CI. Larmet visade att backup sker, inte att den går att återställa. Testet kontrollerar det som faktiskt brukar tappas: att append-only-triggarna följde med och fortfarande biter. M-6 · Regelpaketet verifieras mot HMAC. Ogiltig signatur spärrar avslut; saknad nyckel ger granskningsläge i stället för driftavbrott — det är så säkerhetsfunktioner blir avstängda. m-3 · EXIF-borttagningen är nu avsiktlig och låst av test. Den höll av en slump, och en ändring i stil med "bevara originalkvaliteten" hade tyst börjat publicera var kundens bil stod. m-5 · SBOM och sårbarhetsskanning i CI. m-7 · Promptversionen binds till varje svar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
// EXIF-borttagningen är en säkerhetsegenskap, inte en biprodukt.
|
||||
//
|
||||
// Omritningen via canvas i skalaNerFoto tar bort all metadata, inklusive
|
||||
// GPS-position. Egenskapen höll tidigare av en slump (QUALITY-AUDIT m-3):
|
||||
// ingen hade skrivit ned den, och en ändring i stil med "bevara
|
||||
// originalkvaliteten" hade tyst börjat publicera var kundens bil stod, i
|
||||
// en vy som delas med försäkringsbolag och partners.
|
||||
//
|
||||
// Testet läser källkoden i stället för att köra funktionen, eftersom det
|
||||
// som ska skyddas är *vägen*: att bilden går genom en omritning och inte
|
||||
// vidarebefordras som originalbytes. Ett test som bara körde funktionen i
|
||||
// jsdom hade inte fångat en ändring till `resolve(await fil.text())`.
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const KÄLLA = readFileSync("src/felsokning/format.ts", "utf8");
|
||||
|
||||
describe("foton bär ingen metadata vidare", () => {
|
||||
it("bilden ritas om via canvas — det är det som tar bort EXIF och GPS", () => {
|
||||
const kropp = KÄLLA.slice(KÄLLA.indexOf("export function skalaNerFoto"));
|
||||
expect(kropp).toContain("createElement(\"canvas\")");
|
||||
expect(kropp).toContain("drawImage");
|
||||
// toDataURL på en canvas kan bara producera pixeldata. Returneras
|
||||
// något annat har omritningen kringgåtts.
|
||||
expect(kropp).toContain("canvas.toDataURL");
|
||||
});
|
||||
|
||||
it("originalfilens bytes lämnas aldrig vidare orörda", () => {
|
||||
const kropp = KÄLLA.slice(KÄLLA.indexOf("export function skalaNerFoto"));
|
||||
// readAsDataURL används för att *läsa in* bilden i ett Image-objekt,
|
||||
// aldrig som det som returneras.
|
||||
expect(kropp).not.toMatch(/resolve\(\s*lasare\.result/);
|
||||
expect(kropp).not.toMatch(/resolve\(\s*await fil\./);
|
||||
});
|
||||
|
||||
it("egenskapen är dokumenterad så nästa ändring inte tappar den tyst", () => {
|
||||
expect(KÄLLA).toMatch(/EXIF/);
|
||||
expect(KÄLLA).toMatch(/GPS/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
// @vitest-environment node
|
||||
// Dataskydd, spårbara mätvärden och den egenskap som hittills hållit av
|
||||
// en slump.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
GALLRING_MANADER,
|
||||
IDENTIFIERANDE,
|
||||
MASKERAT,
|
||||
gallringsdatum,
|
||||
skydda,
|
||||
skyddaHändelse,
|
||||
öppna,
|
||||
öppnaHändelse,
|
||||
} from "../../../../services/gemensam/personuppgifter.mjs";
|
||||
import { kalibreradVid } from "../ecm";
|
||||
|
||||
const NYCKEL = Buffer.alloc(32, 7);
|
||||
const ANNAN = Buffer.alloc(32, 9);
|
||||
const nycklar = (poster: [string, Buffer][]) => new Map(poster);
|
||||
|
||||
describe("C-3 · krypto-shredding gör append-only och radering förenliga", () => {
|
||||
it("skyddar och öppnar ett värde med rätt nyckel", () => {
|
||||
const skyddat = skydda("ABC123", "n1", NYCKEL);
|
||||
expect(skyddat).not.toContain("ABC123");
|
||||
expect(öppna(skyddat, nycklar([["n1", NYCKEL]]))).toBe("ABC123");
|
||||
});
|
||||
|
||||
it("en förstörd nyckel maskerar värdet i stället för att kasta", () => {
|
||||
// Det normala utfallet efter en radering — inte ett fel. Ett kastat
|
||||
// undantag skulle göra hela ärendet oläsbart, vilket vore precis
|
||||
// fel: resten av bevisningen ska överleva.
|
||||
const skyddat = skydda("ABC123", "n1", NYCKEL);
|
||||
expect(öppna(skyddat, nycklar([]))).toBe(MASKERAT);
|
||||
});
|
||||
|
||||
it("fel nyckel avslöjas av autentiseringstaggen", () => {
|
||||
const skyddat = skydda("ABC123", "n1", NYCKEL);
|
||||
expect(öppna(skyddat, nycklar([["n1", ANNAN]]))).toBe(MASKERAT);
|
||||
});
|
||||
|
||||
it("manipulerad chiffertext maskeras — den öppnas aldrig delvis", () => {
|
||||
const skyddat = skydda("ABC123", "n1", NYCKEL);
|
||||
const manipulerat = `${skyddat.slice(0, -4)}AAAA`;
|
||||
expect(öppna(manipulerat, nycklar([["n1", NYCKEL]]))).toBe(MASKERAT);
|
||||
});
|
||||
|
||||
it("registreringsnummer och VIN behandlas som personuppgift", () => {
|
||||
// Ett fordon pekar i praktiken ut en ägare, och här finns kopplingen.
|
||||
expect(IDENTIFIERANDE.arbetsorder_skannad).toContain("fordon_regnr");
|
||||
expect(IDENTIFIERANDE.arbetsorder_skannad).toContain("fordon_vin");
|
||||
expect(IDENTIFIERANDE.objekt_identifierat).toContain("identifierare");
|
||||
});
|
||||
|
||||
it("skyddar arbetsorderns identifierande fält men lämnar resten", () => {
|
||||
const h = skyddaHändelse(
|
||||
{
|
||||
typ: "arbetsorder_skannad",
|
||||
falt: [
|
||||
{ id: "kund_namn", varde: "Anders Svensson" },
|
||||
{ id: "fordon_regnr", varde: "ABC123" },
|
||||
{ id: "felbeskrivning", varde: "Vibrerar vid 88 km/h" },
|
||||
],
|
||||
},
|
||||
"n1",
|
||||
NYCKEL,
|
||||
);
|
||||
const värden = Object.fromEntries(h.falt.map((f: { id: string; varde: string }) => [f.id, f.varde]));
|
||||
expect(värden.kund_namn).not.toContain("Anders");
|
||||
expect(värden.fordon_regnr).not.toContain("ABC123");
|
||||
// Felbeskrivningen är verksamhetsdata — den ska överleva raderingen,
|
||||
// annars raderas beviset tillsammans med personuppgiften.
|
||||
expect(värden.felbeskrivning).toBe("Vibrerar vid 88 km/h");
|
||||
});
|
||||
|
||||
it("bevisningen överlever raderingen — bara identifieringen försvinner", () => {
|
||||
const skyddad = skyddaHändelse(
|
||||
{ typ: "objekt_identifierat", objekt: { identifierare: "ABC123", beskrivning: "Volvo XC60" } },
|
||||
"n1",
|
||||
NYCKEL,
|
||||
);
|
||||
const efterRadering = öppnaHändelse(skyddad, nycklar([]));
|
||||
expect(efterRadering.objekt.identifierare).toBe(MASKERAT);
|
||||
expect(efterRadering.objekt.beskrivning).toBe("Volvo XC60");
|
||||
});
|
||||
|
||||
it("händelser utan identifierande fält passerar orörda", () => {
|
||||
const h = { typ: "matvarde", beskrivning: "Lufttryck", varde: "2,4" };
|
||||
expect(skyddaHändelse(h, "n1", NYCKEL)).toEqual(h);
|
||||
});
|
||||
|
||||
it("gallringstiden följer ärendetypen, och okänd typ får standardtiden", () => {
|
||||
const avslut = "2026-01-15T10:00:00.000Z";
|
||||
const garanti = gallringsdatum("Garanti", avslut);
|
||||
const okand = gallringsdatum("Hittepå", avslut);
|
||||
expect(garanti.getFullYear()).toBe(2036);
|
||||
expect(okand.getFullYear()).toBe(2029);
|
||||
// Reklamation följer konsumentköplagens reklamationsrätt.
|
||||
expect(GALLRING_MANADER.Reklamation).toBe(36);
|
||||
});
|
||||
});
|
||||
|
||||
describe("M-1 · mätvärden är spårbara eller nedgraderade", () => {
|
||||
it("kalibreringen bedöms vid mättillfället, inte i dag", () => {
|
||||
// Ett instrument vars kalibrering gått ut i efterhand gör inte en
|
||||
// gammal mätning ogiltig.
|
||||
expect(kalibreradVid("2026-06-30", "2026-01-15T10:00:00.000Z")).toBe(true);
|
||||
// Och ett nyligen kalibrerat instrument räddar inte en mätning som
|
||||
// gjordes när kalibreringen var utgången.
|
||||
expect(kalibreradVid("2026-06-30", "2026-08-15T10:00:00.000Z")).toBe(false);
|
||||
expect(kalibreradVid(undefined, "2026-01-15T10:00:00.000Z")).toBe(false);
|
||||
expect(kalibreradVid("inte-ett-datum", "2026-01-15T10:00:00.000Z")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -51,13 +51,29 @@ describe("Evidence Engine", () => {
|
||||
expect(evidensNiva(byggArende([]))).toBe("E0");
|
||||
expect(evidensNiva(byggArende([{ typ: "observation", text: "x" }]))).toBe("E1");
|
||||
expect(evidensNiva(byggArende([{ typ: "foto", beskrivning: "x", dataUrl: "data:" }]))).toBe("E2");
|
||||
expect(evidensNiva(byggArende([{ typ: "matvarde", beskrivning: "U", varde: "12" }]))).toBe("E4");
|
||||
// Utan spårbart instrument är ett mätvärde teknikerns observation
|
||||
// av en siffra, inte en mätning (QUALITY-AUDIT M-1).
|
||||
expect(evidensNiva(byggArende([{ typ: "matvarde", beskrivning: "U", varde: "12" }]))).toBe("E1");
|
||||
expect(
|
||||
evidensNiva(
|
||||
byggArende([
|
||||
{
|
||||
typ: "matvarde",
|
||||
beskrivning: "U",
|
||||
varde: "12",
|
||||
matdonId: "m1",
|
||||
matdonBeteckning: "Fluke 87V",
|
||||
matdonKalibreradTill: "2099-01-01",
|
||||
},
|
||||
]),
|
||||
),
|
||||
).toBe("E4");
|
||||
expect(evidensNiva(byggArende([{ typ: "arbetsorder_skannad", falt: [] }]))).toBe("E5");
|
||||
expect(
|
||||
evidensNiva(
|
||||
byggArende([
|
||||
{ typ: "foto", beskrivning: "x", dataUrl: "data:" },
|
||||
{ typ: "matvarde", beskrivning: "U", varde: "12" },
|
||||
{ typ: "matvarde", beskrivning: "U", varde: "12", matdonId: "m1", matdonBeteckning: "Fluke 87V", matdonKalibreradTill: "2099-01-01" },
|
||||
]),
|
||||
),
|
||||
).toBe("E6");
|
||||
@@ -71,7 +87,7 @@ describe("Evidence Engine", () => {
|
||||
const poster = evidensposter(arende);
|
||||
expect(poster).toHaveLength(2);
|
||||
expect(poster[0].niva).toBe("E2");
|
||||
expect(poster[1].sammanfattning).toBe("Obalans = 38 g");
|
||||
expect(poster[1].sammanfattning).toBe("Obalans = 38 g (instrument ej angivet)");
|
||||
expect(poster[0].tekniker).toBe("Anna");
|
||||
expect(poster[0].hash).toMatch(/^[0-9a-f]{8}$/);
|
||||
// Samma innehåll → samma hash (deterministisk spårbarhet).
|
||||
@@ -343,9 +359,17 @@ describe("Videoevidens (E3) och signerat avslut", () => {
|
||||
expect(evidensposter(medVideo)[0].kategori).toBe("video");
|
||||
const tvaKallor = byggArende([
|
||||
{ typ: "video", beskrivning: "Motorljud", dataUrl: "data:video/webm;base64,x" },
|
||||
{ typ: "matvarde", beskrivning: "Oljetryck", varde: "3,1", enhet: "bar" },
|
||||
{ typ: "matvarde", beskrivning: "Oljetryck", varde: "3,1", enhet: "bar", matdonId: "m1", matdonBeteckning: "Fluke 87V", matdonKalibreradTill: "2099-01-01" },
|
||||
]);
|
||||
expect(evidensNiva(tvaKallor)).toBe("E6");
|
||||
|
||||
// Utan kalibrerat instrument är det inte två oberoende källor utan
|
||||
// en: video plus en observation av en siffra.
|
||||
const enKalla = byggArende([
|
||||
{ typ: "video", beskrivning: "Motorljud", dataUrl: "data:video/webm;base64,x" },
|
||||
{ typ: "matvarde", beskrivning: "Oljetryck", varde: "3,1", enhet: "bar" },
|
||||
]);
|
||||
expect(evidensNiva(enKalla)).toBe("E3");
|
||||
});
|
||||
|
||||
it("underlagskällan Video valideras mot loggen i felorsaksanalysen", async () => {
|
||||
|
||||
@@ -48,6 +48,21 @@ function demoFoto(text: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Mätdon i demot. Ett mätvärde är E4 bara när det går att visa vad som
|
||||
// mätte och att instrumentet var kalibrerat vid mättillfället — annars är
|
||||
// det teknikerns observation av en siffra (QUALITY-AUDIT M-1). Demot ska
|
||||
// visa den praxis produkten kräver, inte den som fanns innan.
|
||||
const DACKTRYCKSMATARE = {
|
||||
matdonId: "demo-tryck-01",
|
||||
matdonBeteckning: "Däcktrycksmätare PCL DPG7",
|
||||
matdonKalibreradTill: "2027-03-31",
|
||||
};
|
||||
const BALANSMASKIN = {
|
||||
matdonId: "demo-balans-01",
|
||||
matdonBeteckning: "Balanseringsmaskin Hunter Road Force",
|
||||
matdonKalibreradTill: "2027-01-15",
|
||||
};
|
||||
|
||||
export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
|
||||
const start = Date.now() - 97 * 60000; // 1 tim 37 min sedan
|
||||
const vid = (minuter: number) => new Date(start + minuter * 60000).toISOString();
|
||||
@@ -90,8 +105,8 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
|
||||
post(22, { typ: "kontroll_utford", stegId: "visuell", kontrollId: "dot", text: "Dokumentera DOT-/tillverkningsdatum", resultat: "Fram: DOT 2318, bak: DOT 2318 — samtliga från vecka 23 2018" }),
|
||||
post(26, { typ: "observation", text: "Höger framdäck visar ojämnt slitage på innerkanten" }),
|
||||
post(26, { typ: "kontroll_utford", stegId: "visuell", kontrollId: "slitage", text: "Kontrollera däckslitage", resultat: "Ojämnt slitage höger fram, övriga ok" }),
|
||||
post(31, { typ: "matvarde", beskrivning: "Lufttryck vänster fram", varde: "2,4", enhet: "bar" }),
|
||||
post(32, { typ: "matvarde", beskrivning: "Lufttryck höger fram", varde: "2,1", enhet: "bar" }),
|
||||
post(31, { typ: "matvarde", beskrivning: "Lufttryck vänster fram", varde: "2,4", enhet: "bar", ...DACKTRYCKSMATARE }),
|
||||
post(32, { typ: "matvarde", beskrivning: "Lufttryck höger fram", varde: "2,1", enhet: "bar", ...DACKTRYCKSMATARE }),
|
||||
post(33, { typ: "kontroll_utford", stegId: "kontroller", kontrollId: "lufttryck", text: "Kontrollera lufttryck", resultat: "VF 2,4 / HF 2,1 / VB 2,4 / HB 2,4 bar — justerat till 2,4 runtom" }),
|
||||
post(38, { typ: "kontroll_utford", stegId: "kontroller", kontrollId: "hjulmoment", text: "Kontrollera hjulmoment", resultat: "140 Nm samtliga hjul, ok" }),
|
||||
post(40, { typ: "hypotes", text: "Obalans eller kast i höger framhjul", niva: "lag" }),
|
||||
@@ -102,7 +117,7 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
|
||||
post(58, { typ: "kontroll_utford", stegId: "provkorning", kontrollId: "pk_var", text: "Om vibration känns i ratt eller kaross", resultat: "Endast i ratten" }),
|
||||
post(60, { typ: "kategori_byte", kategori: "aktiv_felsokning" }),
|
||||
post(75, { typ: "inaktivitet_forklarad", text: "Lyfte bilen och demonterade höger framhjul för kontroll i balanseringsmaskin.", minuter: 15 }),
|
||||
post(82, { typ: "matvarde", beskrivning: "Obalans höger framhjul", varde: "38", enhet: "g" }),
|
||||
post(82, { typ: "matvarde", beskrivning: "Obalans höger framhjul", varde: "38", enhet: "g", ...BALANSMASKIN }),
|
||||
post(83, { typ: "kontroll_utford", stegId: "kontroller", kontrollId: "balansering", text: "Kontrollera hjulbalansering", resultat: "38 g obalans höger fram — en balansvikt saknas. Övriga hjul inom 5 g." }),
|
||||
post(85, { typ: "observation", text: "Spår efter lossnad klistervikt på höger framfälgs insida" }),
|
||||
post(85, {
|
||||
|
||||
@@ -96,7 +96,22 @@ export type Handelse =
|
||||
// för evidens. Kontrollen räknas som hanterad men flaggas i brief/rapport.
|
||||
| { typ: "kontroll_utford"; stegId: string; kontrollId: string; text: string; resultat?: string; undantag?: string }
|
||||
| { typ: "observation"; text: string }
|
||||
| { typ: "matvarde"; beskrivning: string; varde: string; enhet?: string }
|
||||
| {
|
||||
typ: "matvarde";
|
||||
beskrivning: string;
|
||||
varde: string;
|
||||
enhet?: string;
|
||||
/**
|
||||
* Vilket mätdon som användes. Ett mätvärde rankas som E4 — hög
|
||||
* evidens — och utan kalibrerat instrument är det inte lägre
|
||||
* evidens utan ingen evidens alls (QUALITY-AUDIT M-1). Fältet är
|
||||
* valfritt i typen därför att loggen är append-only och äldre
|
||||
* poster saknar det; evidensmotorn nedgraderar dem i stället.
|
||||
*/
|
||||
matdonId?: string;
|
||||
matdonBeteckning?: string;
|
||||
matdonKalibreradTill?: string;
|
||||
}
|
||||
| { typ: "hypotes"; text: string; niva: Exclude<Tillforlitlighet, "hog"> }
|
||||
| ({ typ: "foto"; beskrivning: string } & Bilaga)
|
||||
// Video med ljud (E3-evidens): för det som låter eller rör sig —
|
||||
|
||||
@@ -62,6 +62,21 @@ export function innehallsHash(innehall: string): string {
|
||||
}
|
||||
|
||||
// Katalogiserar varje evidenspost i loggen med nivå, kategori och hash.
|
||||
/**
|
||||
* Var instrumentet kalibrerat när mätningen gjordes?
|
||||
*
|
||||
* Frågan gäller mättillfället, inte i dag. Ett instrument vars
|
||||
* kalibrering gått ut i efterhand gör inte en gammal mätning ogiltig —
|
||||
* och ett nyligen kalibrerat instrument räddar inte en mätning som
|
||||
* gjordes när kalibreringen var utgången.
|
||||
*/
|
||||
export function kalibreradVid(kalibreradTill: string | undefined, matt: string): boolean {
|
||||
if (!kalibreradTill) return false;
|
||||
const giltigt = Date.parse(kalibreradTill);
|
||||
const tidpunkt = Date.parse(matt);
|
||||
return Number.isFinite(giltigt) && Number.isFinite(tidpunkt) && tidpunkt <= giltigt;
|
||||
}
|
||||
|
||||
export function evidensposter(arende: Arende): Evidenspost[] {
|
||||
const poster: Evidenspost[] = [];
|
||||
const lagg = (post: LoggPost, kategori: string, niva: Evidenspost["niva"], sammanfattning: string) =>
|
||||
@@ -78,7 +93,25 @@ export function evidensposter(arende: Arende): Evidenspost[] {
|
||||
const h = post.handelse;
|
||||
if (h.typ === "foto") lagg(post, "foto", "E2", h.beskrivning);
|
||||
if (h.typ === "video") lagg(post, "video", "E3", h.beskrivning);
|
||||
if (h.typ === "matvarde") lagg(post, "mätvärde", "E4", `${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`);
|
||||
if (h.typ === "matvarde") {
|
||||
// Ett mätvärde är E4 bara när det går att visa vad som mätte och
|
||||
// att instrumentet var kalibrerat vid mättillfället. Utan det är
|
||||
// det teknikerns observation av en siffra — E1 (QUALITY-AUDIT M-1).
|
||||
//
|
||||
// Nedgraderingen är tyst i loggen men syns i rapporten: att låtsas
|
||||
// att en okalibrerad mätning väger som en kalibrerad är precis den
|
||||
// sortens obefogade säkerhet produkten finns för att undvika.
|
||||
const spårbar = Boolean(h.matdonId) && kalibreradVid(h.matdonKalibreradTill, post.tidpunkt);
|
||||
const märkning = h.matdonBeteckning
|
||||
? `${h.matdonBeteckning}${spårbar ? "" : " — kalibrering saknas eller utgången"}`
|
||||
: "instrument ej angivet";
|
||||
lagg(
|
||||
post,
|
||||
"mätvärde",
|
||||
spårbar ? "E4" : "E1",
|
||||
`${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""} (${märkning})`,
|
||||
);
|
||||
}
|
||||
if (h.typ === "matarstallning" && !h.undantag) lagg(post, "mätarställning", "E2", `${h.lage === "ingaende" ? "In" : "Ut"}: ${h.varde}`);
|
||||
if (h.typ === "arbetsorder_skannad") lagg(post, "dokument", "E5", `Arbetsorder, ${h.falt.length} fält`);
|
||||
if (h.typ === "observation") lagg(post, "observation", "E1", h.text);
|
||||
|
||||
@@ -8,7 +8,21 @@ export function tidDatum(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString("sv-SE");
|
||||
}
|
||||
|
||||
// Skalar ner ett foto innan det loggas, så att händelseloggen förblir hanterbar.
|
||||
/**
|
||||
* Skalar ner ett foto innan det loggas, så att händelseloggen förblir
|
||||
* hanterbar.
|
||||
*
|
||||
* **Omritningen via canvas är också det som tar bort EXIF — inklusive
|
||||
* GPS-position.** Den egenskapen höll tidigare av en slump
|
||||
* (QUALITY-AUDIT m-3): ingen hade skrivit ned den, och en framtida
|
||||
* ändring i stil med "bevara originalkvaliteten" hade tyst börjat
|
||||
* publicera var kundens bil stod när fotot togs, i en vy som delas med
|
||||
* försäkringsbolag och partners.
|
||||
*
|
||||
* Egenskapen är nu avsiktlig och låst av test. Den som byter ut
|
||||
* canvas-vägen mot något som bevarar originalbytes måste ta bort
|
||||
* metadata på annat sätt först.
|
||||
*/
|
||||
export function skalaNerFoto(fil: File, maxSida = 1024): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const lasare = new FileReader();
|
||||
|
||||
Reference in New Issue
Block a user