301c477e25
Main är sedan den här grenen skapades en helt annan produkt — Semantika,
en mobilapp med egen CDK-infrastruktur. Den äger nu repots rot: en
npm-workspaces-monorepo med apps/mobile, services/api och infra.
För att båda ska rymmas i samma repo flyttar Guidad Felsökning in i en
egen katalog i stället för att göra anspråk på roten:
felsokning/app webbklienten (Vite, egen package.json och
eslint-/vitest-konfiguration)
felsokning/services plattformstjänsten och AI-orkestern
felsokning/infra Terraform och databasschemat
felsokning/docs vision, moduler, drift
felsokning/supabase edge-funktion och migrationer
Merge:n hade tagit bort 128 filer som Guidad Felsökning bygger på —
värdapplikationens komponenter, Supabase-klienten, tillgångar — eftersom
main raderat dem och den här grenen inte råkat ändra just dem. De är
återställda på sin nya plats. Utan dem gick varken bygget eller
testerna: ai.ts och synk.ts importerar Supabase-klienten.
Semantikas rotfiler är orörda: package.json, eslint.config.js och
.github/workflows/ är deras. Guidad Felsökning har egna motsvarigheter i
sin katalog.
CI flyttar samtidigt från GitHub Actions till .gitea/workflows — samma
syntax, egna runners. .github/workflows/ tillhör Semantika härefter.
Verifierat på den nya platsen: 96 vitest-tester, typkontroll, eslint på
både klient och tjänster, bygge, och integrationstest mot riktig Postgres.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
147 lines
6.0 KiB
PL/PgSQL
147 lines
6.0 KiB
PL/PgSQL
-- Guidad Felsökning: schema för den självhostade plattformen.
|
|
-- Multi-tenant: varje organisation är en egen tenant — all ärendedata är
|
|
-- organisationsknuten och API:t släpper aldrig data över organisationsgränsen.
|
|
-- Append-only garanteras med triggers: historik kan inte ändras eller
|
|
-- raderas oavsett vilken roll som ansluter.
|
|
|
|
create extension if not exists pgcrypto;
|
|
|
|
create table if not exists organisationer (
|
|
id uuid primary key default gen_random_uuid(),
|
|
namn text not null,
|
|
-- Vad som visas när ett ärende startas (objekttyper,
|
|
-- identifieringsmetoder) — sätts av systemadministratören.
|
|
installningar jsonb not null default '{}'::jsonb,
|
|
skapad timestamptz not null default now()
|
|
);
|
|
alter table organisationer add column if not exists installningar jsonb not null default '{}'::jsonb;
|
|
|
|
-- Märkesspecifika kopplingar per organisation. Uppgifterna lagras
|
|
-- krypterade (AES-256-GCM, nyckel ur INTEGRATION_NYCKEL) och lämnar
|
|
-- aldrig servern i klartext — klienten ser bara maskerade värden.
|
|
create table if not exists integrationer (
|
|
organisation_id uuid not null references organisationer(id),
|
|
leverantor text not null,
|
|
uppgifter_krypt text not null,
|
|
aktiv boolean not null default true,
|
|
skapad timestamptz not null default now(),
|
|
uppdaterad timestamptz not null default now(),
|
|
senast_testad timestamptz,
|
|
senaste_status text,
|
|
primary key (organisation_id, leverantor)
|
|
);
|
|
|
|
create table if not exists anvandare (
|
|
id uuid primary key default gen_random_uuid(),
|
|
organisation_id uuid not null references organisationer(id),
|
|
epost text unique not null,
|
|
losen_hash text not null,
|
|
namn text not null,
|
|
roll text not null default 'tekniker'
|
|
check (roll in ('tekniker', 'arbetsledare', 'admin')),
|
|
-- En avaktiverad användare kan varken logga in eller använda en token
|
|
-- som redan är utfärdad.
|
|
aktiv boolean not null default true,
|
|
-- Räknare för återkallelse. Utfärdade tokens bär sitt värde; höjs det
|
|
-- slutar alla tidigare utfärdade att gälla omedelbart. Loggen rörs
|
|
-- inte — historiken är fortfarande knuten till personen.
|
|
token_version integer not null default 0,
|
|
skapad timestamptz not null default now()
|
|
);
|
|
alter table anvandare add column if not exists aktiv boolean not null default true;
|
|
alter table anvandare add column if not exists token_version integer not null default 0;
|
|
|
|
-- Inloggningsförsök: underlag för takt-begränsning som fungerar bakom
|
|
-- flera repliker (den i minnet gör det inte). Inget lösenord lagras —
|
|
-- bara att ett försök skedde och om det lyckades.
|
|
create table if not exists inloggningsforsok (
|
|
id bigserial primary key,
|
|
epost text not null,
|
|
kalla text not null default '',
|
|
lyckades boolean not null,
|
|
tidpunkt timestamptz not null default now()
|
|
);
|
|
create index if not exists inloggningsforsok_epost_idx
|
|
on inloggningsforsok (epost, tidpunkt desc);
|
|
create index if not exists inloggningsforsok_kalla_idx
|
|
on inloggningsforsok (kalla, tidpunkt desc);
|
|
|
|
create table if not exists felsokning_arenden (
|
|
id text primary key,
|
|
organisation_id uuid not null references organisationer(id),
|
|
nummer integer not null,
|
|
skapad timestamptz not null,
|
|
delningskod text unique,
|
|
metodik_id text,
|
|
skapad_av uuid references anvandare(id),
|
|
insatt timestamptz not null default now()
|
|
);
|
|
create index if not exists felsokning_arenden_org_idx
|
|
on felsokning_arenden (organisation_id, skapad desc);
|
|
|
|
-- Bilagor: foton, video och instrumentbilder. Innehållet ligger utanför
|
|
-- händelsen; loggen bär en referens och innehållets SHA-256. Hashen står
|
|
-- i den append-only-skyddade loggen, så en utbytt bild går att upptäcka —
|
|
-- starkare bevisvärde än när bilden låg inbäddad och måste tros på.
|
|
create table if not exists bilagor (
|
|
id text primary key,
|
|
organisation_id uuid not null references organisationer(id),
|
|
arende_id text not null,
|
|
hash text not null,
|
|
mediatyp text not null,
|
|
storlek integer not null,
|
|
laddad_av uuid references anvandare(id),
|
|
skapad timestamptz not null default now()
|
|
);
|
|
create index if not exists bilagor_arende_idx on bilagor (arende_id);
|
|
create index if not exists bilagor_hash_idx on bilagor (hash);
|
|
|
|
-- Själva bytesen när BILAGE_LAGE=databas. Innehållsadresserat: samma
|
|
-- foto som dokumenteras två gånger lagras en gång. I s3-läget är den
|
|
-- här tabellen tom och innehållet ligger i objektlagringen.
|
|
create table if not exists bilage_innehall (
|
|
hash text primary key,
|
|
data bytea not null,
|
|
skapad timestamptz not null default now()
|
|
);
|
|
|
|
create table if not exists felsokning_handelser (
|
|
id text primary key,
|
|
arende_id text not null references felsokning_arenden(id),
|
|
tidpunkt timestamptz not null,
|
|
anvandare text not null,
|
|
handelse jsonb not null,
|
|
insatt timestamptz not null default now()
|
|
);
|
|
create index if not exists felsokning_handelser_arende_idx
|
|
on felsokning_handelser (arende_id, tidpunkt);
|
|
|
|
create or replace function forbjud_andring() returns trigger
|
|
language plpgsql as $$
|
|
begin
|
|
raise exception 'Loggen är append-only — historik kan inte ändras eller raderas';
|
|
end $$;
|
|
|
|
drop trigger if exists handelser_append_only on felsokning_handelser;
|
|
create trigger handelser_append_only
|
|
before update or delete on felsokning_handelser
|
|
for each row execute function forbjud_andring();
|
|
|
|
drop trigger if exists arenden_append_only on felsokning_arenden;
|
|
create trigger arenden_append_only
|
|
before update or delete on felsokning_arenden
|
|
for each row execute function forbjud_andring();
|
|
|
|
-- Live Share-delningar: återkallbara länkar med behörighetsnivå.
|
|
-- (Åtkomststyrning, inte journal — därför ingen append-only-trigger:
|
|
-- återkallelse sätter aterkallad-tidpunkten.)
|
|
create table if not exists delningar (
|
|
kod text primary key,
|
|
arende_id text not null references felsokning_arenden(id),
|
|
niva text not null check (niva in ('kund', 'partner', 'intern')),
|
|
skapad_av uuid references anvandare(id),
|
|
skapad timestamptz not null default now(),
|
|
aterkallad timestamptz
|
|
);
|
|
create index if not exists delningar_arende_idx on delningar (arende_id);
|