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:
Generated
+52
-3
@@ -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",
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user