initial commit

This commit is contained in:
Jon Vanvik 2026-06-12 11:17:07 +02:00
commit 72a78b55fb
9 changed files with 2984 additions and 0 deletions

Binary file not shown.

View file

@ -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
<FilesMatch "\.(json|bak|log|ini|env)$">
Require all denied
</FilesMatch>
# Standard index
DirectoryIndex index.php
# Sett UTF-8 for tekstfiler
AddDefaultCharset UTF-8
# Cache-headers (minimal — PHP setter sine egne)
<FilesMatch "\.(html|htm|php)$">
Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>

View file

@ -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 (P1P4), 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=<name>` 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
# <Directory /var/www/netops-todo>
# AllowOverride All
# Require all granted
# </Directory>
sudo systemctl reload apache2
# Åpne http://din-server.no/netops-todo/ → "Last inn eksempel-data"
```
Se `INSTALL.txt` for fyldigere variant og feilsøking.

View file

@ -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 <Directory>-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:
<Directory /var/www/netops-todo>
AllowOverride All
Require all granted
</Directory>
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.

307
netops-todo-server/api.php Normal file
View file

@ -0,0 +1,307 @@
<?php
/**
* NetOps To-Do REST API
*
* Lagrer oppgaver i data/tasks.json. Alle skrive-operasjoner bruker
* fil-låsing (flock) parallelle requests ikke ødelegger fila.
*
* Endepunkt (alle via POST unntatt list som også godtar GET):
* ?action=list { user, tasks: [...], known_users: [...] }
* ?action=create body: task JSON { task }
* ?action=update body: task JSON { task }
* ?action=delete body: {id} { ok: true }
* ?action=replace body: [task,...] { ok: true, count }
*
* Brukernavn hentes fra (i denne rekkefølgen):
* 1. $_SERVER['REMOTE_USER'] (satt av Apache Basic Auth / mod_auth_*)
* 2. $_SERVER['PHP_AUTH_USER'] (Basic Auth håndtert av PHP)
* 3. $_SERVER['HTTP_X_REMOTE_USER'](en reverse-proxy kan sette dette)
* 4. 'anon'
*/
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
// ---------- Config ----------
define('DATA_DIR', __DIR__ . '/data');
define('DATA_FILE', DATA_DIR . '/tasks.json');
define('USERS_FILE', DATA_DIR . '/known_users.json');
$VALID_STATUS = ['todo', 'progress', 'blocked', 'done'];
$VALID_PRIORITIES = ['P1', 'P2', 'P3', 'P4'];
$VALID_CATEGORIES = ['L1','L2','L3','L4','BGP','PEER','SEC','NMS','AUTO','DOC','INC','PROJ'];
// ---------- Helpers ----------
function current_user(): string {
foreach (['REMOTE_USER', 'PHP_AUTH_USER', 'HTTP_X_REMOTE_USER'] as $k) {
if (!empty($_SERVER[$k])) {
$u = preg_replace('/[^A-Za-z0-9._@\-]/', '', $_SERVER[$k]);
if ($u !== '') return substr($u, 0, 64);
}
}
return 'anon';
}
function json_out($data, int $code = 200): void {
http_response_code($code);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function bad(string $msg, int $code = 400): void {
json_out(['error' => $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);
}

View file

@ -0,0 +1,9 @@
# Nekt all web-tilgang til data-mappa.
# tasks.json skal kun leses/skrives av PHP-prosessen.
Require all denied
<IfModule !mod_authz_core.c>
# Fallback for Apache 2.2
Order deny,allow
Deny from all
</IfModule>

1024
netops-todo-server/index.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -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:0004: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"]}
]

1312
netops-todo.html Normal file

File diff suppressed because it is too large Load diff