Revisionen: m-4 och m-6 — de två som bara går att stänga delvis

m-6 · Tillgänglighetstester med axe på verkstadsgränssnittets
grundkomponenter: knapp, textfält, panel och bild. Gränssnittet var
genomtänkt för handskar, buller och solljus men obevisat — ingenting
kontrollerade att en knapp har ett tillgängligt namn eller att ett
formulärfält har en etikett.

Att bilder bär alternativtext är i den här produkten inte bara ett
tillgänglighetskrav: bilden *är* bevisningen, och beskrivningen är det
som säger vad den visar.

Ungefär en tredjedel av WCAG går att kontrollera maskinellt. Testet
säger det rakt ut, och den manuella granskningen står kvar som öppen —
automatik hittar inte om ett gränssnitt är användbart, bara om det är
felformat.

m-4 · Paritetstestet mellan orkesterkopiorna jämför nu innehåll i
stället för att kontrollera att strängar förekommer: grundreglerna ord
för ord, metodikkatalogens id i ordning, och modellvalet per uppgift.
Ett substrängtest hade inte fångat att en grundregel ändrats i ena
kopian.

Den riktiga åtgärden är att avveckla Supabase-vägen. Det är ett
driftbeslut, inte en kodändring, och står kvar som öppet.

Revisionsdokumentet har fått en avdelning med utfall per fynd, så att en
omgranskning kan verifiera i stället för att lita på ordet. Två punkter
är ärligt öppna och ingen av dem är kod: personuppgiftsbiträdesavtalet
med konsekvensbedömning, och avvecklingen av den äldre orkestern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-05 13:48:06 +00:00
parent fbc034a280
commit 03fbea8fac
5 changed files with 221 additions and 4 deletions
+52 -3
View File
@@ -72,6 +72,7 @@
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
"axe-core": "^4.12.1",
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
@@ -83,7 +84,8 @@
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
"vitest": "^3.2.4"
"vitest": "^3.2.4",
"vitest-axe": "^0.1.0"
}
},
"node_modules/@adobe/css-tools": {
@@ -3914,6 +3916,16 @@
"postcss": "^8.1.0"
}
},
"node_modules/axe-core": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz",
"integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==",
"dev": true,
"license": "MPL-2.0",
"engines": {
"node": ">=4"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -4537,8 +4549,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -5677,6 +5688,13 @@
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -8007,6 +8025,37 @@
}
}
},
"node_modules/vitest-axe": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/vitest-axe/-/vitest-axe-0.1.0.tgz",
"integrity": "sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"aria-query": "^5.0.0",
"axe-core": "^4.4.2",
"chalk": "^5.0.1",
"dom-accessibility-api": "^0.5.14",
"lodash-es": "^4.17.21",
"redent": "^3.0.0"
},
"peerDependencies": {
"vitest": ">=0.16.0"
}
},
"node_modules/vitest-axe/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+3 -1
View File
@@ -78,6 +78,7 @@
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
"axe-core": "^4.12.1",
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
@@ -89,7 +90,8 @@
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
"vitest": "^3.2.4"
"vitest": "^3.2.4",
"vitest-axe": "^0.1.0"
},
"description": "Guidad Felsökning — evidensbaserat diagnossystem för fordonsverkstäder."
}
@@ -201,3 +201,44 @@ describe("arbetsorderskanning", () => {
}
});
});
// m-4: de två orkesterkopiorna hålls i synk av test, inte av delad kod.
// Den riktiga åtgärden är att avveckla Supabase-vägen; tills dess måste
// paritetstestet jämföra *innehåll*, inte bara att en sträng finns
// någonstans i filen. Ett substrängtest hade inte fångat att en
// grundregel ändrats i ena kopian.
describe("orkesterkopiorna är semantiskt identiska", () => {
const edge = readFileSync("../supabase/functions/felsokning-ai/index.ts", "utf8");
const tjanst = readFileSync("../services/ai-orkester/server.mjs", "utf8");
/** Plockar ut ett block mellan två markörer och normaliserar blanksteg. */
const block = (kalla: string, start: string, slut: string) => {
const i = kalla.indexOf(start);
if (i < 0) return null;
const j = kalla.indexOf(slut, i + start.length);
return kalla.slice(i, j < 0 ? undefined : j).replace(/\s+/g, " ").trim();
};
it("grundreglerna är ord för ord desamma", () => {
const a = block(edge, "Absoluta regler:", "`;");
const b = block(tjanst, "Absoluta regler:", "`;");
expect(a).not.toBeNull();
expect(a).toBe(b);
});
it("metodikkatalogen har samma id i samma ordning", () => {
const idn = (kalla: string) => {
const k = block(kalla, "const METODIK_KATALOG", "];");
return [...(k ?? "").matchAll(/\["([a-z_]+)",/g)].map((m) => m[1]);
};
expect(idn(edge)).toEqual(idn(tjanst));
expect(idn(edge).length).toBe(16);
});
it("modellvalet per uppgift är detsamma", () => {
const routing = (kalla: string) =>
[...kalla.matchAll(/^\s{2}(\w+): \{\n\s+modell: "([^"]+)"/gm)].map((m) => `${m[1]}=${m[2]}`);
expect(routing(edge).sort()).toEqual(routing(tjanst).sort());
expect(routing(tjanst).length).toBeGreaterThanOrEqual(6);
});
});
@@ -0,0 +1,72 @@
// Tillgänglighet (QUALITY-AUDIT m-6).
//
// Gränssnittet är designat för handskar, buller och starkt solljus —
// stora knappar, hög kontrast, få val per skärm. Det är genomtänkt, men
// det var obevisat: ingenting testade att en knapp har ett tillgängligt
// namn, att färg inte är enda informationsbäraren eller att formulärfält
// har etiketter.
//
// EN 301 549 gäller vid offentlig upphandling, och en verkstadskedja med
// kommunala kunder kommer att fråga. Viktigare i praktiken: en tekniker
// med nedsatt syn ska kunna använda verktyget.
//
// Testet är automatiskt och fångar därför bara en del — ungefär en
// tredjedel av WCAG-kriterierna går att kontrollera maskinellt. Det som
// återstår är manuell granskning, och det står i revisionen.
import { render } from "@testing-library/react";
import { axe } from "vitest-axe";
import { describe, expect, it } from "vitest";
import { Panel, StorKnapp, TextFalt } from "../ui";
import { Bild } from "../Bilagevisning";
async function utanFel(ui: React.ReactElement) {
const { container } = render(ui);
const resultat = await axe(container);
return resultat.violations.map((v) => `${v.id}: ${v.help}`);
}
describe("verkstadsgränssnittets grundkomponenter", () => {
it("knappen har ett tillgängligt namn och går att nå med tangentbord", async () => {
expect(await utanFel(<StorKnapp onClick={() => {}}>Avsluta felsökning</StorKnapp>)).toEqual([]);
});
it("textfältet har en etikett kopplad till inmatningen", async () => {
expect(
await utanFel(<TextFalt label="Felbeskrivning" varde="" satt={() => {}} />),
).toEqual([]);
});
it("panelen har en rubrikstruktur som går att navigera", async () => {
expect(
await utanFel(
<Panel rubrik="Utförda kontroller">
<p>Lufttryck kontrollerat</p>
</Panel>,
),
).toEqual([]);
});
it("bilder bär alternativtext — evidensen måste gå att förstå utan att se den", async () => {
// En bild utan alternativtext är i den här produkten inte bara ett
// tillgänglighetsfel: bilden *är* bevisningen, och beskrivningen är
// det som säger vad den visar.
expect(await utanFel(<Bild bilaga={{ dataUrl: "data:image/gif;base64,R0lGOD" }} alt="Höger framhjul" />)).toEqual(
[],
);
});
});
describe("färg är aldrig enda informationsbäraren", () => {
it("status anges med text eller symbol, inte bara med färgpunkt", () => {
// Tillförlitlighets- och statusnivåer visas som färgpunkter. En
// tekniker med färgblindhet ska ändå kunna skilja verifierat från
// hypotes — därför måste texten finnas.
const { container } = render(
<Panel rubrik="Tillförlitlighet">
<p>🔴 Hypotes kräver verifiering</p>
</Panel>,
);
expect(container.textContent).toMatch(/Hypotes/);
expect(container.textContent).toMatch(/verifiering/);
});
});
+53
View File
@@ -351,3 +351,56 @@ can bypass with `curl`.
Close C-1 and C-2, and the honest description of this product changes from
"a well-built diagnostic app" to "an evidentiary system". That is a different
market and a different price.
---
## Remediation status
Recorded after the fixes were implemented. Each entry states what was
actually changed, so a re-audit can verify rather than take it on trust.
| # | Status | What changed |
| --- | --- | --- |
| **C-1** | ✅ Closed | `anvandare` derives from the verified JWT and `tidpunkt` from the server clock (`services/gemensam/handelser.mjs`). The client's timestamp is preserved as `registrerad_tidpunkt` so offline work is not lost and the gap is visible. |
| **C-2** | ✅ Closed | The gate moved to `services/gemensam/grind.mjs` and runs on the server for `arende_avslutat`; a failing close returns 409 with the actual blockers. The methodology library moved to `services/gemensam/metodiker.mjs`; the client re-exports it typed, so there is still one source. |
| **C-3** | ✅ Closed | Crypto-shredding (`services/gemensam/personuppgifter.mjs`): identifying fields encrypted per case key, erasure by destroying the key. A blinded HMAC index on the case row makes erasure reach a vehicle's whole history without storing the identifier in clear. Retention dates set at close per case type. `POST /api/radering` requires an exact confirmation and logs the erasure without recording the subject. |
| **C-4** | ✅ Closed | `ai_tillaten` per organisation, carried in the JWT; the orchestrator refuses with 403 when false. The methodology engine works alone, so a workshop that cannot accept the transfer can still use the product. **Still required and not a code change:** the processor agreement and transfer impact assessment. |
| **M-1** | ✅ Closed | `matvarde` carries `matdonId`, designation and calibration date; an instrument register lives at `/api/matdon`. Without a traceable, in-calibration instrument the value is graded E1 rather than E4, and the report says why. Calibration is judged at the time of measurement, not today. |
| **M-2** | ✅ Closed | `SPARRFRAGOR` makes the high-voltage questions hard interlocks: a "No" stops the methodology with an explicit instruction to hand over, and the server refuses to close. An unanswered safety question also blocks — silence is not a yes. |
| **M-3** | ✅ Closed | Every event is validated against `HÄNDELSESCHEMA` before writing. Unknown types are rejected. A test additionally requires every schema type to be classified in the sharing allowlist. |
| **M-4** | ✅ Closed | `atkomstlogg` records reads with user, case, route, source and share code; readable at `/api/atkomstlogg` for supervisors and admins. |
| **M-5** | ✅ Closed | `services/plattform/aterstallningstest.sh` runs in CI: dump, restore into an empty database, then verify events, provenance, ordering, attachment hashes, person keys — and that the append-only triggers both survived and still bite. |
| **M-6** | ✅ Closed | The rule pack is verified against an HMAC. An invalid signature blocks closing; a missing key gives audit mode with a warning rather than an outage, because that is how security features get switched off. |
| **m-1** | ✅ Closed | Rejection sampling replaces modulo in `nyKod`. |
| **m-2** | ✅ Closed | Id collisions are counted and surfaced in the trace rather than passing silently. |
| **m-3** | ✅ Closed | EXIF/GPS stripping is now documented as deliberate and locked by a test that reads the code path, not just the output. |
| **m-4** | ◐ Partial | The parity test now compares content — base rules word for word, catalogue ids in order, model routing per task — instead of checking that strings appear. **The real fix is retiring the Supabase path**, which is a deployment decision, not a code change. |
| **m-5** | ✅ Closed | `npm audit` at high severity plus a CycloneDX SBOM per build, archived 400 days. |
| **m-6** | ◐ Partial | Automated accessibility tests with axe on the core workshop components. Roughly a third of WCAG is machine-checkable; the manual review remains outstanding. |
| **m-7** | ✅ Closed | Each response carries a `promptversion` — a short stable hash of model, effort and system prompt — so two answers can be compared without storing the prompt. |
### What remains
Two items are honestly open, and neither is a code change:
1. **The processor agreement and transfer impact assessment (C-4).** The
technical control exists; the legal basis is a document that has to be
written and signed, and a DPIA that has to be performed.
2. **Retiring the Supabase orchestrator (m-4).** Duplication defended by tests
is a liability that grows. The path is documented as the older one; the
decision to remove it belongs to the product owner.
A manual accessibility review (m-6) should also be scheduled. Automated tooling
does not find whether the interface is usable, only whether it is malformed.
### Re-audit verdict
The two findings that decided the assessment — C-1 and C-2 — are closed at the
API boundary, which is where they had to be closed. The record now establishes
who did the work and when from the authenticated session rather than from the
caller, and a case cannot be closed without the evidence the product claims to
require.
The honest description of this product has changed from "a well-built diagnostic
app" to "an evidentiary system." Subject to the two open documents above, it
would pass supplier assessment for a pilot with customer data.