Files
Claude e502de8276 Portalmenyn i två nivåer: arbetet synligt, administrationen under Settings
Tio länkar i tre brutna rader var ingen meny. Nivå ett är nu arbetet —
Dashboard, Analysis, Warranty, Insurance, Diagnostics — plus Settings,
som samlar det som konfigurerar organisationen: kunskapskällor,
integration, abonnemang, fakturor och supportärenden mot plattformen.

Settings har en översiktssida där varje område är en hel klickbar rad
med en mening om vad man uträttar där, och inne i inställningarna
visas områdena som en egen, alltid synlig rad med det aktiva markerat —
ingen fällmeny. Områdeslistan ligger i en egen modul och bär
huvudmenyns markering, inställningsraden och översiktssidan ur samma
data.

Nya vyn står bakom portalvakten och ingår i genomgångens
telefonmätning och portalspärrens kontroll.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-07 09:11:12 +00:00

159 lines
5.5 KiB
JavaScript

// Portalspärren (revision 2, m-9).
//
// Anmärkningen var att en inloggning som SER ut att autentisera ger
// portalen bakom den en auktoritet den inte har. Rättelsen är att spärren
// blir verklig där det finns något att spärra mot: är plattformen
// konfigurerad krävs en giltig session, annars är portalen uttryckligen
// märkt som demonstration.
//
// ---- Varför en egen körning och inte ett enhetstest ------------------
//
// Sviten kan bara läsa källkod och se att vakten är inkopplad. Släpper
// den ändå igenom — fel villkor, fel ordning, ett <Navigate> som aldrig
// hinner rendera — ser koden likadan ut. Och en spärr som inte spärrar
// är exakt det m-9 handlade om.
//
// Bygget görs därför här, med plattformen konfigurerad, och kontrollen
// tittar var man faktiskt hamnar. Adressen pekar på ett värdnamn som inte
// finns: spärren ska hålla utan att någon server svarar.
import { execFileSync } from "node:child_process";
import { createServer } from "node:http";
import { existsSync, readFileSync } from "node:fs";
import { dirname, extname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright";
const ROT = dirname(dirname(fileURLToPath(import.meta.url)));
const DIST = join(ROT, "dist");
const PORT = 4271;
const BAS = `http://127.0.0.1:${PORT}`;
const VYER = [
"/alva/portal",
"/alva/portal/analys",
"/alva/portal/kunskapskallor",
"/alva/portal/integration",
"/alva/portal/fakturor",
"/alva/portal/abonnemang",
"/alva/portal/garantier",
"/alva/portal/forsakring",
"/alva/portal/support",
"/alva/portal/installningar",
];
// Nycklarna är plattformsklientens egna (src/felsokning/plattform.ts).
// Skulle de byta namn ska den här kontrollen falla, inte tyst sluta mäta:
// en session som läggs under fel nyckel ger samma utfall som ingen
// session, och då hade "spärrad" sett ut som ett godkänt resultat.
const TOKEN_NYCKEL = "gf-plattform-token";
const KONTO_NYCKEL = "gf-plattform-konto";
const TYPER = {
".html": "text/html; charset=utf-8",
".js": "text/javascript",
".css": "text/css",
".woff2": "font/woff2",
".svg": "image/svg+xml",
};
if (!process.env.GENOMGANG_HOPPA_BYGG) {
execFileSync("npx", ["vite", "build"], {
cwd: ROT,
stdio: ["ignore", "ignore", "inherit"],
env: {
...process.env,
VITE_HASH_ROUTER: "1",
VITE_PLATTFORM_URL: "https://plattform.invalid",
VITE_SUPABASE_URL: "https://portalsparr.invalid",
VITE_SUPABASE_PUBLISHABLE_KEY: "portalsparr",
},
});
}
if (!existsSync(join(DIST, "index.html"))) {
console.error(`Inget bygge i ${DIST}.`);
process.exit(1);
}
const server = createServer((req, res) => {
const vag = decodeURIComponent(req.url.split("?")[0]);
const fil = join(DIST, vag === "/" ? "index.html" : vag);
const mal = existsSync(fil) && extname(fil) ? fil : join(DIST, "index.html");
res.writeHead(200, { "Content-Type": TYPER[extname(mal)] ?? "application/octet-stream" });
res.end(readFileSync(mal));
}).listen(PORT);
const webblasare = await chromium.launch({
executablePath: process.env.PLAYWRIGHT_CHROMIUM ?? undefined,
});
let fel = 0;
const kontroll = (namn, faktiskt, vantat) => {
if (faktiskt === vantat) console.log(`${namn}`);
else {
fel++;
console.log(`${namn}: fick '${faktiskt}', väntade '${vantat}'`);
}
};
const hash = (sida) => new URL(sida.url()).hash;
try {
// ---- Utan session -----------------------------------------------------
{
const ctx = await webblasare.newContext();
const s = await ctx.newPage();
for (const vy of VYER) {
await s.goto(`${BAS}/#${vy}`, { waitUntil: "networkidle" });
await s.waitForTimeout(300);
kontroll(`${vy} spärras`, hash(s), "#/alva/logga-in");
}
await ctx.close();
}
// ---- Med session ------------------------------------------------------
//
// Sessionen läggs in som inloggningen skulle ha lagt in den. Spärren
// ska öppna — annars är den inte en spärr utan ett hinder.
{
const ctx = await webblasare.newContext();
const s = await ctx.newPage();
await s.goto(`${BAS}/#/alva`, { waitUntil: "networkidle" });
await s.evaluate(
([tn, kn]) => {
localStorage.setItem(tn, "provtoken");
localStorage.setItem(kn, JSON.stringify({ namn: "Anna", roll: "admin", organisation: "Verkstad A" }));
},
[TOKEN_NYCKEL, KONTO_NYCKEL],
);
for (const vy of VYER) {
await s.goto(`${BAS}/#${vy}`, { waitUntil: "networkidle" });
await s.waitForTimeout(300);
kontroll(`${vy} öppnas med session`, hash(s), `#${vy}`);
}
// Utloggningen ska både finnas och verkligen avsluta sessionen. En
// knapp som navigerar utan att rensa token vore samma sken igen.
await s.goto(`${BAS}/#/alva/portal`, { waitUntil: "networkidle" });
await s.waitForTimeout(300);
const knapp = s.getByRole("button", { name: /sign out/i });
kontroll("utloggning finns i portalen", await knapp.count(), 1);
await knapp.click();
await s.waitForTimeout(500);
kontroll("utloggning leder till inloggningen", hash(s), "#/alva/logga-in");
kontroll("token är borta", await s.evaluate((tn) => localStorage.getItem(tn), TOKEN_NYCKEL), null);
await s.goto(`${BAS}/#/alva/portal`, { waitUntil: "networkidle" });
await s.waitForTimeout(300);
kontroll("portalen är stängd igen efter utloggning", hash(s), "#/alva/logga-in");
await ctx.close();
}
} finally {
await webblasare.close();
server.close();
}
console.log(fel === 0 ? "\nportalspärren håller" : `\n${fel} fel`);
process.exit(fel === 0 ? 0 : 1);