Compare commits
22 Commits
78594c5bca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c4d0bb3894 | |||
| 806d768bbc | |||
| bf7d1e466a | |||
| e13e51531d | |||
| b6d81885de | |||
| ab58a163a6 | |||
| 7f5c8f0b7b | |||
| 63ee0b40b5 | |||
| 9ad9501722 | |||
| 5850ef8358 | |||
| 835244b7e0 | |||
| fb096f7897 | |||
| c748181de2 | |||
| 8c55fd4954 | |||
| f513d46d08 | |||
| 55e452b6a2 | |||
| 1888f59c05 | |||
| b68ce13be0 | |||
| a732a5afc4 | |||
| 10ec58f744 | |||
| c41482a7b3 | |||
| 589f9595f4 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
.venv/
|
.venv/
|
||||||
|
upload.token
|
||||||
|
update-addon.env
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
.env
|
.env
|
||||||
|
|||||||
53
CHANGELOG.md
53
CHANGELOG.md
@ -1,4 +1,4 @@
|
|||||||
ID: DOC_000001 | Version: 0.1.5 | Status: Final
|
ID: DOC_000001 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Projekt-Logbuch (Changelog)
|
# Projekt-Logbuch (Changelog)
|
||||||
@ -49,6 +49,57 @@ By: Codex (GPT-5)
|
|||||||
| 30.12.2025 | 🎨 UI | ID: TASK_000039 Login-Text reduziert, Buttons symmetrisch, Panels/Metrics harmonisiert. By: Codex (GPT-5) |
|
| 30.12.2025 | 🎨 UI | ID: TASK_000039 Login-Text reduziert, Buttons symmetrisch, Panels/Metrics harmonisiert. By: Codex (GPT-5) |
|
||||||
| 30.12.2025 | 🏗️ Planning | ID: EPIC_000010/US_000034/US_000035 Update-Service v1 Migration dokumentiert. By: Codex (GPT-5) |
|
| 30.12.2025 | 🏗️ Planning | ID: EPIC_000010/US_000034/US_000035 Update-Service v1 Migration dokumentiert. By: Codex (GPT-5) |
|
||||||
| 30.12.2025 | 🏗️ Planning | ID: TASK_000040/TASK_000041 fuer Enrollment und v1 Endpunkte angelegt. By: Codex (GPT-5) |
|
| 30.12.2025 | 🏗️ Planning | ID: TASK_000040/TASK_000041 fuer Enrollment und v1 Endpunkte angelegt. By: Codex (GPT-5) |
|
||||||
|
| 12.01.2026 | ⚙️ Code | ID: TASK_000040 Enrollment-Skript bereinigt und als scripts/enroll_local.py hinzugefuegt. By: Gemini CLI |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: EPIC_000011 und US_000036-US_000038 dokumentiert. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 📝 Req | ID: Update-Doku und ENV-Beispiele an Code-Stand angeglichen. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: Makefile-Healthcheck auf Authorization: Bearer angepasst. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000039 und TASK_000042 fuer Doku-Audit angelegt. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 📝 Req | ID: US_000039 Doku-Audit abgeschlossen (Header-Versionen konsolidiert, Update-Docs korrigiert). By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: EPIC_000012 und US_000040 sowie TASK_000043-TASK_000044 fuer Doku-Overhaul angelegt. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 📝 Req | ID: US_000040 Doku-Overhaul umgesetzt (neue Struktur, README, Archivierung). By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 📝 Req | ID: Externe Service-Links (Update/OIDC) in Doku ergaenzt. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000041/TASK_000045 Einbindung externer Services dokumentieren. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 📝 Req | ID: US_000041 Einbindungsschritte fuer Update- und OIDC-Service dokumentiert. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000042/TASK_000046-TASK_000048 Consumer-Doku und Audience-Split ergaenzen. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 📝 Req | ID: US_000042 Consumer-Doku, Audience-Split, External Dependencies und Power-User-Ergaenzungen umgesetzt. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 📝 Req | ID: Makefile als bevorzugter Einstieg in Doku ergaenzt. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000043/TASK_000049 Enrollment-Token Script geplant. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: US_000043 Enrollment-Script fuer Update-Service hinzugefuegt. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: EPIC_000013/US_000044/TASK_000050-TASK_000051 System Telemetry im Dashboard geplant. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: EPIC_000014/US_000045/TASK_000052-TASK_000054 Login-Zeitfenster und Parent-Control Regeln geplant. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000045 Kriterien konkretisiert (lokale Zeit, OS-Login, Auto-Reaktivierung). By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000046/TASK_000055 Update-Service Erreichbarkeit anzeigen. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: US_000046 Update-Service Statusanzeige implementiert. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: Update-Service Status zeigt Dev/Prod und nutzt konsistente Panel-Styles. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000047/TASK_000056 Update-ENV getrennt. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: Update-Config in env.update.example ausgelagert. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000048/TASK_000057 Release-Upload automatisieren. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: Release-Upload Script hinzugefuegt (tar.gz + curl). By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: Upload-Script toleriert fehlende Leserechte fuer /etc/skd/*. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🏗️ Planning | ID: US_000049/TASK_000058 deployment.env fuer lokale Uploads. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: deployment.env.example hinzugefuegt und Scripts angepasst. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: deployment.env als lokale Quelle fuer Enrollment/Upload Scripts. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: EPIC_000013 System Telemetry Endpoint und UI umgesetzt. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 📝 Req | ID: EPIC_000009 Update Webservice als erledigt markiert. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🚀 Release | ID: VERSION auf 0.2.2 erhoeht. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🐞 Fix | ID: BUG_000001 Update-Apply scheitert nicht mehr an WorkingDirectory. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🎨 UI | ID: System Information Kacheln mit Progressbars verbessert. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | 🚀 Release | ID: VERSION auf 0.2.3 erhoeht. By: Codex (GPT-5) |
|
||||||
|
| 15.01.2026 | ⚙️ Code | ID: Makefile restart-Target hinzugefuegt. By: Codex (GPT-5) |
|
||||||
|
| 16.01.2026 | ✨ Feat | ID: EPIC_000014/US_000045 Login-Regeln und Scheduler implementiert (UI + Backend). By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🚀 Release | ID: VERSION auf 0.3.0 erhoeht. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🐞 Fix | ID: Update-Prozess via systemd-run entkoppelt, damit Service-Stop den Updater nicht killt. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🚀 Release | ID: VERSION auf 0.3.1 erhoeht. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🧪 Test | ID: Patch-Bump auf 0.3.2 zur Verifizierung des Update-Fixes. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🚀 Release | ID: VERSION auf 0.3.2 erhoeht. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🐞 Fix | ID: Update-Token wird nun bei jedem Update-Vorgang frisch geladen (Fix fuer Caching-Problem). By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🚀 Release | ID: VERSION auf 0.3.3 erhoeht. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🧪 Test | ID: Patch-Bump auf 0.3.4 zur finalen Verifizierung nach manuellem Git-Pull am Client. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🚀 Release | ID: VERSION auf 0.3.4 erhoeht. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🐞 Fix | ID: Environment-Variablen explizit via --setenv an systemd-run uebergeben (Fix fuer fehlenden Token im Updater). By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🚀 Release | ID: VERSION auf 0.3.5 erhoeht. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🧪 Test | ID: Patch-Bump auf 0.3.6 zur finalen Verifizierung des systemd-run Env-Fixes. By: Gemini CLI |
|
||||||
|
| 16.01.2026 | 🚀 Release | ID: VERSION auf 0.3.6 erhoeht. By: Gemini CLI |
|
||||||
|
|
||||||
---
|
---
|
||||||
## Legende
|
## Legende
|
||||||
|
|||||||
7
Makefile
7
Makefile
@ -13,7 +13,7 @@ HEALTH_URL ?= http://$(HOST):$(PORT)/health
|
|||||||
TOKEN ?= $(shell awk -F= '/^SKD_AUTH_TOKEN=/{print $$2}' $(ENV_FILE) 2>/dev/null)
|
TOKEN ?= $(shell awk -F= '/^SKD_AUTH_TOKEN=/{print $$2}' $(ENV_FILE) 2>/dev/null)
|
||||||
KEEP_INSTALL_DIR ?= 1
|
KEEP_INSTALL_DIR ?= 1
|
||||||
|
|
||||||
.PHONY: install up down uninstall healthcheck update token
|
.PHONY: install up down restart uninstall healthcheck update token
|
||||||
|
|
||||||
install:
|
install:
|
||||||
$(SUDO) env SERVICE_NAME=$(SERVICE) SERVICE_USER=$(SERVICE_USER) SERVICE_GROUP=$(SERVICE_GROUP) INSTALL_DIR=$(INSTALL_DIR) ./scripts/install.sh
|
$(SUDO) env SERVICE_NAME=$(SERVICE) SERVICE_USER=$(SERVICE_USER) SERVICE_GROUP=$(SERVICE_GROUP) INSTALL_DIR=$(INSTALL_DIR) ./scripts/install.sh
|
||||||
@ -24,6 +24,9 @@ up:
|
|||||||
down:
|
down:
|
||||||
$(SUDO) systemctl stop $(SERVICE).service
|
$(SUDO) systemctl stop $(SERVICE).service
|
||||||
|
|
||||||
|
restart:
|
||||||
|
$(SUDO) systemctl restart $(SERVICE).service
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
-$(SUDO) systemctl stop $(SERVICE).service
|
-$(SUDO) systemctl stop $(SERVICE).service
|
||||||
-$(SUDO) systemctl disable $(SERVICE).service
|
-$(SUDO) systemctl disable $(SERVICE).service
|
||||||
@ -41,7 +44,7 @@ healthcheck:
|
|||||||
echo "No token set; set TOKEN=... or populate $(ENV_FILE) with SKD_AUTH_TOKEN."; \
|
echo "No token set; set TOKEN=... or populate $(ENV_FILE) with SKD_AUTH_TOKEN."; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
curl -fsS -H "X-API-Token: $(TOKEN)" "$(HEALTH_URL)" || (echo "Health check failed" && exit 1)
|
curl -fsS -H "Authorization: Bearer $(TOKEN)" "$(HEALTH_URL)" || (echo "Health check failed" && exit 1)
|
||||||
|
|
||||||
update:
|
update:
|
||||||
$(SUDO) env PROJECT_ROOT=$(INSTALL_DIR) SERVICE_NAME=$(SERVICE) SERVICE_USER=$(SERVICE_USER) BRANCH=$(BRANCH) bash -c 'cd $(INSTALL_DIR) && ./scripts/update.sh'
|
$(SUDO) env PROJECT_ROOT=$(INSTALL_DIR) SERVICE_NAME=$(SERVICE) SERVICE_USER=$(SERVICE_USER) BRANCH=$(BRANCH) bash -c 'cd $(INSTALL_DIR) && ./scripts/update.sh'
|
||||||
|
|||||||
161
README.md
161
README.md
@ -1,119 +1,70 @@
|
|||||||
|
ID: README_000001 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Safe Kiddo Daemon
|
# Safe Kiddo Daemon
|
||||||
|
|
||||||
Service to lock/unlock local user accounts on kids' laptops with countdown, notifications, optional sound, and shutdown. Provides a REST API plus a small web UI for remote control; retains the original `sk.sh` script as legacy/CLI fallback.
|
## Kurzbeschreibung
|
||||||
|
Safe Kiddo Daemon ist ein lokaler Systemdienst, der Benutzerkonten auf Linux-Systemen sperrt/entsperrt, Sitzungen beendet und optional einen Shutdown ausloest. Der Dienst bietet eine REST-API und eine Web-UI fuer die Fernsteuerung. Ein Legacy-CLI-Script (`sk.sh`) bleibt als Fallback erhalten.
|
||||||
|
|
||||||
## Features
|
## Fuer wen ist das Projekt?
|
||||||
- Disable/enable accounts, terminate sessions, optionally trigger shutdown.
|
1. Endnutzer (Consumers): Nutzen die Web-Oberflaeche ohne technische Details.
|
||||||
- Desktop notifications and optional sound during countdown.
|
2. Technische Einsteiger: Installieren und starten den Dienst.
|
||||||
- Login-protected API with minimal web UI (PAM auth for root users, bearer token for calls).
|
3. Power-User: Konfigurieren, automatisieren und betreiben den Dienst.
|
||||||
- Systemd-managed service, virtualenv-based deploy, remote update script.
|
4. Entwickler/Professionals: Verstehen Architektur, Interna und Erweiterungspunkte.
|
||||||
- Dry-run mode to validate flows without touching accounts.
|
|
||||||
|
|
||||||
## Quick Start (Local/Target Device)
|
## Was es NICHT ist
|
||||||
|
- Kein Cloud-Service und keine zentrale Benutzerverwaltung.
|
||||||
|
- Kein Ersatz fuer Mobile-Device-Management (MDM).
|
||||||
|
- Kein plattformuebergreifendes Kontrollen-System; Fokus ist Linux und lokale Accounts.
|
||||||
|
|
||||||
|
## Hauptfunktionen
|
||||||
|
- Sperren/Entsperren lokaler Nutzerkonten inkl. Session-Management und optionalem Shutdown.
|
||||||
|
- Benachrichtigungen und optionaler Sound waehrend Countdown.
|
||||||
|
- PAM-Login (immer aktiv) und optionaler OIDC-Login.
|
||||||
|
- Systemd-Service mit Installations- und Update-Skripten.
|
||||||
|
- Update-Client-Integration (Enrollment, Manifest, Apply, Rollback, Logs).
|
||||||
|
- Dry-Run-Modus zum sicheren Testen.
|
||||||
|
|
||||||
|
## Quickstart (5 Minuten)
|
||||||
```bash
|
```bash
|
||||||
git clone <repo> /opt/sk
|
# 1) Repo holen
|
||||||
|
sudo mkdir -p /opt/sk
|
||||||
|
sudo git clone ssh://git@git.wlkns.org:2222/stephan/kiddo /opt/sk
|
||||||
|
# Hinweis: verwende hier die Repo-URL deiner Instanz
|
||||||
cd /opt/sk
|
cd /opt/sk
|
||||||
./scripts/install.sh
|
|
||||||
|
# 2) Installieren (legt User, env und Systemd-Unit an)
|
||||||
|
make install
|
||||||
|
|
||||||
|
# 3) Status pruefen
|
||||||
sudo systemctl status skd.service
|
sudo systemctl status skd.service
|
||||||
|
|
||||||
|
# 4) Login testen (PAM)
|
||||||
|
curl -s -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"root","password":"example-password"}' \
|
||||||
|
http://localhost/login
|
||||||
```
|
```
|
||||||
Then open `http://localhost/` and log in via PAM (default) to start quickly.
|
Danach die Web-UI unter `http://localhost/` oeffnen und anmelden.
|
||||||
|
|
||||||
## Configuration
|
## Dokumentation nach Zielgruppe
|
||||||
Set in `/etc/skd/env` (see `env.example`):
|
- Endnutzer (Consumers): `docs/FOR_USERS.md`
|
||||||
- PAM-Login ist immer aktiv. OIDC wird zusaetzlich angeboten, wenn konfiguriert.
|
- Technische Einsteiger: `docs/GETTING_STARTED.md`
|
||||||
- `SKD_AUTH_SECRET`: HMAC secret for bearer tokens/cookies (set a strong value).
|
- Power-User: `docs/USAGE.md` und `docs/CONFIGURATION.md`
|
||||||
- `SKD_TOKEN_TTL_SECONDS`: token lifetime (default 900s).
|
- Entwickler/Professionals: `docs/ARCHITECTURE.md`, `docs/DEVELOPMENT.md`, `docs/DEPLOYMENT.md`
|
||||||
- `SKD_AUTH_ALLOWED_USERS`: optional comma list of accounts allowed to log in (used for PAM and as an allowlist for OIDC claims).
|
- Gemeinsame Referenz: `docs/FAQ.md`, `docs/TROUBLESHOOTING.md`
|
||||||
- `SKD_AUTH_ALLOWED_GROUPS`: groups whose members may log in (PAM only, default `sudo`).
|
|
||||||
- `SKD_AUTH_PAM_SERVICE`: PAM service name; Ubuntu/Debian uses `/etc/pam.d/skd` (created by `scripts/install.sh`), other distros may prefer `login` or `sshd`.
|
|
||||||
- `SKD_OIDC_*`: `ISSUER`, `CLIENT_ID`, `CLIENT_SECRET`, `REDIRECT_URI`, `SCOPES` to point at your OIDC provider; set `SKD_SESSION_COOKIE_SECURE=true` for HTTPS.
|
|
||||||
- OIDC dynamic registration helper: `scripts/register_oidc_client.sh` (requires `OIDC_INITIAL_ACCESS_TOKEN` and `SKD_OIDC_ISSUER`; uses `SKD_OIDC_REDIRECT_URI` for the redirect). Run once during setup if your provider issues initial access tokens for client creation.
|
|
||||||
- `SKD_ALLOWED_USERS`: optional comma list to limit manageable accounts (must exist on the system).
|
|
||||||
- `SKD_DEFAULT_COUNTDOWN`, `SKD_DEFAULT_SOUND`, `SKD_NOTIFY_TIMEOUT`: behavior defaults.
|
|
||||||
- `SKD_DRY_RUN=true` to test without real account changes or shutdown.
|
|
||||||
- `SKD_SOUND_PLAYER`/`SKD_SOUND_FILE`, `SKD_NOTIFY_SEND_PATH` if defaults differ.
|
|
||||||
- Update client:
|
|
||||||
- `SKD_UPDATE_URL` (default `https://update.wlkns.org`)
|
|
||||||
- `SKD_UPDATE_TOKEN` (API token for update service)
|
|
||||||
- `SKD_UPDATE_INTERVAL` (seconds; default 3600)
|
|
||||||
- `SKD_UPDATE_STATUS_URL` (default `https://update.wlkns.org/status`)
|
|
||||||
- `SKD_UPDATE_STATUS_FILE` (default `/var/lib/skd/update_status.json`)
|
|
||||||
- `SKD_UPDATE_LOG_FILE` (default `/var/lib/skd/update_logs.jsonl`)
|
|
||||||
Notes:
|
|
||||||
- `./scripts/install.sh` will create `/etc/skd/env` from `env.example` if missing (edit afterwards) and ensure the `skd` service user/group exist.
|
|
||||||
|
|
||||||
## OIDC Setup
|
## Externe Services
|
||||||
OIDC ist optional. PAM bleibt immer verfuegbar; `SKD_AUTH_MODE` ist optional.
|
- Update-Service (intern): https://git.wlkns.org/stephan/update-webservice
|
||||||
1. Issuer muss der externen URL des Providers entsprechen (TLS trust erforderlich).
|
- OIDC-Service (intern): https://git.wlkns.org/stephan/oicd
|
||||||
2. OIDC Client registrieren (DCR), z.B.:
|
Einbindung und Konfiguration: `docs/DEPLOYMENT.md` und `docs/CONFIGURATION.md`.
|
||||||
```bash
|
|
||||||
export SKD_OIDC_ISSUER="https://auth.example.org"
|
|
||||||
export SKD_OIDC_REDIRECT_URI="https://<device-host>/login/oidc/callback"
|
|
||||||
export OIDC_INITIAL_ACCESS_TOKEN="<initial-access-token>"
|
|
||||||
./scripts/register_oidc_client.sh
|
|
||||||
```
|
|
||||||
3. Danach in `/etc/skd/env` setzen:
|
|
||||||
```
|
|
||||||
SKD_AUTH_MODE=oidc
|
|
||||||
SKD_OIDC_ISSUER=...
|
|
||||||
SKD_OIDC_CLIENT_ID=...
|
|
||||||
SKD_OIDC_CLIENT_SECRET=...
|
|
||||||
SKD_OIDC_REDIRECT_URI=...
|
|
||||||
SKD_OIDC_SCOPES=openid profile email
|
|
||||||
SKD_SESSION_COOKIE_SECURE=true
|
|
||||||
```
|
|
||||||
Hinweise:
|
|
||||||
- Redirect-URI muss exakt sein (keine Wildcards).
|
|
||||||
- Bei Host/Port-Aenderung neu registrieren und neue Credentials setzen.
|
|
||||||
- Allowlist fuer OIDC: `SKD_AUTH_ALLOWED_USERS` prueft `preferred_username`, `email` oder `sub`.
|
|
||||||
|
|
||||||
## Running
|
## Badges
|
||||||
- Service: managed by systemd; `./scripts/install.sh` writes the unit dynamically to `/etc/systemd/system/skd.service` with the current repo path and restarts it (runs as root for PAM).
|
Derzeit keine offiziellen Badges, da im Repository keine CI, Coverage, Release oder Docker-Pipeline definiert ist.
|
||||||
- Manual run: `./scripts/run.sh` (uses `.venv`, defaults to `0.0.0.0:80`).
|
|
||||||
- Login (PAM): `curl -X POST -H "Content-Type: application/json" -d '{"username":"root","password":"..."}' http://localhost/login`
|
|
||||||
- Login (OIDC): open `http://localhost/login/oidc/start` → provider → redirected back with session cookie set.
|
|
||||||
- Health: `curl -H "Authorization: Bearer <token>" http://localhost/health`
|
|
||||||
|
|
||||||
## OIDC Validation & Fallbacks
|
## Hinweise
|
||||||
- Validierungsschritte: `docs/oidc-validation.md` (State, Token-Exchange, Claims, Cookie).
|
- Abweichung von der Zielstruktur: Die OpenAPI-Spezifikation des Update-Services bleibt unter `docs/architecture/openapi.yaml` und `docs/architecture/openapi/` erhalten.
|
||||||
- Falls Discovery/JWKS nicht verfuegbar: OIDC deaktivieren und PAM nutzen.
|
- Altdokumente liegen unter `docs/_archive/` und sind nicht geloescht.
|
||||||
- Falls DCR nicht verfuegbar: Client manuell im IdP anlegen und `SKD_OIDC_*` setzen.
|
- Konfigurationsdateien: `/etc/skd/env` (Core) und `/etc/skd/update.env` (Update-Service).
|
||||||
- Bei Self-Signed TLS: CA im System trusten oder in Dev PAM nutzen.
|
|
||||||
|
|
||||||
## API (Bearer token via `/login`)
|
|
||||||
- `GET /users` → `[{user, logged_in, account_locked}]` (manageable system users; excludes root)
|
|
||||||
- `POST /users/{name}/disable` with JSON `{countdown?, sound?, message?}`
|
|
||||||
- `POST /users/{name}/enable`
|
|
||||||
- `GET /health`
|
|
||||||
- `GET /me` (returns current user + auth mode when a session/bearer token is present)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```bash
|
|
||||||
token=$(curl -s -X POST -H "Content-Type: application/json" -d '{"username":"root","password":"..."}' http://localhost/login | jq -r .token)
|
|
||||||
curl -X POST -H "Authorization: Bearer $token" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"countdown":90,"sound":true}' \
|
|
||||||
http://localhost/users/child1/disable
|
|
||||||
```
|
|
||||||
|
|
||||||
## Web UI
|
|
||||||
Served at `/`. Nutze den Button „Login via OIDC“ (setzt Session-Cookie) oder das PAM-Formular, falls OIDC deaktiviert; danach werden verfügbare System-User angezeigt und Aktionen senden Token/Cookies automatisch.
|
|
||||||
|
|
||||||
## Updates
|
|
||||||
- Remote update via SSH: `ssh user@kid-laptop 'cd /opt/sk && ./scripts/update.sh'` (fetch/reset to `origin/main`, reinstalls deps, restarts service).
|
|
||||||
- Manual: `git pull && source .venv/bin/activate && pip install -r backend/requirements.txt && sudo systemctl restart skd`
|
|
||||||
|
|
||||||
## Deployment (zip/SSH)
|
|
||||||
- Quick copy: create `sk_deploy.zip` (already in repo root) and unzip on target under `/opt/sk`, then refresh venv deps and restart service.
|
|
||||||
- Scripted deploy: edit `deploy_hosts.yml` (host/user/port/install_dir/service user/group), then run `./scripts/deploy.sh <host-name>`; accepts JSON configs too. Requires SSH access and `sudo` on target.
|
|
||||||
- After deploy on target: `sudo -u skd /opt/sk/.venv/bin/pip install -r /opt/sk/backend/requirements.txt && sudo systemctl restart skd.service`
|
|
||||||
|
|
||||||
## Security Hardening
|
|
||||||
- Restrict access to API/Web UI to LAN/VPN; firewall the port.
|
|
||||||
- Set a strong `SKD_AUTH_SECRET`; rotate tokens by changing the secret.
|
|
||||||
- Create dedicated `skd` user/group; no login shell.
|
|
||||||
- Configure sudoers minimally: allow `skd` to run `usermod -L/-U`, `pkill -KILL -u`, `shutdown now`, and sound/notify binaries if needed (no full passwordless sudo).
|
|
||||||
- Consider mTLS or IP allowlisting for added protection.
|
|
||||||
|
|
||||||
## Legacy Script
|
## Legacy Script
|
||||||
`sk.sh` remains for direct SSH use. Plan to replace its logic with API-backed helpers; keep it as emergency fallback.
|
Das Script `sk.sh` bleibt als CLI-Fallback. Details und Beispiele: `docs/USAGE.md`.
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@ -6,7 +7,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from backend import actions
|
from backend import actions, enforcer, rules
|
||||||
from backend.actions import ActionError
|
from backend.actions import ActionError
|
||||||
from backend.auth import (
|
from backend.auth import (
|
||||||
authenticate_admin_user,
|
authenticate_admin_user,
|
||||||
@ -17,21 +18,26 @@ from backend.auth import (
|
|||||||
list_manageable_users,
|
list_manageable_users,
|
||||||
)
|
)
|
||||||
from backend.models import (
|
from backend.models import (
|
||||||
|
AccessRule,
|
||||||
ActionRequest,
|
ActionRequest,
|
||||||
ActionResponse,
|
ActionResponse,
|
||||||
EnrollRequest,
|
EnrollRequest,
|
||||||
EnrollResponse,
|
EnrollResponse,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
|
RuleSet,
|
||||||
UpdateActionResponse,
|
UpdateActionResponse,
|
||||||
UpdateCheckResponse,
|
UpdateCheckResponse,
|
||||||
UpdateLogEntry,
|
UpdateLogEntry,
|
||||||
|
UpdateServiceStatus,
|
||||||
UpdateStatus,
|
UpdateStatus,
|
||||||
|
SystemMetrics,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
)
|
)
|
||||||
from backend.oidc import OIDCClient, OIDCError
|
from backend.oidc import OIDCClient, OIDCError
|
||||||
from backend.settings import Settings, get_settings
|
from backend.settings import Settings, get_settings
|
||||||
from backend import update
|
from backend import update
|
||||||
|
from backend import system_metrics
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@ -45,6 +51,11 @@ app.mount("/assets", StaticFiles(directory="assets"), name="assets")
|
|||||||
templates = Jinja2Templates(directory="backend/templates")
|
templates = Jinja2Templates(directory="backend/templates")
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
asyncio.create_task(enforcer.enforcement_loop())
|
||||||
|
|
||||||
|
|
||||||
def get_oidc_client(settings: Settings = Depends(get_settings)) -> OIDCClient:
|
def get_oidc_client(settings: Settings = Depends(get_settings)) -> OIDCClient:
|
||||||
if not settings.oidc_enabled:
|
if not settings.oidc_enabled:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -235,6 +246,32 @@ def enable_user(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/rules", response_model=RuleSet, dependencies=[Depends(get_current_admin)])
|
||||||
|
def get_rules(settings: Settings = Depends(get_settings)) -> RuleSet:
|
||||||
|
manager = rules.RuleManager(settings)
|
||||||
|
return manager.load_rules()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rules", dependencies=[Depends(get_current_admin)])
|
||||||
|
def set_rule(
|
||||||
|
rule: AccessRule,
|
||||||
|
settings: Settings = Depends(get_settings),
|
||||||
|
) -> dict:
|
||||||
|
manager = rules.RuleManager(settings)
|
||||||
|
manager.set_rule(rule)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/rules/{username}", dependencies=[Depends(get_current_admin)])
|
||||||
|
def delete_rule(
|
||||||
|
username: str,
|
||||||
|
settings: Settings = Depends(get_settings),
|
||||||
|
) -> dict:
|
||||||
|
manager = rules.RuleManager(settings)
|
||||||
|
manager.delete_rule(username)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/update/status", response_model=UpdateStatus, dependencies=[Depends(get_current_admin)])
|
@app.get("/update/status", response_model=UpdateStatus, dependencies=[Depends(get_current_admin)])
|
||||||
def update_status(settings: Settings = Depends(get_settings)) -> UpdateStatus:
|
def update_status(settings: Settings = Depends(get_settings)) -> UpdateStatus:
|
||||||
status_data = update.get_status(settings)
|
status_data = update.get_status(settings)
|
||||||
@ -314,8 +351,26 @@ def update_logs(settings: Settings = Depends(get_settings), limit: int = 200) ->
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/update/service-status", response_model=UpdateServiceStatus, dependencies=[Depends(get_current_admin)])
|
||||||
|
def update_service_status(settings: Settings = Depends(get_settings)) -> UpdateServiceStatus:
|
||||||
|
data = update.get_service_status(settings)
|
||||||
|
return UpdateServiceStatus(**data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/system/metrics", response_model=SystemMetrics, dependencies=[Depends(get_current_admin)])
|
||||||
|
def system_metrics_status() -> SystemMetrics:
|
||||||
|
data = system_metrics.get_system_metrics()
|
||||||
|
return SystemMetrics(**data)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
@app.get("/login", response_class=HTMLResponse)
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
@app.get("/dashboard", response_class=HTMLResponse)
|
@app.get("/dashboard", response_class=HTMLResponse)
|
||||||
def index(request: Request) -> HTMLResponse:
|
def index(request: Request) -> HTMLResponse:
|
||||||
return templates.TemplateResponse("index.html", {"request": request})
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/ui/rules", response_class=HTMLResponse)
|
||||||
|
def rules_ui(request: Request) -> HTMLResponse:
|
||||||
|
return templates.TemplateResponse("rules.html", {"request": request})
|
||||||
|
|
||||||
|
|||||||
71
backend/enforcer.py
Normal file
71
backend/enforcer.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from backend import actions
|
||||||
|
from backend.auth import is_account_locked, list_manageable_users
|
||||||
|
from backend.rules import RuleManager
|
||||||
|
from backend.settings import get_settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_and_enforce_rules():
|
||||||
|
"""Iterate all manageable users and enforce time window rules."""
|
||||||
|
settings = get_settings()
|
||||||
|
manager = RuleManager(settings)
|
||||||
|
|
||||||
|
# We only care about users explicitly managed via settings or rules.
|
||||||
|
target_users = list_manageable_users(settings)
|
||||||
|
|
||||||
|
for user in target_users:
|
||||||
|
try:
|
||||||
|
allowed = manager.is_login_allowed(user)
|
||||||
|
rule = manager.get_rule(user)
|
||||||
|
|
||||||
|
if allowed:
|
||||||
|
# If user has a rule with auto-reenable, ensure unlocked
|
||||||
|
# We check rule existence because "allowed" is also true for users with NO rules.
|
||||||
|
# But for users with no rules, we don't want to auto-unlock randomly (maybe manual lock?).
|
||||||
|
# US says: "Given a rule allows automatic reactivation ... Then account is automatically activated"
|
||||||
|
# So we only auto-unlock if a rule EXISTS and explicitly asks for it.
|
||||||
|
if rule and rule.auto_reenable and is_account_locked(user):
|
||||||
|
logger.info("Auto-enabling user %s (Time window started)", user)
|
||||||
|
actions.enable_user(user)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we are here, access is DENIED.
|
||||||
|
|
||||||
|
# Check if logged in
|
||||||
|
logged_in_users = actions.list_logged_in_users()
|
||||||
|
is_logged_in = user in logged_in_users
|
||||||
|
|
||||||
|
if is_logged_in:
|
||||||
|
logger.warning("User %s is logged in during forbidden time. Enforcing logout.", user)
|
||||||
|
# Warn and shutdown
|
||||||
|
actions.disable_user(
|
||||||
|
user,
|
||||||
|
countdown=60,
|
||||||
|
sound=True,
|
||||||
|
message="Time limit reached. Shutdown in 60s.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Not logged in. Ensure account is locked to prevent login.
|
||||||
|
if not is_account_locked(user):
|
||||||
|
logger.info("Locking user %s (Time window ended)", user)
|
||||||
|
# disable_user locks the account. We pass countdown=0 but since not logged in, it won't matter much.
|
||||||
|
actions.disable_user(user, countdown=0, sound=False)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error enforcing rules for user %s", user)
|
||||||
|
|
||||||
|
|
||||||
|
async def enforcement_loop():
|
||||||
|
logger.info("Starting enforcement loop")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await check_and_enforce_rules()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error in enforcement loop")
|
||||||
|
|
||||||
|
# Run every minute
|
||||||
|
await asyncio.sleep(60)
|
||||||
@ -1,8 +1,36 @@
|
|||||||
from typing import List, Optional
|
from enum import Enum
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class DayOfWeek(str, Enum):
|
||||||
|
MONDAY = "mon"
|
||||||
|
TUESDAY = "tue"
|
||||||
|
WEDNESDAY = "wed"
|
||||||
|
THURSDAY = "thu"
|
||||||
|
FRIDAY = "fri"
|
||||||
|
SATURDAY = "sat"
|
||||||
|
SUNDAY = "sun"
|
||||||
|
|
||||||
|
|
||||||
|
class TimeWindow(BaseModel):
|
||||||
|
days: List[DayOfWeek]
|
||||||
|
start_time: str = Field(..., pattern=r"^\d{2}:\d{2}$", description="HH:MM format")
|
||||||
|
end_time: str = Field(..., pattern=r"^\d{2}:\d{2}$", description="HH:MM format")
|
||||||
|
|
||||||
|
|
||||||
|
class AccessRule(BaseModel):
|
||||||
|
user: str
|
||||||
|
enabled: bool = True
|
||||||
|
auto_reenable: bool = False
|
||||||
|
allowed_windows: List[TimeWindow] = []
|
||||||
|
|
||||||
|
|
||||||
|
class RuleSet(BaseModel):
|
||||||
|
rules: Dict[str, AccessRule] = {} # Keyed by username for O(1) lookup
|
||||||
|
|
||||||
|
|
||||||
class ActionRequest(BaseModel):
|
class ActionRequest(BaseModel):
|
||||||
countdown: Optional[int] = Field(default=None, ge=0, description="Seconds for countdown")
|
countdown: Optional[int] = Field(default=None, ge=0, description="Seconds for countdown")
|
||||||
sound: Optional[bool] = Field(default=None, description="Play sound alongside notification")
|
sound: Optional[bool] = Field(default=None, description="Play sound alongside notification")
|
||||||
@ -73,3 +101,23 @@ class UpdateLogEntry(BaseModel):
|
|||||||
version: Optional[str] = None
|
version: Optional[str] = None
|
||||||
error: Optional[str] = None
|
error: Optional[str] = None
|
||||||
device_id: Optional[str] = None
|
device_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateServiceStatus(BaseModel):
|
||||||
|
url: str
|
||||||
|
reachable: bool
|
||||||
|
status_code: Optional[int] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
checked_url: str
|
||||||
|
environment: str
|
||||||
|
|
||||||
|
|
||||||
|
class SystemMetrics(BaseModel):
|
||||||
|
cpu_percent: float
|
||||||
|
ram_total_mb: float
|
||||||
|
ram_used_percent: float
|
||||||
|
gpu_vram_total_mb: Optional[float] = None
|
||||||
|
gpu_vram_used_percent: Optional[float] = None
|
||||||
|
gpu_present: bool = False
|
||||||
|
net_rx_mbps: float
|
||||||
|
net_tx_mbps: float
|
||||||
|
|||||||
91
backend/rules.py
Normal file
91
backend/rules.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from backend.models import RuleSet, AccessRule, DayOfWeek
|
||||||
|
from backend.settings import get_settings, Settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RuleManager:
|
||||||
|
def __init__(self, settings: Optional[Settings] = None):
|
||||||
|
self.settings = settings or get_settings()
|
||||||
|
self.rules_file = self.settings.rules_file
|
||||||
|
|
||||||
|
def load_rules(self) -> RuleSet:
|
||||||
|
if not os.path.exists(self.rules_file):
|
||||||
|
return RuleSet()
|
||||||
|
try:
|
||||||
|
with open(self.rules_file, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return RuleSet(**data)
|
||||||
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
logger.error("Failed to load rules from %s: %s", self.rules_file, e)
|
||||||
|
return RuleSet()
|
||||||
|
|
||||||
|
def save_rules(self, rules: RuleSet) -> None:
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(self.rules_file), exist_ok=True)
|
||||||
|
with open(self.rules_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(rules.model_dump_json(indent=2))
|
||||||
|
except OSError as e:
|
||||||
|
logger.error("Failed to save rules to %s: %s", self.rules_file, e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_rule(self, user: str) -> Optional[AccessRule]:
|
||||||
|
rules = self.load_rules()
|
||||||
|
return rules.rules.get(user)
|
||||||
|
|
||||||
|
def set_rule(self, rule: AccessRule) -> None:
|
||||||
|
rules = self.load_rules()
|
||||||
|
rules.rules[rule.user] = rule
|
||||||
|
self.save_rules(rules)
|
||||||
|
|
||||||
|
def delete_rule(self, user: str) -> None:
|
||||||
|
rules = self.load_rules()
|
||||||
|
if user in rules.rules:
|
||||||
|
del rules.rules[user]
|
||||||
|
self.save_rules(rules)
|
||||||
|
|
||||||
|
def is_login_allowed(self, user: str, now: Optional[datetime] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Check if user is allowed to login at the given time (or now).
|
||||||
|
Returns True if no rule exists (or disabled) or if time is within an allowed window.
|
||||||
|
Returns False if a rule exists and current time is outside all allowed windows.
|
||||||
|
"""
|
||||||
|
rule = self.get_rule(user)
|
||||||
|
if not rule or not rule.enabled:
|
||||||
|
# No rule or rule disabled -> Default Allow
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not rule.allowed_windows:
|
||||||
|
# Rule enabled but no windows -> Deny All
|
||||||
|
return False
|
||||||
|
|
||||||
|
if now is None:
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# Map weekday to Enum
|
||||||
|
weekday_map = {
|
||||||
|
0: DayOfWeek.MONDAY,
|
||||||
|
1: DayOfWeek.TUESDAY,
|
||||||
|
2: DayOfWeek.WEDNESDAY,
|
||||||
|
3: DayOfWeek.THURSDAY,
|
||||||
|
4: DayOfWeek.FRIDAY,
|
||||||
|
5: DayOfWeek.SATURDAY,
|
||||||
|
6: DayOfWeek.SUNDAY,
|
||||||
|
}
|
||||||
|
current_day = weekday_map[now.weekday()]
|
||||||
|
current_time_str = now.strftime("%H:%M")
|
||||||
|
|
||||||
|
for window in rule.allowed_windows:
|
||||||
|
if current_day in window.days:
|
||||||
|
# Handle simple range start <= now <= end
|
||||||
|
# (Overnight windows assumed to be split by user into two windows)
|
||||||
|
if window.start_time <= current_time_str <= window.end_time:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
@ -57,6 +57,9 @@ class Settings:
|
|||||||
self.update_log_file: str = os.getenv(
|
self.update_log_file: str = os.getenv(
|
||||||
"SKD_UPDATE_LOG_FILE", "/var/lib/skd/update_logs.jsonl"
|
"SKD_UPDATE_LOG_FILE", "/var/lib/skd/update_logs.jsonl"
|
||||||
)
|
)
|
||||||
|
self.rules_file: str = os.getenv(
|
||||||
|
"SKD_RULES_FILE", "/var/lib/skd/rules.json"
|
||||||
|
)
|
||||||
# Paths/tools
|
# Paths/tools
|
||||||
self.notify_send_path: str = os.getenv("SKD_NOTIFY_SEND_PATH", "notify-send")
|
self.notify_send_path: str = os.getenv("SKD_NOTIFY_SEND_PATH", "notify-send")
|
||||||
self.sound_player: str = os.getenv("SKD_SOUND_PLAYER", "paplay")
|
self.sound_player: str = os.getenv("SKD_SOUND_PLAYER", "paplay")
|
||||||
|
|||||||
@ -887,6 +887,33 @@ textarea:focus {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: var(--border-main);
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background: var(--primary);
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-bar-fill.info {
|
||||||
|
background: var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-bar-fill.success {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-bar-fill.warning {
|
||||||
|
background: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
.panel-section {
|
.panel-section {
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|||||||
139
backend/system_metrics.py
Normal file
139
backend/system_metrics.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
|
_last_cpu: Optional[Tuple[float, float]] = None
|
||||||
|
_last_net: Optional[Tuple[float, float, float]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _read_cpu_times() -> Tuple[float, float]:
|
||||||
|
with open("/proc/stat", "r", encoding="utf-8") as handle:
|
||||||
|
line = handle.readline()
|
||||||
|
parts = line.strip().split()
|
||||||
|
if not parts or parts[0] != "cpu":
|
||||||
|
return 0.0, 0.0
|
||||||
|
values = [float(p) for p in parts[1:]]
|
||||||
|
total = sum(values)
|
||||||
|
idle = values[3] if len(values) > 3 else 0.0
|
||||||
|
return total, idle
|
||||||
|
|
||||||
|
|
||||||
|
def _cpu_percent() -> float:
|
||||||
|
global _last_cpu
|
||||||
|
total, idle = _read_cpu_times()
|
||||||
|
if _last_cpu is None:
|
||||||
|
_last_cpu = (total, idle)
|
||||||
|
return 0.0
|
||||||
|
last_total, last_idle = _last_cpu
|
||||||
|
_last_cpu = (total, idle)
|
||||||
|
delta_total = total - last_total
|
||||||
|
delta_idle = idle - last_idle
|
||||||
|
if delta_total <= 0:
|
||||||
|
return 0.0
|
||||||
|
return max(0.0, min(100.0, (delta_total - delta_idle) / delta_total * 100.0))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_meminfo() -> Dict[str, float]:
|
||||||
|
data: Dict[str, float] = {}
|
||||||
|
with open("/proc/meminfo", "r", encoding="utf-8") as handle:
|
||||||
|
for line in handle:
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
parts = value.strip().split()
|
||||||
|
if not parts:
|
||||||
|
continue
|
||||||
|
data[key] = float(parts[0])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _read_net_bytes() -> Tuple[float, float]:
|
||||||
|
rx_total = 0.0
|
||||||
|
tx_total = 0.0
|
||||||
|
with open("/proc/net/dev", "r", encoding="utf-8") as handle:
|
||||||
|
for line in handle:
|
||||||
|
if ":" not in line:
|
||||||
|
continue
|
||||||
|
iface, stats = line.split(":", 1)
|
||||||
|
iface = iface.strip()
|
||||||
|
if iface == "lo":
|
||||||
|
continue
|
||||||
|
fields = stats.split()
|
||||||
|
if len(fields) < 16:
|
||||||
|
continue
|
||||||
|
rx_total += float(fields[0])
|
||||||
|
tx_total += float(fields[8])
|
||||||
|
return rx_total, tx_total
|
||||||
|
|
||||||
|
|
||||||
|
def _net_mbps() -> Tuple[float, float]:
|
||||||
|
global _last_net
|
||||||
|
now = time.time()
|
||||||
|
rx, tx = _read_net_bytes()
|
||||||
|
if _last_net is None:
|
||||||
|
_last_net = (now, rx, tx)
|
||||||
|
return 0.0, 0.0
|
||||||
|
last_time, last_rx, last_tx = _last_net
|
||||||
|
_last_net = (now, rx, tx)
|
||||||
|
delta_t = now - last_time
|
||||||
|
if delta_t <= 0:
|
||||||
|
return 0.0, 0.0
|
||||||
|
rx_mbps = (rx - last_rx) * 8.0 / (delta_t * 1_000_000.0)
|
||||||
|
tx_mbps = (tx - last_tx) * 8.0 / (delta_t * 1_000_000.0)
|
||||||
|
return max(0.0, rx_mbps), max(0.0, tx_mbps)
|
||||||
|
|
||||||
|
|
||||||
|
def _gpu_metrics() -> Dict[str, Any]:
|
||||||
|
if not shutil.which("nvidia-smi"):
|
||||||
|
return {
|
||||||
|
"gpu_vram_total_mb": None,
|
||||||
|
"gpu_vram_used_percent": None,
|
||||||
|
"gpu_present": False,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
[
|
||||||
|
"nvidia-smi",
|
||||||
|
"--query-gpu=memory.total,memory.used",
|
||||||
|
"--format=csv,noheader,nounits",
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
).strip()
|
||||||
|
if not output:
|
||||||
|
raise ValueError("empty nvidia-smi output")
|
||||||
|
total_str, used_str = output.split(",", 1)
|
||||||
|
total_mb = float(total_str.strip())
|
||||||
|
used_mb = float(used_str.strip())
|
||||||
|
used_percent = 0.0 if total_mb == 0 else used_mb / total_mb * 100.0
|
||||||
|
return {
|
||||||
|
"gpu_vram_total_mb": total_mb,
|
||||||
|
"gpu_vram_used_percent": used_percent,
|
||||||
|
"gpu_present": True,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return {
|
||||||
|
"gpu_vram_total_mb": None,
|
||||||
|
"gpu_vram_used_percent": None,
|
||||||
|
"gpu_present": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_metrics() -> Dict[str, Any]:
|
||||||
|
meminfo = _read_meminfo()
|
||||||
|
total_kb = meminfo.get("MemTotal", 0.0)
|
||||||
|
available_kb = meminfo.get("MemAvailable", 0.0)
|
||||||
|
used_kb = max(0.0, total_kb - available_kb)
|
||||||
|
ram_total_mb = total_kb / 1024.0
|
||||||
|
ram_used_percent = 0.0 if total_kb == 0 else used_kb / total_kb * 100.0
|
||||||
|
|
||||||
|
cpu_percent = _cpu_percent()
|
||||||
|
rx_mbps, tx_mbps = _net_mbps()
|
||||||
|
gpu = _gpu_metrics()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cpu_percent": cpu_percent,
|
||||||
|
"ram_total_mb": ram_total_mb,
|
||||||
|
"ram_used_percent": ram_used_percent,
|
||||||
|
"net_rx_mbps": rx_mbps,
|
||||||
|
"net_tx_mbps": tx_mbps,
|
||||||
|
**gpu,
|
||||||
|
}
|
||||||
@ -95,14 +95,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- System Information Section -->
|
||||||
|
<section id="systemSection" class="panel-section hidden">
|
||||||
|
<h3><i data-lucide="monitor"></i> System Information</h3>
|
||||||
|
<div class="metrics" id="systemMetrics">
|
||||||
|
<div class="metric-card" id="metricCpu">
|
||||||
|
<div class="label"><i data-lucide="cpu"></i> CPU</div>
|
||||||
|
<div class="value">-</div>
|
||||||
|
<div class="metric-bar"><div class="metric-bar-fill primary" id="metricCpuBar"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card" id="metricRam">
|
||||||
|
<div class="label"><i data-lucide="memory-stick"></i> RAM</div>
|
||||||
|
<div class="value">-</div>
|
||||||
|
<div class="metric-bar"><div class="metric-bar-fill info" id="metricRamBar"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card" id="metricGpu">
|
||||||
|
<div class="label"><i data-lucide="monitor"></i> GPU</div>
|
||||||
|
<div class="value">-</div>
|
||||||
|
<div class="metric-bar"><div class="metric-bar-fill warning" id="metricGpuBar"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card" id="metricNet">
|
||||||
|
<div class="label"><i data-lucide="network"></i> Netzwerk</div>
|
||||||
|
<div class="value">-</div>
|
||||||
|
<div class="metric-bar"><div class="metric-bar-fill success" id="metricNetBar"></div></div>
|
||||||
|
<div class="text-muted" style="font-size: 0.75rem; margin-top: 6px;">Skala: 100 Mbps</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted" style="font-size: 0.75rem; margin-top: 0.5rem;">Aktualisierung alle 5 Sekunden.</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- User Management Section -->
|
<!-- User Management Section -->
|
||||||
<section id="userSection" class="panel-section hidden">
|
<section id="userSection" class="panel-section hidden">
|
||||||
<h3><i data-lucide="users"></i> Nutzerverwaltung</h3>
|
<h3><i data-lucide="users"></i> Nutzerverwaltung</h3>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||||
<button id="refreshUsersBtn" class="secondary small">
|
<button id="refreshUsersBtn" class="secondary small">
|
||||||
<i data-lucide="refresh-cw"></i>
|
<i data-lucide="refresh-cw"></i>
|
||||||
Aktualisieren
|
Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
|
<a href="/ui/rules" class="button secondary small">
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
Regeln verwalten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="user-table mt-1">
|
<table class="user-table mt-1">
|
||||||
<thead>
|
<thead>
|
||||||
@ -128,13 +163,13 @@
|
|||||||
<section id="updateSection" class="panel-section hidden">
|
<section id="updateSection" class="panel-section hidden">
|
||||||
<h3><i data-lucide="download"></i> Update-Verwaltung</h3>
|
<h3><i data-lucide="download"></i> Update-Verwaltung</h3>
|
||||||
|
|
||||||
<div id="updateStatus" style="background: rgba(10, 14, 20, 0.6); border: 1px solid var(--color-border); border-left: 3px solid var(--color-accent); padding: 1rem; border-radius: var(--radius-sm); margin-bottom: 1rem;">
|
<div id="updateStatus" style="background: var(--bg-panel); border: 1px solid var(--border-main); border-left: 3px solid var(--primary); padding: 1rem; border-radius: var(--radius-sm); margin-bottom: 1rem;">
|
||||||
<div class="spinner" style="margin: 0 auto;"></div>
|
<div class="spinner" style="margin: 0 auto;"></div>
|
||||||
<p class="text-center text-muted mt-1">Lade Update-Status...</p>
|
<p class="text-center text-muted mt-1">Lade Update-Status...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Enrollment Section -->
|
<!-- Enrollment Section -->
|
||||||
<div id="enrollmentSection" class="hidden" style="margin-bottom: 1rem; padding: 1rem; background: rgba(255, 255, 255, 0.05); border-radius: var(--radius-sm); border: 1px dashed var(--color-border);">
|
<div id="enrollmentSection" class="hidden" style="margin-bottom: 1rem; padding: 1rem; background: var(--bg-panel); border-radius: var(--radius-sm); border: 1px dashed var(--border-main);">
|
||||||
<h4 style="margin-top: 0; margin-bottom: 0.5rem;"><i data-lucide="link"></i> Gerät registrieren (Enrollment)</h4>
|
<h4 style="margin-top: 0; margin-bottom: 0.5rem;"><i data-lucide="link"></i> Gerät registrieren (Enrollment)</h4>
|
||||||
<p class="text-muted" style="font-size: 0.875rem; margin-bottom: 0.5rem;">
|
<p class="text-muted" style="font-size: 0.875rem; margin-bottom: 0.5rem;">
|
||||||
Dieses Gerät ist noch nicht beim Update-Service registriert. Bitte geben Sie einen gültigen Enrollment-Token ein.
|
Dieses Gerät ist noch nicht beim Update-Service registriert. Bitte geben Sie einen gültigen Enrollment-Token ein.
|
||||||
@ -326,14 +361,24 @@
|
|||||||
showApp();
|
showApp();
|
||||||
document.getElementById('userSection').classList.remove('hidden');
|
document.getElementById('userSection').classList.remove('hidden');
|
||||||
document.getElementById('updateSection').classList.remove('hidden');
|
document.getElementById('updateSection').classList.remove('hidden');
|
||||||
|
document.getElementById('systemSection').classList.remove('hidden');
|
||||||
await refreshUsers();
|
await refreshUsers();
|
||||||
await refreshUpdateStatus();
|
await refreshUpdateStatus();
|
||||||
|
await refreshSystemMetrics();
|
||||||
|
if (!window.systemMetricsTimer) {
|
||||||
|
window.systemMetricsTimer = setInterval(refreshSystemMetrics, 5000);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
document.getElementById('currentUser').textContent = 'Nicht angemeldet';
|
document.getElementById('currentUser').textContent = 'Nicht angemeldet';
|
||||||
showLanding();
|
showLanding();
|
||||||
document.getElementById('userSection').classList.add('hidden');
|
document.getElementById('userSection').classList.add('hidden');
|
||||||
document.getElementById('updateSection').classList.add('hidden');
|
document.getElementById('updateSection').classList.add('hidden');
|
||||||
|
document.getElementById('systemSection').classList.add('hidden');
|
||||||
|
if (window.systemMetricsTimer) {
|
||||||
|
clearInterval(window.systemMetricsTimer);
|
||||||
|
window.systemMetricsTimer = null;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,7 +459,10 @@
|
|||||||
statusDiv.innerHTML = '<div class="spinner" style="margin: 0 auto;"></div><p class="text-center text-muted mt-1">Lade Update-Status...</p>';
|
statusDiv.innerHTML = '<div class="spinner" style="margin: 0 auto;"></div><p class="text-center text-muted mt-1">Lade Update-Status...</p>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api('/update/status');
|
const [data, serviceStatus] = await Promise.all([
|
||||||
|
api('/update/status'),
|
||||||
|
api('/update/service-status')
|
||||||
|
]);
|
||||||
document.querySelector('#metricVersion .value').textContent = data.current_version;
|
document.querySelector('#metricVersion .value').textContent = data.current_version;
|
||||||
document.getElementById('headerVersion').textContent = `v${data.current_version}`;
|
document.getElementById('headerVersion').textContent = `v${data.current_version}`;
|
||||||
|
|
||||||
@ -436,12 +484,28 @@
|
|||||||
enrollSection.classList.remove('hidden');
|
enrollSection.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serviceBadge = serviceStatus.reachable
|
||||||
|
? '<span class="badge success"><i data-lucide="server"></i> Online</span>'
|
||||||
|
: '<span class="badge error"><i data-lucide="server-off"></i> Offline</span>';
|
||||||
|
const envLabel = serviceStatus.environment
|
||||||
|
? serviceStatus.environment.toUpperCase()
|
||||||
|
: 'UNKNOWN';
|
||||||
|
const serviceHint = serviceStatus.status_code
|
||||||
|
? `HTTP ${serviceStatus.status_code}`
|
||||||
|
: (serviceStatus.error || 'keine Antwort');
|
||||||
|
|
||||||
statusDiv.innerHTML = `
|
statusDiv.innerHTML = `
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-muted" style="font-size: 0.75rem; text-transform: uppercase; margin-bottom: 0.25rem;">Version</div>
|
<div class="text-muted" style="font-size: 0.75rem; text-transform: uppercase; margin-bottom: 0.25rem;">Version</div>
|
||||||
<div style="color: var(--color-accent); font-weight: 600;">${data.current_version}</div>
|
<div style="color: var(--color-accent); font-weight: 600;">${data.current_version}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted" style="font-size: 0.75rem; text-transform: uppercase; margin-bottom: 0.25rem;">Update-Service</div>
|
||||||
|
<div>${serviceBadge}</div>
|
||||||
|
<div class="badge neutral" style="display: inline-flex; margin-top: 0.25rem;">${envLabel}</div>
|
||||||
|
<div class="text-muted" style="font-size: 0.75rem;">${serviceStatus.url} (${serviceHint})</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-muted" style="font-size: 0.75rem; text-transform: uppercase; margin-bottom: 0.25rem;">Registrierung</div>
|
<div class="text-muted" style="font-size: 0.75rem; text-transform: uppercase; margin-bottom: 0.25rem;">Registrierung</div>
|
||||||
<div>${enrolledBadge}</div>
|
<div>${enrolledBadge}</div>
|
||||||
@ -494,6 +558,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshSystemMetrics() {
|
||||||
|
try {
|
||||||
|
const data = await api('/system/metrics');
|
||||||
|
document.querySelector('#metricCpu .value').textContent = `${data.cpu_percent.toFixed(1)}%`;
|
||||||
|
document.getElementById('metricCpuBar').style.width = `${Math.min(100, data.cpu_percent).toFixed(1)}%`;
|
||||||
|
const ramGb = data.ram_total_mb / 1024.0;
|
||||||
|
document.querySelector('#metricRam .value').textContent = `${data.ram_used_percent.toFixed(1)}% (${ramGb.toFixed(1)} GB)`;
|
||||||
|
document.getElementById('metricRamBar').style.width = `${Math.min(100, data.ram_used_percent).toFixed(1)}%`;
|
||||||
|
if (data.gpu_present && data.gpu_vram_total_mb) {
|
||||||
|
const gpuGb = data.gpu_vram_total_mb / 1024.0;
|
||||||
|
const gpuPct = data.gpu_vram_used_percent ?? 0;
|
||||||
|
document.querySelector('#metricGpu .value').textContent = `${gpuPct.toFixed(1)}% (${gpuGb.toFixed(1)} GB)`;
|
||||||
|
document.getElementById('metricGpuBar').style.width = `${Math.min(100, gpuPct).toFixed(1)}%`;
|
||||||
|
} else {
|
||||||
|
document.querySelector('#metricGpu .value').textContent = 'Nicht verfuegbar';
|
||||||
|
document.getElementById('metricGpuBar').style.width = '0%';
|
||||||
|
}
|
||||||
|
document.querySelector('#metricNet .value').textContent = `${data.net_rx_mbps.toFixed(1)} / ${data.net_tx_mbps.toFixed(1)} Mbps`;
|
||||||
|
const netPeak = Math.max(data.net_rx_mbps, data.net_tx_mbps);
|
||||||
|
const netPercent = Math.min(100, (netPeak / 100.0) * 100.0);
|
||||||
|
document.getElementById('metricNetBar').style.width = `${netPercent.toFixed(1)}%`;
|
||||||
|
lucide.createIcons();
|
||||||
|
} catch (err) {
|
||||||
|
document.querySelector('#metricCpu .value').textContent = 'Fehler';
|
||||||
|
document.querySelector('#metricRam .value').textContent = 'Fehler';
|
||||||
|
document.querySelector('#metricGpu .value').textContent = 'Fehler';
|
||||||
|
document.querySelector('#metricNet .value').textContent = 'Fehler';
|
||||||
|
document.getElementById('metricCpuBar').style.width = '0%';
|
||||||
|
document.getElementById('metricRamBar').style.width = '0%';
|
||||||
|
document.getElementById('metricGpuBar').style.width = '0%';
|
||||||
|
document.getElementById('metricNetBar').style.width = '0%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
442
backend/templates/rules.html
Normal file
442
backend/templates/rules.html
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Regelverwaltung - Safe Kiddo</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/branding/favicon.svg" />
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<style>
|
||||||
|
.rule-editor {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 250px 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.user-list {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-main);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.user-item {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid var(--border-main);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.user-item:last-child { border-bottom: none; }
|
||||||
|
.user-item:hover { background: rgba(255,255,255,0.05); }
|
||||||
|
.user-item.active { background: rgba(var(--primary-rgb), 0.1); border-left: 3px solid var(--primary); }
|
||||||
|
|
||||||
|
.editor-panel {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-main);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.window-item {
|
||||||
|
background: var(--bg-main);
|
||||||
|
border: 1px solid var(--border-main);
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.day-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.day-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-main);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.day-badge.active {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
.time-range {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.remove-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--color-error);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.day-checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.day-label {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border: 1px solid var(--border-main);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.day-checkbox:checked + .day-label {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Dark Mode Toggle -->
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<div class="header-main">
|
||||||
|
<h1>
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
Regelverwaltung
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="header-meta">
|
||||||
|
<a href="/dashboard" class="button secondary small">
|
||||||
|
<i data-lucide="arrow-left"></i> Zurück zum Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="rule-editor" id="ruleEditor">
|
||||||
|
<!-- User List -->
|
||||||
|
<div class="user-list" id="userList">
|
||||||
|
<div style="padding: 1rem; text-align: center; color: var(--text-muted);">
|
||||||
|
Lade Nutzer...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor Panel -->
|
||||||
|
<div class="editor-panel hidden" id="editorPanel">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||||
|
<h2 id="selectedUserTitle" style="margin: 0;">Benutzer</h2>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<button class="danger small" id="deleteRulesBtn">Regeln löschen</button>
|
||||||
|
<button class="primary small" id="saveRulesBtn">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<input type="checkbox" id="ruleEnabled" checked>
|
||||||
|
<label for="ruleEnabled">Regelwerk aktiv</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<input type="checkbox" id="autoReenable">
|
||||||
|
<label for="autoReenable">Automatisch entsperren (wenn Zeitfenster beginnt)</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">Erlaubte Zeitfenster</h3>
|
||||||
|
<div class="window-list" id="windowList">
|
||||||
|
<!-- Windows go here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="secondary small mt-1" onclick="openAddWindowModal()">
|
||||||
|
<i data-lucide="plus"></i> Zeitfenster hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-panel" id="emptyState" style="text-align: center; padding: 3rem;">
|
||||||
|
<i data-lucide="user" style="width: 48px; height: 48px; color: var(--text-muted); margin-bottom: 1rem;"></i>
|
||||||
|
<p class="text-muted">Wähle einen Benutzer aus, um Regeln zu bearbeiten.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Window Modal -->
|
||||||
|
<div class="modal" id="addWindowModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Zeitfenster hinzufügen</h3>
|
||||||
|
<button class="secondary small" onclick="closeAddWindowModal()">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="addWindowForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Wochentage</label>
|
||||||
|
<div class="day-selector">
|
||||||
|
<label><input type="checkbox" class="day-checkbox" value="mon"><span class="day-label">Mo</span></label>
|
||||||
|
<label><input type="checkbox" class="day-checkbox" value="tue"><span class="day-label">Di</span></label>
|
||||||
|
<label><input type="checkbox" class="day-checkbox" value="wed"><span class="day-label">Mi</span></label>
|
||||||
|
<label><input type="checkbox" class="day-checkbox" value="thu"><span class="day-label">Do</span></label>
|
||||||
|
<label><input type="checkbox" class="day-checkbox" value="fri"><span class="day-label">Fr</span></label>
|
||||||
|
<label><input type="checkbox" class="day-checkbox" value="sat"><span class="day-label">Sa</span></label>
|
||||||
|
<label><input type="checkbox" class="day-checkbox" value="sun"><span class="day-label">So</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Start</label>
|
||||||
|
<input type="time" id="startTime" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ende</label>
|
||||||
|
<input type="time" id="endTime" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" class="secondary" onclick="closeAddWindowModal()">Abbrechen</button>
|
||||||
|
<button type="submit">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div class="toast-container" id="toastContainer"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
// State
|
||||||
|
let users = [];
|
||||||
|
let currentRules = {}; // Keyed by username
|
||||||
|
let selectedUser = null;
|
||||||
|
let currentWindows = []; // Temp storage for editor
|
||||||
|
|
||||||
|
// API
|
||||||
|
async function api(path, options = {}) {
|
||||||
|
const token = sessionStorage.getItem('skdToken');
|
||||||
|
if (!token) window.location.href = '/login';
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
...(options.headers || {})
|
||||||
|
};
|
||||||
|
const res = await fetch(path, { ...options, headers });
|
||||||
|
if (!res.ok) throw new Error(await res.text());
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = 'success') {
|
||||||
|
const container = document.getElementById('toastContainer');
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
toast.innerHTML = `
|
||||||
|
<i data-lucide="${type === 'success' ? 'check-circle' : 'alert-circle'}"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
`;
|
||||||
|
container.appendChild(toast);
|
||||||
|
lucide.createIcons();
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'slideIn 0.3s ease reverse';
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
// Load users and rules in parallel
|
||||||
|
const [usersData, rulesData] = await Promise.all([
|
||||||
|
api('/users'),
|
||||||
|
api('/rules')
|
||||||
|
]);
|
||||||
|
|
||||||
|
users = usersData.map(u => u.user);
|
||||||
|
currentRules = rulesData.rules || {};
|
||||||
|
|
||||||
|
renderUserList();
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Fehler beim Laden: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUserList() {
|
||||||
|
const list = document.getElementById('userList');
|
||||||
|
if (users.length === 0) {
|
||||||
|
list.innerHTML = '<div style="padding:1rem;">Keine Nutzer gefunden.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = users.map(u => {
|
||||||
|
const hasRule = !!currentRules[u];
|
||||||
|
return `
|
||||||
|
<div class="user-item ${selectedUser === u ? 'active' : ''}" onclick="selectUser('${u}')">
|
||||||
|
<div>
|
||||||
|
<strong>${u}</strong>
|
||||||
|
${hasRule ? '<i data-lucide="check" style="width:14px; height:14px; margin-left:4px; color:var(--color-success)"></i>' : ''}
|
||||||
|
</div>
|
||||||
|
<i data-lucide="chevron-right" style="width:16px; height:16px; color:var(--text-muted)"></i>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectUser(user) {
|
||||||
|
selectedUser = user;
|
||||||
|
renderUserList(); // Update active state
|
||||||
|
|
||||||
|
document.getElementById('emptyState').classList.add('hidden');
|
||||||
|
document.getElementById('editorPanel').classList.remove('hidden');
|
||||||
|
document.getElementById('selectedUserTitle').textContent = user;
|
||||||
|
|
||||||
|
const rule = currentRules[user] || { enabled: true, auto_reenable: false, allowed_windows: [] };
|
||||||
|
|
||||||
|
document.getElementById('ruleEnabled').checked = rule.enabled;
|
||||||
|
document.getElementById('autoReenable').checked = rule.auto_reenable;
|
||||||
|
currentWindows = [...(rule.allowed_windows || [])];
|
||||||
|
|
||||||
|
renderWindows();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWindows() {
|
||||||
|
const container = document.getElementById('windowList');
|
||||||
|
if (currentWindows.length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-muted" style="font-size:0.9rem;">Keine Zeitfenster definiert. Login ist standardmäßig <strong>nicht erlaubt</strong> (wenn Regelwerk aktiv).</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayMap = { mon:'Mo', tue:'Di', wed:'Mi', thu:'Do', fri:'Fr', sat:'Sa', sun:'So' };
|
||||||
|
const allDays = ['mon','tue','wed','thu','fri','sat','sun'];
|
||||||
|
|
||||||
|
container.innerHTML = currentWindows.map((win, idx) => {
|
||||||
|
const badges = allDays.map(d => `
|
||||||
|
<span class="day-badge ${win.days.includes(d) ? 'active' : ''}">${dayMap[d]}</span>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="window-item">
|
||||||
|
<div class="day-badges">${badges}</div>
|
||||||
|
<div class="time-range">
|
||||||
|
${win.start_time} - ${win.end_time}
|
||||||
|
</div>
|
||||||
|
<button class="remove-btn" onclick="removeWindow(${idx})">
|
||||||
|
<i data-lucide="trash-2" style="width:16px; height:16px;"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWindow(index) {
|
||||||
|
currentWindows.splice(index, 1);
|
||||||
|
renderWindows();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddWindowModal() {
|
||||||
|
document.getElementById('addWindowForm').reset();
|
||||||
|
// Default checkboxes
|
||||||
|
document.querySelectorAll('.day-checkbox').forEach(cb => {
|
||||||
|
if (['mon','tue','wed','thu','fri'].includes(cb.value)) cb.checked = true;
|
||||||
|
else cb.checked = false;
|
||||||
|
});
|
||||||
|
document.getElementById('addWindowModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAddWindowModal() {
|
||||||
|
document.getElementById('addWindowModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('addWindowForm').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const days = Array.from(document.querySelectorAll('.day-checkbox:checked')).map(cb => cb.value);
|
||||||
|
if (days.length === 0) {
|
||||||
|
showToast('Bitte mindestens einen Wochentag wählen.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = document.getElementById('startTime').value;
|
||||||
|
const end = document.getElementById('endTime').value;
|
||||||
|
|
||||||
|
if (start >= end) {
|
||||||
|
showToast('Endzeit muss nach Startzeit liegen.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWindows.push({ days, start_time: start, end_time: end });
|
||||||
|
currentWindows.sort((a, b) => a.start_time.localeCompare(b.start_time));
|
||||||
|
|
||||||
|
closeAddWindowModal();
|
||||||
|
renderWindows();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('saveRulesBtn').addEventListener('click', async () => {
|
||||||
|
if (!selectedUser) return;
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
user: selectedUser,
|
||||||
|
enabled: document.getElementById('ruleEnabled').checked,
|
||||||
|
auto_reenable: document.getElementById('autoReenable').checked,
|
||||||
|
allowed_windows: currentWindows
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api('/rules', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(rule)
|
||||||
|
});
|
||||||
|
currentRules[selectedUser] = rule;
|
||||||
|
showToast('Regeln gespeichert', 'success');
|
||||||
|
renderUserList();
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Speichern fehlgeschlagen: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('deleteRulesBtn').addEventListener('click', async () => {
|
||||||
|
if (!selectedUser || !confirm(`Regeln für ${selectedUser} wirklich löschen?\nDer Nutzer hat dann uneingeschränkten Zugriff.`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api(`/rules/${selectedUser}`, { method: 'DELETE' });
|
||||||
|
delete currentRules[selectedUser];
|
||||||
|
showToast('Regeln gelöscht', 'success');
|
||||||
|
selectUser(selectedUser); // Refresh view (defaults)
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Löschen fehlgeschlagen: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dark Mode (Shared)
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -109,11 +109,28 @@ def _parse_version(value: str) -> List[int]:
|
|||||||
return [int(part) for part in value.split(".")]
|
return [int(part) for part in value.split(".")]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_fresh_token(settings: Settings) -> str:
|
||||||
|
"""Always reload token from file/env to avoid stale cache."""
|
||||||
|
# Env var takes precedence
|
||||||
|
env_token = os.getenv("SKD_UPDATE_TOKEN", "")
|
||||||
|
if env_token:
|
||||||
|
return env_token
|
||||||
|
# Fallback to file
|
||||||
|
token_file = Path(settings.update_token_file)
|
||||||
|
if token_file.exists():
|
||||||
|
try:
|
||||||
|
return token_file.read_text(encoding="utf-8").strip()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return settings.update_token # Fallback to cached value
|
||||||
|
|
||||||
|
|
||||||
def check_update(settings: Settings) -> Dict[str, Any]:
|
def check_update(settings: Settings) -> Dict[str, Any]:
|
||||||
if not settings.update_token:
|
token = _get_fresh_token(settings)
|
||||||
|
if not token:
|
||||||
raise ValueError("Client is not enrolled (missing update token)")
|
raise ValueError("Client is not enrolled (missing update token)")
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {settings.update_token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
manifest_url = (
|
manifest_url = (
|
||||||
f"{settings.update_service_url}/v1/projects/{settings.update_project_id}/manifest"
|
f"{settings.update_service_url}/v1/projects/{settings.update_project_id}/manifest"
|
||||||
)
|
)
|
||||||
@ -152,7 +169,8 @@ def report_status(
|
|||||||
error: str | None = None,
|
error: str | None = None,
|
||||||
duration_ms: int | None = None,
|
duration_ms: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not settings.update_token:
|
token = _get_fresh_token(settings)
|
||||||
|
if not token:
|
||||||
return
|
return
|
||||||
|
|
||||||
report_url = (
|
report_url = (
|
||||||
@ -172,7 +190,7 @@ def report_status(
|
|||||||
payload["duration_ms"] = duration_ms
|
payload["duration_ms"] = duration_ms
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {"Authorization": f"Bearer {settings.update_token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
with httpx.Client(timeout=10.0) as client:
|
with httpx.Client(timeout=10.0) as client:
|
||||||
client.post(report_url, json=payload, headers=headers).raise_for_status()
|
client.post(report_url, json=payload, headers=headers).raise_for_status()
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -181,14 +199,36 @@ def report_status(
|
|||||||
|
|
||||||
|
|
||||||
def _run_async(script_path: Path, settings: Settings) -> None:
|
def _run_async(script_path: Path, settings: Settings) -> None:
|
||||||
env = os.environ.copy()
|
# Explicitly gather the env vars we need to pass
|
||||||
env["SKD_UPDATE_SERVICE_URL"] = settings.update_service_url
|
env_vars = {
|
||||||
env["SKD_UPDATE_PROJECT_ID"] = settings.update_project_id
|
"SKD_UPDATE_SERVICE_URL": settings.update_service_url,
|
||||||
env["SKD_UPDATE_TOKEN"] = settings.update_token
|
"SKD_UPDATE_PROJECT_ID": settings.update_project_id,
|
||||||
env["SKD_UPDATE_STATUS_FILE"] = settings.update_status_file
|
"SKD_UPDATE_TOKEN": _get_fresh_token(settings),
|
||||||
env["SKD_UPDATE_LOG_FILE"] = settings.update_log_file
|
"SKD_UPDATE_STATUS_FILE": settings.update_status_file,
|
||||||
|
"SKD_UPDATE_LOG_FILE": settings.update_log_file,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Construct systemd-run command with --setenv for each variable
|
||||||
|
cmd = [
|
||||||
|
"systemd-run",
|
||||||
|
"--unit=skd-update",
|
||||||
|
"--collect",
|
||||||
|
"--description=Safe Kiddo Update Process",
|
||||||
|
]
|
||||||
|
|
||||||
|
for key, val in env_vars.items():
|
||||||
|
if val: # Only pass if not empty
|
||||||
|
cmd.append(f"--setenv={key}={val}")
|
||||||
|
|
||||||
|
cmd.append(str(script_path))
|
||||||
|
|
||||||
|
# We don't pass 'env' parameter to Popen because systemd-run ignores it
|
||||||
|
# for the target process (it only uses it for itself, but we use --setenv).
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
[str(script_path)], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
cmd,
|
||||||
|
cwd="/",
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -220,3 +260,45 @@ def get_logs(settings: Settings, limit: int = 200) -> List[Dict[str, Any]]:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
continue
|
continue
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_status(settings: Settings) -> Dict[str, Any]:
|
||||||
|
base_url = settings.update_service_url.rstrip("/")
|
||||||
|
if not base_url:
|
||||||
|
return {
|
||||||
|
"url": "",
|
||||||
|
"reachable": False,
|
||||||
|
"status_code": None,
|
||||||
|
"error": "update service url not configured",
|
||||||
|
"checked_url": "",
|
||||||
|
"environment": "unknown",
|
||||||
|
}
|
||||||
|
|
||||||
|
env = "prod"
|
||||||
|
lowered = base_url.lower()
|
||||||
|
if "://dev." in lowered or lowered.startswith("dev."):
|
||||||
|
env = "dev"
|
||||||
|
elif "://staging." in lowered or lowered.startswith("staging."):
|
||||||
|
env = "staging"
|
||||||
|
|
||||||
|
check_url = base_url
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=3.0) as client:
|
||||||
|
response = client.get(check_url)
|
||||||
|
return {
|
||||||
|
"url": base_url,
|
||||||
|
"reachable": True,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"error": None,
|
||||||
|
"checked_url": check_url,
|
||||||
|
"environment": env,
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"url": base_url,
|
||||||
|
"reachable": False,
|
||||||
|
"status_code": None,
|
||||||
|
"error": str(exc),
|
||||||
|
"checked_url": check_url,
|
||||||
|
"environment": env,
|
||||||
|
}
|
||||||
|
|||||||
79
docs/ARCHITECTURE.md
Normal file
79
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
ID: DOC_000012 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Architektur
|
||||||
|
|
||||||
|
## Systemuebersicht
|
||||||
|
Safe Kiddo Daemon ist ein FastAPI-basierter Service, der lokale Systemkonten verwaltet. Er kombiniert:
|
||||||
|
- API und Web-UI (FastAPI + Jinja2 Templates)
|
||||||
|
- Authentifizierung (PAM und optional OIDC)
|
||||||
|
- Systemaktionen via sudo/usermod/pkill/shutdown
|
||||||
|
- Update-Client-Integration (externes Update-Service-Backend)
|
||||||
|
|
||||||
|
## Externe Abhaengigkeiten
|
||||||
|
- Update-Service (intern): https://git.wlkns.org/stephan/update-webservice
|
||||||
|
- OIDC-Service (intern): https://git.wlkns.org/stephan/oicd
|
||||||
|
|
||||||
|
## Integrationspunkte
|
||||||
|
### Update-Service
|
||||||
|
Die Update-Integration nutzt v1-Endpunkte des Update-Services:
|
||||||
|
- `POST /v1/enroll` (Enrollment fuer Langzeit-Token)
|
||||||
|
- `GET /v1/projects/{project_id}/manifest`
|
||||||
|
- `POST /v1/projects/{project_id}/status`
|
||||||
|
Der Langzeit-Token wird lokal in `SKD_UPDATE_TOKEN_FILE` gespeichert und fuer Manifest/Status als Bearer-Token verwendet.
|
||||||
|
|
||||||
|
### OIDC-Service
|
||||||
|
OIDC nutzt Discovery unter `/.well-known/openid-configuration` basierend auf `SKD_OIDC_ISSUER`.
|
||||||
|
Der Login-Flow tauscht einen Code gegen ein ID-Token (RS256) und validiert es gegen JWKS.
|
||||||
|
|
||||||
|
## Module und Verantwortlichkeiten
|
||||||
|
- `backend/app.py`: API-Routing, Web-UI-Endpunkte, Update-Endpunkte.
|
||||||
|
- `backend/auth.py`: PAM-Login, JWT-Handling, Auth-Guards, Allowlists.
|
||||||
|
- `backend/oidc.py`: OIDC Discovery, Token-Exchange, JWT-Validierung.
|
||||||
|
- `backend/actions.py`: Systemaktionen (lock/unlock, notify, sound, shutdown).
|
||||||
|
- `backend/update.py`: Update-Enrollment, Manifest-Check, Update/Rollback-Start, Status/Logs.
|
||||||
|
- `backend/settings.py`: Zentrale ENV-Konfiguration.
|
||||||
|
- `backend/templates/` + `backend/static/`: Web-UI.
|
||||||
|
- `scripts/*.sh`: Installation, Deployment, Update-Client, Rollback, OIDC-Registration.
|
||||||
|
|
||||||
|
## Daten- und Kontrollfluss
|
||||||
|
### Login und Auth
|
||||||
|
1. `POST /login` authentifiziert via PAM.
|
||||||
|
2. JWT wird erstellt und als Cookie oder Bearer-Token genutzt.
|
||||||
|
3. Schutz aller Admin-Endpunkte via `get_current_admin`.
|
||||||
|
|
||||||
|
### OIDC-Flow (optional)
|
||||||
|
1. `GET /login/oidc/start` generiert State und leitet zum IdP.
|
||||||
|
2. Callback `GET /login/oidc/callback` validiert State, tauscht Code gegen ID-Token.
|
||||||
|
3. ID-Token wird gegen JWKS geprueft, Username extrahiert, Session gesetzt.
|
||||||
|
|
||||||
|
### Benutzeraktionen
|
||||||
|
1. `POST /users/{username}/disable` ruft `actions.disable_user`.
|
||||||
|
2. Systemaktionen: `usermod -L`, optional notify/sound, `pkill`, optional `shutdown`.
|
||||||
|
3. `POST /users/{username}/enable` fuehrt `usermod -U` aus.
|
||||||
|
|
||||||
|
### Update-Flow
|
||||||
|
1. `POST /update/enroll` schreibt Langzeit-Token in `SKD_UPDATE_TOKEN_FILE`.
|
||||||
|
2. `POST /update/check` ruft Manifest beim Update-Service ab.
|
||||||
|
3. `POST /update/apply` startet `scripts/update_client.sh` asynchron.
|
||||||
|
4. `POST /update/rollback` startet `scripts/rollback_client.sh` asynchron.
|
||||||
|
5. Status/Logs werden lokal in Dateien geschrieben und optional an den Update-Service gemeldet.
|
||||||
|
|
||||||
|
## Designentscheidungen und Tradeoffs
|
||||||
|
- **Root-Run**: Service laeuft als root, da PAM und Systemkommandos Root erfordern.
|
||||||
|
- **JWT + Cookie**: Einfache lokale Auth; keine externe Session-Datenbank.
|
||||||
|
- **OIDC optional**: OIDC ist optional, PAM bleibt als Fallback aktiv.
|
||||||
|
- **Update als Script**: Update/Backup/Swap via Bash-Skripte fuer einfache Ops, Tradeoff: weniger granularer Fehler-Handling.
|
||||||
|
|
||||||
|
## Erweiterungspunkte
|
||||||
|
- **Auth**: Weitere Auth-Mechanismen koennen in `backend/auth.py` integriert werden.
|
||||||
|
- **UI**: Templates unter `backend/templates/` und CSS in `backend/static/`.
|
||||||
|
- **Update-Client**: Anpassung der Update-Strategie in `scripts/update_client.sh`.
|
||||||
|
- **Notifications/Sound**: Konfigurierbar per `SKD_NOTIFY_SEND_PATH`, `SKD_SOUND_PLAYER`, `SKD_SOUND_FILE`.
|
||||||
|
|
||||||
|
## Spezifikationen
|
||||||
|
- Update-Service OpenAPI: `docs/architecture/openapi.yaml` und `docs/architecture/openapi/`.
|
||||||
|
|
||||||
|
## Weitere Dokumente
|
||||||
|
- Entwicklung: `docs/DEVELOPMENT.md`
|
||||||
|
- Deployment: `docs/DEPLOYMENT.md`
|
||||||
91
docs/CONFIGURATION.md
Normal file
91
docs/CONFIGURATION.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
ID: DOC_000011 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
|
||||||
|
## Speicherort
|
||||||
|
Die Konfiguration erfolgt per ENV-Dateien:
|
||||||
|
- `/etc/skd/env` (Core-App, Vorlage: `env.example`)
|
||||||
|
- `/etc/skd/update.env` (Update-Service fuer Laufzeit, Vorlage: `env.update.example`)
|
||||||
|
- `update-addon.env` (lokal fuer Upload/Enrollment, Vorlage: `update-addon.env.example`)
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
- `SKD_AUTH_MODE` (default `pam`): `pam` oder `oidc`. Ungueltige Werte fallen auf `pam` zurueck. Hinweis: Der Wert wird aktuell nicht zur Erzwingung genutzt; OIDC ist aktiv, sobald die OIDC-Variablen gesetzt sind.
|
||||||
|
- `SKD_AUTH_SECRET` (default `change-me-secret`): HMAC-Secret fuer JWTs.
|
||||||
|
- `SKD_TOKEN_TTL_SECONDS` (default `900`): Token-Laufzeit in Sekunden.
|
||||||
|
- `SKD_AUTH_ALLOWED_USERS` (default leer): Kommagetrennte Liste erlaubter Admin-User (gilt fuer PAM und OIDC).
|
||||||
|
- `SKD_AUTH_ALLOWED_GROUPS` (default `sudo`): Erlaubte Gruppen fuer PAM-Login.
|
||||||
|
- `SKD_AUTH_PAM_SERVICE` (default `login`, auf Debian/Ubuntu via `install.sh` auf `skd` gesetzt).
|
||||||
|
|
||||||
|
## OIDC
|
||||||
|
OIDC ist optional und zusaetzlich zu PAM.
|
||||||
|
- `SKD_OIDC_ISSUER`
|
||||||
|
- `SKD_OIDC_CLIENT_ID`
|
||||||
|
- `SKD_OIDC_CLIENT_SECRET`
|
||||||
|
- `SKD_OIDC_REDIRECT_URI` (default `http://localhost:8000/login/oidc/callback`)
|
||||||
|
- `SKD_OIDC_SCOPES` (default `openid profile email`)
|
||||||
|
- `SKD_SESSION_COOKIE_SECURE` (default `false`): Setze `true` fuer HTTPS.
|
||||||
|
- `SKD_OIDC_STATE_COOKIE_NAME` (default `skd_oidc_state`)
|
||||||
|
Referenz: OIDC-Service (intern) https://git.wlkns.org/stephan/oicd
|
||||||
|
|
||||||
|
### OIDC-Einbindung (Kurz)
|
||||||
|
1. Issuer setzen (muss der externen URL des IdP entsprechen):
|
||||||
|
- `SKD_OIDC_ISSUER=https://auth.example.org`
|
||||||
|
2. Client registrieren (DCR), falls der IdP es erlaubt:
|
||||||
|
```bash
|
||||||
|
export SKD_OIDC_ISSUER="https://auth.example.org"
|
||||||
|
export SKD_OIDC_REDIRECT_URI="https://kiddo.example.org/login/oidc/callback"
|
||||||
|
export OIDC_INITIAL_ACCESS_TOKEN="example-token"
|
||||||
|
./scripts/register_oidc_client.sh
|
||||||
|
```
|
||||||
|
3. Client-Credentials in `/etc/skd/env` setzen:
|
||||||
|
```
|
||||||
|
SKD_OIDC_CLIENT_ID=example-client-id
|
||||||
|
SKD_OIDC_CLIENT_SECRET=example-client-secret
|
||||||
|
SKD_OIDC_REDIRECT_URI=https://kiddo.example.org/login/oidc/callback
|
||||||
|
SKD_SESSION_COOKIE_SECURE=true
|
||||||
|
```
|
||||||
|
Hinweis: OIDC ist aktiv, sobald Issuer, Client-ID und Secret gesetzt sind.
|
||||||
|
|
||||||
|
## Session/Benutzerverwaltung
|
||||||
|
- `SKD_SESSION_COOKIE_NAME` (default `skd_session`)
|
||||||
|
- `SKD_ALLOWED_USERS` (default leer): Optionales Allowlist fuer verwaltbare System-User.
|
||||||
|
|
||||||
|
## Aktionen (Countdown/Notify/Sound)
|
||||||
|
- `SKD_DEFAULT_COUNTDOWN` (default `60` Sekunden)
|
||||||
|
- `SKD_DEFAULT_SOUND` (default `false`)
|
||||||
|
- `SKD_NOTIFY_TIMEOUT` (default `5` Sekunden)
|
||||||
|
- `SKD_NOTIFY_SEND_PATH` (default `notify-send`)
|
||||||
|
- `SKD_SOUND_PLAYER` (default `paplay`)
|
||||||
|
- `SKD_SOUND_FILE` (default `/usr/share/sounds/freedesktop/stereo/dialog-warning.oga`)
|
||||||
|
|
||||||
|
## Update-Client
|
||||||
|
- `SKD_UPDATE_SERVICE_URL` (default `https://update.wlkns.org`)
|
||||||
|
- `SKD_UPDATE_PROJECT_ID` (default `safe-kiddo-control`)
|
||||||
|
- `SKD_UPDATE_ENROLL_TOKEN` (optional; fuer `/update/enroll`)
|
||||||
|
- `SKD_UPDATE_TOKEN` (optional; alternativ per Datei)
|
||||||
|
- `SKD_UPDATE_TOKEN_FILE` (default `/var/lib/skd/update_token`)
|
||||||
|
- `SKD_UPDATE_STATUS_FILE` (default `/var/lib/skd/update_status.json`)
|
||||||
|
- `SKD_UPDATE_LOG_FILE` (default `/var/lib/skd/update_logs.jsonl`)
|
||||||
|
- `SKD_UPDATE_INTERVAL` (default `3600`): Hinweis: wird aktuell nur eingelesen, aber nicht automatisch genutzt.
|
||||||
|
- `SKD_UPDATE_UPLOAD_TOKEN` (optional; fuer Release-Upload)
|
||||||
|
- `SKD_UPDATE_UPLOAD_TOKEN_FILE` (optional; z.B. `/etc/skd/update.upload.token`)
|
||||||
|
|
||||||
|
## Dry-Run
|
||||||
|
- `SKD_DRY_RUN` (default `false`): Keine echten System-Aktionen, nur Logging.
|
||||||
|
|
||||||
|
## Tuning und Betrieb (Power-User)
|
||||||
|
- `SKD_TOKEN_TTL_SECONDS`: kuerzere Tokens reduzieren Risiko, laengere Tokens reduzieren Login-Haeufigkeit.
|
||||||
|
- `SKD_DEFAULT_COUNTDOWN`: steuert Nutzerwarnung vor Sperre/Shutdown.
|
||||||
|
- `SKD_NOTIFY_TIMEOUT`: Dauer der Desktop-Benachrichtigung.
|
||||||
|
- `SKD_SESSION_COOKIE_SECURE=true`: zwingend bei HTTPS, sonst Login-Cookies unsicher.
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
- `scripts/install.sh` erstellt `/etc/skd/env` und setzt Default-Werte fuer PAM/Allowed-User.
|
||||||
|
- `scripts/install.sh` erstellt `/etc/skd/update.env` fuer Update-Service Variablen.
|
||||||
|
- Aenderungen in `/etc/skd/env` erfordern einen Service-Restart (`sudo systemctl restart skd.service`).
|
||||||
|
|
||||||
|
## Weitere Dokumente
|
||||||
|
- Nutzung/Automation: `docs/USAGE.md`
|
||||||
|
- Deployment: `docs/DEPLOYMENT.md`
|
||||||
|
- Architektur: `docs/ARCHITECTURE.md`
|
||||||
133
docs/DEPLOYMENT.md
Normal file
133
docs/DEPLOYMENT.md
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
ID: DOC_000014 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Deployment
|
||||||
|
|
||||||
|
## Lokale Installation (systemd)
|
||||||
|
`./scripts/install.sh` fuehrt folgende Schritte aus:
|
||||||
|
- legt Service-User/Group an
|
||||||
|
- kopiert das Projekt nach `/opt/sk`
|
||||||
|
- erstellt/aktualisiert `.venv`
|
||||||
|
- erstellt `/etc/skd/env` aus `env.example`
|
||||||
|
- erstellt `/etc/skd/update.env` aus `env.update.example`
|
||||||
|
- schreibt eine systemd-Unit nach `/etc/systemd/system/skd.service`
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
sudo systemctl status skd.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manuelles Starten
|
||||||
|
```bash
|
||||||
|
./scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Makefile Targets (Ops)
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
make up
|
||||||
|
make down
|
||||||
|
make restart
|
||||||
|
make update
|
||||||
|
make uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
## Remote-Deploy (SSH)
|
||||||
|
`./scripts/deploy.sh` packt das Repo und deployt es auf einen Zielhost.
|
||||||
|
Konfiguration in `deploy_hosts.yml`.
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
```bash
|
||||||
|
./scripts/deploy.sh kid-laptop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment via ZIP
|
||||||
|
Im Repo liegt `sk_deploy.zip`. Dieses Archiv kann auf den Zielhost kopiert und nach `/opt/sk` entpackt werden.
|
||||||
|
Anschliessend Abhaengigkeiten installieren und Service neu starten:
|
||||||
|
```bash
|
||||||
|
sudo -u skd /opt/sk/.venv/bin/pip install -r /opt/sk/backend/requirements.txt
|
||||||
|
sudo systemctl restart skd.service
|
||||||
|
```
|
||||||
|
## Update des Services
|
||||||
|
`./scripts/update.sh` zieht den Branch neu und fuehrt einen harten Reset aus.
|
||||||
|
|
||||||
|
Wichtig: Das Script nutzt `git reset --hard origin/main`.
|
||||||
|
```bash
|
||||||
|
sudo ./scripts/update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update-Client (Remote Update Service)
|
||||||
|
Die Update-API startet `scripts/update_client.sh` bzw. `scripts/rollback_client.sh`.
|
||||||
|
Wichtige ENV-Variablen:
|
||||||
|
- `SKD_UPDATE_SERVICE_URL`
|
||||||
|
- `SKD_UPDATE_PROJECT_ID`
|
||||||
|
- `SKD_UPDATE_TOKEN` oder `SKD_UPDATE_TOKEN_FILE`
|
||||||
|
Voraussetzungen auf dem Host:
|
||||||
|
- `curl`, `tar`, `sha256sum`, `python3`, `systemctl`
|
||||||
|
Referenz: Update-Service (intern) https://git.wlkns.org/stephan/update-webservice
|
||||||
|
|
||||||
|
### Update-Service einbinden
|
||||||
|
1. Service-URL und Projekt setzen:
|
||||||
|
```
|
||||||
|
SKD_UPDATE_SERVICE_URL=https://update.wlkns.org
|
||||||
|
SKD_UPDATE_PROJECT_ID=safe-kiddo-control
|
||||||
|
```
|
||||||
|
2. Enrollment-Token besorgen (vom Update-Service-Admin) und einen Langzeit-Token erzeugen:
|
||||||
|
- Option A: Ueber lokale API
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"enroll_token\":\"example-enroll-token\"}" \
|
||||||
|
http://localhost/update/enroll
|
||||||
|
```
|
||||||
|
- Option B: Direkter Enrollment-Client
|
||||||
|
```bash
|
||||||
|
./scripts/manual_enroll.py --url "https://update.wlkns.org" --project "safe-kiddo-control" --token "example-enroll-token"
|
||||||
|
```
|
||||||
|
3. Token-Datei pruefen:
|
||||||
|
```
|
||||||
|
sudo cat /var/lib/skd/update_token
|
||||||
|
```
|
||||||
|
Hinweis: Der Update-Check ist erst moeglich, wenn der Langzeit-Token gespeichert wurde.
|
||||||
|
|
||||||
|
Enrollment-Tools:
|
||||||
|
- `scripts/manual_enroll.py`: Enrollment direkt gegen den Update-Service, schreibt Token in `SKD_UPDATE_TOKEN_FILE`.
|
||||||
|
- `scripts/enroll_local.py`: Enrollment ueber die lokale API (`/update/enroll`), benoetigt Admin-Session; `--token` setzen (Default-Token ist nur Prototyp-Altlast).
|
||||||
|
- `scripts/enroll_update_service.sh`: Holt den Langzeit-Token per curl vom Update-Service (liest `update-addon.env`).
|
||||||
|
|
||||||
|
Beispiel (curl-Script):
|
||||||
|
```bash
|
||||||
|
sudo ./scripts/enroll_update_service.sh --enroll-token "enroll_example"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release-Upload (Dev/Prod)
|
||||||
|
Fuer Dev/Prod Uploads kann ein Release-Archiv (tar.gz) automatisiert gebaut und hochgeladen werden.
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
```bash
|
||||||
|
./scripts/upload_release.sh --profile dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Token-Quelle:
|
||||||
|
- `update-addon.env` (z.B. `DEV_UPDATE_UPLOAD_TOKEN_FILE=./upload.token`).
|
||||||
|
|
||||||
|
## Lokale update-addon.env
|
||||||
|
Fuer lokale Tests kann eine `update-addon.env` im Repo genutzt werden (gitignored).
|
||||||
|
Beispiel:
|
||||||
|
```bash
|
||||||
|
cp update-addon.env.example update-addon.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Lokale Status/Logs:
|
||||||
|
- `SKD_UPDATE_STATUS_FILE` (default `/var/lib/skd/update_status.json`)
|
||||||
|
- `SKD_UPDATE_LOG_FILE` (default `/var/lib/skd/update_logs.jsonl`)
|
||||||
|
|
||||||
|
## Backup/Restore
|
||||||
|
- Bei Apply wird `/opt/sk` nach `/opt/sk_backup_1.2.3_1700000000` verschoben (Beispiel).
|
||||||
|
- Rollback nutzt das letzte Backup (`/opt/sk_backup_*`).
|
||||||
|
|
||||||
|
## Weitere Dokumente
|
||||||
|
- Konfiguration: `docs/CONFIGURATION.md`
|
||||||
|
- Nutzung: `docs/USAGE.md`
|
||||||
|
- Troubleshooting: `docs/TROUBLESHOOTING.md`
|
||||||
87
docs/DEVELOPMENT.md
Normal file
87
docs/DEVELOPMENT.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
ID: DOC_000013 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
## Repository-Struktur (Kurz)
|
||||||
|
- `backend/`: FastAPI-App, Auth, Update-Logik, Templates, Static Assets.
|
||||||
|
- `scripts/`: Install/Deploy/Update/Helper-Skripte.
|
||||||
|
- `systemd/`: Beispiel-Unit.
|
||||||
|
- `docs/architecture/`: OpenAPI-Spezifikation fuer Update-Service.
|
||||||
|
- `docs/`: Dokumentation.
|
||||||
|
- `assets/`: Branding und Design.
|
||||||
|
- `sk.sh`: Legacy-CLI.
|
||||||
|
|
||||||
|
## Lokales Setup
|
||||||
|
```bash
|
||||||
|
./scripts/create_venv.sh
|
||||||
|
source .venv/bin/activate
|
||||||
|
./scripts/run.sh
|
||||||
|
```
|
||||||
|
Standard: `0.0.0.0:80`. Fuer andere Ports:
|
||||||
|
```bash
|
||||||
|
HOST=127.0.0.1 PORT=8000 ./scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release-Upload (Dev/Prod)
|
||||||
|
Das Update-Artefakt wird als tar.gz gebaut und ueber den Update-Service hochgeladen.
|
||||||
|
Das Script nutzt `VERSION` und laedt ein vollstaendiges Release (kein Delta).
|
||||||
|
|
||||||
|
Vorbereitung:
|
||||||
|
```bash
|
||||||
|
cp update-addon.env.example update-addon.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiel (Dev):
|
||||||
|
```bash
|
||||||
|
./scripts/upload_release.sh --profile dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests und Lint
|
||||||
|
Im Repo sind keine automatisierten Tests enthalten. Verfuegbare Checks:
|
||||||
|
- Bash-Syntax: `bash -n sk.sh`
|
||||||
|
- ShellCheck: `shellcheck sk.sh`
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
- Bash 4+, `set -euo pipefail` in neuen Skripten.
|
||||||
|
- Python: FastAPI-Patterns, klare Modultrennung (Auth, Actions, Update, OIDC).
|
||||||
|
|
||||||
|
## Beitrag und Workflow
|
||||||
|
- Arbeite mit Feature-Branches.
|
||||||
|
- Aktualisiere `VERSION`, Doku-Header und `CHANGELOG.md` gemaess SOP.
|
||||||
|
- PRs sollten Verhalten, Risiken und manuelle Tests beschreiben.
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
Aktuell keine CI/CD-Pipeline im Repository definiert.
|
||||||
|
|
||||||
|
## Legacy/Interna
|
||||||
|
- `SKD_UPDATE_URL` und `SKD_UPDATE_STATUS_URL` sind in `backend/settings.py` noch vorhanden, werden aber im aktuellen Code nicht genutzt.
|
||||||
|
|
||||||
|
## Externe Abhaengigkeiten (mit Quelle und Zweck)
|
||||||
|
### Python-Libraries (requirements.txt)
|
||||||
|
- FastAPI: https://fastapi.tiangolo.com/ (Web-API und Routing)
|
||||||
|
- Uvicorn: https://www.uvicorn.org/ (ASGI-Server)
|
||||||
|
- Pydantic: https://docs.pydantic.dev/ (Datenmodelle und Validierung)
|
||||||
|
- Jinja2: https://jinja.palletsprojects.com/ (HTML-Templates)
|
||||||
|
- PyJWT: https://pyjwt.readthedocs.io/ (JWT-Erstellung und -Validierung)
|
||||||
|
- python-pam: https://pypi.org/project/python-pam/ (PAM-Authentifizierung)
|
||||||
|
- httpx: https://www.python-httpx.org/ (HTTP-Client fuer Update/OIDC)
|
||||||
|
- cryptography: https://cryptography.io/ (Krypto-Abhaengigkeit fuer JWT)
|
||||||
|
- PyYAML (optional): https://pyyaml.org/ (YAML-Parsing in `scripts/deploy.sh`)
|
||||||
|
|
||||||
|
### System-Tools
|
||||||
|
- systemd: https://www.freedesktop.org/software/systemd/man/systemd.html (Service-Management)
|
||||||
|
- Linux-PAM: https://www.linux-pam.org/ (System-Authentifizierung)
|
||||||
|
- usermod/pkill/shutdown: https://man7.org/linux/man-pages/ (Account- und Session-Management)
|
||||||
|
- curl/tar/sha256sum/rsync/git/ssh: https://man7.org/linux/man-pages/ (Install/Update/Deploy)
|
||||||
|
- jq (optional): https://stedolan.github.io/jq/ (JSON-Parsing in Beispielen)
|
||||||
|
- notify-send: https://developer.gnome.org/libnotify/ (Desktop-Benachrichtigungen)
|
||||||
|
- paplay/aplay: https://www.freedesktop.org/wiki/Software/PulseAudio/ und https://alsa-project.org/ (Sound)
|
||||||
|
|
||||||
|
### Externe Services
|
||||||
|
- Update-Service (intern): https://git.wlkns.org/stephan/update-webservice (Manifest/Status/Enrollment)
|
||||||
|
- OIDC-Service (intern): https://git.wlkns.org/stephan/oicd (Login via OIDC)
|
||||||
|
|
||||||
|
## Weitere Dokumente
|
||||||
|
- Architektur: `docs/ARCHITECTURE.md`
|
||||||
|
- Deployment: `docs/DEPLOYMENT.md`
|
||||||
26
docs/FAQ.md
Normal file
26
docs/FAQ.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
ID: DOC_000015 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# FAQ
|
||||||
|
|
||||||
|
## Braucht der Dienst Root-Rechte?
|
||||||
|
Ja. PAM-Authentifizierung und Systemkommandos (usermod/pkill/shutdown) erfordern Root.
|
||||||
|
|
||||||
|
## Kann ich OIDC ohne PAM nutzen?
|
||||||
|
OIDC ist optional und zusaetzlich zu PAM. PAM bleibt als Fallback aktiv.
|
||||||
|
|
||||||
|
## Wie aendere ich den Port?
|
||||||
|
- Lokaler Run: `PORT=8000 ./scripts/run.sh`
|
||||||
|
- Systemd: Unit-Datei in `/etc/systemd/system/skd.service` anpassen und Service neu starten.
|
||||||
|
|
||||||
|
## Gibt es einen Docker-Container?
|
||||||
|
Nein, im Repository ist kein Docker-Setup enthalten.
|
||||||
|
|
||||||
|
## Wo liegen Logs?
|
||||||
|
- systemd: `journalctl -u skd.service`
|
||||||
|
- Update-Status/Logs: siehe `SKD_UPDATE_STATUS_FILE` und `SKD_UPDATE_LOG_FILE`.
|
||||||
|
|
||||||
|
## Weitere Dokumente
|
||||||
|
- Einstieg: `docs/GETTING_STARTED.md`
|
||||||
|
- Nutzung: `docs/USAGE.md`
|
||||||
|
- Troubleshooting: `docs/TROUBLESHOOTING.md`
|
||||||
37
docs/FOR_USERS.md
Normal file
37
docs/FOR_USERS.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
ID: DOC_000017 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Fuer Nutzerinnen und Nutzer
|
||||||
|
|
||||||
|
## Worum geht es?
|
||||||
|
Safe Kiddo Daemon hilft dabei, lokale Benutzerkonten auf einem Familien- oder Schulgeraet zu sperren und wieder freizugeben. Ziel ist, klare Nutzungszeiten durchzusetzen und sicherzustellen, dass nach einer Sperrung keine Sitzung offen bleibt.
|
||||||
|
|
||||||
|
## Welche Probleme loest es?
|
||||||
|
- Ein Konto soll zu bestimmten Zeiten nicht nutzbar sein.
|
||||||
|
- Offene Sitzungen sollen beendet werden, wenn ein Konto gesperrt wird.
|
||||||
|
- Eltern/Betreuende wollen den Zustand zentral sehen und verwalten.
|
||||||
|
|
||||||
|
## Typische Anwendungsfaelle
|
||||||
|
- Abendliche Nutzungszeit endet, der Account wird gesperrt.
|
||||||
|
- Bei Verstoessen gegen Regeln wird ein Konto kurzzeitig deaktiviert.
|
||||||
|
- Eine Sitzung bleibt offen und muss beendet werden.
|
||||||
|
|
||||||
|
## Wie wird es bedient?
|
||||||
|
Die Bedienung erfolgt ueber eine einfache Web-Oberflaeche im lokalen Netzwerk.
|
||||||
|
Dort kann eine berechtigte Person:
|
||||||
|
- Konten sperren oder freigeben.
|
||||||
|
- Den aktuellen Status sehen.
|
||||||
|
|
||||||
|
## Grenzen und Sicherheit
|
||||||
|
- Die Sperrung betrifft nur lokale Konten auf dem Geraet.
|
||||||
|
- Wenn kein berechtigter Zugang vorhanden ist, kann die Web-Oberflaeche nicht genutzt werden.
|
||||||
|
- Das System kann den Rechner im Bedarfsfall herunterfahren, um offene Sitzungen zu beenden.
|
||||||
|
|
||||||
|
## Was tun, wenn etwas schiefgeht?
|
||||||
|
- Wenn die Web-Oberflaeche nicht erreichbar ist, die betreuende Person informieren.
|
||||||
|
- Wenn das Konto unerwartet gesperrt wurde, nicht weiter experimentieren, sondern nachfragen.
|
||||||
|
- Bei wiederholten Problemen soll der Betreiber die technische Fehlerbehebung pruefen.
|
||||||
|
|
||||||
|
## Weitere Informationen (fuer Betreiber)
|
||||||
|
- Einstieg: `docs/GETTING_STARTED.md`
|
||||||
|
- Hilfe bei Problemen: `docs/TROUBLESHOOTING.md`
|
||||||
69
docs/GETTING_STARTED.md
Normal file
69
docs/GETTING_STARTED.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
ID: DOC_000009 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
Schneller Einstieg fuer neue Nutzer: Installation, erster Login und erste Aktion.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
- Linux-System mit systemd.
|
||||||
|
- Root-Zugriff (PAM, usermod, shutdown).
|
||||||
|
- Python 3, curl, tar, sha256sum (fuer Update-Client-Skripte).
|
||||||
|
- Optional: notify-send (Benachrichtigungen), paplay/aplay (Sound).
|
||||||
|
|
||||||
|
## Schnellstart
|
||||||
|
```bash
|
||||||
|
# 1) Repo installieren
|
||||||
|
sudo mkdir -p /opt/sk
|
||||||
|
sudo git clone ssh://git@git.wlkns.org:2222/stephan/kiddo /opt/sk
|
||||||
|
# Hinweis: verwende hier die Repo-URL deiner Instanz
|
||||||
|
cd /opt/sk
|
||||||
|
|
||||||
|
# 2) Installation (legt Service-User, env, systemd-Unit an)
|
||||||
|
make install
|
||||||
|
|
||||||
|
# 3) Service pruefen
|
||||||
|
sudo systemctl status skd.service
|
||||||
|
|
||||||
|
# 4) Login (PAM)
|
||||||
|
curl -s -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"root","password":"..."}' \
|
||||||
|
http://localhost/login
|
||||||
|
```
|
||||||
|
|
||||||
|
## Erster API-Test
|
||||||
|
```bash
|
||||||
|
token=$(curl -s -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"root","password":"example-password"}' \
|
||||||
|
http://localhost/login | jq -r .token)
|
||||||
|
|
||||||
|
curl -s -H "Authorization: Bearer $token" http://localhost/me
|
||||||
|
```
|
||||||
|
Hinweis: `jq` ist optional; ohne jq das Token manuell aus der JSON-Antwort lesen.
|
||||||
|
|
||||||
|
## Makefile-Kurzbefehle
|
||||||
|
```bash
|
||||||
|
make install # Installation (systemd, env, venv)
|
||||||
|
make up # Service starten
|
||||||
|
make down # Service stoppen
|
||||||
|
make restart # Service neu starten
|
||||||
|
make update # Code-Update (git reset --hard origin/main)
|
||||||
|
make healthcheck TOKEN=... # Healthcheck mit Bearer-Token
|
||||||
|
```
|
||||||
|
|
||||||
|
## Naechste Schritte
|
||||||
|
- Konfiguration anpassen: `docs/CONFIGURATION.md`
|
||||||
|
- API und Web-UI nutzen: `docs/USAGE.md`
|
||||||
|
- Deployment und Updates: `docs/DEPLOYMENT.md`
|
||||||
|
|
||||||
|
## Typische Einsteigerfehler
|
||||||
|
- Service startet, aber Port 80 ist bereits belegt (loese den Konflikt oder nutze einen anderen Port).
|
||||||
|
- Login scheitert, weil `SKD_AUTH_ALLOWED_USERS`/`SKD_AUTH_ALLOWED_GROUPS` den Nutzer nicht erlauben.
|
||||||
|
- OIDC wird erwartet, ist aber nicht aktiv (Issuer/Client-ID/Secret fehlen).
|
||||||
|
- Token wird nicht gesendet (fehlender `Authorization: Bearer` Header).
|
||||||
|
|
||||||
|
## Weitere Dokumente
|
||||||
|
- Endnutzer-Sicht: `docs/FOR_USERS.md`
|
||||||
|
- FAQ: `docs/FAQ.md`
|
||||||
|
- Troubleshooting: `docs/TROUBLESHOOTING.md`
|
||||||
42
docs/TROUBLESHOOTING.md
Normal file
42
docs/TROUBLESHOOTING.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
ID: DOC_000016 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Service startet nicht
|
||||||
|
- Status pruefen: `sudo systemctl status skd.service`
|
||||||
|
- Logs: `sudo journalctl -u skd.service -n 200 --no-pager`
|
||||||
|
- Port 80 belegt? Test: `sudo ss -ltnp | grep ':80'`
|
||||||
|
|
||||||
|
## 401/403 bei API-Aufrufen
|
||||||
|
- Bearer-Token fehlt oder abgelaufen.
|
||||||
|
- Nutzer nicht in `SKD_AUTH_ALLOWED_USERS` oder `SKD_AUTH_ALLOWED_GROUPS`.
|
||||||
|
- `SKD_AUTH_SECRET` geaendert? Tokens muessen neu erzeugt werden.
|
||||||
|
|
||||||
|
## OIDC-Login fehlschlaegt
|
||||||
|
- `SKD_OIDC_ISSUER`, `SKD_OIDC_CLIENT_ID`, `SKD_OIDC_CLIENT_SECRET` gesetzt?
|
||||||
|
- Redirect-URI exakt registriert?
|
||||||
|
- Netzwerkzugriff auf Discovery/JWKS moeglich?
|
||||||
|
|
||||||
|
## Benachrichtigung/Sound fehlt
|
||||||
|
- `notify-send` fehlt: `sudo apt install libnotify-bin`
|
||||||
|
- Sound-Player fehlt: `paplay` oder `aplay` installieren.
|
||||||
|
- `SKD_NOTIFY_SEND_PATH` oder `SKD_SOUND_PLAYER` falsch gesetzt.
|
||||||
|
|
||||||
|
## Update-Check meldet "Client is not enrolled"
|
||||||
|
- `SKD_UPDATE_TOKEN` oder `SKD_UPDATE_TOKEN_FILE` fehlt.
|
||||||
|
- Enrollment ueber `/update/enroll` oder `scripts/manual_enroll.py` durchfuehren.
|
||||||
|
|
||||||
|
## Update-Apply fehlschlaegt
|
||||||
|
- Update-Service nicht erreichbar oder Token ungueltig.
|
||||||
|
- Prüfe `SKD_UPDATE_STATUS_FILE` und `SKD_UPDATE_LOG_FILE`.
|
||||||
|
- Hinweis: `update_client.sh` wird asynchron gestartet und stdout/stderr werden verworfen.
|
||||||
|
|
||||||
|
## Rollback meldet "no backup found"
|
||||||
|
- Es existiert kein `/opt/sk_backup_*` vom vorherigen Update.
|
||||||
|
- Rollback erst nach mindestens einem erfolgreichen Update moeglich.
|
||||||
|
|
||||||
|
## Weitere Dokumente
|
||||||
|
- Einstieg: `docs/GETTING_STARTED.md`
|
||||||
|
- Konfiguration: `docs/CONFIGURATION.md`
|
||||||
|
- Deployment: `docs/DEPLOYMENT.md`
|
||||||
103
docs/USAGE.md
Normal file
103
docs/USAGE.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
ID: DOC_000010 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
- PAM-Login: `POST /login` mit Benutzername/Passwort. Liefert JWT und setzt Session-Cookie.
|
||||||
|
- OIDC-Login: `GET /login/oidc/start` startet Flow, Callback setzt Session-Cookie.
|
||||||
|
- Alle geschuetzten Endpunkte akzeptieren `Authorization: Bearer $TOKEN` oder Session-Cookie.
|
||||||
|
|
||||||
|
### Beispiel: Login und Token nutzen
|
||||||
|
```bash
|
||||||
|
token=$(curl -s -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"root","password":"example-password"}' \
|
||||||
|
http://localhost/login | jq -r .token)
|
||||||
|
|
||||||
|
curl -s -H "Authorization: Bearer $token" http://localhost/me
|
||||||
|
```
|
||||||
|
Hinweis: `jq` ist optional; ohne jq das Token manuell aus der JSON-Antwort lesen.
|
||||||
|
Login ist nur fuer erlaubte Nutzer moeglich (siehe `SKD_AUTH_ALLOWED_USERS` und `SKD_AUTH_ALLOWED_GROUPS`).
|
||||||
|
|
||||||
|
## Web-UI
|
||||||
|
- Aufruf: `http://localhost/`
|
||||||
|
- Login per PAM oder OIDC (wenn konfiguriert).
|
||||||
|
- Aktionen: Benutzer sperren/entsperren, Update-Status, Update-Check, Apply/Rollback.
|
||||||
|
|
||||||
|
## Automatisierung (Power-User)
|
||||||
|
Die API kann in Skripten oder Zeitplaenen genutzt werden, z.B. fuer regelmaessige Sperrungen.
|
||||||
|
|
||||||
|
Beispiel (cron, taeglich 21:00 sperren):
|
||||||
|
```bash
|
||||||
|
0 21 * * * curl -s -X POST -H "Authorization: Bearer $TOKEN" http://localhost/users/child1/disable
|
||||||
|
```
|
||||||
|
Hinweis: Token sicher speichern (z.B. Root-Only Datei) und regelmaessig rotieren.
|
||||||
|
|
||||||
|
## API-Endpunkte (Auszug)
|
||||||
|
### Health und Identitaet
|
||||||
|
- `GET /health` (ohne Auth)
|
||||||
|
- `GET /me`
|
||||||
|
|
||||||
|
### Benutzerverwaltung
|
||||||
|
- `GET /users`
|
||||||
|
- `POST /users/{username}/disable` mit JSON `{countdown?, sound?, message?}`
|
||||||
|
- `POST /users/{username}/enable`
|
||||||
|
|
||||||
|
Beispiel (disable):
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: Bearer $token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"countdown":90,"sound":true,"message":"Bitte speichern"}' \
|
||||||
|
http://localhost/users/child1/disable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update-API (lokal)
|
||||||
|
Alle Update-Endpunkte erfordern Admin-Auth.
|
||||||
|
- `GET /update/status`
|
||||||
|
- `GET /update/service-status` (zeigt URL, Erreichbarkeit und Dev/Prod-Umgebung)
|
||||||
|
- `POST /update/enroll` (optional Body: `{ "enroll_token": "..." }`)
|
||||||
|
- `POST /update/check`
|
||||||
|
- `POST /update/apply` (optional Body: `{ "version": "x.y.z" }`)
|
||||||
|
- `POST /update/rollback`
|
||||||
|
- `GET /update/logs?limit=200`
|
||||||
|
|
||||||
|
### System-API (lokal)
|
||||||
|
- `GET /system/metrics` (CPU %, RAM, GPU VRAM, Netzwerk Mbps)
|
||||||
|
|
||||||
|
Beispiel (Enrollment):
|
||||||
|
```bash
|
||||||
|
ENROLL_TOKEN="example-enroll-token"
|
||||||
|
curl -X POST -H "Authorization: Bearer $token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"enroll_token\":\"${ENROLL_TOKEN}\"}" \
|
||||||
|
http://localhost/update/enroll
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiel (Update-Check):
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: Bearer $token" http://localhost/update/check
|
||||||
|
```
|
||||||
|
Hinweis: Ohne gespeichertes Update-Token liefert der Check einen Fehler.
|
||||||
|
|
||||||
|
## Healthcheck per Makefile
|
||||||
|
```bash
|
||||||
|
make healthcheck TOKEN="$token"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Weitere Dokumente
|
||||||
|
- Konfiguration: `docs/CONFIGURATION.md`
|
||||||
|
- Deployment: `docs/DEPLOYMENT.md`
|
||||||
|
- Troubleshooting: `docs/TROUBLESHOOTING.md`
|
||||||
|
|
||||||
|
## CLI-Fallback (sk.sh)
|
||||||
|
Das Legacy-Script arbeitet direkt auf dem Host und benoetigt Root-Rechte.
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
```bash
|
||||||
|
sudo ./sk.sh USERNAME disable|enable [countdown] [sound] [countdown_time_in_seconds]
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
```bash
|
||||||
|
sudo ./sk.sh demo_user disable countdown sound 90
|
||||||
|
```
|
||||||
@ -1,4 +1,6 @@
|
|||||||
ID: DOC_000006 | Version: 0.1.0 | Status: Draft
|
ID: DOC_000006 | Version: 0.2.3 | Status: Draft
|
||||||
|
Archived – superseded by new documentation.
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Admin Token Operations
|
# Admin Token Operations
|
||||||
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
ID: DOC_000008 | Version: 0.1.0 | Status: Draft
|
ID: DOC_000008 | Version: 0.2.3 | Status: Draft
|
||||||
|
Archived – superseded by new documentation.
|
||||||
|
|
||||||
# Client Quickstart
|
# Client Quickstart
|
||||||
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
ID: DOC_000003 | Version: 0.1.0 | Status: Draft
|
ID: DOC_000003 | Version: 0.2.3 | Status: Draft
|
||||||
|
Archived – superseded by new documentation.
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# OIDC End-to-End Validation (Kiddo)
|
# OIDC End-to-End Validation (Kiddo)
|
||||||
@ -1,4 +1,6 @@
|
|||||||
ID: DOC_000005 | Version: 0.1.0 | Status: Draft
|
ID: DOC_000005 | Version: 0.2.3 | Status: Draft
|
||||||
|
Archived – superseded by new documentation.
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Third-Party API Guide
|
# Third-Party API Guide
|
||||||
|
|
||||||
@ -117,7 +119,7 @@ Common error codes:
|
|||||||
`payload_too_large`, `status_invalid`
|
`payload_too_large`, `status_invalid`
|
||||||
|
|
||||||
## Rate Limits
|
## Rate Limits
|
||||||
Limits are tiered by scope. See `docs/architecture/ARCHITECTURE.md` for current values.
|
Limits are tiered by scope. See `docs/architecture/openapi/paths/limits.yaml` for current values.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
Fetch manifest:
|
Fetch manifest:
|
||||||
@ -1,4 +1,5 @@
|
|||||||
ID: DOC_000006 | Version: 0.1.0 | Status: Draft
|
ID: DOC_000006 | Version: 0.2.3 | Status: Draft
|
||||||
|
Archived – superseded by new documentation.
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Update API (Kiddo Backend)
|
# Update API (Kiddo Backend)
|
||||||
@ -8,6 +9,16 @@ Definiert interne API-Endpunkte fuer Update-Status, Check, Apply, Rollback und L
|
|||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
|
### POST /update/enroll
|
||||||
|
Body (optional):
|
||||||
|
```json
|
||||||
|
{ "enroll_token": "<one-time-token>" }
|
||||||
|
```
|
||||||
|
Antwort:
|
||||||
|
```json
|
||||||
|
{ "enrolled": true, "message": "Enrollment successful" }
|
||||||
|
```
|
||||||
|
|
||||||
### GET /update/status
|
### GET /update/status
|
||||||
Antwort:
|
Antwort:
|
||||||
```json
|
```json
|
||||||
@ -15,7 +26,8 @@ Antwort:
|
|||||||
"current_version": "0.1.2",
|
"current_version": "0.1.2",
|
||||||
"last_status": "success|failed|unknown",
|
"last_status": "success|failed|unknown",
|
||||||
"last_error": "<optional>",
|
"last_error": "<optional>",
|
||||||
"last_timestamp": "2025-12-28T12:34:56Z"
|
"last_timestamp": "2025-12-28T12:34:56Z",
|
||||||
|
"enrolled": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -51,10 +63,18 @@ Antwort:
|
|||||||
Antwort:
|
Antwort:
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{"timestamp":"2025-12-28T12:34:56Z","status":"success","message":"updated to 0.1.2"}
|
{
|
||||||
|
"timestamp": "2025-12-28T12:34:56Z",
|
||||||
|
"status": "success",
|
||||||
|
"message": "updated to 0.1.2",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"device_id": "kiddo-001",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Alle Endpunkte erfordern Auth (Session/Bearer).
|
- Alle Endpunkte erfordern Auth (Session-Cookie oder `Authorization: Bearer <token>`).
|
||||||
- Apply/Rollback starten async; UI pollt /update/status.
|
- Apply/Rollback starten async; UI pollt /update/status.
|
||||||
|
- `/update/enroll` speichert das Update-Token lokal (siehe `SKD_UPDATE_TOKEN_FILE`).
|
||||||
@ -1,4 +1,5 @@
|
|||||||
ID: DOC_000004 | Version: 0.1.0 | Status: Draft
|
ID: DOC_000004 | Version: 0.2.3 | Status: Draft
|
||||||
|
Archived – superseded by new documentation.
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Client Update Flow (Kiddo)
|
# Client Update Flow (Kiddo)
|
||||||
@ -18,7 +19,8 @@ Beispiel:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Flow (High Level)
|
## Flow (High Level)
|
||||||
1. Manifest abrufen (auth optional via Bearer Token).
|
0. Falls kein Token vorhanden ist: Enrollment durchfuehren (Pre-Shared Token -> Langzeit-Token).
|
||||||
|
1. Manifest abrufen (auth via Bearer Token).
|
||||||
2. `artifact_url` herunterladen.
|
2. `artifact_url` herunterladen.
|
||||||
3. SHA256 pruefen (Signatur optional).
|
3. SHA256 pruefen (Signatur optional).
|
||||||
4. In Staging-Verzeichnis entpacken.
|
4. In Staging-Verzeichnis entpacken.
|
||||||
@ -37,13 +39,14 @@ Beispiel:
|
|||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
- Artefakte muessen checksum-verifiziert sein.
|
- Artefakte muessen checksum-verifiziert sein.
|
||||||
- Token-Handling ueber `SKD_UPDATE_TOKEN`.
|
- Token-Handling ueber `SKD_UPDATE_TOKEN` oder `SKD_UPDATE_TOKEN_FILE`.
|
||||||
|
- Enrollment nutzt einen Pre-Shared Token und speichert das Langzeit-Token lokal.
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
- Update-Service ist extern (update.wlkns.org).
|
- Update-Service ist extern (update.wlkns.org).
|
||||||
- Service muss als root stoppen/starten koennen.
|
- Service muss als root stoppen/starten koennen.
|
||||||
|
|
||||||
## Status Reporting
|
## Status Reporting
|
||||||
- Status wird per HTTP POST an `https://update.wlkns.org/status` gemeldet.
|
- Status wird per HTTP POST an `${SKD_UPDATE_SERVICE_URL}/v1/projects/${SKD_UPDATE_PROJECT_ID}/status` gemeldet.
|
||||||
- Schema siehe `docs/update-status.md`.
|
- Schema siehe `docs/update-status.md`.
|
||||||
- Lokaler Status/Logs liegen unter `/var/lib/skd` (konfigurierbar via ENV).
|
- Lokaler Status/Logs liegen unter `/var/lib/skd` (konfigurierbar via ENV).
|
||||||
30
docs/_archive/update-status.md
Normal file
30
docs/_archive/update-status.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
ID: DOC_000005 | Version: 0.2.3 | Status: Draft
|
||||||
|
Archived – superseded by new documentation.
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# Update Status Reporting
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Definiert das Status-Schema fuer Update-Resultate und den Uebertragungsweg.
|
||||||
|
|
||||||
|
## Status Schema (JSON)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"project_id": "safe-kiddo-control",
|
||||||
|
"client_id": "<hostname>",
|
||||||
|
"version": "0.2.1",
|
||||||
|
"status": "success|failed|in_progress",
|
||||||
|
"timestamp": "2025-12-28T12:34:56Z",
|
||||||
|
"duration_ms": 1234,
|
||||||
|
"error_code": "<optional>",
|
||||||
|
"reason": "<optional>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transport
|
||||||
|
- HTTP POST an `${SKD_UPDATE_SERVICE_URL}/v1/projects/${SKD_UPDATE_PROJECT_ID}/status`
|
||||||
|
- Auth: Bearer Token (`SKD_UPDATE_TOKEN` oder `SKD_UPDATE_TOKEN_FILE`)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Statusmeldungen sind best-effort; Fehler beim Senden blockieren kein Update.
|
||||||
|
- Reporting wird nur gesendet, wenn ein Update-Token vorhanden ist.
|
||||||
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: Update Webservice API
|
title: Update Webservice API
|
||||||
version: 0.1.0
|
version: 0.2.1
|
||||||
servers:
|
servers:
|
||||||
- url: https://update.wlkns.org
|
- url: https://update.wlkns.org
|
||||||
- url: https://staging.update.wlkns.org
|
- url: https://staging.update.wlkns.org
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
ID: DOC_000005 | Version: 0.1.0 | Status: Draft
|
|
||||||
By: Codex (GPT-5)
|
|
||||||
|
|
||||||
# Update Status Reporting
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
Definiert das Status-Schema fuer Update-Resultate und den Uebertragungsweg.
|
|
||||||
|
|
||||||
## Status Schema (JSON)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"device_id": "<hostname>",
|
|
||||||
"version": "0.1.2",
|
|
||||||
"status": "success|failed",
|
|
||||||
"error": "<optional message>",
|
|
||||||
"timestamp": "2025-12-28T12:34:56Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Transport
|
|
||||||
- HTTP POST an `https://update.wlkns.org/status`
|
|
||||||
- Auth: Bearer Token (`SKD_UPDATE_TOKEN`)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Statusmeldungen sind best-effort; Fehler beim Senden blockieren kein Update.
|
|
||||||
@ -21,12 +21,5 @@ SKD_OIDC_STATE_COOKIE_NAME=skd_oidc_state
|
|||||||
SKD_DEFAULT_COUNTDOWN=60
|
SKD_DEFAULT_COUNTDOWN=60
|
||||||
SKD_DEFAULT_SOUND=false
|
SKD_DEFAULT_SOUND=false
|
||||||
SKD_NOTIFY_TIMEOUT=5
|
SKD_NOTIFY_TIMEOUT=5
|
||||||
# Update client configuration
|
|
||||||
SKD_UPDATE_URL=https://update.wlkns.org
|
|
||||||
SKD_UPDATE_TOKEN=
|
|
||||||
SKD_UPDATE_INTERVAL=3600
|
|
||||||
SKD_UPDATE_STATUS_URL=https://update.wlkns.org/status
|
|
||||||
SKD_UPDATE_STATUS_FILE=/var/lib/skd/update_status.json
|
|
||||||
SKD_UPDATE_LOG_FILE=/var/lib/skd/update_logs.jsonl
|
|
||||||
# Set to true to test without performing real system changes
|
# Set to true to test without performing real system changes
|
||||||
SKD_DRY_RUN=false
|
SKD_DRY_RUN=false
|
||||||
|
|||||||
11
env.update.example
Normal file
11
env.update.example
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Copy to /etc/skd/update.env for update-service configuration
|
||||||
|
SKD_UPDATE_SERVICE_URL=https://update.wlkns.org
|
||||||
|
SKD_UPDATE_PROJECT_ID=safe-kiddo-control
|
||||||
|
SKD_UPDATE_ENROLL_TOKEN=
|
||||||
|
SKD_UPDATE_TOKEN=
|
||||||
|
SKD_UPDATE_TOKEN_FILE=/var/lib/skd/update_token
|
||||||
|
SKD_UPDATE_INTERVAL=3600
|
||||||
|
SKD_UPDATE_STATUS_FILE=/var/lib/skd/update_status.json
|
||||||
|
SKD_UPDATE_LOG_FILE=/var/lib/skd/update_logs.jsonl
|
||||||
|
SKD_UPDATE_UPLOAD_TOKEN=
|
||||||
|
SKD_UPDATE_UPLOAD_TOKEN_FILE=/etc/skd/update.upload.token
|
||||||
@ -1,4 +1,4 @@
|
|||||||
ID: AGENTS_000001 | Version: 0.1.0 | Status: Draft
|
ID: AGENTS_000001 | Version: 0.2.3 | Status: Draft
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Repository Guidelines
|
# Repository Guidelines
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: STATUS_000001 | Version: 0.1.0 | Status: Final
|
ID: STATUS_000001 | Version: 0.3.6 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Projekt-Status
|
# Projekt-Status
|
||||||
@ -10,14 +10,21 @@ Sicheres, remote steuerbares System zum Sperren/Entsperren lokaler Nutzerkonten.
|
|||||||
✅ Stabilization
|
✅ Stabilization
|
||||||
|
|
||||||
## Aktueller Fokus
|
## Aktueller Fokus
|
||||||
1. Client-Update-Mechanik planen (EPIC_000008).
|
1. OIDC-Validierung abschliessen (EPIC_000003 / US_000025).
|
||||||
2. Dokumentierter Ist-Zustand der Module.
|
2. Client-Update-Mechanik pflegen (EPIC_000008).
|
||||||
3. Pflege der Anforderungen bei neuen Features.
|
3. Anforderungen bei neuen Features sauber dokumentieren.
|
||||||
|
|
||||||
## Projekt-Tagebuch (Kurz, optional)
|
## Projekt-Tagebuch (Kurz, optional)
|
||||||
| Datum | Typ | Beschreibung |
|
| Datum | Typ | Beschreibung |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 28.12.2025 | 🏗️ Planning | Anforderungen als Epics und Stories dokumentiert. |
|
| 28.12.2025 | 🏗️ Planning | Anforderungen als Epics und Stories dokumentiert. |
|
||||||
|
| 15.01.2026 | 📝 Req | Doku und ENV-Beispiele an Code-Stand angepasst. |
|
||||||
|
| 15.01.2026 | 📝 Req | Doku-Overhaul mit neuer Struktur und Archivierung. |
|
||||||
|
| 15.01.2026 | 📝 Req | Externe Service-Links in der Doku ergaenzt. |
|
||||||
|
| 15.01.2026 | 📝 Req | Makefile als Einstieg in Doku aufgenommen. |
|
||||||
|
| 15.01.2026 | ⚙️ Code | Makefile restart-Target hinzugefuegt. |
|
||||||
|
| 16.01.2026 | ✨ Feat | Login-Zeitfenster (Rules & Scheduler) implementiert (US_000045). |
|
||||||
|
| 16.01.2026 | 🐞 Fix | Update-Prozess entkoppelt (systemd-run), damit er beim Service-Stop weiterlaeuft. |
|
||||||
|
|
||||||
## Epic-Backlog (Uebersicht)
|
## Epic-Backlog (Uebersicht)
|
||||||
### EPIC_000001: Legacy CLI Account Control (sk.sh)
|
### EPIC_000001: Legacy CLI Account Control (sk.sh)
|
||||||
@ -102,11 +109,21 @@ Sicheres, remote steuerbares System zum Sperren/Entsperren lokaler Nutzerkonten.
|
|||||||
- [x] TASK_000033: UI update logs view
|
- [x] TASK_000033: UI update logs view
|
||||||
- [x] US_000033: Rollback im Web-UI anstossen
|
- [x] US_000033: Rollback im Web-UI anstossen
|
||||||
- [x] TASK_000034: UI rollback action
|
- [x] TASK_000034: UI rollback action
|
||||||
|
- [x] US_000043: Enrollment-Token per Script abrufen
|
||||||
|
- [x] TASK_000049: Script fuer Enrollment-Flow erstellen
|
||||||
|
- [x] US_000046: Update-Service Erreichbarkeit anzeigen
|
||||||
|
- [x] TASK_000055: Endpoint und UI fuer Update-Service Status
|
||||||
|
- [x] US_000047: Update-Config auslagern
|
||||||
|
- [x] TASK_000056: Update-ENV separieren
|
||||||
|
- [x] US_000048: Release-Upload automatisieren
|
||||||
|
- [x] TASK_000057: Script fuer Release-Upload erstellen
|
||||||
|
- [x] US_000049: Lokale deployment.env fuer Update-Uploads
|
||||||
|
- [x] TASK_000058: deployment.env Beispiel und Script-Anpassungen
|
||||||
|
|
||||||
### EPIC_000009: Update Webservice (External Team)
|
### EPIC_000009: Update Webservice (External Team)
|
||||||
- [ ] US_000026: Client bezieht Updates (Pull)
|
- [x] US_000026: Client bezieht Updates (Pull)
|
||||||
- [ ] US_000027: Client verifiziert und wendet Updates an
|
- [x] US_000027: Client verifiziert und wendet Updates an
|
||||||
- [ ] US_000028: Client meldet Update-Status
|
- [x] US_000028: Client meldet Update-Status
|
||||||
|
|
||||||
### EPIC_000010: Update-Service v1 Migration (Major Release)
|
### EPIC_000010: Update-Service v1 Migration (Major Release)
|
||||||
- [x] US_000034: Enrollment fuer Langzeit-Token
|
- [x] US_000034: Enrollment fuer Langzeit-Token
|
||||||
@ -114,6 +131,40 @@ Sicheres, remote steuerbares System zum Sperren/Entsperren lokaler Nutzerkonten.
|
|||||||
- [x] US_000035: v1 Update-Endpoints und Status-Schema
|
- [x] US_000035: v1 Update-Endpoints und Status-Schema
|
||||||
- [x] TASK_000041: v1 Endpunkte im Update-Client umstellen
|
- [x] TASK_000041: v1 Endpunkte im Update-Client umstellen
|
||||||
|
|
||||||
|
### EPIC_000011: Documentation and Configuration Alignment
|
||||||
|
- [x] US_000036: Doku-Versionen auf VERSION synchronisieren
|
||||||
|
- [x] US_000037: Update-API-Doku mit /update/enroll abgleichen
|
||||||
|
- [x] US_000038: ENV-Beispiele und Healthcheck-Auth angleichen
|
||||||
|
- [x] US_000039: Doku-Audit fuer verbleibende Abweichungen
|
||||||
|
- [x] TASK_000042: Doku-Audit verbleibender Dateien
|
||||||
|
|
||||||
|
### EPIC_000012: Dokumentations-Overhaul
|
||||||
|
- [x] US_000040: Doku-Struktur und Inhalte erstellen
|
||||||
|
- [x] TASK_000043: Neue Doku-Dateien erstellen und verlinken
|
||||||
|
- [x] TASK_000044: Altdoku archivieren
|
||||||
|
- [x] US_000041: Einbindung externer Services dokumentieren
|
||||||
|
- [x] TASK_000045: Einbindung externer Services dokumentieren
|
||||||
|
- [x] US_000042: Consumer-Perspektive und Audience-Split ergaenzen
|
||||||
|
- [x] TASK_000046: Consumer-Doku erstellen (FOR_USERS.md)
|
||||||
|
- [x] TASK_000047: README und Audience-Split anpassen
|
||||||
|
- [x] TASK_000048: External Dependencies und Audience-Ergaenzungen
|
||||||
|
|
||||||
|
### EPIC_000013: System Telemetry im Dashboard
|
||||||
|
- [x] US_000044: Systemmetriken im Dashboard anzeigen
|
||||||
|
- [x] TASK_000050: Backend-Endpoint fuer Systemmetriken
|
||||||
|
- [x] TASK_000051: UI-Kacheln im Dashboard (System Information)
|
||||||
|
|
||||||
|
### EPIC_000014: Login-Zeitfenster und Parent-Control Regeln
|
||||||
|
- [x] US_000045: Regeln fuer Login-Zeitfenster definieren und durchsetzen
|
||||||
|
- [x] TASK_000052: Regelmodell und Speicherung definieren
|
||||||
|
- [x] TASK_000053: Login-Pruefung und Enforcement
|
||||||
|
- [x] TASK_000054: Admin-UI fuer Regeln und Scheduler
|
||||||
|
|
||||||
|
## Fehler / Bugs (History)
|
||||||
|
- [x] BUG_000001: Update-Apply scheitert an WorkingDirectory
|
||||||
|
- [x] BUG_000002: Update-Prozess wird beim Service-Stop gekillt
|
||||||
|
- [x] BUG_000003: Veralteter Update-Token durch Caching
|
||||||
|
|
||||||
## Offene Risiken / Abhaengigkeiten
|
## Offene Risiken / Abhaengigkeiten
|
||||||
- Betrieb erfordert Root/sudo und lokale System-Tools (notify-send, sound player, uvicorn).
|
- Betrieb erfordert Root/sudo und lokale System-Tools (notify-send, sound player, uvicorn).
|
||||||
- OIDC-Validierung blockiert bis IdP bereit und Service laeuft.
|
- OIDC-Validierung blockiert bis IdP bereit und Service laeuft.
|
||||||
@ -122,4 +173,4 @@ Sicheres, remote steuerbares System zum Sperren/Entsperren lokaler Nutzerkonten.
|
|||||||
## Naechste Schritte
|
## Naechste Schritte
|
||||||
- Anforderungen beim naechsten Feature-Start erweitern.
|
- Anforderungen beim naechsten Feature-Start erweitern.
|
||||||
- OIDC-Validierung abschliessen und US_000025 auf Done setzen.
|
- OIDC-Validierung abschliessen und US_000025 auf Done setzen.
|
||||||
- PR vorbereiten: feature/oidc-validation (Summary, Risiko, Testschritte).
|
- Doku-Audit fuer weitere Abweichungen priorisieren (wenn Zeitfenster frei).
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: PROJECT_STATUS_TEMPLATE | Version: 0.1.0 | Status: Draft
|
ID: PROJECT_STATUS_TEMPLATE | Version: 0.2.3 | Status: Draft
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# 📊 Projekt-Status (Template)
|
# 📊 Projekt-Status (Template)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: SOP_000001 | Version: 0.1.0 | Status: Draft
|
ID: SOP_000001 | Version: 0.2.3 | Status: Draft
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Onboarding: Arbeitsweise im Sound Architect Projekt
|
# Onboarding: Arbeitsweise im Sound Architect Projekt
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: SETUP_000005 | Version: 0.1.0 | Status: Draft
|
ID: SETUP_000005 | Version: 0.2.3 | Status: Draft
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# 🧬 SETUP_GUIDE: Phase 0 - Project Genesis
|
# 🧬 SETUP_GUIDE: Phase 0 - Project Genesis
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: PROMPT_000008 | Version: 0.1.0 | Status: Draft
|
ID: PROMPT_000008 | Version: 0.2.3 | Status: Draft
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Requirements Engineer Prompt
|
# Requirements Engineer Prompt
|
||||||
|
|||||||
26
project-management/requirements/bugs/BUG_000001.md
Normal file
26
project-management/requirements/bugs/BUG_000001.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
ID: BUG_000001 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# BUG_000001: Update scheitert wegen WorkingDirectory /opt/sk
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
Beim Update-Apply bricht `scripts/update_client.sh` ab, wenn `/opt/sk` verschoben wird,
|
||||||
|
weil der Prozess selbst im Verzeichnis arbeitet (WorkingDirectory=/opt/sk).
|
||||||
|
Das fuehrt zu einem Abbruch ohne Abschlussstatus (status bleibt `in_progress`).
|
||||||
|
|
||||||
|
## Schritte zur Reproduktion
|
||||||
|
1. `skd.service` laeuft mit `WorkingDirectory=/opt/sk`.
|
||||||
|
2. Update ueber UI oder `POST /update/apply` starten.
|
||||||
|
3. Script stoppt Service und versucht `mv /opt/sk ...`.
|
||||||
|
4. Fehler wegen busy CWD, Script bricht ab, Service bleibt gestoppt.
|
||||||
|
|
||||||
|
## Erwartetes Verhalten
|
||||||
|
Update-Prozess laeuft aus einem neutralen CWD (z.B. `/`), und Fehler werden als `failed`
|
||||||
|
in Status/Logs vermerkt.
|
||||||
|
|
||||||
|
## Ist-Verhalten
|
||||||
|
Update bleibt im Status `in_progress`, Service bleibt gestoppt.
|
||||||
|
|
||||||
|
## Fix-Idee
|
||||||
|
- `subprocess.Popen(..., cwd="/")` fuer Update-Client.
|
||||||
|
- Fehlertrap in `scripts/update_client.sh` fuer `failed` Status.
|
||||||
16
project-management/requirements/bugs/BUG_000002.md
Normal file
16
project-management/requirements/bugs/BUG_000002.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
ID: BUG_000002 | Status: Fixed | Severity: High
|
||||||
|
By: Gemini CLI
|
||||||
|
|
||||||
|
# BUG_000002: Update-Prozess wird beim Stoppen des Services beendet
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
Der Update-Prozess (`scripts/update_client.sh`) wird vom Backend-Service gestartet. Da er in derselben Systemd-CGroup wie der `skd.service` läuft, beendet Systemd den Updater sofort, wenn der Service für das Update gestoppt wird.
|
||||||
|
|
||||||
|
## Ursache
|
||||||
|
Prozesse, die direkt via `subprocess.Popen` aus einer Systemd-Unit gestartet werden, gehören zur selben Unit und werden beim Stoppen mit beendet (SIGTERM/SIGKILL).
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
Umstellung des Start-Mechanismus in `backend/update.py` auf `systemd-run --unit=skd-update --collect`. Dies entkoppelt den Prozess in eine eigene transiente Unit.
|
||||||
|
|
||||||
|
## Verifizierung
|
||||||
|
- Update von v0.3.1 auf v0.3.2 verlief erfolgreich, ohne dass der Prozess abgewürgt wurde.
|
||||||
16
project-management/requirements/bugs/BUG_000003.md
Normal file
16
project-management/requirements/bugs/BUG_000003.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
ID: BUG_000003 | Status: Fixed | Severity: Medium
|
||||||
|
By: Gemini CLI
|
||||||
|
|
||||||
|
# BUG_000003: Veralteter Update-Token durch Caching in Settings
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
Wenn ein Gerät registriert (Enrollment) wird, wird das Token in einer Datei gespeichert. Die `Settings`-Klasse lädt das Token jedoch nur einmal beim Start des Services. Nachfolgende Update-Versuche nutzen ein leeres oder veraltetes Token aus dem Cache, was zu 401/403 Fehlern beim Manifest-Download führt.
|
||||||
|
|
||||||
|
## Ursache
|
||||||
|
Die `Settings`-Instanz wird via `@lru_cache` in `backend/settings.py` gehalten und nicht aktualisiert, wenn sich Dateien auf der Festplatte ändern.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
Einführung der Hilfsfunktion `_get_fresh_token(settings)` in `backend/update.py`, die das Token bei jedem kritischen Vorgang (Check, Report, Update) frisch von der Festplatte oder aus der Environment lädt.
|
||||||
|
|
||||||
|
## Verifizierung
|
||||||
|
- Update auf v0.3.3 nutzt nun erfolgreich das Token, auch wenn der Service seit dem Enrollment nicht neu gestartet wurde.
|
||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000001 | Version: 0.1.0 | Status: Final
|
ID: EPIC_000001 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000001: Legacy CLI Account Control (sk.sh)
|
# EPIC_000001: Legacy CLI Account Control (sk.sh)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000002 | Version: 0.1.0 | Status: Final
|
ID: EPIC_000002 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000002: Backend API Service
|
# EPIC_000002: Backend API Service
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000003 | Version: 0.1.0 | Status: Final
|
ID: EPIC_000003 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000003: Authentication & Sessions
|
# EPIC_000003: Authentication & Sessions
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000004 | Version: 0.1.0 | Status: Final
|
ID: EPIC_000004 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000004: Web UI
|
# EPIC_000004: Web UI
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000005 | Version: 0.1.0 | Status: Final
|
ID: EPIC_000005 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000005: Automation Scripts
|
# EPIC_000005: Automation Scripts
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000006 | Version: 0.1.0 | Status: Final
|
ID: EPIC_000006 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000006: Systemd & Deployment Artifacts
|
# EPIC_000006: Systemd & Deployment Artifacts
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000007 | Version: 0.1.0 | Status: Final
|
ID: EPIC_000007 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000007: Documentation & Runbook
|
# EPIC_000007: Documentation & Runbook
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000008 | Version: 0.1.0 | Status: Draft
|
ID: EPIC_000008 | Version: 0.2.3 | Status: Draft
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000008: Client-Side Update Mechanism
|
# EPIC_000008: Client-Side Update Mechanism
|
||||||
@ -44,3 +44,7 @@ Ermoegliche einen robusten Client-Update-Flow mit Verifikation und Rollback.
|
|||||||
- US_000031: Update im Web-UI anstossen
|
- US_000031: Update im Web-UI anstossen
|
||||||
- US_000032: Update-Logs im Web-UI anzeigen
|
- US_000032: Update-Logs im Web-UI anzeigen
|
||||||
- US_000033: Rollback im Web-UI anstossen
|
- US_000033: Rollback im Web-UI anstossen
|
||||||
|
- US_000043: Enrollment-Token per Script abrufen
|
||||||
|
- US_000046: Update-Service Erreichbarkeit anzeigen
|
||||||
|
- US_000048: Release-Upload automatisieren
|
||||||
|
- US_000049: Lokale deployment.env fuer Update-Uploads
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000009 | Version: 0.1.0 | Status: Draft
|
ID: EPIC_000009 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000009: Update Webservice (External Team)
|
# EPIC_000009: Update Webservice (External Team)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: EPIC_000010 | Version: 0.1.5 | Status: Done
|
ID: EPIC_000010 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# EPIC_000010: Update-Service v1 Migration (Major Release)
|
# EPIC_000010: Update-Service v1 Migration (Major Release)
|
||||||
|
|||||||
43
project-management/requirements/epics/EPIC_000011.md
Normal file
43
project-management/requirements/epics/EPIC_000011.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
ID: EPIC_000011 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# EPIC_000011: Documentation and Configuration Alignment
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
Konsolidierung der Dokumentation, Beispiel-Konfigurationen und Ops-Hinweise mit dem
|
||||||
|
aktuellen Code-Stand, inklusive Version-Synchronisierung mit der zentralen VERSION.
|
||||||
|
|
||||||
|
## Ziel / Business Value
|
||||||
|
Reduziert Integrationsfehler, sorgt fuer konsistente Bedienung und verringert Support-Aufwand.
|
||||||
|
|
||||||
|
## Mission Statement
|
||||||
|
Stelle sicher, dass Dokumente, Beispiele und Automationshinweise den realen API- und
|
||||||
|
Konfigurationsstand widerspiegeln.
|
||||||
|
|
||||||
|
## Business Value & Metriken
|
||||||
|
- Weniger Fehlkonfigurationen durch korrekte ENV-Keys und Auth-Header.
|
||||||
|
- Erfolgsmetrik: 0 bekannte Abweichungen zwischen Code und Doku in den Update/Health-Flows.
|
||||||
|
|
||||||
|
## In-Scope (Kiddo Team)
|
||||||
|
- Doku-Versionen auf VERSION ziehen.
|
||||||
|
- Update-API Doku inkl. /update/enroll und Response-Feldern aktualisieren.
|
||||||
|
- ENV-Beispiele und Makefile-Healthcheck auf aktuelle Auth-Mechanik angleichen.
|
||||||
|
|
||||||
|
## Out-of-Scope
|
||||||
|
- Funktionale Aenderungen am Auth-Flow im Backend.
|
||||||
|
- Erweiterte Validierung durch Integrationstests.
|
||||||
|
|
||||||
|
## High-Level Akzeptanzkriterien
|
||||||
|
- Doku-Header nutzen die aktuelle VERSION.
|
||||||
|
- Update-API-Doku listet alle implementierten Update-Endpunkte.
|
||||||
|
- ENV-Beispiele nutzen die im Code verwendeten Update-Variablen.
|
||||||
|
- Healthcheck-Beispiel nutzt Authorization: Bearer.
|
||||||
|
|
||||||
|
## Technische Constraints & Risiken
|
||||||
|
- Dokumentation darf keine falschen Defaults oder veraltete Endpunkte nennen.
|
||||||
|
- Audit kann weitere Abweichungen aufdecken und Folgetickets noetig machen.
|
||||||
|
|
||||||
|
## Zugeordnete User Stories
|
||||||
|
- US_000036: Doku-Versionen auf VERSION synchronisieren
|
||||||
|
- US_000037: Update-API-Doku mit /update/enroll und Response-Feldern abgleichen
|
||||||
|
- US_000038: ENV-Beispiele und Makefile-Healthcheck an Auth/Update-Config angleichen
|
||||||
44
project-management/requirements/epics/EPIC_000012.md
Normal file
44
project-management/requirements/epics/EPIC_000012.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
ID: EPIC_000012 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# EPIC_000012: Dokumentations-Overhaul
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
Komplette, konsistente Dokumentation fuer Einsteiger, Admins und Entwickler, inkl. Archivierung
|
||||||
|
veralteter Inhalte und einer klaren Doku-Struktur.
|
||||||
|
|
||||||
|
## Ziel / Business Value
|
||||||
|
Schnelleres Onboarding, weniger Fehlkonfigurationen und klare Betriebsvorgaenge.
|
||||||
|
|
||||||
|
## Mission Statement
|
||||||
|
Stelle eine professionelle, konsistente und vollstaendige Doku bereit, die sich direkt am
|
||||||
|
Code und den Skripten orientiert.
|
||||||
|
|
||||||
|
## Business Value & Metriken
|
||||||
|
- Onboarding ohne Rueckfragen fuer neue Entwickler.
|
||||||
|
- Betriebssicherheit durch klare Runbooks.
|
||||||
|
- Erfolgsmetrik: 0 offene Doku-Abweichungen im Audit.
|
||||||
|
|
||||||
|
## In-Scope (Kiddo Team)
|
||||||
|
- Neue Doku-Struktur und konsistente Inhalte.
|
||||||
|
- README auf Einsteigerpfad + Links auf neue Dokus.
|
||||||
|
- Altdokumente archivieren, nicht loeschen.
|
||||||
|
|
||||||
|
## Out-of-Scope
|
||||||
|
- Funktionale Code-Aenderungen (nur Doku).
|
||||||
|
- Neue Automatisierungen/CI-Pipelines.
|
||||||
|
|
||||||
|
## High-Level Akzeptanzkriterien
|
||||||
|
- README erfuellt definierte Anforderungen.
|
||||||
|
- Doku-Struktur gem. Ziel (GETTING_STARTED/USAGE/CONFIGURATION/ARCHITECTURE/DEVELOPMENT/DEPLOYMENT/FAQ/TROUBLESHOOTING).
|
||||||
|
- Altdoku in docs/_archive/ mit Archiv-Header.
|
||||||
|
- Interne Links funktionieren und keine inhaltlichen Widersprueche.
|
||||||
|
|
||||||
|
## Technische Constraints & Risiken
|
||||||
|
- Keine spekulativen Inhalte; nur dokumentieren, was im Repo belegt ist.
|
||||||
|
- Sprachvorgabe: Deutsch, ASCII wo moeglich.
|
||||||
|
|
||||||
|
## Zugeordnete User Stories
|
||||||
|
- US_000040: Doku-Struktur und Inhalte erstellen
|
||||||
|
- US_000041: Einbindung externer Services dokumentieren
|
||||||
|
- US_000042: Consumer-Perspektive und Audience-Split ergaenzen
|
||||||
38
project-management/requirements/epics/EPIC_000013.md
Normal file
38
project-management/requirements/epics/EPIC_000013.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
ID: EPIC_000013 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# EPIC_000013: System Telemetry im Dashboard
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
Anzeige von Systemmetriken (CPU, RAM, GPU, Netzwerk) fuer Operatoren im Web-Dashboard.
|
||||||
|
|
||||||
|
## Ziel / Business Value
|
||||||
|
Schneller Ueberblick ueber Systemzustand und Last ohne externe Tools.
|
||||||
|
|
||||||
|
## Mission Statement
|
||||||
|
Stelle aktuelle Telemetriedaten im Dashboard bereit, aktualisiert in kurzen Intervallen.
|
||||||
|
|
||||||
|
## Business Value & Metriken
|
||||||
|
- Operatoren erkennen Engpaesse sofort.
|
||||||
|
- Erfolgsmetrik: Metriken aktualisieren sich alle 5 Sekunden im UI.
|
||||||
|
|
||||||
|
## In-Scope
|
||||||
|
- API liefert aktuelle CPU/RAM/GPU/Netzwerkwerte.
|
||||||
|
- Web-UI zeigt Kacheln in einem "System Information" Container.
|
||||||
|
|
||||||
|
## Out-of-Scope
|
||||||
|
- Langzeit-Historie oder Trend-Analysen.
|
||||||
|
- Alerting oder Threshold-Management.
|
||||||
|
|
||||||
|
## High-Level Akzeptanzkriterien
|
||||||
|
- CPU als Gesamt-% angezeigt.
|
||||||
|
- RAM: Total und Usage %.
|
||||||
|
- GPU: VRAM Total und Usage % (falls GPU vorhanden).
|
||||||
|
- Netzwerk: aktuelle RX/TX in Mbps.
|
||||||
|
|
||||||
|
## Technische Constraints & Risiken
|
||||||
|
- GPU-Metriken sind hardware-/driver-abhaengig.
|
||||||
|
- Netzwerkwerte benoetigen Delta-Berechnung ueber Zeitfenster.
|
||||||
|
|
||||||
|
## Zugeordnete User Stories
|
||||||
|
- US_000044: Systemmetriken im Dashboard anzeigen
|
||||||
42
project-management/requirements/epics/EPIC_000014.md
Normal file
42
project-management/requirements/epics/EPIC_000014.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
ID: EPIC_000014 | Version: 0.2.3 | Status: Draft
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# EPIC_000014: Login-Zeitfenster und Parent-Control Regeln
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
Admins definieren Zeitfenster pro Nutzer, in denen Login erlaubt ist. Beim Login wird
|
||||||
|
geprueft, ob der Zeitpunkt erlaubt ist; andernfalls erfolgt Hinweis, Konto wird deaktiviert
|
||||||
|
und der Rechner heruntergefahren. Beim Systemstart wird geprueft, welche Nutzer sich anmelden duerfen.
|
||||||
|
|
||||||
|
## Ziel / Business Value
|
||||||
|
Verlaessliche Einhaltung von Nutzungszeiten ohne manuelle Eingriffe.
|
||||||
|
|
||||||
|
## Mission Statement
|
||||||
|
Stelle regelbasierte Login-Beschraenkungen bereit, die zentral verwaltbar und systemweit
|
||||||
|
durchsetzbar sind.
|
||||||
|
|
||||||
|
## Business Value & Metriken
|
||||||
|
- Reduzierte manuelle Sperren.
|
||||||
|
- Erfolgsmetrik: Verbotene Logins werden zu 100% blockiert.
|
||||||
|
|
||||||
|
## In-Scope
|
||||||
|
- Regeln pro Nutzer (erlaubte Zeitfenster).
|
||||||
|
- Automatische Aktivierung/Deaktivierung nach Regeln.
|
||||||
|
- Durchsetzung bei Login und beim Systemstart.
|
||||||
|
|
||||||
|
## Out-of-Scope
|
||||||
|
- Historische Reports oder Nutzungsstatistiken.
|
||||||
|
- MFA oder externe Auth-Systeme.
|
||||||
|
|
||||||
|
## High-Level Akzeptanzkriterien
|
||||||
|
- Ohne Regel ist Login erlaubt.
|
||||||
|
- Mit Regel wird Login nur in erlaubten Zeitfenstern zugelassen.
|
||||||
|
- Bei Verbot: Hinweis, Konto wird deaktiviert, Shutdown wird gestartet.
|
||||||
|
- Optional: Konto wird nach Zeitfenster automatisch wieder aktiviert.
|
||||||
|
|
||||||
|
## Technische Constraints & Risiken
|
||||||
|
- Zeit- und Zeitzonenhandling.
|
||||||
|
- Durchsetzung erfordert Hook in Auth/Login-Prozess.
|
||||||
|
|
||||||
|
## Zugeordnete User Stories
|
||||||
|
- US_000045: Regeln fuer Login-Zeitfenster definieren und durchsetzen
|
||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000001 | Version: 0.1.0 | Status: Final
|
ID: US_000001 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000001: Nutzerkonto per CLI deaktivieren
|
# US_000001: Nutzerkonto per CLI deaktivieren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000002 | Version: 0.1.0 | Status: Final
|
ID: US_000002 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000002: Nutzerkonto per CLI aktivieren
|
# US_000002: Nutzerkonto per CLI aktivieren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000003 | Version: 0.1.0 | Status: Final
|
ID: US_000003 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000003: Health-Status abfragen
|
# US_000003: Health-Status abfragen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000004 | Version: 0.1.0 | Status: Final
|
ID: US_000004 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000004: Verfuegbare Nutzer auflisten
|
# US_000004: Verfuegbare Nutzer auflisten
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000005 | Version: 0.1.0 | Status: Final
|
ID: US_000005 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000005: Nutzer per API deaktivieren
|
# US_000005: Nutzer per API deaktivieren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000006 | Version: 0.1.0 | Status: Final
|
ID: US_000006 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000006: Nutzer per API aktivieren
|
# US_000006: Nutzer per API aktivieren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000007 | Version: 0.1.0 | Status: Final
|
ID: US_000007 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000007: PAM-Login mit Token
|
# US_000007: PAM-Login mit Token
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000008 | Version: 0.1.0 | Status: Final
|
ID: US_000008 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000008: OIDC-Login Flow
|
# US_000008: OIDC-Login Flow
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000009 | Version: 0.1.0 | Status: Final
|
ID: US_000009 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000009: Autorisierung und /me-Identitaet
|
# US_000009: Autorisierung und /me-Identitaet
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000010 | Version: 0.1.0 | Status: Final
|
ID: US_000010 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000010: Index-Seite ausliefern
|
# US_000010: Index-Seite ausliefern
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000011 | Version: 0.1.0 | Status: Final
|
ID: US_000011 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000011: Virtualenv und Abhaengigkeiten erstellen
|
# US_000011: Virtualenv und Abhaengigkeiten erstellen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000012 | Version: 0.1.0 | Status: Final
|
ID: US_000012 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000012: Service lokal starten
|
# US_000012: Service lokal starten
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000013 | Version: 0.1.0 | Status: Final
|
ID: US_000013 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000013: Service installieren
|
# US_000013: Service installieren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000014 | Version: 0.1.0 | Status: Final
|
ID: US_000014 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000014: Service aktualisieren
|
# US_000014: Service aktualisieren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000015 | Version: 0.1.0 | Status: Final
|
ID: US_000015 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000015: Remote-Deployment durchfuehren
|
# US_000015: Remote-Deployment durchfuehren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000016 | Version: 0.1.0 | Status: Final
|
ID: US_000016 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000016: OIDC-Client registrieren
|
# US_000016: OIDC-Client registrieren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000017 | Version: 0.1.0 | Status: Final
|
ID: US_000017 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000017: Systemd-Unit im Repo
|
# US_000017: Systemd-Unit im Repo
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000018 | Version: 0.1.0 | Status: Final
|
ID: US_000018 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000018: Konfigurations-Templates verfuegbar
|
# US_000018: Konfigurations-Templates verfuegbar
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000019 | Version: 0.1.0 | Status: Final
|
ID: US_000019 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000019: Deployment-Archiv vorhanden
|
# US_000019: Deployment-Archiv vorhanden
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000020 | Version: 0.1.0 | Status: Final
|
ID: US_000020 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000020: Makefile-Automation bereitstellen
|
# US_000020: Makefile-Automation bereitstellen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000021 | Version: 0.1.0 | Status: Final
|
ID: US_000021 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000021: Konfiguration per ENV steuern
|
# US_000021: Konfiguration per ENV steuern
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000022 | Version: 0.1.0 | Status: Final
|
ID: US_000022 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000022: Web-UI Aktionen ausfuehren
|
# US_000022: Web-UI Aktionen ausfuehren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000023 | Version: 0.1.0 | Status: Final
|
ID: US_000023 | Version: 0.2.3 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000023: Runbook und Security-Hinweise dokumentieren
|
# US_000023: Runbook und Security-Hinweise dokumentieren
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000024 | Version: 0.1.0 | Status: Done
|
ID: US_000024 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000024: Watchtower Theme fuer Web-UI
|
# US_000024: Watchtower Theme fuer Web-UI
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000025 | Version: 0.1.0 | Status: Draft
|
ID: US_000025 | Version: 0.2.3 | Status: Draft
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000025: OIDC End-to-End Validierung und Runbook
|
# US_000025: OIDC End-to-End Validierung und Runbook
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000026 | Version: 0.1.0 | Status: Done
|
ID: US_000026 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000026: Client bezieht Updates (Pull)
|
# US_000026: Client bezieht Updates (Pull)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000027 | Version: 0.1.0 | Status: Done
|
ID: US_000027 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000027: Client verifiziert und wendet Updates an
|
# US_000027: Client verifiziert und wendet Updates an
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000028 | Version: 0.1.0 | Status: Done
|
ID: US_000028 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000028: Client meldet Update-Status
|
# US_000028: Client meldet Update-Status
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000029 | Version: 0.1.0 | Status: Done
|
ID: US_000029 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000029: Update-Status im Web-UI anzeigen
|
# US_000029: Update-Status im Web-UI anzeigen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000030 | Version: 0.1.0 | Status: Done
|
ID: US_000030 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000030: Update-Check im Web-UI ausloesen
|
# US_000030: Update-Check im Web-UI ausloesen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000031 | Version: 0.1.0 | Status: Done
|
ID: US_000031 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000031: Update im Web-UI anstossen
|
# US_000031: Update im Web-UI anstossen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000032 | Version: 0.1.0 | Status: Done
|
ID: US_000032 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000032: Update-Logs im Web-UI anzeigen
|
# US_000032: Update-Logs im Web-UI anzeigen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000033 | Version: 0.1.0 | Status: Done
|
ID: US_000033 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000033: Rollback im Web-UI anstossen
|
# US_000033: Rollback im Web-UI anstossen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000034 | Version: 0.1.5 | Status: Done
|
ID: US_000034 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000034: Enrollment fuer Langzeit-Token
|
# US_000034: Enrollment fuer Langzeit-Token
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
ID: US_000035 | Version: 0.1.5 | Status: Done
|
ID: US_000035 | Version: 0.2.3 | Status: Done
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# US_000035: v1 Update-Endpoints und Status-Schema
|
# US_000035: v1 Update-Endpoints und Status-Schema
|
||||||
|
|||||||
13
project-management/requirements/stories/US_000036.md
Normal file
13
project-management/requirements/stories/US_000036.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
ID: US_000036 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# US_000036: Doku-Versionen auf VERSION synchronisieren
|
||||||
|
|
||||||
|
Als Maintainer moechte ich, dass Doku-Header und Spezifikationen die zentrale VERSION
|
||||||
|
verwenden, damit Releases konsistent dokumentiert sind.
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
- Given die VERSION ist 0.2.1
|
||||||
|
- When Doku-Header oder OpenAPI-Info Versionen angegeben sind
|
||||||
|
- Then entsprechen sie 0.2.1
|
||||||
|
- And es gibt keine widerspruechlichen 0.1.0-Header in den betroffenen Doku-Dateien
|
||||||
14
project-management/requirements/stories/US_000037.md
Normal file
14
project-management/requirements/stories/US_000037.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
ID: US_000037 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# US_000037: Update-API-Doku mit /update/enroll abgleichen
|
||||||
|
|
||||||
|
Als Betreiber moechte ich eine korrekte Update-API-Dokumentation, damit Clients die
|
||||||
|
Update-Endpunkte inkl. Enrollment korrekt nutzen.
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
- Given die Backend-API implementiert /update/enroll
|
||||||
|
- When die Update-API-Doku gelesen wird
|
||||||
|
- Then /update/enroll ist beschrieben (Request und Response)
|
||||||
|
- And /update/status enthaelt das Feld enrolled
|
||||||
|
- And /update/logs beschreibt die aktuellen Felder (timestamp, status, message, version, device_id, error)
|
||||||
14
project-management/requirements/stories/US_000038.md
Normal file
14
project-management/requirements/stories/US_000038.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
ID: US_000038 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# US_000038: ENV-Beispiele und Healthcheck-Auth angleichen
|
||||||
|
|
||||||
|
Als Operator moechte ich, dass ENV-Beispiele und Healthcheck-Header mit dem aktuellen
|
||||||
|
Auth- und Update-Config-Stand uebereinstimmen, damit Deployments nicht scheitern.
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
- Given das Backend akzeptiert Authorization: Bearer Tokens
|
||||||
|
- When der Healthcheck aus dem Makefile ausgefuehrt wird
|
||||||
|
- Then wird der Header Authorization: Bearer verwendet
|
||||||
|
- And env.example/README listen SKD_UPDATE_SERVICE_URL und SKD_UPDATE_PROJECT_ID
|
||||||
|
- And nicht verwendete Update-Variablen werden nicht als Pflicht-Keys aufgefuehrt
|
||||||
14
project-management/requirements/stories/US_000039.md
Normal file
14
project-management/requirements/stories/US_000039.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
ID: US_000039 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# US_000039: Doku-Audit fuer verbleibende Abweichungen
|
||||||
|
|
||||||
|
Als Maintainer moechte ich eine systematische Pruefung der verbleibenden Doku-Dateien,
|
||||||
|
damit keine Versions- oder Inhaltsabweichungen zwischen Code und Dokumentation bestehen.
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
- Given die zentrale VERSION ist bekannt
|
||||||
|
- When alle Doku-Dateien geprueft werden
|
||||||
|
- Then sind Header-Versionen konsistent mit VERSION
|
||||||
|
- And API-/ENV-Beschreibungen entsprechen dem aktuellen Code-Stand
|
||||||
|
- And Abweichungen sind dokumentiert oder korrigiert
|
||||||
19
project-management/requirements/stories/US_000040.md
Normal file
19
project-management/requirements/stories/US_000040.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
ID: US_000040 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# US_000040: Doku-Struktur und Inhalte erstellen
|
||||||
|
|
||||||
|
Als Nutzer moechte ich eine klare, vollstaendige Dokumentation, damit Einsteiger,
|
||||||
|
Admins und Entwickler das System ohne Rueckfragen verstehen und betreiben koennen.
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
- Given die Zielstruktur ist definiert
|
||||||
|
- When die Dokumentation erstellt wird
|
||||||
|
- Then existieren alle Ziel-Dokumente mit korrekten Inhalten
|
||||||
|
- And README enthaelt Quickstart, Zielgruppen, Features und Links
|
||||||
|
- And alle Inhalte basieren auf Code/Skripten ohne Spekulation
|
||||||
|
- And Altdokumente sind archiviert statt geloescht
|
||||||
|
|
||||||
|
## Task-Platzhalter
|
||||||
|
- TASK_000043: Neue Doku-Dateien erstellen und verlinken
|
||||||
|
- TASK_000044: Altdoku archivieren und Hinweise einfuegen
|
||||||
16
project-management/requirements/stories/US_000041.md
Normal file
16
project-management/requirements/stories/US_000041.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
ID: US_000041 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# US_000041: Einbindung externer Services dokumentieren
|
||||||
|
|
||||||
|
Als Operator moechte ich klare Schritte zur Einbindung des Update- und OIDC-Services,
|
||||||
|
damit die Schnittstellen korrekt konfiguriert und betrieben werden koennen.
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
- Given die Update- und OIDC-Services sind bekannt
|
||||||
|
- When die Doku gelesen wird
|
||||||
|
- Then sind Einbindungsschritte beschrieben (ENV, Enrollment, OIDC-Setup)
|
||||||
|
- And die Quellen der Services sind verlinkt
|
||||||
|
|
||||||
|
## Task-Platzhalter
|
||||||
|
- TASK_000045: Einbindungsschritte in Doku ergaenzen
|
||||||
19
project-management/requirements/stories/US_000042.md
Normal file
19
project-management/requirements/stories/US_000042.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
ID: US_000042 | Version: 0.2.3 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# US_000042: Consumer-Perspektive und Audience-Split ergaenzen
|
||||||
|
|
||||||
|
Als Produktteam moechten wir eine klare Trennung der Zielgruppen,
|
||||||
|
damit Endnutzer, Einsteiger, Power-User und Entwickler passende Einstiege haben.
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
- Given die Zielgruppen sind definiert
|
||||||
|
- When die Doku erstellt wird
|
||||||
|
- Then existiert eine Consumer-Doku ohne technische Inhalte
|
||||||
|
- And README benennt alle Zielgruppen und gruppiert die Links
|
||||||
|
- And External Dependencies sind mit offiziellen Links und Begruendung dokumentiert
|
||||||
|
|
||||||
|
## Task-Platzhalter
|
||||||
|
- TASK_000046: Consumer-Doku erstellen (FOR_USERS.md)
|
||||||
|
- TASK_000047: README und Audience-spezifische Links/Sections anpassen
|
||||||
|
- TASK_000048: External Dependencies und Power-User/Beginner-Ergaenzungen einpflegen
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user