Auth (app-modus):
- Roller admin/standard. Admin oppretter/sletter kontoer, resetter passord,
endrer roller (konto-modal). Eksisterende brukere → admin ved migrering;
bootstrap-bruker via proxy-bypass → admin. Siste admin vernet.
- Tilbakekalling via per-bruker epoke i tokenet: passordbytte, admin-reset,
«Logg ut alle enheter» og sletting dreper alle aktive sesjoner. Token-format
endret (3- → 4-delt) — alle logges ut én gang ved oppgradering.
Dagsplan: søkbar/velgbar planliste («📋 Planer») avledet fra planIndex —
finn plan på navn/dato/person uten å gjette dato.
GUI: login-neon trukket inn i hovedappen (header/knapper/KPI-kanter), og
login-siden fikk mer tech (HUD-hjørner, glitch-tittel, scanlines).
40 integrasjonstester (6 nye), alle grønne. README + CLAUDE.md oppdatert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
225 lines
13 KiB
Markdown
225 lines
13 KiB
Markdown
# NetOps To-Do (Node) — Handoff / Project context
|
||
|
||
> Egnet som `CLAUDE.md` for 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 `comment` på `PUT /api/tasks/:id`
|
||
festes som kommentar med `kind:'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 per `createServer`-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, hvis `TRUST_PROXY_HEADER`,
|
||
en proxy-identitet (`proxyUser()` = header/Basic, IKKE navn-cookie) som
|
||
"trusted bypass". En guard-middleware 401-er API og serverer `login.html` på
|
||
navigasjon når uautentisert. WS-upgrade gates likt.
|
||
- Bypass-poenget: bootstrap fra Basic Auth → sett app-passord → skru av Basic
|
||
Auth. `POST /api/auth/password` tillater å sette passord når man er
|
||
proxy-innlogget; krever nåværende passord kun når man endrer via en app-sesjon.
|
||
- `resolveAuth` brukes 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.epoch.HMAC` (ingen server-side lager).
|
||
Hemmelighet fra `AUTH_SECRET` eller persistert `data/auth.secret`. Cookie er
|
||
HttpOnly, SameSite=Lax, Secure når `X-Forwarded-Proto: https`.
|
||
- **Roller + tilbakekalling:** hver bruker i auth.json har `role`
|
||
(admin/standard) og `epoch`. Tokenet bærer epoken; `verifyToken` avviser hvis
|
||
epoken ikke matcher brukerens nåværende (eller kontoen er slettet). `setPassword`,
|
||
admin-reset, `bumpEpoch()` («logg ut alle enheter») og sletting tilbakekaller
|
||
dermed alle aktive sesjoner. Migrering: eksisterende brukere → admin, epoch=1.
|
||
Ny bruker via proxy-bypass → admin (bootstrap). `isAdminReq()` i server.js
|
||
regner proxy-bypass uten konto som admin. Siste admin kan ikke slettes/degraderes.
|
||
- **Token-format endret** med herdingen (3- → 4-delt), så alle eksisterende
|
||
sesjoner blir ugyldige én gang ved oppgradering — brukerne logger inn på nytt.
|
||
|
||
### 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:
|
||
|
||
```json
|
||
{
|
||
"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`)
|
||
|
||
```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/:id` med 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
|
||
muterer `state.plan.items` og kaller `savePlan()` (PUT) som reconciler mot
|
||
serverens validerte svar. Container-nivå drag-lyttere bindes én gang
|
||
(`bindPlanContainer`); per-element bindes ved hver render (`bindPlanDnD`).
|
||
- **Søkbar planliste** (`derivePlanList()`) bygges på klienten ved å gruppere
|
||
`state.planIndex` på (bruker, dato) — ingen eget endepunkt, holder seg i synk
|
||
via `plan`-WS-meldingene. «📋 Planer»-knappen åpner en søkbar dropdown.
|
||
- **Neon-aksenter** fra login-siden er trukket inn i hoved-GUI via en liten
|
||
blokk i style.css (header-glød, primærknapp, aktiv visning, KPI-kanter).
|
||
- **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/login` avviser 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 mode `0600`.
|
||
`proxyUser()` (bypass-kilden) stoler IKKE på navn-cookien, kun proxy-satte
|
||
headere. Forutsetning for bypass: app-porten er kun nåbar fra proxyen — sett
|
||
`TRUST_PROXY_HEADER=0` for å fjerne bypass etter bootstrapping.
|
||
|
||
## Vanlige utvidelser
|
||
|
||
- **Nytt felt per oppgave:** input i modal (index.html) → `saveForm()` og
|
||
`parseQuickAdd()` 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) mot `node:sqlite` —
|
||
resten av appen er uberørt siden alt går gjennom Store-metodene.
|
||
- **Varsler ved frist:** server har allerede deadline per task; en
|
||
`setInterval` i server.js som broadcaster `{type:'due', …}` + Notification
|
||
API i frontend er den naturlige veien.
|
||
|
||
## Testing-status
|
||
|
||
- `npm test`: 40 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, proxy-bypass, passord-bootstrap, sesjon, feil passord,
|
||
TRUST_PROXY_HEADER=0, roller/admin-CRUD, epoke-tilbakekalling ved passordbytte/
|
||
reset/logout-all, vern av siste admin). 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.
|