Compare commits
3 Commits
29caaa2e0e
...
3761497421
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3761497421 | ||
|
|
5dda8abe2e | ||
|
|
90baa6130f |
37
AGENTS.md
37
AGENTS.md
@@ -107,7 +107,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
|
|
||||||
- `GET /player/<site>/<screen>` renders the playlist and auto-reloads from `/playlist/<site>/<screen>/hash`.
|
- `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.
|
- `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.
|
- URL playlist items are stored as dicts like `{"url": "https://...", "zoom": 0.8}` and the zoom value must survive save/reorder flows. File items may also be dicts (`{"name": "bild.jpg", ...}`); `save_playlist` copies existing dicts, so extra keys like `schedule` survive reordering.
|
||||||
- `.html` items in `media/` are rendered inline as content, not in an iframe.
|
- `.html` items in `media/` are rendered inline as content, not in an iframe.
|
||||||
- `config.priority.enabled` makes the priority playlist show on every screen.
|
- `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.
|
- `POST /api/customer` generates `welcome.html` and inserts it at the front of the lobby playlist for the specified site.
|
||||||
@@ -133,6 +133,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
- `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv (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.
|
- `config.json["server_url"]` (z. B. `http://signage.ccmake.de`) wird in der Admin-Ansicht für die Player-URLs verwendet.
|
||||||
- **Query-Parameter-Passthrough**: Query-Parameter an der Player-URL (`/player/<site>/<screen>`) können pro Playlist-URL im Edit-Modal (Checkbox) gesteuert werden. Standardmäßig **aus** – nur URLs mit aktivierter Checkbox erhalten die Parameter. Beispiel: `/player/stuttgart/lobby?customer=acme` hängt `?customer=acme` nur an URLs mit Passthrough=an. In der Playlist-Zeile zeigt ein Link-Icon (`ti-link`, grau) den Status an: sichtbar = an, durchgestrichen (`ti-link-off`, hellgrau) = aus. Funktioniert über `appendParams()` in `player.html` + `player_params` im Flask-Route.
|
- **Query-Parameter-Passthrough**: Query-Parameter an der Player-URL (`/player/<site>/<screen>`) können pro Playlist-URL im Edit-Modal (Checkbox) gesteuert werden. Standardmäßig **aus** – nur URLs mit aktivierter Checkbox erhalten die Parameter. Beispiel: `/player/stuttgart/lobby?customer=acme` hängt `?customer=acme` nur an URLs mit Passthrough=an. In der Playlist-Zeile zeigt ein Link-Icon (`ti-link`, grau) den Status an: sichtbar = an, durchgestrichen (`ti-link-off`, hellgrau) = aus. Funktioniert über `appendParams()` in `player.html` + `player_params` im Flask-Route.
|
||||||
|
- **Zeitsteuerung**: Optionales `schedule`-Objekt pro Playlist-Eintrag (URL *und* Datei, Screen-Playlist *und* Priority). Ausgewertet ausschliesslich serverseitig in `is_item_active()`; siehe Abschnitt "Zeitsteuerung" unten.
|
||||||
- Screen-Card-Body hat Tabler-Tabs: **Playlist** (1, aktiv), **Einstellungen** (2), **Aktionen** (3), **Digital Voice Agent** (4), **Medien** (5), **Info** (6); Priority-Seite ebenfalls Tabs **Playlist** und **Medien**.
|
- Screen-Card-Body hat Tabler-Tabs: **Playlist** (1, aktiv), **Einstellungen** (2), **Aktionen** (3), **Digital Voice Agent** (4), **Medien** (5), **Info** (6); Priority-Seite ebenfalls Tabs **Playlist** und **Medien**.
|
||||||
- `stay_on_first`: Wenn aktiviert bleibt der Player auf dem ersten Playlist-Element stehen (kein Durchlauf).
|
- `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 → Info.
|
- Tab-Reihenfolge in Screen-Cards: Playlist → Einstellungen → Aktionen → Digital Voice Agent → Medien → Info.
|
||||||
@@ -148,7 +149,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
- `users.json` is NOT gitignored (tracked for initial admin setup, contains no secrets by default).
|
- `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.
|
- 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.
|
- 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()`, `get_client_info()`, `add_history_entry()`.
|
- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `is_item_active()`, `_schedule_time()`, `_window_start_date()`, `_in_validity_range()`, `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()`, `get_client_info()`, `add_history_entry()`.
|
||||||
- Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`.
|
- 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.
|
- 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.
|
- `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort.
|
||||||
@@ -158,6 +159,38 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
- `admin_priority` rendert `priority.html` mit `site_list`, `current_site`, `priority_files`, `server_url`.
|
- `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).
|
- `static/dva.png`: Bild für den Voice-Agent-Button (optional, per Admin-Toggle ein-/ausblendbar).
|
||||||
|
|
||||||
|
## Zeitsteuerung (seit v6.2.0)
|
||||||
|
|
||||||
|
Jeder Playlist-Eintrag kann ein optionales `schedule`-Objekt tragen — URL wie Datei, in Screen-Playlists wie in der Priority-Playlist.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "mode": "daily", "start": "08:00", "end": "12:00",
|
||||||
|
"valid_from": "2026-09-10", "valid_until": "2026-09-20" } // valid_* optional
|
||||||
|
{ "mode": "weekly", "start": "09:00", "end": "17:00", "days": ["0", "2", "4"] }
|
||||||
|
{ "mode": "once", "start": "2026-09-10T08:00", "end": "2026-09-20T12:00" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regeln
|
||||||
|
|
||||||
|
- `once` vergleicht vollständige Zeitstempel. `daily`/`weekly` werten **nur die Uhrzeit** aus; ein Datum wird dort ausschliesslich über `valid_from`/`valid_until` gesetzt (beide optional, beide inklusiv).
|
||||||
|
- Bei `daily` gilt **jeder** Wochentag; ein vorhandenes `days` wird in diesem Modus nicht ausgewertet (bleibt aber erhalten, damit ein Moduswechsel die Auswahl nicht vernichtet).
|
||||||
|
- `start` > `end` bedeutet ein Fenster **über Mitternacht**. Ein solches Fenster gehört zum Wochentag *und* Gültigkeitstag seines **Starts** — deshalb liefert `_window_start_date()` das Datum des Fensterbeginns und nicht einfach `now.date()`.
|
||||||
|
- Unlesbare oder unvollständige Zeitangaben geben `True` zurück. Ein Konfigurationsfehler soll den Screen nicht stillschweigend leeren — beim Ändern dieser Logik unbedingt beibehalten.
|
||||||
|
- `_schedule_time()` liest `"HH:MM"`, `"HH:MM:SS"` und volle ISO-Zeitstempel. Letzteres ist nötig, weil ältere Versionen bei `daily`/`weekly` einen kompletten Zeitstempel gespeichert haben, dessen Datumsanteil ignoriert wurde.
|
||||||
|
- Auswertung erfolgt **nur serverseitig** (`datetime.now()` des Servers, nicht des Players).
|
||||||
|
|
||||||
|
### Reload-Kopplung
|
||||||
|
|
||||||
|
`playlist_hash()` hasht neben der Konfiguration auch `[is_item_active(item) for item in ...]` für Screen- und Priority-Playlist. Ohne das würde der Player ein ablaufendes Zeitfenster erst beim nächsten Reload aus anderem Grund bemerken, da sich die Konfiguration dabei nicht ändert.
|
||||||
|
|
||||||
|
### Beteiligte Stellen
|
||||||
|
|
||||||
|
- `is_item_active()`, `_window_start_date()`, `_in_validity_range()`, `_schedule_time()` in `app.py`
|
||||||
|
- Jinja-Filter `schedule_text()` für den Tooltip am "Zeit"-Badge
|
||||||
|
- `add_url()` liest bei `daily`/`weekly` die Felder `schedule_start_time`/`schedule_end_time`, bei `once` `schedule_start`/`schedule_end`
|
||||||
|
- `update_playlist_item()` behandelt `new_url: null` als Datei-Eintrag und schreibt `{"name": ...}`; `schedule: {}` entfernt die Zeitsteuerung, ein fehlender Key behält sie
|
||||||
|
- UI in `admin.html` und `priority.html`: zwei Feldpaare (`datetime-local` für `once`, `time` für `daily`/`weekly`), per Modus umgeschaltet
|
||||||
|
|
||||||
## Heartbeat / Online-Status (seit v6.1.0)
|
## Heartbeat / Online-Status (seit v6.1.0)
|
||||||
|
|
||||||
Der Player-Status (Online/Offline) wird mittels dateibasiertem Heartbeat ermittelt.
|
Der Player-Status (Online/Offline) wird mittels dateibasiertem Heartbeat ermittelt.
|
||||||
|
|||||||
11
DESIGN.md
11
DESIGN.md
@@ -141,6 +141,7 @@ The palette is rooted in CANCOM's corporate red and high-contrast neutrals.
|
|||||||
- **Muted (#6B7280 / #9AA4B2 Dark):** Secondary text, captions, metadata, placeholders.
|
- **Muted (#6B7280 / #9AA4B2 Dark):** Secondary text, captions, metadata, placeholders.
|
||||||
- **Online/Offline:** Green (#2FB344) for online status badge, Red (#D63939) for offline.
|
- **Online/Offline:** Green (#2FB344) for online status badge, Red (#D63939) for offline.
|
||||||
- **Role Badges:** Red for Admin, Purple (#8B5CF6) for Superuser, Blue (#3B82F6) for User.
|
- **Role Badges:** Red for Admin, Purple (#8B5CF6) for Superuser, Blue (#3B82F6) for User.
|
||||||
|
- **Schedule Badge:** Bootstrap `bg-info` with white text, label "Zeit", marks a scheduled playlist item; the tooltip carries the plain-text schedule from the `schedule_text` filter.
|
||||||
- **Newsticker:** Red bar (#DA002D) with white text.
|
- **Newsticker:** Red bar (#DA002D) with white text.
|
||||||
|
|
||||||
### States
|
### States
|
||||||
@@ -194,7 +195,9 @@ Screen management cards have:
|
|||||||
|
|
||||||
### Grid Areas
|
### Grid Areas
|
||||||
|
|
||||||
The playlist row uses a fixed grid: `28px 1fr 90px 80px 90px 32px` for checkbox, name, type badge, size, delete button, drag handle.
|
The playlist row uses a fixed grid: `28px 1fr 90px 80px 50px 40px 32px` for checkbox, name, type badge, size, delete button, edit button, drag handle — identical in `admin.html` and `priority.html`.
|
||||||
|
|
||||||
|
**The column count is fixed.** Anything added to a row becomes an extra grid item and shifts every following column. Status markers therefore live *inside* the `.playlist-name` span, not as siblings — this applies to the passthrough link icon and the "Zeit" badge.
|
||||||
|
|
||||||
## Elevation & Depth
|
## Elevation & Depth
|
||||||
|
|
||||||
@@ -250,6 +253,10 @@ Red background (`--ccm-primary`). Contains white outline buttons (`btn-white`):
|
|||||||
- Body contains Tabler tabs (6 tabs for screen cards)
|
- Body contains Tabler tabs (6 tabs for screen cards)
|
||||||
- Tab background matches card surface; tab link text uses muted color; active tab uses surface background
|
- Tab background matches card surface; tab link text uses muted color; active tab uses surface background
|
||||||
|
|
||||||
|
**`.card` is styled globally**, so the red left accent and grey header apply to *every* element carrying the class. The card is the container for a top-level entity — a screen, a help topic, the priority playlist. It is not a way to group fields.
|
||||||
|
|
||||||
|
Grouping form fields inside a modal or a tab uses plain rows (`mb-3`, `form-check`) with an `<hr>` and an `<h3>` where a separator is wanted. A `.card` there reads as a foreign tile inside the form and violates the one-red-accent-per-card rule.
|
||||||
|
|
||||||
### Player Media Elements
|
### Player Media Elements
|
||||||
|
|
||||||
- img, video, iframe: fullscreen (`100vw × 100vh`), fixed position, centered via `top:50%; left:50%; transform:translate(-50%,-50%)`
|
- img, video, iframe: fullscreen (`100vw × 100vh`), fixed position, centered via `top:50%; left:50%; transform:translate(-50%,-50%)`
|
||||||
@@ -323,6 +330,8 @@ Same base as action button plus:
|
|||||||
- Do keep player pages completely chromeless — no headers, navs, or footers
|
- Do keep player pages completely chromeless — no headers, navs, or footers
|
||||||
- Don't add shadows to cards; use border + left accent instead
|
- Don't add shadows to cards; use border + left accent instead
|
||||||
- Don't use more than one red accent per card (the 6px left border is sufficient)
|
- Don't use more than one red accent per card (the 6px left border is sufficient)
|
||||||
|
- Don't use `.card` to group form fields inside a modal or tab — it inherits the global red accent and grey header; use plain rows with `<hr>` + `<h3>` instead
|
||||||
|
- Don't add elements as direct children of a playlist row — the grid has a fixed column count; put status markers inside `.playlist-name`
|
||||||
- Don't mix corner radii: cards use 1rem, player buttons use 20px — keep each category consistent
|
- Don't mix corner radii: cards use 1rem, player buttons use 20px — keep each category consistent
|
||||||
- Do set `data-bs-theme` synchronously before rendering to prevent dark-mode flash
|
- Do set `data-bs-theme` synchronously before rendering to prevent dark-mode flash
|
||||||
- Do persist theme preference in `localStorage("signage-theme")`
|
- Do persist theme preference in `localStorage("signage-theme")`
|
||||||
|
|||||||
68
README.md
68
README.md
@@ -25,6 +25,7 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
|
|||||||
- **Admin Dashboard**: `/admin/dashboard` – Statistiken (Sites, Screens, User, Admins, Superuser) mit Sparkline-Charts + Trendanzeige, Aktivitätsverlauf
|
- **Admin Dashboard**: `/admin/dashboard` – Statistiken (Sites, Screens, User, Admins, Superuser) mit Sparkline-Charts + Trendanzeige, Aktivitätsverlauf
|
||||||
- **Aktivitätsverlauf**: Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden in `history.json` protokolliert und im Dashboard angezeigt
|
- **Aktivitätsverlauf**: Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden in `history.json` protokolliert und im Dashboard angezeigt
|
||||||
- Tab-basierte Admin-UI pro Screen: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien, Info (Tabler Tabs)
|
- Tab-basierte Admin-UI pro Screen: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien, Info (Tabler Tabs)
|
||||||
|
- **Zeitsteuerung**: Pro Playlist-Eintrag (URL *und* Datei, auch in der Priority-Playlist) ein Zeitfenster festlegen – einmalig (Datum + Uhrzeit), täglich oder wöchentlich (Uhrzeit + optionaler Gültigkeitszeitraum, wöchentlich zusätzlich mit Wochentagen). Fenster über Mitternacht werden unterstützt. Ein "Zeit"-Badge in der Playlist-Zeile zeigt die Einstellung im Tooltip
|
||||||
- **Query-Parameter-Passthrough**: Per Player-URL übergebene Query-Parameter (z. B. `/player/stuttgart/lobby?customer=acme`) können pro Playlist-URL im Edit-Modal aktiviert werden – standardmäßig aus. Link-Icon in der Playlist-Zeile zeigt Status (an/aus) an.
|
- **Query-Parameter-Passthrough**: Per Player-URL übergebene Query-Parameter (z. B. `/player/stuttgart/lobby?customer=acme`) können pro Playlist-URL im Edit-Modal aktiviert werden – standardmäßig aus. Link-Icon in der Playlist-Zeile zeigt Status (an/aus) an.
|
||||||
- Priority-Seite ebenfalls mit Tabs: Playlist und Medien
|
- Priority-Seite ebenfalls mit Tabs: Playlist und Medien
|
||||||
- Dark Mode (localStorage-persistiert)
|
- Dark Mode (localStorage-persistiert)
|
||||||
@@ -194,6 +195,7 @@ Server läuft auf `http://localhost:5005` (gunicorn mit 4 Workern, non-root User
|
|||||||
| `POST /admin/<site>/add-url/<screen>` | URL zur Playlist hinzufügen |
|
| `POST /admin/<site>/add-url/<screen>` | URL zur Playlist hinzufügen |
|
||||||
| `POST /admin/<site>/delete/<screen>/<filename>` | Datei löschen |
|
| `POST /admin/<site>/delete/<screen>/<filename>` | Datei löschen |
|
||||||
| `POST /admin/<site>/playlist/<screen>` | Playlist-Reihenfolge speichern (JSON) |
|
| `POST /admin/<site>/playlist/<screen>` | Playlist-Reihenfolge speichern (JSON) |
|
||||||
|
| `POST /admin/<site>/update-playlist-item/<screen>` | Einzelnen Playlist-Eintrag ändern: URL, Zoom, Passthrough, Zeitsteuerung (JSON) |
|
||||||
| `POST /admin/<site>/delete-screen/<screen>` | Screen + Medien löschen |
|
| `POST /admin/<site>/delete-screen/<screen>` | Screen + Medien löschen |
|
||||||
| `GET /admin/<site>/add-screen?name=<name>` | Neuen Screen anlegen |
|
| `GET /admin/<site>/add-screen?name=<name>` | Neuen Screen anlegen |
|
||||||
| `GET /add-site?name=<name>` | Neuen Standort anlegen |
|
| `GET /add-site?name=<name>` | Neuen Standort anlegen |
|
||||||
@@ -404,7 +406,7 @@ OPENAI_API_KEY=sk-...
|
|||||||
|
|
||||||
| Feld | Typ | Beschreibung |
|
| Feld | Typ | Beschreibung |
|
||||||
|------|-----|-------------|
|
|------|-----|-------------|
|
||||||
| `playlist` | Array | Playlist-Einträge (Strings oder Dicts mit `url`/`zoom`) |
|
| `playlist` | Array | Playlist-Einträge (Strings oder Dicts, siehe [Playlist-Einträge](#playlist-eintraege)) |
|
||||||
| `interval` | int | Anzeige-Intervall in Sekunden |
|
| `interval` | int | Anzeige-Intervall in Sekunden |
|
||||||
| `show_images` | bool | Bilder anzeigen |
|
| `show_images` | bool | Bilder anzeigen |
|
||||||
| `show_videos` | bool | Videos anzeigen |
|
| `show_videos` | bool | Videos anzeigen |
|
||||||
@@ -422,6 +424,64 @@ OPENAI_API_KEY=sk-...
|
|||||||
| `voice_agent_target` | string | `"overlay"` (iframe) oder `"redirect"` |
|
| `voice_agent_target` | string | `"overlay"` (iframe) oder `"redirect"` |
|
||||||
| `voice_agent_position` | string | Position des Buttons: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right` |
|
| `voice_agent_position` | string | Position des Buttons: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right` |
|
||||||
|
|
||||||
|
<a id="playlist-eintraege"></a>
|
||||||
|
## Playlist-Einträge
|
||||||
|
|
||||||
|
Ein Eintrag ist entweder ein String (Dateiname oder URL, Altbestand) oder ein Dict:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// URL-Eintrag
|
||||||
|
{
|
||||||
|
"url": "https://example.com",
|
||||||
|
"zoom": 1.0, // Zoom-Faktor im Player
|
||||||
|
"enabled": true, // Checkbox in der Playlist
|
||||||
|
"passthrough_params": false, // Query-Parameter der Player-URL durchreichen
|
||||||
|
"schedule": { ... } // optional, siehe unten
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei-Eintrag
|
||||||
|
{
|
||||||
|
"name": "bild.jpg",
|
||||||
|
"enabled": true,
|
||||||
|
"schedule": { ... } // optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zeitsteuerung (`schedule`)
|
||||||
|
|
||||||
|
Optional pro Eintrag. Ohne `schedule` läuft der Eintrag immer (sofern `enabled`).
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `mode` | string | `"once"`, `"daily"` oder `"weekly"` |
|
||||||
|
| `start` / `end` | string | Bei `once` voller Zeitstempel (`"2026-09-10T08:00"`), bei `daily`/`weekly` nur die Uhrzeit (`"08:00"`) |
|
||||||
|
| `days` | Array | Nur bei `weekly`: Wochentage als Strings, `"0"` = Montag … `"6"` = Sonntag |
|
||||||
|
| `valid_from` | string | Optional, nur bei `daily`/`weekly`: Datum (`"2026-09-10"`), ab dem das Fenster gilt – inklusiv |
|
||||||
|
| `valid_until` | string | Optional, nur bei `daily`/`weekly`: Datum, bis zu dem das Fenster gilt – inklusiv |
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// Täglich 08:00–12:00, nur vom 10.09. bis 20.09.2026
|
||||||
|
{ "mode": "daily", "start": "08:00", "end": "12:00",
|
||||||
|
"valid_from": "2026-09-10", "valid_until": "2026-09-20" }
|
||||||
|
|
||||||
|
// Mo/Mi/Fr von 09:00 bis 17:00, unbefristet
|
||||||
|
{ "mode": "weekly", "start": "09:00", "end": "17:00", "days": ["0", "2", "4"] }
|
||||||
|
|
||||||
|
// Einmalig, durchgehend über mehrere Tage
|
||||||
|
{ "mode": "once", "start": "2026-09-10T08:00", "end": "2026-09-20T12:00" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verhaltensregeln:**
|
||||||
|
|
||||||
|
- Bei `daily` und `weekly` zählt **nur die Uhrzeit** von `start`/`end`. Ein Gültigkeitszeitraum wird über `valid_from`/`valid_until` gesetzt, nicht über den Datumsanteil.
|
||||||
|
- Bei `daily` sind **alle Wochentage** gemeint; `days` wird in diesem Modus nicht ausgewertet.
|
||||||
|
- Ist `start` > `end`, läuft das Fenster **über Mitternacht** (z. B. `22:00`–`02:00`). Ein solches Fenster gehört zum Wochentag bzw. Gültigkeitstag seines **Starts**: „Mi 22:00–02:00" läuft Mittwochabend bis Donnerstagfrüh.
|
||||||
|
- Unlesbare oder unvollständige Zeitangaben blenden den Eintrag **nicht** aus – ein Konfigurationsfehler soll nicht dazu führen, dass stillschweigend nichts mehr läuft.
|
||||||
|
- `_schedule_time()` liest neben `"08:00"` auch volle ISO-Zeitstempel, damit Einträge aus älteren Versionen unverändert weiterlaufen.
|
||||||
|
- Der Aktiv-Zustand fliesst in `/playlist/<site>/<screen>/hash` ein, damit der Player beim Öffnen und Schliessen eines Zeitfensters von selbst neu lädt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Helper-Funktionen (`app.py`)
|
## Helper-Funktionen (`app.py`)
|
||||||
|
|
||||||
- `load_config()` / `save_config()` – JSON lesen/schreiben
|
- `load_config()` / `save_config()` – JSON lesen/schreiben
|
||||||
@@ -430,7 +490,11 @@ OPENAI_API_KEY=sk-...
|
|||||||
- `is_url(item)` – prüft ob Item eine URL ist
|
- `is_url(item)` – prüft ob Item eine URL ist
|
||||||
- `normalize_url(item)` – normalisiert URL-Item zu `{"url", "zoom"}`
|
- `normalize_url(item)` – normalisiert URL-Item zu `{"url", "zoom"}`
|
||||||
- `playlist_item_name(item)` – Name aus String oder Dict extrahieren
|
- `playlist_item_name(item)` – Name aus String oder Dict extrahieren
|
||||||
- `playlist_item_enabled(item)` – Enabled-Status prüfen
|
- `is_item_active(item)` – prüft `enabled` **und** die Zeitsteuerung
|
||||||
|
- `_schedule_time(value)` – Uhrzeit aus `"HH:MM"` oder vollem ISO-Zeitstempel lesen
|
||||||
|
- `_window_start_date(now, start, end)` – Kalendertag, an dem das laufende Zeitfenster begann (`None` = ausserhalb)
|
||||||
|
- `_in_validity_range(schedule, day)` – optionalen Gültigkeitszeitraum prüfen
|
||||||
|
- `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips
|
||||||
- `load_priority_files()` – Priority-Playlist + Dateien laden
|
- `load_priority_files()` – Priority-Playlist + Dateien laden
|
||||||
- `prio_redirect(site)` – Redirect-Pfad zur Priority-Seite
|
- `prio_redirect(site)` – Redirect-Pfad zur Priority-Seite
|
||||||
- `get_background_url(site)` – URL zum benutzerdefinierten Hintergrundbild oder `None`
|
- `get_background_url(site)` – URL zum benutzerdefinierten Hintergrundbild oder `None`
|
||||||
|
|||||||
2
app.py
2
app.py
@@ -50,7 +50,7 @@ CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # Persistente Konfiguration
|
|||||||
USERS_FILE = os.path.join(BASE_DIR, "users.json") # User-Datenbank
|
USERS_FILE = os.path.join(BASE_DIR, "users.json") # User-Datenbank
|
||||||
HISTORY_FILE = os.path.join(BASE_DIR, "history.json") # Aktivitätsverlauf
|
HISTORY_FILE = os.path.join(BASE_DIR, "history.json") # Aktivitätsverlauf
|
||||||
|
|
||||||
APP_VERSION = "6.1.0"
|
APP_VERSION = "6.2.0"
|
||||||
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
<a class="nav-link py-1 px-2 small" href="#help-standort"><i class="ti ti-building-community me-1"></i>Standort</a>
|
<a class="nav-link py-1 px-2 small" href="#help-standort"><i class="ti ti-building-community me-1"></i>Standort</a>
|
||||||
<a class="nav-link py-1 px-2 small" href="#help-screen"><i class="ti ti-devices me-1"></i>Screen</a>
|
<a class="nav-link py-1 px-2 small" href="#help-screen"><i class="ti ti-devices me-1"></i>Screen</a>
|
||||||
<a class="nav-link py-1 px-2 small" href="#help-playlist"><i class="ti ti-list me-1"></i>Playlist</a>
|
<a class="nav-link py-1 px-2 small" href="#help-playlist"><i class="ti ti-list me-1"></i>Playlist</a>
|
||||||
|
<a class="nav-link py-1 px-2 small" href="#help-zeitsteuerung"><i class="ti ti-clock me-1"></i>Zeitsteuerung</a>
|
||||||
<a class="nav-link py-1 px-2 small" href="#help-priority"><i class="ti ti-player-play me-1"></i>Priority</a>
|
<a class="nav-link py-1 px-2 small" href="#help-priority"><i class="ti ti-player-play me-1"></i>Priority</a>
|
||||||
<a class="nav-link py-1 px-2 small" href="#help-actions"><i class="ti ti-external-link me-1"></i>Action Button</a>
|
<a class="nav-link py-1 px-2 small" href="#help-actions"><i class="ti ti-external-link me-1"></i>Action Button</a>
|
||||||
<a class="nav-link py-1 px-2 small" href="#help-voice"><i class="ti ti-robot me-1"></i>Voice Agent</a>
|
<a class="nav-link py-1 px-2 small" href="#help-voice"><i class="ti ti-robot me-1"></i>Voice Agent</a>
|
||||||
@@ -105,12 +106,67 @@
|
|||||||
<li><strong>Sortieren:</strong> Per Drag & Drop die Reihenfolge der Playlist-Einträge ändern (Ziehgriff links).</li>
|
<li><strong>Sortieren:</strong> Per Drag & Drop die Reihenfolge der Playlist-Einträge ändern (Ziehgriff links).</li>
|
||||||
<li><strong>Deaktivieren:</strong> Mit der Checkbox einzelne Einträge temporär ausblenden.</li>
|
<li><strong>Deaktivieren:</strong> Mit der Checkbox einzelne Einträge temporär ausblenden.</li>
|
||||||
<li><strong>Löschen:</strong> Über den roten Papierkorb-Button.</li>
|
<li><strong>Löschen:</strong> Über den roten Papierkorb-Button.</li>
|
||||||
|
<li><strong>Bearbeiten:</strong> Der <em>Edit</em>-Button öffnet einen Dialog. Bei URLs lassen sich dort Adresse, Zoom, Query-Parameter-Passthrough und Zeitsteuerung ändern; bei Bildern, Videos und HTML-Seiten die Zeitsteuerung.</li>
|
||||||
|
<li><strong>Zeitsteuerung:</strong> Einträge können nur zu bestimmten Zeiten laufen – siehe <a href="#help-zeitsteuerung">Zeitsteuerung</a>. Ein Badge <span class="badge bg-info text-white">Zeit</span> in der Playlist-Zeile zeigt an, dass ein Eintrag zeitgesteuert ist; der Tooltip nennt die Einstellung im Klartext.</li>
|
||||||
<li><strong>Query-Parameter-Passthrough:</strong> Per URL-Edit-Button (Stift-Icon) im Playlist-Eintrag kann pro URL gesteuert werden, ob Query-Parameter von der Player-URL an die Playlist-URL übergeben werden. Standardmäßig <strong>aus</strong> – Checkbox "Query-Parameter an diese URL übergeben" aktivieren wenn gewünscht. Ein Link-Icon in der Playlist-Zeile zeigt den Status: <i class="ti ti-link"></i> = an, <i class="ti ti-link-off"></i> = aus.</li>
|
<li><strong>Query-Parameter-Passthrough:</strong> Per URL-Edit-Button (Stift-Icon) im Playlist-Eintrag kann pro URL gesteuert werden, ob Query-Parameter von der Player-URL an die Playlist-URL übergeben werden. Standardmäßig <strong>aus</strong> – Checkbox "Query-Parameter an diese URL übergeben" aktivieren wenn gewünscht. Ein Link-Icon in der Playlist-Zeile zeigt den Status: <i class="ti ti-link"></i> = an, <i class="ti ti-link-off"></i> = aus.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Änderungen an der Playlist werden automatisch gespeichert. Der Player lädt die Seite automatisch neu (via Hash-Prüfung).</p>
|
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Änderungen an der Playlist werden automatisch gespeichert. Der Player lädt die Seite automatisch neu (via Hash-Prüfung).</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header"><h3 class="card-title" id="help-zeitsteuerung"><i class="ti ti-clock me-1"></i>Zeitsteuerung</h3></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Jeder Playlist-Eintrag – URL, Bild, Video oder HTML-Seite – kann nur zu bestimmten Zeiten laufen. Das gilt für die Playlist eines Screens genauso wie für die Priority-Playlist.</p>
|
||||||
|
|
||||||
|
<p><strong>Einstellen:</strong> beim Anlegen im Tab "Medien" unter <em>Zeitsteuerung</em>, oder nachträglich über den <em>Edit</em>-Button des Eintrags. Häkchen bei "Zeitgesteuerte Wiedergabe" setzen und einen Modus wählen:</p>
|
||||||
|
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr><th style="width:110px">Modus</th><th>Bedeutung</th><th>Felder</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Einmalig</strong></td>
|
||||||
|
<td>Läuft einmal in einem zusammenhängenden Zeitraum – auch über mehrere Tage am Stück.</td>
|
||||||
|
<td>Start und Ende jeweils mit Datum <em>und</em> Uhrzeit</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Täglich</strong></td>
|
||||||
|
<td>Läuft jeden Tag im selben Zeitfenster.</td>
|
||||||
|
<td>Von / Bis als Uhrzeit, optional "Gültig von / bis"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Wöchentlich</strong></td>
|
||||||
|
<td>Wie täglich, aber nur an den angehakten Wochentagen.</td>
|
||||||
|
<td>Von / Bis als Uhrzeit, Wochentage, optional "Gültig von / bis"</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p><strong>Gültig von / bis</strong> ist optional und begrenzt, an welchen <em>Tagen</em> das Zeitfenster überhaupt greift. Beide Grenzen lassen sich einzeln setzen und zählen den genannten Tag mit. Ohne Angabe läuft die Wiedergabe unbefristet.</p>
|
||||||
|
|
||||||
|
<p class="mb-1"><strong>Beispiele:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Täglich, 08:00 bis 12:00, ohne Gültigkeitszeitraum → jeden Vormittag, unbefristet.</li>
|
||||||
|
<li>Täglich, 08:00 bis 12:00, gültig vom 10.09. bis 20.09. → nur an diesen elf Tagen.</li>
|
||||||
|
<li>Wöchentlich, 09:00 bis 17:00, Mo/Mi/Fr → nur an diesen drei Tagen während der Bürozeit.</li>
|
||||||
|
<li>Täglich, 22:00 bis 02:00 → abends bis in die Nacht hinein, über Mitternacht hinweg.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mb-1"><strong>Gut zu wissen:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Bei "Täglich" und "Wöchentlich" zählt nur die <em>Uhrzeit</em> – ein Datum wird ausschliesslich unter "Gültig von / bis" angegeben.</li>
|
||||||
|
<li>Bei "Täglich" gilt automatisch jeder Wochentag; die Auswahl der Wochentage erscheint deshalb nur bei "Wöchentlich".</li>
|
||||||
|
<li>Ein Fenster über Mitternacht gehört zum Tag seines <em>Starts</em>: "Mi 22:00–02:00" läuft Mittwochabend bis Donnerstagfrüh.</li>
|
||||||
|
<li>Die Checkbox links im Playlist-Eintrag wirkt weiterhin unabhängig davon – ein deaktivierter Eintrag läuft auch innerhalb seines Zeitfensters nicht.</li>
|
||||||
|
<li>Der Player merkt von selbst, wenn ein Zeitfenster beginnt oder endet, und lädt neu. Ein manueller Eingriff am Screen ist nicht nötig.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Es gilt die Uhrzeit des Servers, nicht die des Players.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header"><h3 class="card-title" id="help-priority"><i class="ti ti-player-play me-1"></i>Priority-Playlist</h3></div>
|
<div class="card-header"><h3 class="card-title" id="help-priority"><i class="ti ti-player-play me-1"></i>Priority-Playlist</h3></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -119,6 +175,7 @@
|
|||||||
<li>Aufruf über <code>/admin/<standort>/priority</code></li>
|
<li>Aufruf über <code>/admin/<standort>/priority</code></li>
|
||||||
<li>Globale Medien und URLs verwalten, ebenfalls per Drag & Drop</li>
|
<li>Globale Medien und URLs verwalten, ebenfalls per Drag & Drop</li>
|
||||||
<li>Priority muss in der Konfiguration aktiviert sein (<code>config.json → priority.enabled</code>)</li>
|
<li>Priority muss in der Konfiguration aktiviert sein (<code>config.json → priority.enabled</code>)</li>
|
||||||
|
<li>Die <a href="#help-zeitsteuerung">Zeitsteuerung</a> steht hier genauso zur Verfügung wie in den Screen-Playlists</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user