Valgfri app-auth ved siden av proxy-modus (default uendret). Signert sesjons-cookie (scrypt-passord, HMAC, ingen avhengigheter), gating av API + WebSocket, og en selvstendig neon-login (public/login.html) for uautentiserte. Trusted bypass: en proxy-identitet (X-Remote-User / Basic Auth) regnes som innlogget uten app-passord (TRUST_PROXY_HEADER, default på), så man kan bootstrappe: logg inn via NPM Basic Auth → sett app-passord i konto-modalen → skru av Basic Auth. Konto-UI for sett/endre passord + logg ut. AUTH_MODE leses per createServer-instans (testbart). 34 integrasjonstester (8 nye for app-auth), alle grønne. README/CLAUDE.md/setup-skript oppdatert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
NetOps To-Do (Node) — Handoff / Project context
Egnet som
CLAUDE.mdfor fremtidige Claude-sesjoner. Dette er videreutviklingen av PHP-versjonen i../netops-todo-server/— samme formål og datamodell, men ombygd til Node/npm med sanntidssynk og flere samarbeidsfunksjoner. PHP-versjonen ligger urørt ved siden av.
Hva er dette
To-do-applikasjon for en network engineer / et lite driftsteam. Kategorier L1–L4, BGP, peering, security, NMS, automatisering, dokumentasjon, incident, prosjekt. Hver oppgave har prioritet (P1–P4), status, estimat, lokasjon, frist, tags, eier, kommentarer og audit-felter.
Nytt mot PHP-versjonen:
- WebSocket-sanntidssynk (erstatter 60s-polling) + presence + "redigerer nå"
- Kanban-tavle med drag & drop, i tillegg til tabellvisning
- Dagsplan (tredje visning) — per bruker, per dato; ordnet liste man bygger ved å plukke fra backlog. Drag-sortering, kapasitet i timer, navn, flytt hele planen til en annen dato, og delbar (default delt; toggle for privat). Andres delte planer kan ses skrivebeskyttet. Plan-membership vises som badge på oppgavekort/-rader (dag + person).
- Egendefinerte kategorier — i tillegg til de 12 innebygde; lagres i categories.json, deles via WS, valideres dynamisk.
- Feltmodus —
body.field-mode-toggle for større berøringsmål på mobil. - Kommentar ved statusbytte — valgfri
commentpåPUT /api/tasks/:idfestes som kommentar medkind:'status'og legges i aktivitets-detaljen. - Kommentarer per oppgave
- Aktivitetslogg (hvem gjorde hva, server-side, capped til 400 entries)
- Optimistisk låsing —
v-felt per oppgave, 409 ved konflikt - Hurtig-registrering med token-parsing (
#kat @eier p1 !frist ~timer loc:sted) - Angre sletting, tastatursnarveier, persistente filtre (localStorage)
Filer
netops-todo-node/
├── server.js Express + ws. REST-API, WebSocket-hub, auth-oppslag/gating.
├── lib/store.js Datalager: normalisering, validering, aktivitetslogg,
│ atomisk JSON-persistens. Kilden til sannhet for skjema.
├── lib/auth.js Innebygd auth: scrypt-passord, signert sesjons-token.
├── public/
│ ├── index.html Markup. Ingen templating — app.js fyller alt.
│ ├── app.js All frontend-logikk (vanilla JS, ingen rammeverk).
│ ├── login.html Selvstendig neon-login (AUTH_MODE=app). Inline CSS/JS.
│ └── style.css Mørkt tema, samme visuelle identitet som PHP-versjonen.
├── seed.json 16 eksempel-oppgaver.
├── setup-debian13.sh Engangs-oppsett på Debian 13 LXC (apt + npm ci + systemd).
├── test/api.test.js Integrasjonstester (node:test) — REST + WS ende-til-ende.
├── data/ Opprettes ved kjøring: tasks.json, activity.json,
│ users.json, plans.json, categories.json,
│ auth.json (passordhasher), auth.secret (HMAC-nøkkel).
└── README.md Installasjon, proxy-oppsett, API-referanse.
Arkitektur
- Alle mutasjoner via REST (lett å curl-e/feilsøke). WebSocket (
/ws) brukes kun til push: endringer, presence, editing-signaler. - Én node-prosess eier datakatalogen. Lagring i minnet, skriv til disk er
atomisk (tmp + rename) og serialisert gjennom en promise-kjede i
store.js. - To auth-modi (
AUTH_MODE, lest percreateServer-instans så det er testbart):proxy(default):X-Remote-User> Basic Auth >netops_user-cookie >anon. Ingen guard — alt åpent. Bakoverkompatibelt.app: innebygd innlogging.resolveAuth(req)→ sesjons-cookie (netops_session, verifisert i lib/auth.js) ELLER, hvisTRUST_PROXY_HEADER, en proxy-identitet (proxyUser()= header/Basic, IKKE navn-cookie) som "trusted bypass". En guard-middleware 401-er API og servererlogin.htmlpå navigasjon når uautentisert. WS-upgrade gates likt.- Bypass-poenget: bootstrap fra Basic Auth → sett app-passord → skru av Basic
Auth.
POST /api/auth/passwordtillater å sette passord når man er proxy-innlogget; krever nåværende passord kun når man endrer via en app-sesjon. resolveAuthbrukes både i HTTP-middleware og WS (server.on('upgrade')+wss.on('connection')), så samme identitet/gating gjelder begge.
- Sesjon: signert cookie
b64url(user).exp.HMAC(ingen server-side lager). Hemmelighet fraAUTH_SECRETeller persistertdata/auth.secret. Cookie er HttpOnly, SameSite=Lax, Secure nårX-Forwarded-Proto: https.
WebSocket-protokoll (server → klient)
| type | Innhold | Når |
|---|---|---|
hello |
user, online[], editing{} |
Ved tilkobling |
sync |
tasks[], activity[], evt. by |
Ved tilkobling + etter replace/seed |
task |
op: create/update/delete, task/id, by, activity |
Etter hver mutasjon |
presence |
online[] |
Når noen kobler til/fra |
editing |
editing{} = {taskId: [brukere]} |
Når noen åpner/lukker modal |
plan |
user, date, name, items[], shared |
Etter plan-endring. Delte planer → alle; private → kun eieren (andre får tom items for å fjerne badge) |
categories |
categories[], by |
Etter POST /api/categories — til alle |
Klient → server: kun { type: 'editing', id, active }.
Task-skjema (kanonisk form — normalizeTask() i lib/store.js)
Som PHP-versjonen, pluss:
{
"v": 3,
"comments": [{ "id": "c_…", "by": "jon", "at": "ISO", "text": "≤1000",
"kind": "status", "status": "done" }]
}
v bumpes ved hver felt-oppdatering (også når en comment følger med et
statusbytte via update(), siden det ER en oppdatering — men IKKE via det
separate POST .../comments-endepunktet, så en ren kommentar ikke gir falsk
konflikt for noen som redigerer felter samtidig). kind/status settes kun på
kommentarer som kommer fra et statusbytte; vanlige kommentarer mangler dem.
Klienten sender v den redigerte fra; mismatch → 409 med serverens task i
svaret. Frontend håndterer det i handleSaveError(): oppdaterer visningen,
viser varsel, og lar brukeren lagre på nytt (som da overskriver bevisst).
Dagsplan-skjema (plans.json)
{ "jon": { "2026-06-15": { "name": "Feltdag Bergen", "items": ["t_abc", "t_def"], "shared": true } } }
Per bruker, per dato. shared default true (planer er synlige for alle som
hovedregel). _planEntry() normaliserer både gammelt array-format og nytt
objektformat (migrering skjer i _prunePlans() ved oppstart). getPlan()
filtrerer bort slettede oppgaver; getPlanFor(viewer, owner, date) håndhever
tilgang (egen, eller delt → ellers null/403); setPlan/setPlanName/setPlanShared
muterer; movePlan() slår sammen til måldato uten datatap; _scrubFromPlans()
rydder ved sletting; _prunePlans() dropper datoer eldre enn 30 dager.
planIndex(viewer) returnerer {taskId: [{user,date,name}]} filtrert per viewer
(delte + egne private) — driver plan-badgene på kort/rader.
Kategorier (categories.json)
DEFAULT_CATEGORIES (12, i koden) + egendefinerte fra categories.json
([{id,name,color,desc,custom:true}]). store.categories()/categoryIds() gir
det effektive settet; normalizeTask validerer mot opts.validCategories = this.categoryIds(). addCategory() genererer en unik id fra navnet og tildeler
farge fra en palett. Frontend får lista fra /api/state og via categories-WS;
state.categories + catColor/catName/catById-hjelperne erstatter den gamle
hardkodede CATEGORIES/CAT_COLORS.
Designvalg verdt å vite
- Ingen database, ingen frontend-rammeverk, to npm-avhengigheter (express, ws). Samme "lett å patche for neste vakthavende"-filosofi som før.
- Broadcast går til alle, inkludert avsender. Klienten upserter idempotent på id, så dobbel oppdatering er harmløs. REST-svaret brukes også, slik at UI-et fungerer selv om WS er nede (da poller frontend hvert 30s).
- Delvis update er lov:
PUT /api/tasks/:idmed bare{v, status}— normalisering merger mot eksisterende. Quick-actions bruker dette. - Statuskommentar går gjennom samme update-kall (transient
comment-felt), ikke et eget endepunkt — så statusbytte + kommentar + aktivitetslinje blir atomisk og ett enkelt WS-broadcast.changeStatusWithComment()i app.js viser dialogen for ferdig/blokkert; tavle-drag og tabell-✓ ruter gjennom den. - Dagsplanen er optimistisk i frontend:
addToPlan/removeFromPlan/drag mutererstate.plan.itemsog kallersavePlan()(PUT) som reconciler mot serverens validerte svar. Container-nivå drag-lyttere bindes én gang (bindPlanContainer); per-element bindes ved hver render (bindPlanDnD). - Kommentar-sletting er begrenset til egen bruker (eneste autorisasjonsregel i appen — alt annet er åpent for alle innloggede).
- Chart.js fra CDN; frontend degraderer pent (skjuler grafer) uten nett.
Sikkerhetsstatus
Som PHP-versjonen (XSS-escaping i alle render-paths, whitelist-enums,
strenglengde-klamping, brukernavn-regex [A-Za-z0-9._@-]{1,64}), pluss:
/api/loginavviser ugyldige navn i stedet for å sanitere stille.- Cookie settes med
SameSite=Lax— gir grunnleggende CSRF-vern for cookie-auth-modus. Bak proxy-auth gjelder samme vurdering som før (intern bruk OK; offentlig nett → legg på token). express.json({ limit: '2mb' })begrenser payload (import av store lister).- App-auth (AUTH_MODE=app): scrypt-hashing +
timingSafeEqual, signert HMAC-sesjons-cookie (HttpOnly, SameSite=Lax, Secure over https), passord min. 6 tegn. Auth-filer (auth.json,auth.secret) skrives med mode0600.proxyUser()(bypass-kilden) stoler IKKE på navn-cookien, kun proxy-satte headere. Forutsetning for bypass: app-porten er kun nåbar fra proxyen — settTRUST_PROXY_HEADER=0for å fjerne bypass etter bootstrapping.
Vanlige utvidelser
- Nytt felt per oppgave: input i modal (index.html) →
saveForm()ogparseQuickAdd()om ønskelig (app.js) →normalizeTask()(store.js) →diffDetail()for aktivitetsloggen →renderCard()/renderTable(). - Ny kategori: egendefinerte legges til i UI-et («+ Ny kategori …») og
lagres i categories.json. Nye innebygde defaults endres i
DEFAULT_CATEGORIES(både store.js og app.js har lista; serveren er kilden, app.js har en fallback). - SQLite: bytt persistens i
Store(load/_persist) motnode:sqlite— resten av appen er uberørt siden alt går gjennom Store-metodene. - Varsler ved frist: server har allerede deadline per task; en
setIntervali server.js som broadcaster{type:'due', …}+ Notification API i frontend er den naturlige veien.
Testing-status
npm test: 34 integrasjonstester (node:test), booter ekte servere på tilfeldig port med temp-datakatalog (én i proxy-modus, én i app-modus). Dekker normalisering, versjonskonflikt, kommentar-autorisasjon, statuskommentar, dagsplan (CRUD, per-bruker, validering, scrub, dato-fallback, navn, flytt/merge, deling + tilgang, planIndex per viewer), egendefinerte kategorier, aktivitetslogg, WS-broadcast (delt vs. privat plan), persistens, cookie-auth, og app-auth (401-guard, login-side på navigasjon, proxy-bypass, passord-bootstrap, sesjon, feil passord, TRUST_PROXY_HEADER=0). Alle grønne per 2026-06-15 (Node 25).- Manuell røyk-test over HTTP: kategori-opprettelse (auto-id + farge), navngitt delt plan, annen brukers tilgang (delt 200 / privat 403), flytt med bevart navn+delingsstatus, og planIndex verifisert.
- Ikke testet i nettleser med flere samtidige brukere — første ekte test bør
være to nettleservinduer mot
npm start: del en dagsplan og se den hos den andre brukeren + badge på oppgaven, flytt planen til en annen dag, legg til en egendefinert kategori, og slå på feltmodus på en smal skjerm.