From 72a78b55fb66aa1d04c8caba6ab1e626fe4dba9b Mon Sep 17 00:00:00 2001 From: Jon Vanvik Date: Fri, 12 Jun 2026 11:17:07 +0200 Subject: [PATCH] initial commit --- netops-todo-apache-pakke.zip | Bin 0 -> 25143 bytes netops-todo-server/.htaccess | 39 + netops-todo-server/CLAUDE.md | 175 ++++ netops-todo-server/INSTALL.txt | 100 +++ netops-todo-server/api.php | 307 +++++++ netops-todo-server/data/.htaccess | 9 + netops-todo-server/index.php | 1024 ++++++++++++++++++++++ netops-todo-server/seed.json | 18 + netops-todo.html | 1312 +++++++++++++++++++++++++++++ 9 files changed, 2984 insertions(+) create mode 100644 netops-todo-apache-pakke.zip create mode 100644 netops-todo-server/.htaccess create mode 100644 netops-todo-server/CLAUDE.md create mode 100644 netops-todo-server/INSTALL.txt create mode 100644 netops-todo-server/api.php create mode 100644 netops-todo-server/data/.htaccess create mode 100644 netops-todo-server/index.php create mode 100644 netops-todo-server/seed.json create mode 100644 netops-todo.html diff --git a/netops-todo-apache-pakke.zip b/netops-todo-apache-pakke.zip new file mode 100644 index 0000000000000000000000000000000000000000..ee15fb4e2a05b8e09e57dfc509e820c6e99e221b GIT binary patch literal 25143 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 + 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 +
+
+ + + + + + +