Härdning: kedjesvep i drift — och svepets första egna fynd
KEDJESVEPET. Verifieringen fanns men anropades bara vid tvist: ett brott kunde stå oupptäckt i åratal och upptäckas i sämsta tänkbara ögonblick. Nu går driften igenom varje kedja och varje försegling i varje organisation — POST /api/kedjesvep för arbetsledaren, `node server.mjs --kedjesvep` för nattlig cron utan server, med felkod vid brott så att larmet är gratis. Endpoint och svep delar EN implementation (kedjestatus): två implementationer av "är kedjan hel?" kommer att svara olika den dag det betyder något — det var T-13/T-14:s rot, tillämpad i förebyggande syfte. SVEPETS FÖRSTA FYND VAR PÅ RIKTIGT. Första körningen mot integrationsmiljön rapporterade TRE brott där testerna saboterat två. Det tredje var en bugg: leverantörshändelser bär `enhet: undefined`, och den kanoniska formen serialiserade nyckeln som null medan databasens rundresa släpper den. Skriv-digest och läs-digest skilde sig — varje leverantörshändelse bröt sin egen kedjelänk. Klienthändelser gick fria av en slump: JSON.parse kan inte producera undefined. Fixat vid roten: kanonisk form är nu exakt den form som överlever rundresan, inte en nästan likadan. Mekanismen fungerade precis som avsett — dag ett, mot sina egna upphovsmän. TRANSPORTEN. Säkerhetshuvuden på varje API-svar: nosniff, no-store (ett API vars svar är personuppgifter får inte bli en cacheträff), frame-ancestors 'none', no-referrer, HSTS. För stor kropp svarar 413 i stället för ett intetsägande 500. Kroppstak 4 MiB och batchtak 500 verifierade som redan på plats — bokfört, inte antaget. En skriptläxa på vägen: cron-svepets felkod är själva poängen, men under `set -e` dödade den testskriptet vid tilldelningen — felkoden måste fångas i samma andetag. Utgåva 3.4. API-specen dokumenterar svepet. TÜV-2-rapporten har bilaga A med samma-dags-härdningen, inklusive svepets eget fynd. 768 enhetstester, 224 integrationskontroller mot riktig Postgres, återställningsprov, genomgång 4/4, portalspärr, typkontroll och lint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -21,6 +21,19 @@
|
||||
// TÜV-härdningen ändrade vad loggen garanterar.
|
||||
|
||||
export const UTGAVOR = [
|
||||
{
|
||||
version: "3.4",
|
||||
datum: "2026-08-06",
|
||||
commit: "c1c56e6",
|
||||
rubrik: "Kedjesvep, registret gäller alla vägar, och ett fynd svepet gjorde själv",
|
||||
andringar: [
|
||||
"Kedjesvepet: driftens nattliga fråga \"är allt fortfarande sant?\". Varje kedja och försegling i varje organisation verifieras — på begäran av arbetsledaren, nattligen av driften, med felkod vid brott så larmet är gratis.",
|
||||
"Svepet hittade en riktig bugg första gången det kördes: leverantörshändelser bröt sina egna kedjelänkar därför att den kanoniska formen behandlade undefined som null medan databasen släpper nyckeln. Kanonisk form är nu exakt den form som överlever rundresan.",
|
||||
"Kalibreringsfaktumet fryses vid mottagandet: taket \"hög\" kräver ett mätdon vars kalibrering gällde när mätningen togs — inte bara ett mätdon.",
|
||||
"Mätdonsregistret gäller även leverantörsprotokoll. Okänt instrument nedgraderas öppet i stället för att tros på.",
|
||||
"Säkerhetshuvuden på varje API-svar, ärlig 413 för för stora kroppar, och startvarning vid oförseglad drift.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "3.3",
|
||||
datum: "2026-08-06",
|
||||
|
||||
@@ -1117,6 +1117,40 @@ PROTH2=$(curl -s "$BAS/api/arenden/arende-kal/handelser" -H "Authorization: Bear
|
||||
kontroll "leverantörens mätvärde bär registrets stämpel" \
|
||||
"$(echo "$PROTH2" | falt '.handelser.filter(h=>h.handelse.beskrivning==="Kastmatning")[0].handelse.kalibreradVidMatning')" "true"
|
||||
|
||||
# 18j. Kedjesvepet hittar det som redan är saboterat (TÜV-2-härdning)
|
||||
#
|
||||
# Verifieringen fanns men anropades bara vid tvist. Svepet är driftens
|
||||
# nattliga fråga "är allt fortfarande sant?" — och testerna 18c/18f har
|
||||
# redan lämnat två saboterade ärenden efter sig, så svepet provas mot
|
||||
# riktiga brott i stället för arrangerade.
|
||||
SVEP=$(curl -s -X POST "$BAS/api/kedjesvep" -H "Authorization: Bearer $TOKEN_A")
|
||||
kontroll "svepet hittar båda de saboterade ärendena" "$(echo "$SVEP" | falt '.brutna.length')" "2"
|
||||
kontroll "svepet pekar ut arende-kedja" \
|
||||
"$(echo "$SVEP" | falt '.brutna.some(b=>b.arende==="arende-kedja")')" "true"
|
||||
kontroll "svepet pekar ut den ogiltiga förseglingen" \
|
||||
"$(echo "$SVEP" | falt '.ogiltigaForseglingar.includes("arende-forseglat")')" "true"
|
||||
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/kedjesvep" -H "Authorization: Bearer $TOKEN_J")
|
||||
kontroll "tekniker nekas svepet" "$KOD" "403"
|
||||
|
||||
# Cron-läget: samma svep utan server, felkod vid brott — larmet är gratis.
|
||||
# Felkoden är själva poängen — men under `set -e` dödar den skriptet om
|
||||
# den inte fångas i samma andetag.
|
||||
CLI_KOD=0
|
||||
CLI_UT=$(DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
||||
JWT_SECRET=integrationshemlighet FORSEGLING_NYCKEL=forseglingsnyckel node server.mjs --kedjesvep 2>/dev/null) || CLI_KOD=$?
|
||||
kontroll "cron-svepet avslutar med felkod vid brott" "$CLI_KOD" "1"
|
||||
kontroll "cron-svepet rapporterar brottet i loggformat" \
|
||||
"$(echo "$CLI_UT" | grep -c 'arende-kedja')" "1"
|
||||
|
||||
# 18k. Säkerhetshuvuden och ärlig 413
|
||||
HUVUDEN=$(curl -s -D - -o /dev/null "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A")
|
||||
kontroll "nosniff sätts" "$(echo "$HUVUDEN" | grep -ci 'x-content-type-options: nosniff')" "1"
|
||||
kontroll "API-svar cachas aldrig" "$(echo "$HUVUDEN" | grep -ci 'cache-control: no-store')" "1"
|
||||
kontroll "inbäddning är stängd" "$(echo "$HUVUDEN" | grep -ci "frame-ancestors 'none'")" "1"
|
||||
KOD=$(head -c 5242880 /dev/zero | curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-test1/handelser" \
|
||||
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' --data-binary @-)
|
||||
kontroll "för stor kropp svarar 413, inte 500" "$KOD" "413"
|
||||
|
||||
# 19. Support och felanmälan (ALVA-PROC-0050)
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
|
||||
@@ -730,6 +730,45 @@ paths:
|
||||
"400": { $ref: "#/components/responses/Fel" }
|
||||
"401": { $ref: "#/components/responses/Fel" }
|
||||
|
||||
/api/kedjesvep:
|
||||
post:
|
||||
tags: [Ärenden]
|
||||
summary: Verifiera varje kedja i organisationen (ALVA-SPEC-070)
|
||||
description: >
|
||||
Driftens fråga "är allt fortfarande sant?". Går igenom samtliga
|
||||
ärenden och pekar ut varje kedjebrott och varje ogiltig
|
||||
försegling. Kräver arbetsledare eller administratör. Samma svep
|
||||
körs nattligen av driften via `node server.mjs --kedjesvep`, som
|
||||
avslutar med felkod vid brott.
|
||||
responses:
|
||||
"200":
|
||||
description: Svepets utfall.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
arenden: { type: integer }
|
||||
brutna:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
arende: { type: string }
|
||||
brott:
|
||||
type: object
|
||||
properties:
|
||||
index: { type: integer }
|
||||
id: { type: string }
|
||||
ogiltigaForseglingar:
|
||||
type: array
|
||||
items: { type: string }
|
||||
oforseglade:
|
||||
type: integer
|
||||
description: Ärenden med kedjade händelser men ingen försegling (t.ex. ännu inte avslutade).
|
||||
"401": { $ref: "#/components/responses/Fel" }
|
||||
"403": { $ref: "#/components/responses/Fel" }
|
||||
|
||||
/api/arenden/{arendeId}/kedja:
|
||||
parameters:
|
||||
- name: arendeId
|
||||
|
||||
@@ -414,6 +414,19 @@ function svara(res, status, kropp) {
|
||||
// Ursprunget sätts en gång per anrop i hanteraren nedan.
|
||||
...korsHuvuden(res),
|
||||
"Access-Control-Allow-Headers": "authorization, content-type",
|
||||
// ---- Säkerhetshuvuden (TÜV-2-härdning) ------------------------------
|
||||
// API:t svarar JSON och bara JSON. `nosniff` gör att en webbläsare
|
||||
// aldrig gissar om det; `no-store` håller loggar och fakturor borta
|
||||
// från mellanliggande cachar — ett API vars svar är personuppgifter
|
||||
// får inte bli en cacheträff; `frame-ancestors` stänger inbäddning.
|
||||
// HSTS sätts även om TLS termineras framför oss: huvudet är
|
||||
// verkningslöst över http och rätt över https, så det kostar inget
|
||||
// att alltid tala sanning.
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Cache-Control": "no-store",
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"Content-Security-Policy": "frame-ancestors 'none'",
|
||||
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
||||
});
|
||||
res.end(JSON.stringify(kropp));
|
||||
}
|
||||
@@ -577,6 +590,71 @@ async function skrivKedjat(arendeId, poster) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kedje- och förseglingsstatus för ETT ärende (ALVA-SPEC-070).
|
||||
*
|
||||
* En enda implementation, delad av verifierings-endpointen och
|
||||
* kedjesvepet. TÜV-2 T-14 handlade om en kontroll som fanns vid en grind
|
||||
* men inte vid resursen — samma läxa gäller verifiering: två
|
||||
* implementationer av "är kedjan hel?" kommer att svara olika den dag
|
||||
* det betyder något.
|
||||
*/
|
||||
async function kedjestatus(arendeId) {
|
||||
const rader = await pool.query(
|
||||
`select id, tidpunkt, anvandare, handelse, sekvens, kedjehash
|
||||
from felsokning_handelser where arende_id = $1
|
||||
order by sekvens nulls first, tidpunkt, id`,
|
||||
[arendeId],
|
||||
);
|
||||
const kedjade = rader.rows.filter((r) => r.sekvens != null);
|
||||
const tillLank = (r) => ({
|
||||
id: r.id,
|
||||
tidpunkt: new Date(r.tidpunkt).toISOString(),
|
||||
anvandare: r.anvandare,
|
||||
digest: innehallsHash(kanoniskt(r.handelse)),
|
||||
kedjehash: r.kedjehash,
|
||||
});
|
||||
const kedja = verifieraKedja(arendeId, kedjade.map(tillLank));
|
||||
const okedjade = rader.rows.length - kedjade.length;
|
||||
|
||||
const arende = await pool.query(
|
||||
`select kedjerot, forsegling, forseglad from felsokning_arenden where id = $1`,
|
||||
[arendeId],
|
||||
);
|
||||
const a = arende.rows[0] ?? {};
|
||||
let forsegling = "saknas";
|
||||
let efterForsegling = null;
|
||||
if (a.forsegling) {
|
||||
if (!FORSEGLING_NYCKEL) {
|
||||
forsegling = "kan inte prövas — FORSEGLING_NYCKEL saknas";
|
||||
} else {
|
||||
const idx = kedjade.findIndex((r) => r.kedjehash === a.kedjerot);
|
||||
// Prefixet prövas för sig: ett brott EFTER förseglingspunkten gör
|
||||
// inte prefixet mindre bevisat — brottet pekas ut för sig.
|
||||
const prefixOk = idx >= 0 && verifieraKedja(arendeId, kedjade.slice(0, idx + 1).map(tillLank)).ok;
|
||||
if (
|
||||
prefixOk &&
|
||||
provaForsegling(FORSEGLING_NYCKEL, arendeId, a.kedjerot, new Date(a.forseglad).toISOString(), a.forsegling)
|
||||
) {
|
||||
forsegling = "giltig";
|
||||
efterForsegling = kedjade.length - (idx + 1);
|
||||
} else {
|
||||
forsegling = "OGILTIG";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: kedja.ok,
|
||||
rot: kedja.rot,
|
||||
brott: kedja.brott ?? null,
|
||||
kedjade: kedjade.length,
|
||||
okedjade,
|
||||
forsegling,
|
||||
efterForsegling,
|
||||
};
|
||||
}
|
||||
|
||||
async function grindHinder(pool, arendeId, nya, sprak = STANDARD) {
|
||||
const rader = await pool.query(
|
||||
`select h.handelse, a.metodik_id from felsokning_handelser h
|
||||
@@ -937,7 +1015,18 @@ async function fakturamedStatus(id) {
|
||||
function kanoniskt(värde) {
|
||||
if (Array.isArray(värde)) return `[${värde.map(kanoniskt).join(",")}]`;
|
||||
if (värde && typeof värde === "object") {
|
||||
// Nycklar med undefined SLÄPPS, precis som JSON.stringify gör.
|
||||
//
|
||||
// Kedjesvepet hittade felet första natten det fanns: protokollvägen
|
||||
// bygger händelser med `enhet: plocka(...) ?? undefined`, och den
|
||||
// här funktionen serialiserade nyckeln som null medan jsonb — som
|
||||
// får objektet via JSON.stringify — släppte den. Skriv-digest och
|
||||
// läs-digest skilde sig, och varje leverantörshändelse bröt sin
|
||||
// egen kedjelänk. Klienthändelser gick fria av en slump: JSON.parse
|
||||
// kan inte producera undefined. Kanonisk form måste vara EXAKT den
|
||||
// form som överlever databasens rundresa, inte en nästan likadan.
|
||||
return `{${Object.keys(värde)
|
||||
.filter((n) => värde[n] !== undefined)
|
||||
.sort()
|
||||
.map((n) => `${JSON.stringify(n)}:${kanoniskt(värde[n])}`)
|
||||
.join(",")}}`;
|
||||
@@ -2575,107 +2664,59 @@ export function skapaServer() {
|
||||
if (!(await arendeIOrg(kedjeVag[1], anspr.org))) {
|
||||
return svara(res, 404, { error: "Ärendet är inte tillgängligt." });
|
||||
}
|
||||
const rader = await pool.query(
|
||||
`select id, tidpunkt, anvandare, handelse, sekvens, kedjehash
|
||||
from felsokning_handelser where arende_id = $1
|
||||
order by sekvens nulls first, tidpunkt, id`,
|
||||
[kedjeVag[1]],
|
||||
);
|
||||
const kedja = verifieraKedja(
|
||||
kedjeVag[1],
|
||||
rader.rows
|
||||
.filter((r) => r.sekvens != null)
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
tidpunkt: new Date(r.tidpunkt).toISOString(),
|
||||
anvandare: r.anvandare,
|
||||
digest: innehallsHash(kanoniskt(r.handelse)),
|
||||
kedjehash: r.kedjehash,
|
||||
})),
|
||||
);
|
||||
const okedjade = rader.rows.filter((r) => r.sekvens == null).length;
|
||||
|
||||
// Förseglingen täcker sitt PREFIX, inte kedjans nuvarande rot.
|
||||
//
|
||||
// Skillnaden är inte akademisk. Offline-synk är en kärnfunktion:
|
||||
// en annan enhet kan lämna in händelser EFTER att ärendet
|
||||
// stängts, och då flyttar kedjans rot förbi den förseglade.
|
||||
// Revision 3 (K-1) fann att verifieringen då svarade OGILTIG —
|
||||
// ett falsklarm i normal drift, och en verifiering som larmar
|
||||
// falskt avfärdas snart som trasig. Det är så ett skydd dör.
|
||||
//
|
||||
// Rätt semantik: den förseglade roten ska vara EN LÄNK I den
|
||||
// omräknade kedjan. Är den det bevisar förseglingen loggen fram
|
||||
// till den punkten, och det som kom efter redovisas som
|
||||
// oförseglat i stället för att smittas eller smitta. En
|
||||
// angripare som räknat om kedjan stoppas fortfarande: den
|
||||
// omräknade kedjan innehåller inte den gamla roten, och en ny
|
||||
// försegling kan inte skapas utan nyckeln.
|
||||
const arende = await pool.query(
|
||||
`select kedjerot, forsegling, forseglad from felsokning_arenden where id = $1`,
|
||||
[kedjeVag[1]],
|
||||
);
|
||||
const a = arende.rows[0] ?? {};
|
||||
let forsegling = "saknas";
|
||||
let efterForsegling = null;
|
||||
if (a.forsegling) {
|
||||
if (!FORSEGLING_NYCKEL) {
|
||||
forsegling = "kan inte prövas — FORSEGLING_NYCKEL saknas";
|
||||
} else {
|
||||
const kedjade = rader.rows.filter((r) => r.sekvens != null);
|
||||
const idx = kedjade.findIndex((r) => r.kedjehash === a.kedjerot);
|
||||
// Prefixet prövas för sig: ett brott EFTER förseglingspunkten
|
||||
// gör inte prefixet mindre bevisat — brottet pekas ut ovan.
|
||||
const prefixOk =
|
||||
idx >= 0 &&
|
||||
verifieraKedja(
|
||||
kedjeVag[1],
|
||||
kedjade.slice(0, idx + 1).map((r) => ({
|
||||
id: r.id,
|
||||
tidpunkt: new Date(r.tidpunkt).toISOString(),
|
||||
anvandare: r.anvandare,
|
||||
digest: innehallsHash(kanoniskt(r.handelse)),
|
||||
kedjehash: r.kedjehash,
|
||||
})),
|
||||
).ok;
|
||||
if (
|
||||
prefixOk &&
|
||||
provaForsegling(
|
||||
FORSEGLING_NYCKEL,
|
||||
kedjeVag[1],
|
||||
a.kedjerot,
|
||||
new Date(a.forseglad).toISOString(),
|
||||
a.forsegling,
|
||||
)
|
||||
) {
|
||||
forsegling = "giltig";
|
||||
efterForsegling = kedjade.length - (idx + 1);
|
||||
} else {
|
||||
forsegling = "OGILTIG";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const status = await kedjestatus(kedjeVag[1]);
|
||||
// Vad svaret bevisar och inte: innehållet är oförändrat sedan
|
||||
// mottagandet. Ingenting om tiden före serverns klocka, ingenting
|
||||
// om sanningshalten. Texten står i svaret så att den följer med
|
||||
// in i varje rapport som citerar det.
|
||||
return svara(res, 200, {
|
||||
ok: kedja.ok,
|
||||
rot: kedja.rot,
|
||||
brott: kedja.brott ?? null,
|
||||
kedjade: rader.rows.length - okedjade,
|
||||
okedjade,
|
||||
forsegling,
|
||||
// Hur många kedjade händelser som ligger EFTER förseglingen.
|
||||
// Noll är det normala; ett tal här är inte ett fel utan ett
|
||||
// faktum som läsaren ska se: sen synk, sena kundbesked.
|
||||
efterForsegling,
|
||||
...status,
|
||||
bevisar:
|
||||
"Content unchanged since receipt, in the recorded order. Nothing about the time before receipt, nothing about accuracy.",
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Kedjesvepet (TÜV-2-härdning) -------------------------------
|
||||
//
|
||||
// Verifieringen fanns men anropades bara vid tvist — ett brott
|
||||
// kunde alltså stå oupptäckt i åratal och upptäckas i det sämsta
|
||||
// tänkbara ögonblicket. Svepet går igenom organisationens samtliga
|
||||
// ärenden och pekar ut varje brott och varje ogiltig försegling.
|
||||
// Körs av arbetsledare/admin på begäran, och av driften nattligen
|
||||
// via `node server.mjs --kedjesvep` (alla organisationer, avslutar
|
||||
// med felkod vid brott så cron-larmet är gratis).
|
||||
if (req.method === "POST" && vag === "/api/kedjesvep") {
|
||||
if (anspr.roll === "tekniker") {
|
||||
return svara(res, 403, { error: "Kräver arbetsledare eller administratör." });
|
||||
}
|
||||
const alla = await pool.query(
|
||||
`select id from felsokning_arenden where organisation_id = $1 order by nummer`,
|
||||
[anspr.org],
|
||||
);
|
||||
const brutna = [];
|
||||
const ogiltigaForseglingar = [];
|
||||
let oforseglade = 0;
|
||||
for (const rad of alla.rows) {
|
||||
const status = await kedjestatus(rad.id);
|
||||
if (!status.ok) brutna.push({ arende: rad.id, brott: status.brott });
|
||||
if (status.forsegling === "OGILTIG") ogiltigaForseglingar.push(rad.id);
|
||||
if (status.forsegling === "saknas" && status.kedjade > 0) oforseglade += 1;
|
||||
}
|
||||
if (brutna.length > 0 || ogiltigaForseglingar.length > 0) {
|
||||
logga("varning", "kedjesvep fann brott", {
|
||||
organisation: anspr.org,
|
||||
brutna: brutna.length,
|
||||
ogiltigaForseglingar: ogiltigaForseglingar.length,
|
||||
});
|
||||
}
|
||||
return svara(res, 200, {
|
||||
arenden: alla.rows.length,
|
||||
brutna,
|
||||
ogiltigaForseglingar,
|
||||
oforseglade,
|
||||
});
|
||||
}
|
||||
|
||||
const handelserVag = vag.match(/^\/api\/arenden\/([A-Za-z0-9_-]+)\/handelser$/);
|
||||
if (handelserVag) {
|
||||
// Organisationsgränsen: ärendet måste tillhöra användarens org.
|
||||
@@ -2840,6 +2881,9 @@ export function skapaServer() {
|
||||
} catch (fel) {
|
||||
// Spår-id:t i raden gör att hela kedjan går att hitta i Logs
|
||||
// Insights utifrån larmet.
|
||||
if (fel?.message === "för stor kropp") {
|
||||
return svara(res, 413, { error: "Kroppen är för stor." });
|
||||
}
|
||||
logga("fel", "förfrågan misslyckades", {
|
||||
spårId: res.spår.spårId,
|
||||
väg: vag,
|
||||
@@ -2851,6 +2895,35 @@ export function skapaServer() {
|
||||
});
|
||||
}
|
||||
|
||||
// Driftläget: `node server.mjs --kedjesvep` verifierar varje kedja i
|
||||
// varje organisation och avslutar med felkod vid brott. Ingen server
|
||||
// startas — det här är cron-jobbet, och ett cron-jobb som råkar öppna
|
||||
// en lyssnare är en drifthändelse ingen bett om. Utskriften är en enda
|
||||
// JSON-rad per organisation: det format en loggpipeline redan äter.
|
||||
if (process.argv.includes("--kedjesvep")) {
|
||||
const orgar = await pool.query(`select id, namn from organisationer order by namn`);
|
||||
let brott = 0;
|
||||
for (const org of orgar.rows) {
|
||||
const alla = await pool.query(
|
||||
`select id from felsokning_arenden where organisation_id = $1`,
|
||||
[org.id],
|
||||
);
|
||||
const brutna = [];
|
||||
const ogiltiga = [];
|
||||
for (const rad of alla.rows) {
|
||||
const status = await kedjestatus(rad.id);
|
||||
if (!status.ok) brutna.push({ arende: rad.id, brott: status.brott });
|
||||
if (status.forsegling === "OGILTIG") ogiltiga.push(rad.id);
|
||||
}
|
||||
brott += brutna.length + ogiltiga.length;
|
||||
console.log(
|
||||
JSON.stringify({ svep: "kedja", organisation: org.id, arenden: alla.rows.length, brutna, ogiltigaForseglingar: ogiltiga }),
|
||||
);
|
||||
}
|
||||
await pool.end();
|
||||
process.exit(brott > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
// Oförseglad drift ska synas VID START, inte upptäckas i en tvist.
|
||||
// Varje avslut varnar redan för sig, men en rad per avslut i en
|
||||
|
||||
Reference in New Issue
Block a user