))}
diff --git a/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts b/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts
new file mode 100644
index 0000000..118689f
--- /dev/null
+++ b/felsokning/app/src/felsokning/__tests__/alva-yta.test.ts
@@ -0,0 +1,100 @@
+// @vitest-environment node
+// Disciplinen är det karakteristiska, inte färgerna.
+//
+// Ett designsystem som bara finns i ett dokument blir urvattnat på tredje
+// sidan någon lägger till. Testet läser den faktiska källkoden för
+// webbplatsen och portalen och låser de regler som annars glider:
+// inga gradienter, ingen animation, inga emojier, inga marknadsföringsord,
+// och ingen färg utanför paletten.
+//
+// Det testar utseende genom att läsa kod, vilket är trubbigt. Men det
+// fångar exakt den sortens tillägg som faktiskt sker — en `animate-pulse`
+// här, en `bg-gradient-to-r` där — och som ingen enskilt tycker är fel.
+import { readdirSync, readFileSync } from "node:fs";
+import { describe, expect, it } from "vitest";
+import { FORBJUDNA_ORD } from "@/alva/sprak";
+
+const KATALOGER = ["src/alva", "src/pages/alva"];
+
+const filer = KATALOGER.flatMap((katalog) =>
+ readdirSync(katalog)
+ .filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"))
+ .map((f) => [`${katalog}/${f}`, readFileSync(`${katalog}/${f}`, "utf8")] as const),
+);
+
+/** Kommentarer får diskutera det som är förbjudet i gränssnittet. */
+const utanKommentarer = (kod: string) =>
+ kod.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
+
+describe("ALVA-ytan följer designsystemet", () => {
+ it("hittar filerna att granska", () => {
+ expect(filer.length).toBeGreaterThanOrEqual(7);
+ });
+
+ it.each(filer)("%s använder ingen gradient, glaseffekt eller skugga", (_namn, kod) => {
+ const k = utanKommentarer(kod);
+ expect(k).not.toMatch(/gradient/i);
+ expect(k).not.toMatch(/backdrop-blur|backdrop-filter/i);
+ expect(k).not.toMatch(/\bshadow-(sm|md|lg|xl|2xl)\b/);
+ });
+
+ it.each(filer)("%s innehåller ingen animation", (_namn, kod) => {
+ const k = utanKommentarer(kod);
+ // Rörelse som inte bär information är brus i ett utrymme där
+ // teknikern redan har för mycket.
+ expect(k).not.toMatch(/\banimate-|\btransition-|@keyframes/);
+ });
+
+ it.each(filer)("%s använder inga emojier", (_namn, kod) => {
+ const k = utanKommentarer(kod);
+ // Fyra tecken är tillåtna: ✓ ○ □ →
+ const emojier = k.match(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu) ?? [];
+ expect(emojier.filter((t) => !["✓", "○", "□", "→"].includes(t))).toEqual([]);
+ });
+
+ it.each(filer)("%s använder bara färger ur paletten", (_namn, kod) => {
+ const k = utanKommentarer(kod);
+ const palett = ["#1B1E22", "#4D5662", "#D7DCE2", "#F6F7F8", "#005CA9", "#FFFFFF"];
+ const funna = (k.match(/#[0-9A-Fa-f]{3,8}\b/g) ?? []).map((f) => f.toUpperCase());
+ expect(funna.filter((f) => !palett.includes(f))).toEqual([]);
+ });
+
+ it.each(filer.filter(([n]) => !n.endsWith("sprak.ts")))(
+ "%s innehåller inga marknadsföringsord",
+ (_namn, kod) => {
+ // sprak.ts undantas: den definierar listan och måste innehålla den.
+ const text = utanKommentarer(kod).toLowerCase();
+ for (const ord of FORBJUDNA_ORD) expect(text, ord).not.toContain(ord);
+ },
+ );
+
+ it("hela ytan undviker AI-språk — produkten beskrivs som en metod", () => {
+ for (const [namn, kod] of filer) {
+ const text = utanKommentarer(kod);
+ expect(text, namn).not.toMatch(/\bAI[- ]?(assistant|assistent|powered|driven)\b/i);
+ expect(text, namn).not.toMatch(/\bchatt?bot\b/i);
+ }
+ });
+
+ it("rubriker är versaler — sektionsetiketter, inte meningar", () => {
+ const komponenter = readFileSync("src/alva/komponenter.tsx", "utf8");
+ expect(komponenter).toMatch(/uppercase/);
+ // Både Etikett och Rubrik måste sätta versaler.
+ const rubrik = komponenter.slice(komponenter.indexOf("export function Rubrik"));
+ expect(rubrik.slice(0, 600)).toMatch(/uppercase/);
+ });
+
+ it("avståndsmått följer 8 px-rutnätet", () => {
+ // Rutnätet gäller layout, inte typografi: teckenstorlek och
+ // radavstånd sätts av läsbarhet, inte av rytmen i sidan. Det som
+ // mäts här är Tailwinds avståndsklasser, som är 4 px-steg — ett
+ // jämnt steg är alltså en 8 px-multipel.
+ const udda: string[] = [];
+ for (const [namn, kod] of filer) {
+ for (const m of utanKommentarer(kod).matchAll(/\b(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap)-(\d+)\b/g)) {
+ if (Number(m[2]) % 2 !== 0) udda.push(`${namn}: ${m[0]}`);
+ }
+ }
+ expect(udda).toEqual([]);
+ });
+});
diff --git a/felsokning/app/src/pages/alva/Ansokan.tsx b/felsokning/app/src/pages/alva/Ansokan.tsx
new file mode 100644
index 0000000..135cdfa
--- /dev/null
+++ b/felsokning/app/src/pages/alva/Ansokan.tsx
@@ -0,0 +1,126 @@
+// Kontoansökan.
+//
+// Ingen självbetjäning, ingen kortbetalning, ingen provperiod. Ansökan
+// granskas manuellt och det står på sidan — inte som en ursäkt utan som
+// en uppgift. Ett system som avgör vad som får kallas verifierat i en
+// garantitvist kan inte dela ut konton automatiskt.
+
+import { type FormEvent, useState } from "react";
+import { Block, Etikett, FARG, Knapp, Rubrik, Statusmärke } from "@/alva/komponenter";
+import { Ram } from "./Ram";
+
+interface Falt {
+ namn: string;
+ etikett: string;
+ typ?: string;
+ kravs?: boolean;
+ hjalp?: string;
+}
+
+const FALT: Falt[] = [
+ { namn: "foretag", etikett: "Company", kravs: true },
+ { namn: "orgnummer", etikett: "Organization number", kravs: true },
+ { namn: "kontakt", etikett: "Contact person", kravs: true },
+ { namn: "epost", etikett: "Email", typ: "email", kravs: true },
+ { namn: "telefon", etikett: "Phone", typ: "tel", kravs: true },
+ { namn: "tekniker", etikett: "Number of technicians", typ: "number", kravs: true },
+ { namn: "bransch", etikett: "Industry", kravs: true },
+ { namn: "land", etikett: "Country", kravs: true },
+ { namn: "anvandare", etikett: "Expected number of users", typ: "number" },
+];
+
+export default function Ansokan() {
+ const [skickad, setSkickad] = useState(false);
+ const [referens, setReferens] = useState("");
+
+ const skicka = (e: FormEvent) => {
+ e.preventDefault();
+ // Referensen bildas lokalt så sökanden har något att hänvisa till
+ // omedelbart. Den slutliga beteckningen sätts vid granskning.
+ const data = new FormData(e.currentTarget);
+ const bas = String(data.get("orgnummer") ?? "").replace(/\D/g, "").slice(-4) || "0000";
+ setReferens(`ALVA-ORG-${bas}`);
+ setSkickad(true);
+ };
+
+ if (skickad) {
+ return (
+
+
+ Application
+
+ Submitted
+
+
+
+
+
Status
+
+
+
+
+
+
Reference
+
{referens}
+
+
+
+ Applications are reviewed manually. An invoice is issued on approval. The organization is activated
+ once payment is registered.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Registration
+
+ Request account
+
+
+ Applications are reviewed manually. No payment is taken at this stage.
+
+
+
+
+
+ );
+}
diff --git a/felsokning/app/src/pages/alva/Kunskapskallor.tsx b/felsokning/app/src/pages/alva/Kunskapskallor.tsx
new file mode 100644
index 0000000..a9cf384
--- /dev/null
+++ b/felsokning/app/src/pages/alva/Kunskapskallor.tsx
@@ -0,0 +1,197 @@
+// Kunskapskällor — Operational Knowledge Infrastructure.
+//
+// Arkitekturprincipen: ALVA antar aldrig en viss leverantör. Varje källa
+// implementerar samma gränssnitt, och organisationen ansluter det den har
+// licens till. Det gör att produkten aldrig behöver marknadsföras med
+// "vi stödjer X" — den använder organisationens auktoriserade källor.
+//
+// Praktiskt betyder det att en betainstallation fungerar med enbart egna
+// PDF:er, och att externa leverantörer aktiveras senare genom separata
+// anslutningar utan att plattformens kärna ändras.
+//
+// Livscykeln är fem tillstånd med avsikt. "Configured" och "Connected"
+// är inte samma sak — en nyckel kan vara ifylld och ändå fel — och
+// "Validated" skiljer sig från "Connected": anslutningen svarar, men har
+// den data för de fordon organisationen faktiskt arbetar med? Det är den
+// frågan som avgör om källan är till nytta.
+
+import { Block, Etikett, FARG, Knapp, Rubrik, Statusmärke, Symbol, Tabell } from "@/alva/komponenter";
+import type { Status } from "@/alva/sprak";
+import { Ram } from "./Ram";
+
+export type Livscykel = "installed" | "configured" | "connected" | "validated" | "operational";
+
+export const LIVSCYKEL: { id: Livscykel; namn: string; innebord: string }[] = [
+ { id: "installed", namn: "Installed", innebord: "Connector present. No credentials." },
+ { id: "configured", namn: "Configured", innebord: "Credentials stored. Not yet contacted." },
+ { id: "connected", namn: "Connected", innebord: "Provider responds to authentication." },
+ { id: "validated", namn: "Validated", innebord: "Coverage confirmed for the organization's fleet." },
+ { id: "operational", namn: "Operational", innebord: "In use during diagnostics." },
+];
+
+export interface Kunskapskalla {
+ beteckning: string;
+ namn: string;
+ livscykel: Livscykel;
+ autentisering: "API key" | "OAuth" | "File import" | "Network share" | "Internal";
+ formagor: string[];
+ senastSynk: string;
+ prioritet: number;
+}
+
+// Beta: interna dokument räcker för att systemet ska fungera. De externa
+// raderna visar gränssnittet, inte en färdig integration — och statusen
+// säger det rakt ut i stället för att antyda något annat.
+const KALLOR: Kunskapskalla[] = [
+ {
+ beteckning: "ALVA-SRC-001",
+ namn: "Internal company procedures",
+ livscykel: "operational",
+ autentisering: "Internal",
+ formagor: ["Procedures", "Inspection protocols", "Warranty policy"],
+ senastSynk: "Continuous",
+ prioritet: 1,
+ },
+ {
+ beteckning: "ALVA-SRC-002",
+ namn: "Document library",
+ livscykel: "operational",
+ autentisering: "File import",
+ formagor: ["Service manuals", "Technical bulletins", "PDF documentation"],
+ senastSynk: "On upload",
+ prioritet: 2,
+ },
+ {
+ beteckning: "ALVA-SRC-010",
+ namn: "OEM documentation",
+ livscykel: "installed",
+ autentisering: "API key",
+ formagor: ["Repair procedures", "Technical bulletins", "Service information"],
+ senastSynk: "—",
+ prioritet: 3,
+ },
+ {
+ beteckning: "ALVA-SRC-011",
+ namn: "Wiring diagram provider",
+ livscykel: "installed",
+ autentisering: "API key",
+ formagor: ["Wiring diagrams", "Component locations"],
+ senastSynk: "—",
+ prioritet: 4,
+ },
+ {
+ beteckning: "ALVA-SRC-012",
+ namn: "Vehicle data provider",
+ livscykel: "installed",
+ autentisering: "API key",
+ formagor: ["Vehicle identification", "Equipment level", "Service campaigns"],
+ senastSynk: "—",
+ prioritet: 5,
+ },
+ {
+ beteckning: "ALVA-SRC-013",
+ namn: "Repair time catalogue",
+ livscykel: "installed",
+ autentisering: "API key",
+ formagor: ["Operation times", "Parts catalogue"],
+ senastSynk: "—",
+ prioritet: 6,
+ },
+];
+
+const STATUSAV: Record = {
+ installed: "pending",
+ configured: "in_progress",
+ connected: "in_progress",
+ validated: "passed",
+ operational: "complete",
+};
+
+export default function Kunskapskallor() {
+ return (
+
+
+ Operational Knowledge Infrastructure
+
+ Knowledge sources
+
+
+ ALVA is provider-agnostic. Each source implements the same interface; the organization connects those
+ it holds licenses for. No provider is assumed by the platform.
+
+ Configuration requires the organization administrator role.
+
+
+
+
+
+ {LIVSCYKEL.map((l, i) => (
+
+
+ {String(i + 1).padStart(2, "0")}
+
+
+
+ {l.namn}
+
+
+ {l.innebord}
+
+
+
+ ))}
+
+
+
+
+
+ Sources are consulted in order. The technician does not search across systems; relevant information
+ is presented within the procedure with full source attribution.
+
+
+ );
+}
diff --git a/felsokning/app/src/pages/alva/LoggaIn.tsx b/felsokning/app/src/pages/alva/LoggaIn.tsx
new file mode 100644
index 0000000..06199d8
--- /dev/null
+++ b/felsokning/app/src/pages/alva/LoggaIn.tsx
@@ -0,0 +1,80 @@
+// Inloggning.
+//
+// Användarnamn, lösenord, organisation. Ingen social inloggning, ingen
+// "kom ihåg mig", ingen illustration. Ett industrisystem frågar vem du
+// är och i vilken organisation — inte om du vill fortsätta med Google.
+
+import { type FormEvent, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Block, Etikett, FARG, Knapp, Rubrik } from "@/alva/komponenter";
+import { Ram } from "./Ram";
+
+const FALT = [
+ { namn: "organisation", etikett: "Organization", typ: "text" },
+ { namn: "anvandarnamn", etikett: "Username", typ: "text" },
+ { namn: "losenord", etikett: "Password", typ: "password" },
+];
+
+export default function LoggaIn() {
+ const navigera = useNavigate();
+ const [fel, setFel] = useState("");
+
+ const skicka = (e: FormEvent) => {
+ e.preventDefault();
+ const data = new FormData(e.currentTarget);
+ if (!data.get("organisation") || !data.get("anvandarnamn")) {
+ // Statusspråket gäller även fel: ett konstaterande, inte en ursäkt.
+ setFel("Authentication incomplete. Organization and username required.");
+ return;
+ }
+ navigera("/alva/portal");
+ };
+
+ return (
+
+
+ Access
+
+ Login
+
+
+
+
+
+ );
+}
diff --git a/felsokning/app/src/pages/alva/Portal.tsx b/felsokning/app/src/pages/alva/Portal.tsx
new file mode 100644
index 0000000..0ac9404
--- /dev/null
+++ b/felsokning/app/src/pages/alva/Portal.tsx
@@ -0,0 +1,115 @@
+// Kundportalen.
+//
+// Ingen välkomsthälsning, ingen aktivitetsström, ingen graf som rör sig.
+// Portalen redovisar organisationens tillstånd i namngivna fält, på
+// samma sätt som en instrumentpanel gör. Den som öppnar den vill veta
+// tre saker: är licensen giltig, fungerar systemet, och vad har hänt.
+
+import { Link } from "react-router-dom";
+import { PLATTFORMSVERSION, arendebeteckning } from "@/alva/system";
+import { Block, Etikett, FARG, Falt, Knapp, Rubrik, Statusmärke, Tabell } from "@/alva/komponenter";
+import { MEDDELANDE } from "@/alva/sprak";
+import { Ram } from "./Ram";
+
+export default function Portal() {
+ return (
+
+
+
+
+ Organization
+
+ Dashboard
+
+
+
+ ALVA-ORG-0142
+
+
+
+
+
+
+
+
+ Platform + 12 user licenses
+ 12 of 12
+ {PLATTFORMSVERSION}
+
+
+
+
+
+
+ {MEDDELANDE.kalla_ansluten}
+ Signature verified.
+ Last performed on build. Passed.
+ Cases scheduled per case type.
+
+
+
+
+ {/* Platshållare. Att visa tomma nyckeltal vore att lova något
+ som inte finns; att inte visa dem vore att dölja riktningen.
+ Fälten redovisas med status NOT APPLICABLE tills det finns
+ underlag för dem. */}
+
+
+ Available from Phase 3. Metrics are derived from verified outcomes only; a case closed without
+ confirmed root cause contributes nothing.
+
+ ],
+ ["Completion time", "Case duration by phase", ],
+ ["Verification ratio", "Cases with confirmed root cause", ],
+ ["Common fault categories", "Root cause distribution", ],
+ ["Knowledge development", "Documents and coverage", ],
+ ["Procedure adoption", "Exemption rate per step", ],
+ ]}
+ />
+
+
+
+ );
+}
diff --git a/felsokning/app/src/pages/alva/Ram.tsx b/felsokning/app/src/pages/alva/Ram.tsx
new file mode 100644
index 0000000..349037a
--- /dev/null
+++ b/felsokning/app/src/pages/alva/Ram.tsx
@@ -0,0 +1,77 @@
+// Sidramen. Samma på webbplatsen och i portalen — det är därför
+// inloggningen inte känns som ett byte av system.
+//
+// Navigationen är en rad text, inte en meny med ikoner. Den som söker
+// något i ett industrisystem vet vad det heter.
+
+import type { ReactNode } from "react";
+import { Link, useLocation } from "react-router-dom";
+import { ALVA, PLATTFORMSVERSION } from "@/alva/system";
+import { FARG } from "@/alva/komponenter";
+
+const PUBLIKT = [
+ { till: "/alva", text: "Overview" },
+ { till: "/alva/ansokan", text: "Request account" },
+ { till: "/alva/logga-in", text: "Login" },
+];
+
+const PORTAL = [
+ { till: "/alva/portal", text: "Dashboard" },
+ { till: "/alva/portal/kunskapskallor", text: "Knowledge sources" },
+ { till: "/alva/portal/dokument", text: "Document library" },
+ { till: "/felsokning", text: "Diagnostics" },
+];
+
+export function Ram({ children, portal = false }: { children: ReactNode; portal?: boolean }) {
+ const plats = useLocation();
+ const lankar = portal ? PORTAL : PUBLIKT;
+
+ return (
+
+
+
+
+
+ {ALVA.namn}
+
+
+ {ALVA.utlast}
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/felsokning/app/src/pages/alva/Start.tsx b/felsokning/app/src/pages/alva/Start.tsx
new file mode 100644
index 0000000..a532411
--- /dev/null
+++ b/felsokning/app/src/pages/alva/Start.tsx
@@ -0,0 +1,293 @@
+// ALVA — publik webbplats.
+//
+// Webbplatsen och plattformen är samma system. Besökaren ska aldrig
+// uppleva att den lämnar marknadswebben när den loggar in, eftersom den
+// inte gör det: samma komponentbibliotek, samma rutnät, samma ord.
+//
+// Det som saknas här är lika viktigt som det som finns. Ingen hjälte-
+// bild, ingen gradient, ingen animation, inga kundlogotyper, ingen
+// uppmaning att boka demo. En sida som försöker övertyga läser som
+// marknadsföring; en sida som redovisar läser som dokumentation. Den
+// senare är vad en teknisk chef fattar beslut utifrån.
+
+import { Link } from "react-router-dom";
+import { ALVA, FASER, PLATTFORMSVERSION } from "@/alva/system";
+import { Block, Etikett, FARG, Knapp, Rubrik, Symbol, Tabell } from "@/alva/komponenter";
+import { Ram } from "./Ram";
+
+function Sektion({ etikett, rubrik, children }: { etikett: string; rubrik?: string; children: React.ReactNode }) {
+ return (
+
+
+ {etikett}
+ {rubrik && (
+
+ {rubrik}
+
+ )}
+ {children}
+
+
+ );
+}
+
+export default function Start() {
+ return (
+
+ {/* ---- Hero. En mening. Ingen slogan. ---- */}
+
+
+
+ {ALVA.namn}
+
+
+ {ALVA.utlast}
+
+
+ {ALVA.position}
+
+
+
+ {ALVA.definition}
+
+
+
+
+ Request account
+
+
+ Customer login
+
+
+
+
+
+ {/* ---- Metoden ---- */}
+
+
+ ALVA is a standardized method for systematic analysis, localization, verification and action. Every
+ decision is traceable, every conclusion verifiable, every action reproducible.
+
+
+
+ {/* ---- Arbetsgång. Numrerad, inte illustrerad. ---- */}
+
+
+ {[
+ ["Create organization", "Registration is submitted and reviewed."],
+ ["Users receive accounts", "Roles are assigned by the organization administrator."],
+ ["Guided diagnostic procedures", "Each case follows a defined procedure."],
+ ["Verification", "Root cause is confirmed before corrective action."],
+ ["Documentation", "The record is generated from the case log."],
+ ["Continuous improvement", "Verified outcomes refine subsequent procedures."],
+ ].map(([rubrik, text], i) => (
+
+ The platform continuously refines diagnostic procedures using verified operational experience from
+ your own organization.
+
+
+ Every completed diagnostic contributes to subsequent guidance. Only verified outcomes are used —
+ a case closed without confirmed root cause contributes nothing, by design.
+
+
+ The knowledge base belongs to the organization. It is derived from your cases, your procedures and
+ your documentation, and it is not aggregated across customers.
+
+ Each quarter the organization receives an automatically generated operational report. The format is
+ that of an engineering audit: findings, evidence, recommendation.
+
+
+
+
+ {/* ---- Roadmap. Faser, inte löften med datum. ---- */}
+
+
+ No online payment. No subscription sign-up. Invoicing follows review of the application.
+
+
+
+
+ {[
+ "Organization submits registration.",
+ "Application is reviewed.",
+ "Invoice is issued.",
+ "Payment is registered.",
+ "Organization is activated.",
+ "Additional users are billed monthly.",
+ ].map((rad, i) => (
+
+
+ {String(i + 1).padStart(2, "0")}
+
+ {rad}
+
+ ))}
+
+
+
+
+
+ {/* ---- Kunskapskällor ---- */}
+
+
+
+
+ ALVA uses the organization’s own authorized knowledge sources. The architecture is
+ provider-agnostic: every source implements the same interface, and no provider is assumed.
+
+
+ A beta installation operates on internal documentation alone. External providers are enabled later
+ through separate connectors, without change to the platform core.
+