Backend-grund, synklager, Live Share-vy och elsystemmetodik

- Supabase-migration: felsokning_arenden + felsokning_handelser med
  RLS för inloggade och append-only garanterat i databasen (inga
  update/delete-rättigheter). Live Share förberedd via security
  definer-funktionen hamta_delat_arende(delningskod) så att anonyma
  läsare aldrig når tabellerna direkt; kategoribyten filtreras bort.
  Realtime-publicering aktiverad för händelsetabellen.
- Synklager i klienten (synk.ts): deterministisk, konfliktfri
  ihopflätning av händelser per id (testad: dedupe, tidsordning,
  idempotens), push av lokala och pull av kollegors händelser var
  15:e sekund. Utan inloggning: lokalt läge, status i ärendehuvudet.
- Live Share-vy (/felsokning/dela/:id): skrivskyddad livevy med
  status ✔/🔄/, bilder, mätvärdestabell, tidslinje och rekommenderat
  nästa steg. Varje ärende får slumpad delningskod vid skapande.
- Ny metodik: Elsystem/strömförsörjning enligt relä-exemplet i
  visionen, med mätvärdeskrav på alla mätpunkter.

Verifierat: 17 vitest-tester gröna, produktionsbygge ok, Playwright-
röktest inkl. Live Share-vyn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-02 22:48:03 +00:00
parent 7b6619133f
commit fcded6bd43
11 changed files with 488 additions and 7 deletions
+45 -1
View File
@@ -17,6 +17,7 @@ import {
tidsfordelningsRader,
} from "@/felsokning/projektioner";
import { metodikForArende, useFelsokning } from "@/felsokning/store";
import { synkroniseraArende, type SynkStatus } from "@/felsokning/synk";
import { FelsokningSkal, NivaBadge, Panel, StorKnapp, TextFalt } from "@/felsokning/ui";
import { skalaNerFoto, tidKlockslag } from "@/felsokning/format";
@@ -29,12 +30,50 @@ const FLIKAR = [
type Flik = (typeof FLIKAR)[number]["id"];
const SYNKSTATUS_LABEL: Record<SynkStatus, string> = {
lokal: "Lokalt läge",
synkar: "Synkar …",
synkad: "Synkad",
offline: "Offline",
};
// Synkar ärendet mot backend: direkt vid nya händelser och därefter var
// 15:e sekund (hämtar kollegors händelser). Utan inloggning: lokalt läge.
function useSynk(arende: Arende | undefined): SynkStatus {
const [status, setStatus] = useState<SynkStatus>("lokal");
const sammanfoga = useFelsokning((s) => s.sammanfoga);
const arendeId = arende?.id;
const antal = arende?.handelser.length ?? 0;
useEffect(() => {
if (!arende || !arendeId) return;
let aktiv = true;
const kor = async () => {
setStatus((s) => (s === "synkad" ? "synkar" : s));
const resultat = await synkroniseraArende(arende);
if (!aktiv) return;
setStatus(resultat.status);
if (resultat.handelser) sammanfoga(arendeId, resultat.handelser);
};
kor();
const timer = setInterval(kor, 15000);
return () => {
aktiv = false;
clearInterval(timer);
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- antal driver omsynk vid nya händelser
}, [arendeId, antal, sammanfoga]);
return status;
}
export default function ArendeSida() {
const { id } = useParams<{ id: string }>();
const arende = useFelsokning((s) => (id ? s.arenden[id] : undefined));
const laggTill = useFelsokning((s) => s.laggTill);
const [flik, setFlik] = useState<Flik>("guide");
const [nu, setNu] = useState(() => new Date().toISOString());
const synkStatus = useSynk(arende);
useEffect(() => {
const timer = setInterval(() => setNu(new Date().toISOString()), 30000);
@@ -63,7 +102,9 @@ export default function ArendeSida() {
hoger={
<div className="text-right">
<p className="text-lg font-extrabold text-amber-400">{total}</p>
<p className="text-xs font-bold uppercase text-zinc-500">{avslutat ? "Avslutat" : "Pågår"}</p>
<p className="text-xs font-bold uppercase text-zinc-500">
{avslutat ? "Avslutat" : "Pågår"} · {SYNKSTATUS_LABEL[synkStatus]}
</p>
</div>
}
>
@@ -600,6 +641,9 @@ function RapportFlik({
Exportera JSON
</StorKnapp>
</div>
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
<StorKnapp variant="sekundar">🟢 Öppna Live Share-vy</StorKnapp>
</Link>
</Panel>
<Panel rubrik={`Ärende #${arende.nummer}`}>
{b.objekt && (