initial commit
This commit is contained in:
commit
72a78b55fb
9 changed files with 2984 additions and 0 deletions
39
netops-todo-server/.htaccess
Normal file
39
netops-todo-server/.htaccess
Normal 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>
|
||||
175
netops-todo-server/CLAUDE.md
Normal file
175
netops-todo-server/CLAUDE.md
Normal 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 (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=<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.
|
||||
100
netops-todo-server/INSTALL.txt
Normal file
100
netops-todo-server/INSTALL.txt
Normal 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
307
netops-todo-server/api.php
Normal 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) så 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);
|
||||
}
|
||||
9
netops-todo-server/data/.htaccess
Normal file
9
netops-todo-server/data/.htaccess
Normal 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
1024
netops-todo-server/index.php
Normal file
File diff suppressed because it is too large
Load diff
18
netops-todo-server/seed.json
Normal file
18
netops-todo-server/seed.json
Normal 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: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"]}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue