Files
signage/AGENTS.md
2026-06-26 15:10:38 +02:00

182 lines
12 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CANCOM Simple Signage — Agent Guide
## Start
```bash
pip install -r requirements.txt
python app.py
docker compose up -d
```
`python app.py` (via `.venv/bin/python app.py`) serves on `http://localhost:5005`. `app.py` runs Flask with `debug=True`, `host="0.0.0.0"`, and `port=5005`.
## Source Of Truth
- Single Flask app in `app.py`; there is no database.
- Persistent state is `config.json` plus files under `media/<site>/<screen>/`.
- User data in `users.json` (hashed passwords via werkzeug.security scrypt).
- Port 5005; `README.md` hat den korrekten Port.
- Existing repo instructions in this file are the main local guidance; there is no `opencode.json` or workflow config in this repo.
## Multi-Standort-URL-Struktur
Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
| Route | Beschreibung |
|-------|-------------|
| `GET /player/<site>/<screen>` | Player für Screen an einem Standort |
| `GET /admin` | Redirect zum ersten zugänglichen Standort |
| `GET /admin/<site>` | Admin-Dashboard für einen Standort (`@site_access_required`) |
| `GET /admin/<site>/priority` | Priority-Playlist als separate Seite |
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
| `GET /media/priority/<file>` | Priority-Medien (global) |
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme für Auto-Reload + zeichnet Heartbeat auf (Online-Status) |
| `GET /willkommen?site=<site>` | Willkommensseite für Standort generieren |
| `POST /admin/<site>/upload-background` | Hintergrundbild für Willkommensseite hochladen |
| `POST /admin/<site>/delete-background` | Hintergrundbild zurücksetzen auf Standard |
| `POST /api/customer` | API-Endpunkt (JSON mit "site"-Feld) |
| `GET /admin/<site>/add-screen?name=<name>` | Neuen Screen anlegen |
| `POST /admin/<site>/delete-screen/<screen>` | Screen + Medien löschen |
| `GET /add-site?name=<name>` | Neuen Standort anlegen (nur Admin) |
| `POST /admin/<site>/update-actions/<screen>` | Aktionen-Einstellungen speichern (Custom-URL + Position) |
| `POST /admin/<site>/update-voice/<screen>` | Voice-Agent-Einstellungen speichern (Enabled, Label, Target, Position) |
| `POST /admin/<site>/delete-site` | Standort + alle Screens/Medien löschen (nur Admin) |
| `GET /change-password` | Passwort ändern-Seite |
| `GET /admin/users` | User-Liste (nur Admin) |
| `GET /admin/users/create` | User anlegen (nur Admin) |
| `GET /admin/users/edit/<email>` | User bearbeiten (nur Admin) |
## User-Verwaltung (seit v5.6.0)
- Login via E-Mail + Passwort (scrypt-gehasht in `users.json`)
- Drei Rollen:
- **Admin**: Zugriff auf alle Standorte + Userverwaltung (anlegen/bearbeiten/löschen/Reset)
- **Superuser**: Zugriff auf alle Standorte, keine Userverwaltung
- **User**: Zugriff nur auf zugewiesene Standorte
- Erster Login erfordert Passwort-Änderung (`must_change_password=True`)
- Admin-Reset generiert temporäres Passwort (wird im UI angezeigt)
- Fehlender/leerer `password_hash` → Login ohne Prüfung, direkt zu `/change-password`
- Letzter Admin kann nicht gelöscht werden
- `users.json` wird beim ersten Start aus `config.json.admin` befüllt
- Notfall: `users.json` löschen → Server-Neustart erzeugt neuen Admin
### User-Routen
| Route | Beschreibung |
|-------|-------------|
| `POST /admin/users/create` | User anlegen |
| `POST /admin/users/delete/<email>` | User löschen (letzter Admin geschützt) |
| `POST /admin/users/reset-password/<email>` | Passwort-Reset (temporäres Passwort) |
| `POST /admin/users/edit/<email>` | User bearbeiten speichern |
### Zugriffs-Dekoratoren
| Dekorator | Wirkung |
|-----------|---------|
| `@login_required` | User muss eingeloggt sein |
| `@admin_required` | Nur Admins |
| `@site_access_required` | Admin/Superuser → alle Sites, User → nur freigegebene |
## Behavior To Preserve
- `GET /player/<site>/<screen>` renders the playlist and auto-reloads from `/playlist/<site>/<screen>/hash`.
- `GET /admin` requires login; redirects to first accessible site for the user.
- URL playlist items are stored as dicts like `{"url": "https://...", "zoom": 0.8}` and the zoom value must survive save/reorder flows.
- `.html` items in `media/` are rendered inline as content, not in an iframe.
- `config.priority.enabled` makes the priority playlist show on every screen.
- `POST /api/customer` generates `welcome.html` and inserts it at the front of the lobby playlist for the specified site.
- New standorte can be added by creating `media/<neuer-standort>/<screen>/` directories and optionally adding config to `config.json["sites"][<neuer-standort>]`.
- Priority playlist (`config.priority`) is global and affects all sites/screens.
- Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row (portrait: column).
- `generate_welcome_html(customer_names, logo_urls)` takes lists for up to 3 customers; logos have equal width (280px) with `max-height: 180px`.
- `customer_names` are preserved in form fields after POST (via `value`-Attribute).
- Admin-UI nutzt keyadmin-Design: `brand-surface` (#2b2f36), `nav-surface` (rot #DA002D), Dark Mode per `localStorage("signage-theme")`.
- Gemeinsame HTML-Bausteine: `_header.html`, `_footer.html`, `_styles.html` (CSS-Variablen `--ccm-*`, Dark Mode, Card-Border-Radius 1rem).
- `add_customer_to_lobby_playlist` entfernt `welcome.html` sowohl als String als auch als Dict aus der Playlist vor dem Einfügen.
- `add_screen` legt ein Verzeichnis unter `media/<site>/<screen>/` an und einen Config-Eintrag.
- `delete_screen` entfernt den Screen aus der Config und löscht das Verzeichnis rekursiv.
- Custom-URL-Button: pro Screen konfigurierbar (`custom_url` + `custom_url_label` + `custom_url_enabled` + `custom_url_target` + `custom_url_position`) im Admin-Formular (Aktionen-Tab); wird im Player als Button an wählbarer Position (9 Positionen: oben/mitte/unten × links/mitte/rechts) angezeigt und öffnet die URL wahlweise in einem iframe-Overlay mit Zurück-Button (`overlay`) oder per Direkt-Weiterleitung (`redirect`); Player pausiert während das Overlay geöffnet ist. Die unteren Positionen weichen automatisch 16px über dem Newsticker-Balken aus.
- Voice-Agent-Button: globale `config.voice_agent_url` wird pro Screen im Tab "Digital Voice Agent" konfiguriert; Einstellungen (`voice_agent_enabled`, `voice_agent_label`, `voice_agent_target`, `voice_agent_position`) funktionieren identisch zum Aktionen-Tab. Die URL ist read-only im Admin sichtbar und wird global in `config.json` gesetzt.
- Voice-Agent-Typewriter: Tagline im Button zeigt wechselnde mehrsprachige Texte mit Buchstaben-für-Buchstaben-Effekt; nach 2,5s Pause fade-out über 0,8s, dann nächster Text.
- Voice-Agent-Overlay: positioniert den Zurück-Button exakt an der Position des geklickten Buttons (identisches Positionssystem).
- `voice_agent_show_image` (per Screen): optionales Bild (`static/dva.png`) über dem Button, per Admin-Toggle schaltbar (Beta).
- Action-Button (custom_url) und Voice-Agent-Button haben einheitliches Styling: weißer Hintergrund, schwarzer Text, 20px Border-Radius, fette Schrift.
- Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=<name>` angelegt werden (nur Admins).
- `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv (nur Admins).
- `config.json["server_url"]` (z.B. `http://signage.ccmake.de`) wird in der Admin-Ansicht für die Player-URLs verwendet.
- Screen-Card-Body hat Tabler-Tabs: **Playlist** (1, aktiv), **Einstellungen** (2), **Aktionen** (3), **Digital Voice Agent** (4), **Medien** (5); Priority-Seite ebenfalls Tabs **Playlist** und **Medien**.
- `stay_on_first`: Wenn aktiviert bleibt der Player auf dem ersten Playlist-Element stehen (kein Durchlauf).
- Tab-Reihenfolge in Screen-Cards: Playlist → Einstellungen → Aktionen → Digital Voice Agent → Medien.
- Player-URL im Screen-Header ist ein klickbarer Link in grauer Farbe.
- `add_customer` und `/customer` erfordern jetzt Login (`@login_required`) mit Site-Zugriffsprüfung.
- `/admin` redirectet zum ersten für den User zugänglichen Standort (nicht mehr global ersten).
- User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, Userverwaltung (Admin), Abmelden.
## Repo Quirks
- `media/` and media file extensions are gitignored.
- `users.json` is NOT gitignored (tracked for initial admin setup, contains no secrets by default).
- The app has no configured tests, lint, typecheck, formatter, or CI.
- Hardcoded secrets exist in tracked files; do not commit new secrets or reshuffle them casually.
- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `playlist_item_enabled()`, `load_priority_files()`, `prio_redirect()`, `get_background_url()`, `load_users()`, `save_users()`, `get_user()`, `init_user_db()`, `get_accessible_sites()`, `record_heartbeat()`, `screen_is_active()`.
- Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`.
- Hintergrundbild der Willkommensseite wird pro Standort unter `media/<site>/background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert.
- `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort.
- `generate_welcome_html(customer_names, logo_urls, background_url=None)` akzeptiert optionalen `background_url`-Parameter.
- `welcome.html` wird in `media/<site>/lobby/` gespeichert.
- `search_customer_logo` in `generate_welcome_page.py` nutzt OpenAI GPT-4 + Brandfetch CDN.
- `admin_priority` rendert `priority.html` mit `site_list`, `current_site`, `priority_files`, `server_url`.
- `static/dva.png`: Bild für den Voice-Agent-Button (optional, per Admin-Toggle ein-/ausblendbar).
## Heartbeat / Online-Status (seit v6.1.0)
Der Player-Status (Online/Offline) wird mittels dateibasiertem Heartbeat ermittelt.
### Funktionsweise
| Schritt | Beschreibung |
|---------|-------------|
| **Player-pollt** alle 5s `/playlist/<site>/<screen>/hash` (`checkForUpdates`) | Bestehender Mechanismus, kein Extra-Request |
| **Server zeichnet Heartbeat auf** | `record_heartbeat(site, screen)` erstellt/updated `/tmp/signage-heartbeat/<site>/<screen>` via `path.touch()` |
| **Admin-UI fragt Status ab** | `screen_is_active(site, screen)` prüft ob `mtime < 60s` alt |
| **Anzeige** | Grüner "Online"-Button (filled success) oder roter "Offline"-Button (filled danger) im Card-Header neben "Löschen" |
### Helper-Funktionen in `app.py`
```python
HEARTBEAT_DIR = Path("/tmp/signage-heartbeat")
HEARTBEAT_TIMEOUT = 60 # Sekunden
def record_heartbeat(site, screen):
path = HEARTBEAT_DIR / site / screen
path.parent.mkdir(parents=True, exist_ok=True)
path.touch()
def screen_is_active(site, screen):
path = HEARTBEAT_DIR / site / screen
if not path.exists():
return False
return (time.time() - path.stat().st_mtime) < HEARTBEAT_TIMEOUT
```
### Integration
- `playlist_hash()`-Route ruft `record_heartbeat()` direkt am Anfang auf
- `admin()`-Route setzt `screen_status["online"/"offline"]` via `screen_is_active()` für jeden Screen
- Admin-Template (`admin.html`) zeigt beide Buttons als `<button class="btn btn-sm btn-success/btn-danger" onclick="return false;">`
- Die Lösch-Buttons (`btn-outline-danger btn-sm`) haben gleiche Größe durch gleiche btn-sm-Klasse, Form-Wrapper hat `m-0 p-0`
### Docker-Hinweis
Im Docker-Container liegt der Heartbeat-Pfad unter `/tmp/signage-heartbeat/` im Container. Da die Docker-Umgebung keine Volumes mounted, sind Heartbeats container-lokal. Nach `docker compose restart` werden alle Screens kurz rot, bis die Player innerhalb von max. 60s wieder pollen.
### Debugging
```bash
# Alle Heartbeat-Dateien anzeigen
ls -la /tmp/signage-heartbeat/*/
find /tmp/signage-heartbeat -type f -exec ls -la {} \;
```