commit 72a78b55fb66aa1d04c8caba6ab1e626fe4dba9b Author: Jon Vanvik Date: Fri Jun 12 11:17:07 2026 +0200 initial commit diff --git a/netops-todo-apache-pakke.zip b/netops-todo-apache-pakke.zip new file mode 100644 index 0000000..ee15fb4 Binary files /dev/null and b/netops-todo-apache-pakke.zip differ diff --git a/netops-todo-server/.htaccess b/netops-todo-server/.htaccess new file mode 100644 index 0000000..8c82327 --- /dev/null +++ b/netops-todo-server/.htaccess @@ -0,0 +1,39 @@ +# NetOps To-Do — Apache-konfigurasjon +# +# === 1) Basic Auth (enkleste måte) =================================== +# Opprett .htpasswd et sted UTENFOR web-roten: +# htpasswd -c /etc/apache2/netops.htpasswd jon +# htpasswd /etc/apache2/netops.htpasswd kari # legg til flere +# +# Juster pathen nedenfor og fjern kommentar-tegnene: +# +# AuthType Basic +# AuthName "NetOps To-Do" +# AuthUserFile /etc/apache2/netops.htpasswd +# Require valid-user + +# === 2) Hvis du heller vil bruke reverse-proxy + ekstern auth ======== +# Proxy-en kan sette X-Remote-User: +# RequestHeader set X-Remote-User "%{REMOTE_USER}s" (i proxy-konfig) +# PHP-koden leser REMOTE_USER / PHP_AUTH_USER / HTTP_X_REMOTE_USER automatisk. + +# === 3) Generelle beskyttelser ======================================== +# Forby tilgang til data-mappa via web (en ekstra .htaccess ligger der også) +RewriteEngine On +RewriteRule ^data/ - [F,L] + +# Forby eksplisitt tilgang til seed.json og evt .json-filer i rot + + Require all denied + + +# Standard index +DirectoryIndex index.php + +# Sett UTF-8 for tekstfiler +AddDefaultCharset UTF-8 + +# Cache-headers (minimal — PHP setter sine egne) + + Header set Cache-Control "no-cache, must-revalidate" + diff --git a/netops-todo-server/CLAUDE.md b/netops-todo-server/CLAUDE.md new file mode 100644 index 0000000..868eb18 --- /dev/null +++ b/netops-todo-server/CLAUDE.md @@ -0,0 +1,175 @@ +# NetOps To-Do — Handoff / Project context + +> Også egnet som `CLAUDE.md` for fremtidige Claude-sesjoner som jobber videre med koden. + +## Hva er dette + +En enkel, selvstendig to-do-applikasjon tilpasset en network engineer som jobber bredt: L1 felt-kabling, L2 switching, L3 ruting, L4 lastbalansering, BGP, peering, security, NMS, automatisering, dokumentasjon, incident og prosjekt. Hver oppgave har prioritet (P1–P4), estimert tid, status, lokasjon/site, frist, tags og eier. + +Det finnes **to versjoner** levert i samme arbeidsstrøm: + +| Versjon | Fil | Lagring | Bruksområde | +|---|---|---|---| +| Standalone | `netops-todo.html` (ligger ved siden av denne mappa) | `localStorage` i nettleseren | Personlig bruk på én maskin, ingen server nødvendig | +| Server / Apache | Hele `netops-todo-server/`-mappa | `data/tasks.json` på server, fil-låst | Delt for et team, auth i Apache eller proxy | + +Server-versjonen er der man kommer til å være etter første "single user, deretter delt"-overgang. + +## Filer i server-pakka + +``` +netops-todo-server/ +├── index.php Frontend (HTML + JS). Tynt PHP-lag øverst leser brukernavn fra Apache. +├── api.php REST-API (single file). Alle actions ligger i én switch. +├── seed.json 16 eksempel-oppgaver. Lastes via "Last inn eksempel-data"-knappen. +├── .htaccess Auth-skeleton + sperrer data/ for direkte web-tilgang. +├── data/ +│ ├── .htaccess Nekter all web-tilgang til lagringskatalogen. +│ ├── tasks.json Opprettes av api.php ved første skriv. Liste av task-objekter. +│ └── known_users.json Liste over brukere som har vært innom (for owner-dropdown). +├── INSTALL.txt Installasjonsguide for Apache. +└── CLAUDE.md (denne filen) +``` + +## Arkitektur + +Frontend er én HTML-side med vanilig JS (ingen rammeverk) og Chart.js fra CDN. +Backend er to PHP-filer, null dependencies, kjører rett på Apache mod_php / php-fpm. + +### Dataflyt +1. Apache autentiserer brukeren (Basic Auth, ekstern proxy, eller hva enn man velger). +2. `index.php` leser `REMOTE_USER` / `PHP_AUTH_USER` / `HTTP_X_REMOTE_USER`, injiserer det i siden som `CURRENT_USER`. +3. JS kaller `api.php?action=list` ved oppstart, så `create` / `update` / `delete` ved endringer. +4. `api.php` validerer input mot whitelister, normaliserer task-objektet, og skriver `data/tasks.json` med `flock(LOCK_EX)`. +5. Hvert minutt pollere frontend `list`-endepunktet for å hente endringer fra andre brukere. + +### API +Alle endepunkt: `POST api.php?action=` med JSON-body. `list` godtar også GET. + +| Action | Body | Svar | +|----------|---------------------|--------------------------------------------| +| list | — | `{ user, tasks: [...], known_users: [...] }` | +| create | task uten id | `{ task }` (med tildelt id og audit-felt) | +| update | task med id | `{ task }` | +| delete | `{ id }` | `{ ok: true }` | +| replace | array av tasks | `{ ok: true, count }` (for import) | +| seed | — | `{ ok: true, count }` (kun hvis db er tom) | + +### Task-skjema (kanonisk form) +```json +{ + "id": "t_abc123", + "title": "string ≤200", + "desc": "string ≤4000", + "category": "L1|L2|L3|L4|BGP|PEER|SEC|NMS|AUTO|DOC|INC|PROJ", + "priority": "P1|P2|P3|P4", + "status": "todo|progress|blocked|done", + "estHours": 0, + "location": "string ≤120", + "deadline": "YYYY-MM-DD eller tom", + "tags": ["≤20 strings, hver ≤40"], + "owner": "brukernavn eller tom = utildelt", + "created": "ISO timestamp", + "createdBy": "brukernavn", + "updated": "ISO timestamp", + "updatedBy": "brukernavn", + "completed": "YYYY-MM-DD eller null", + "completedBy": "brukernavn eller null" +} +``` + +`normalize_task()` i `api.php` er kilden til sannhet. Den whitelister enums, klamper strenger, valider deadline-format, og setter audit-feltene automatisk basert på `current_user()`. + +## Designvalg som er verdt å vite + +- **Ingen database.** JSON-fil + `flock` holder fint helt opp i tusenvis av oppgaver. Hvis det skulle bli ytelsestrøbbel, er det enkleste å bytte til SQLite (PHP `pdo_sqlite` følger med). +- **Auth utenfor koden.** Innloggings-logikk er bevisst utelatt. Apache / proxy gjør jobben. Det gjør at koden er trivielt enkel å revidere og bytte ut. +- **Felles datalager, ikke per-bruker.** Brukerne ser samme oppgaveliste, men `owner` per oppgave + audit-felt gir attribusjon. +- **No-framework frontend.** Holdt bevisst gammeldags vanlig JS for at det skal være lett å patche av neste vakthavende. +- **Polling, ikke websocket.** En `list`-request hvert 60s er nok for et team. Hvis flere skal jobbe samtidig kan intervallet senkes, men brukerne kan også trykke ↻. + +## Sikkerhetsstatus + +| Risiko | Tiltak | +|---|---| +| XSS i task-tekst | `escapeHtml()` i alle render-paths, `htmlspecialchars()` for brukernavn i PHP | +| Path traversal | Filsti er hardkodet (`__DIR__ . '/data/tasks.json'`), ingen brukerinput i path | +| Direct read av tasks.json | Egen `.htaccess` i `data/` med `Require all denied` | +| Race conditions | `flock(LOCK_EX)` ved skriving, `LOCK_SH` ved lesing | +| Injection i enums | Whitelist-validering: `in_array($val, $VALID_*, true)` | +| Sanitering av brukernavn | `preg_replace('/[^A-Za-z0-9._@\-]/', '', $user)`, maks 64 tegn | +| CSRF | Ikke håndtert eksplisitt. For en intern admin-side bak proxy med Basic Auth er det typisk akseptabelt, men hvis det monteres på offentlig nett bør det legges til en `X-Requested-With`-sjekk eller token. | + +## Vanlige utvidelser + +Hvis du jobber videre med dette, dette er de mest sannsynlige neste stegene og hvor du putter dem: + +**Nye kategorier eller prioriteter:** +- Frontend: legg til i `CATEGORIES`-array og `CAT_COLORS`-map i `index.php` (JS-delen). +- Backend: legg til i `$VALID_CATEGORIES` / `$VALID_PRIORITIES` i `api.php`. + +**Nytt felt per oppgave:** +- Legg til input i modal i `index.php`. +- Plukk opp i `saveForm()` (JS) og send med i body. +- Legg til i `normalize_task()` (PHP) med passende sanitering. +- Vis det i `renderTable()` om ønskelig. + +**Bedre samarbeid:** +- Server-Sent Events fra PHP: ny `?action=stream` som holder en åpen koblet med `Cache-Control: no-cache`, sjekker fil-mtime og pusher endringer. +- Eller bare senke `setInterval(loadAll, 60000)` i `init()`. + +**Audit-logg / historikk:** +- I `normalize_task()` legg til append til `data/audit.log` med `[$updatedBy] $action $taskId`. +- Vis logg under hver oppgave-modal med en ny `?action=history&id=...`. + +**Per-bruker syn:** +- Endre `list` til å filtrere på `$user` hvis et flagg er satt. +- Eller bare la frontend defaulte filter "Eier=Meg" — enklere. + +**SQLite-migrering** (hvis JSON blir tregt): +- Bytt `read_tasks()` og `write_tasks()` mot PDO-kall. +- Skjemaet matcher allerede et fornuftig SQL-skjema. + +## Kjente begrensninger + +- Ingen optimistisk locking på enkeltoppgave-nivå. Hvis to brukere lagrer samme oppgave samtidig, vinner den siste. Dette er hentet inn ved at filen i sin helhet låses, men man får ikke conflict-deteksjon. +- Ikke i18n. Norsk er hardkodet i UI. Strings er samlet, så det er ikke vondt å trekke ut. +- Ingen pagination i tabellen. Fungerer fint opp til noen hundre åpne oppgaver, deretter bør man legge til virtual scrolling eller side-knapper. +- Klokken nederst i hover er klient-tid, ikke server-tid. + +## Testing-status + +- PHP-syntaks verifisert med AST-parser (`php-parser` via node, PHP 8-modus). +- Kryssreferanse JS ↔ PHP: alle `api('X')`-kall fra frontend har matchende `case 'X':` i api.php. +- Kryssreferanse DOM-IDer ↔ JS: alle `getElementById('x')` har matchende `id="x"` i HTML. +- Onclick-handlere ↔ funksjonsdefinisjoner: ingen referanser til udefinerte funksjoner. +- Ikke kjørt end-to-end (sandbox manglet PHP-runtime). Første ekte test bør være: `php -S localhost:8080` i mappa og åpne i nettleser. + +## Hurtigreferanse — installasjon + +```bash +# 1. Kopier til server +sudo cp -r netops-todo-server /var/www/netops-todo + +# 2. Skriverettigheter for data/ +sudo chown -R www-data:www-data /var/www/netops-todo/data +sudo chmod 750 /var/www/netops-todo/data + +# 3. Basic Auth-brukere (utenfor docroot!) +sudo htpasswd -c /etc/apache2/netops.htpasswd jon +sudo htpasswd /etc/apache2/netops.htpasswd kari + +# 4. Aktiver auth-blokken i .htaccess (kommenter ut linjene under "1) Basic Auth") +sudo nano /var/www/netops-todo/.htaccess + +# 5. Sørg for at AllowOverride All er aktivt i vhost +# +# AllowOverride All +# Require all granted +# + +sudo systemctl reload apache2 +# Åpne http://din-server.no/netops-todo/ → "Last inn eksempel-data" +``` + +Se `INSTALL.txt` for fyldigere variant og feilsøking. diff --git a/netops-todo-server/INSTALL.txt b/netops-todo-server/INSTALL.txt new file mode 100644 index 0000000..0e8765f --- /dev/null +++ b/netops-todo-server/INSTALL.txt @@ -0,0 +1,100 @@ +NetOps To-Do — Installasjon på Apache +====================================== + +KRAV +---- +- Apache 2.4+ med mod_php ELLER php-fpm (PHP 7.4+ holder, 8.x anbefales) +- mod_rewrite aktivert (for .htaccess-reglene) +- AllowOverride All (eller minst AllowOverride AuthConfig Limit FileInfo) + i -blokken som peker på denne mappa. + + +INSTALLASJON (3 steg) +--------------------- + +1. Kopier alt innholdet i denne mappa inn på webserveren, f.eks: + /var/www/netops-todo/ + Strukturen skal være: + netops-todo/ + ├── index.php + ├── api.php + ├── seed.json + ├── .htaccess + └── data/ + └── .htaccess (nekter direkte web-tilgang) + +2. Sett filrettigheter slik at Apache-brukeren kan skrive i data/: + chown -R www-data:www-data /var/www/netops-todo/data + chmod 750 /var/www/netops-todo/data + + (bytt www-data med din Apache-bruker hvis den heter noe annet — + 'httpd' på RHEL, 'apache' på CentOS, osv.) + +3. Sett opp autentisering. Enkleste variant med Basic Auth: + + sudo htpasswd -c /etc/apache2/netops.htpasswd jon + sudo htpasswd /etc/apache2/netops.htpasswd kari # flere brukere + + Rediger deretter .htaccess i rot-mappa og fjern kommentar-tegnene på + blokk (1). Pek AuthUserFile til riktig path. + + Restart/reload Apache: systemctl reload apache2 + + +KONFIGURASJON AV APACHE VHOST +------------------------------ +Hvis du kjører bak en reverse proxy (f.eks. nginx foran Apache), +sørg for at Apache får brukernavnet. Eksempel på vhost-snippet: + + + AllowOverride All + Require all granted + + +Hvis autentisering skjer i ytre proxy, send brukernavnet videre: + # i nginx-proxy: proxy_set_header X-Remote-User $remote_user; + # PHP leser automatisk HTTP_X_REMOTE_USER. + + +FØRSTE GANG DU ÅPNER SIDEN +-------------------------- +Databasen (data/tasks.json) er tom. Siden viser en knapp +"Last inn eksempel-data" som fyller på med 16 typiske NetOps-oppgaver +for å demonstrere funksjonaliteten. Du kan slette alt og starte med +blank liste når som helst — bruk Eksporter/Importer for å ta backup. + + +BRUKERE OG EIERSKAP +------------------- +- Påloggingsbrukernavnet (fra Apache/proxy) vises som chip i header. +- Hver oppgave har et "Eier"-felt (owner) som kan settes til hvilket + som helst brukernavn, eller være tomt (utildelt). +- Ved ny oppgave settes eier automatisk til deg selv (kan endres). +- "Ta eierskap"-knapp (👤) på utildelte oppgaver. +- Filter: "Meg" / "Utildelt" / bruker-spesifikk / alle. +- Hver endring logges med createdBy/updatedBy/completedBy. + + +BACKUP +------ +Alt ligger i data/tasks.json. Backup kan være en enkel cronjobb: + + 0 2 * * * cp /var/www/netops-todo/data/tasks.json \ + /var/backups/netops-todo-$(date +\%F).json + +Eller bruk "Eksporter"-knappen i UI for en manuell kopi. + + +FEILSØKING +---------- +- "Uventet svar fra server" i toast → api.php ble ikke parset som PHP. + Sjekk at mod_php / php-fpm er aktivt og at Apache gjenkjenner .php. + +- 403 Forbidden på data/tasks.json → det er riktig! Data-mappa er + sperret for direkte web-tilgang. + +- "Kunne ikke opprette data-mappe" → Apache-brukeren mangler + skriverettigheter. Se steg 2 over. + +- Brukernavn viser "anon" → autentisering er ikke satt opp, eller + proxy-en sender ikke REMOTE_USER videre. diff --git a/netops-todo-server/api.php b/netops-todo-server/api.php new file mode 100644 index 0000000..376da40 --- /dev/null +++ b/netops-todo-server/api.php @@ -0,0 +1,307 @@ + $msg], $code); +} + +function ensure_data_dir(): void { + if (!is_dir(DATA_DIR)) { + if (!@mkdir(DATA_DIR, 0750, true)) bad('Kunne ikke opprette data-mappe', 500); + } + if (!file_exists(DATA_FILE)) { + @file_put_contents(DATA_FILE, "[]\n"); + @chmod(DATA_FILE, 0640); + } +} + +function read_tasks(): array { + ensure_data_dir(); + $fp = @fopen(DATA_FILE, 'r'); + if (!$fp) bad('Kunne ikke lese data', 500); + try { + if (!flock($fp, LOCK_SH)) bad('Kunne ikke låse for lesing', 500); + $size = filesize(DATA_FILE); + $raw = $size > 0 ? fread($fp, $size) : '[]'; + flock($fp, LOCK_UN); + } finally { fclose($fp); } + $data = json_decode($raw, true); + if (!is_array($data)) return []; + return $data; +} + +function write_tasks(array $tasks): void { + ensure_data_dir(); + $fp = @fopen(DATA_FILE, 'c+'); + if (!$fp) bad('Kunne ikke åpne data for skriving', 500); + try { + if (!flock($fp, LOCK_EX)) bad('Kunne ikke låse for skriving', 500); + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, json_encode(array_values($tasks), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + fflush($fp); + flock($fp, LOCK_UN); + } finally { fclose($fp); } +} + +function add_known_user(string $user): void { + ensure_data_dir(); + $known = @file_exists(USERS_FILE) ? json_decode(@file_get_contents(USERS_FILE), true) : []; + if (!is_array($known)) $known = []; + if (!in_array($user, $known, true)) { + $known[] = $user; + sort($known); + @file_put_contents(USERS_FILE, json_encode($known, JSON_UNESCAPED_UNICODE)); + } +} + +function known_users(): array { + if (!file_exists(USERS_FILE)) return []; + $list = json_decode(@file_get_contents(USERS_FILE), true); + return is_array($list) ? $list : []; +} + +function uid(): string { + return 't_' . bin2hex(random_bytes(5)); +} + +function body_json(): array { + $raw = file_get_contents('php://input'); + if ($raw === '' || $raw === false) return []; + $d = json_decode($raw, true); + return is_array($d) ? $d : []; +} + +function sanitize_str($v, int $max = 500): string { + if (!is_string($v)) return ''; + $v = trim($v); + // Fjern kontrolltegn (bortsett fra \n \r \t) + $v = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $v); + if (mb_strlen($v) > $max) $v = mb_substr($v, 0, $max); + return $v; +} + +function sanitize_tags($v): array { + if (!is_array($v)) return []; + $out = []; + foreach ($v as $t) { + $t = sanitize_str($t, 40); + if ($t !== '') $out[] = $t; + if (count($out) >= 20) break; + } + return $out; +} + +function normalize_task($in, string $user, ?array $existing = null): array { + global $VALID_STATUS, $VALID_PRIORITIES, $VALID_CATEGORIES; + + $t = $existing ?: []; + + $t['id'] = $existing['id'] ?? (isset($in['id']) && is_string($in['id']) ? preg_replace('/[^a-z0-9_]/i', '', $in['id']) : uid()); + if (!$t['id']) $t['id'] = uid(); + + $t['title'] = sanitize_str($in['title'] ?? ($existing['title'] ?? ''), 200); + $t['desc'] = sanitize_str($in['desc'] ?? ($existing['desc'] ?? ''), 4000); + + $cat = $in['category'] ?? ($existing['category'] ?? 'L3'); + $t['category'] = in_array($cat, $VALID_CATEGORIES, true) ? $cat : 'L3'; + + $prio = $in['priority'] ?? ($existing['priority'] ?? 'P3'); + $t['priority'] = in_array($prio, $VALID_PRIORITIES, true) ? $prio : 'P3'; + + $stat = $in['status'] ?? ($existing['status'] ?? 'todo'); + $t['status'] = in_array($stat, $VALID_STATUS, true) ? $stat : 'todo'; + + $t['estHours'] = isset($in['estHours']) ? max(0, min(999, (float)$in['estHours'])) : ($existing['estHours'] ?? 0); + $t['location'] = sanitize_str($in['location'] ?? ($existing['location'] ?? ''), 120); + + $dl = $in['deadline'] ?? ($existing['deadline'] ?? ''); + if (is_string($dl) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dl)) $t['deadline'] = $dl; + else $t['deadline'] = ''; + + $t['tags'] = sanitize_tags($in['tags'] ?? ($existing['tags'] ?? [])); + + // Owner + $owner = isset($in['owner']) ? sanitize_str($in['owner'], 64) : ($existing['owner'] ?? ''); + $t['owner'] = $owner !== '' ? $owner : ($existing['owner'] ?? $user); + + // Audit + $t['created'] = $existing['created'] ?? gmdate('c'); + $t['createdBy'] = $existing['createdBy'] ?? $user; + $t['updated'] = gmdate('c'); + $t['updatedBy'] = $user; + + // Completed timestamp logic + $wasDone = isset($existing['status']) && $existing['status'] === 'done'; + if ($t['status'] === 'done' && !$wasDone) { + $t['completed'] = gmdate('Y-m-d'); + $t['completedBy'] = $user; + } elseif ($t['status'] !== 'done') { + $t['completed'] = null; + $t['completedBy'] = null; + } else { + // still done, keep existing + $t['completed'] = $existing['completed'] ?? gmdate('Y-m-d'); + $t['completedBy'] = $existing['completedBy'] ?? $user; + } + + return $t; +} + +// ---------- Dispatch ---------- +$user = current_user(); +add_known_user($user); + +$action = $_GET['action'] ?? ''; +$method = $_SERVER['REQUEST_METHOD']; + +try { + switch ($action) { + + case 'list': { + $tasks = read_tasks(); + // Samle eiere som også er kjent fra oppgavene + $owners = known_users(); + foreach ($tasks as $t) { + foreach (['owner','createdBy','updatedBy','completedBy'] as $k) { + if (!empty($t[$k]) && !in_array($t[$k], $owners, true)) $owners[] = $t[$k]; + } + } + sort($owners); + json_out([ + 'user' => $user, + 'tasks' => $tasks, + 'known_users' => array_values(array_unique($owners)), + ]); + break; + } + + case 'create': { + if ($method !== 'POST') bad('POST required', 405); + $in = body_json(); + if (empty($in['title'])) bad('title er påkrevd'); + $tasks = read_tasks(); + $new = normalize_task($in, $user, null); + $tasks[] = $new; + write_tasks($tasks); + json_out(['task' => $new]); + break; + } + + case 'update': { + if ($method !== 'POST') bad('POST required', 405); + $in = body_json(); + if (empty($in['id'])) bad('id er påkrevd'); + $tasks = read_tasks(); + $updated = null; + foreach ($tasks as $i => $t) { + if (($t['id'] ?? '') === $in['id']) { + $tasks[$i] = normalize_task($in, $user, $t); + $updated = $tasks[$i]; + break; + } + } + if (!$updated) bad('Fant ikke oppgaven', 404); + write_tasks($tasks); + json_out(['task' => $updated]); + break; + } + + case 'delete': { + if ($method !== 'POST') bad('POST required', 405); + $in = body_json(); + if (empty($in['id'])) bad('id er påkrevd'); + $tasks = read_tasks(); + $before = count($tasks); + $tasks = array_values(array_filter($tasks, fn($t) => ($t['id'] ?? '') !== $in['id'])); + if (count($tasks) === $before) bad('Fant ikke oppgaven', 404); + write_tasks($tasks); + json_out(['ok' => true]); + break; + } + + case 'replace': { + if ($method !== 'POST') bad('POST required', 405); + $in = body_json(); + if (!is_array($in) || !isset($in[0]) && !empty($in)) bad('Forventet array'); + $out = []; + foreach ($in as $raw) { + if (!is_array($raw) || empty($raw['title'])) continue; + $out[] = normalize_task($raw, $user, null); + } + write_tasks($out); + json_out(['ok' => true, 'count' => count($out)]); + break; + } + + case 'seed': { + // Last inn eksempel-data. Nekter hvis allerede data finnes. + if ($method !== 'POST') bad('POST required', 405); + $existing = read_tasks(); + if (count($existing) > 0) bad('Data finnes allerede', 409); + $seed = json_decode(@file_get_contents(__DIR__ . '/seed.json'), true); + if (!is_array($seed)) bad('Mangler seed.json', 500); + $out = []; + foreach ($seed as $raw) { + if (!is_array($raw) || empty($raw['title'])) continue; + $out[] = normalize_task($raw, $user, null); + } + write_tasks($out); + json_out(['ok' => true, 'count' => count($out)]); + break; + } + + default: + bad('Ukjent action. Bruk list/create/update/delete/replace/seed'); + } +} catch (Throwable $e) { + bad('Serverfeil: ' . $e->getMessage(), 500); +} diff --git a/netops-todo-server/data/.htaccess b/netops-todo-server/data/.htaccess new file mode 100644 index 0000000..a750e8e --- /dev/null +++ b/netops-todo-server/data/.htaccess @@ -0,0 +1,9 @@ +# Nekt all web-tilgang til data-mappa. +# tasks.json skal kun leses/skrives av PHP-prosessen. +Require all denied + + + # Fallback for Apache 2.2 + Order deny,allow + Deny from all + diff --git a/netops-todo-server/index.php b/netops-todo-server/index.php new file mode 100644 index 0000000..41ca4ed --- /dev/null +++ b/netops-todo-server/index.php @@ -0,0 +1,1024 @@ + + + + + +NetOps To-Do · Network Engineer + + + + +
+ +
+
+

NetOps To-Do

+
network-engineer@nms:~$
+
+
+ + + + + +
+
+ +
+
+
Åpne oppgaver
+
0
+
av 0 totalt
+
+
+
Mine åpne
+
0
+
tildelt meg
+
+
+
P1 Kritisk
+
0
+
umiddelbar handling
+
+
+
Est. tid igjen
+
0t
+
arbeidstimer
+
+
+
Forfalte
+
0
+
over frist
+
+
+
Ferdig (7d)
+
0
+
siste 7 dager
+
+
+ +
+
+

Åpne oppgaver per kategori

+
+
+
+

Fordeling etter prioritet

+
+
+
+

Åpne oppgaver per eier

+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+ +
+
+

Oppgaveliste 0

+
klikk kolonne for å sortere
+
+
+ + + + + + + + + + + + + + + +
PrioOppgaveKategoriStatusEierLokasjonEst (t)FristHandling
+
+ +
+ +
+ Data lagres på serveren · Endringer fra andre brukere synes etter neste oppdatering (↻) +
+
+ + + +
+ + + + diff --git a/netops-todo-server/seed.json b/netops-todo-server/seed.json new file mode 100644 index 0000000..04f7dfe --- /dev/null +++ b/netops-todo-server/seed.json @@ -0,0 +1,18 @@ +[ + {"title":"OSPF adjacency drops mellom RTR-OSL-01 og RTR-OSL-02","desc":"Intermittent flapping ca. hvert 15. min. Sjekk MTU, hello/dead timers og interface-feil. Logger på NMS.","category":"L3","priority":"P1","status":"progress","estHours":4,"location":"DC-OSL-1","tags":["feilsøking","prod"]}, + {"title":"Høy latency mot NIX peering","desc":"Pakketap 0.3%, latency 8→18ms mot AS-path via NIX. Kan være congestion – kontakt NIX NOC.","category":"PEER","priority":"P1","status":"todo","estHours":2,"location":"remote","tags":["peering","nix"]}, + {"title":"Konfigurer nye eBGP-peers mot Telenor AS2119","desc":"To nye sesjoner på redundant uplink. Husk MD5, max-prefix, og route-map inn/ut. Dokumentér AS-set.","category":"BGP","priority":"P2","status":"todo","estHours":3,"location":"DC-OSL-1","tags":["bgp","telenor"]}, + {"title":"Bytt defekt SFP+ i core-sw01 port 24","desc":"CRC-feil øker, link flapper. RMA er bestilt, reservedel på lager B-12.","category":"L1","priority":"P2","status":"todo","estHours":1,"location":"DC-OSL-1","tags":["hardware"]}, + {"title":"Sett opp HAProxy for nytt API-cluster","desc":"Backend: 4 noder, TLS termination, HTTP/2, health check /healthz. ACL for rate limit.","category":"L4","priority":"P2","status":"todo","estHours":4,"location":"remote","tags":["prosjekt"]}, + {"title":"Fiberkoble nytt rack B12","desc":"24x LC-LC single-mode, 3m patch. Dokumentér i kabelregister. Labels iht. standard.","category":"L1","priority":"P3","status":"todo","estHours":6,"location":"DC-BGO-1","tags":["felt","bergen"]}, + {"title":"Oppgrader IOS-XE på dist-sw03 til 05","desc":"Vedlikeholdsvindu 02:00–04:00. Backup konfig, ISSU der det støttes. Rollback plan.","category":"L2","priority":"P2","status":"todo","estHours":3,"location":"DC-OSL-1","tags":["vedlikehold","oppgradering"]}, + {"title":"Legg til route reflector i iBGP-mesh","desc":"Reduser full-mesh. RR-klienter: alle PE-rutere. Test failover mellom RR1/RR2.","category":"BGP","priority":"P3","status":"todo","estHours":5,"location":"remote","tags":["design"]}, + {"title":"Review FW-regler før Q2 audit","desc":"Finn og rydd vekk ubrukte regler, dupliserte objekter. Eksportér rapport til sec-team.","category":"SEC","priority":"P2","status":"blocked","estHours":8,"location":"remote","tags":["audit","compliance"]}, + {"title":"Bytt batteri på UPS i Trondheim POP","desc":"Batteritest feiler. Kontakt kjøpetjeneste for reservedel. Krever fysisk oppmøte.","category":"L1","priority":"P2","status":"todo","estHours":2,"location":"POP-TRD","tags":["felt","trondheim"]}, + {"title":"Oppdater LibreNMS poller-nodes","desc":"v24.x → v25.x. Backup MySQL, test med staging-poller først.","category":"NMS","priority":"P4","status":"todo","estHours":1,"location":"remote","tags":["vedlikehold"]}, + {"title":"Dokumentér nye VLAN-er (120-135) i IPAM","desc":"Oppdater NetBox. Inkluder prefix, site, role og tenant.","category":"DOC","priority":"P3","status":"progress","estHours":2,"location":"remote","tags":["netbox"]}, + {"title":"Ansible-playbook for standard switch-config","desc":"Base config, NTP, SNMP, AAA, banner. Idempotent. Test i lab først.","category":"AUTO","priority":"P3","status":"todo","estHours":6,"location":"remote","tags":["automatisering"]}, + {"title":"Postmortem for uptime-hendelse 2026-04-19","desc":"45 min utfall etter oppgradering. RCA + tiltaksliste. Del med ledergruppe.","category":"INC","priority":"P2","status":"progress","estHours":3,"location":"remote","tags":["postmortem"]}, + {"title":"Planlegg 100G oppgradering DC-BGO ↔ DC-OSL","desc":"Kostestimat, lead time optikk, migreringsvindu, rollback.","category":"PROJ","priority":"P3","status":"todo","estHours":10,"location":"remote","tags":["planlegging","capex"]}, + {"title":"Verifisér BFD timere på alle OSPF-lenker","desc":"Mål: 150ms detection. Sjekk at alle rutere støtter og har konfigurert.","category":"L3","priority":"P4","status":"todo","estHours":2,"location":"remote","tags":["optimalisering"]} +] diff --git a/netops-todo.html b/netops-todo.html new file mode 100644 index 0000000..61438c9 --- /dev/null +++ b/netops-todo.html @@ -0,0 +1,1312 @@ + + + + + +NetOps To-Do · Network Engineer + + + + +
+ +
+
+

NetOps To-Do

+
network-engineer@nms:~$
+
+
+ + + + +
+
+ + +
+
+
Åpne oppgaver
+
0
+
av 0 totalt
+
+
+
P1 Kritisk
+
0
+
krever umiddelbar handling
+
+
+
Est. tid igjen
+
0t
+
arbeidstimer
+
+
+
Forfalte
+
0
+
over frist
+
+
+
Ferdig (7d)
+
0
+
siste 7 dager
+
+
+ + +
+
+

Åpne oppgaver per kategori

+
+
+
+

Fordeling etter prioritet

+
+
+
+

Ferdigstilt siste 14 dager

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+
+

Oppgaveliste 0

+
klikk kolonne for å sortere
+
+
+ + + + + + + + + + + + + + +
PrioOppgaveKategoriStatusLokasjonEst (t)FristHandling
+
+ +
+ +
+ Lagres lokalt i nettleseren · Kan eksporteres til JSON for backup +
+
+ + + + + + +