Compare commits
11 Commits
9d317ee592
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f754c7f0a3 | ||
|
|
efcbaac5a4 | ||
|
|
96355f6897 | ||
|
|
2c72f48e28 | ||
|
|
302811fe29 | ||
|
|
a09375cb31 | ||
|
|
ac5bf8f739 | ||
|
|
e3afda9a8f | ||
|
|
20055c3b74 | ||
|
|
40ee841553 | ||
|
|
3a2ff20815 |
@@ -154,6 +154,8 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
- User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, MFA einrichten (mit "Aktiv"-Badge wenn MFA eingeschaltet), Userverwaltung (Admin), Abmelden.
|
- User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, MFA einrichten (mit "Aktiv"-Badge wenn MFA eingeschaltet), Userverwaltung (Admin), Abmelden.
|
||||||
- Info-Tab in Screen-Cards: zeigt Client-Info-Tabelle (IP, Browser, Auflösung, zuletzt gesehen) aus dem Heartbeat-JSON für den jeweiligen Screen
|
- Info-Tab in Screen-Cards: zeigt Client-Info-Tabelle (IP, Browser, Auflösung, zuletzt gesehen) aus dem Heartbeat-JSON für den jeweiligen Screen
|
||||||
|
|
||||||
|
- **Medien-Cache**: `player()` gibt jedem Datei-Eintrag `v` (mtime) mit, der Player haengt `?v=<mtime>` an. `send_media()` setzt dann `max-age` 1 Jahr + `immutable`; ohne `?v` bleibt Flasks `no-cache`. Eine ersetzte Datei bekommt so automatisch eine neue URL. Grosse Videos cacht der Browser trotzdem oft nicht (eigener Media-Cache) - dafuer braeuchte es einen Service Worker.
|
||||||
|
|
||||||
## Betrieb / Diagnose
|
## Betrieb / Diagnose
|
||||||
|
|
||||||
- `docker compose up -d` startet nur neu und **baut nicht**. Nach Code-Aenderungen `--build` verwenden, sonst laeuft weiter das alte Image - das hat bei der Fehlersuche mehrfach zu falschen Schluessen gefuehrt.
|
- `docker compose up -d` startet nur neu und **baut nicht**. Nach Code-Aenderungen `--build` verwenden, sonst laeuft weiter das alte Image - das hat bei der Fehlersuche mehrfach zu falschen Schluessen gefuehrt.
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -9,8 +9,8 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
|
|||||||
- Browserbasierter Player (Kiosk-Modus, Chromium, Firefox)
|
- Browserbasierter Player (Kiosk-Modus, Chromium, Firefox)
|
||||||
- Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge
|
- Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge
|
||||||
- **Multi-Standort**: Standorte (sites) gruppieren Screens
|
- **Multi-Standort**: Standorte (sites) gruppieren Screens
|
||||||
- **Priority-Playlist**: pro Standort; die Inhalte laufen auf allen Screens **dieses** Standorts, nicht standortübergreifend
|
- **Priority-Playlist**: pro Standort; die Inhalte laufen auf allen Screens **dieses** Standorts, nicht standortübergreifend. Sie werden zusätzlich als **reine Vorschau** oberhalb jeder Screen-Playlist angezeigt (grau, mit „Prio"-Badge), damit sie beim Bearbeiten nicht übersehen werden – bedient wird weiterhin nur auf der Priority-Seite
|
||||||
- **Willkommensseite**: bis zu 3 Kundenlogos via OpenAI + Brandfetch oder **manueller Upload mit automatischem Freistellen** (transparenter Hintergrund), pro Standort konfigurierbares Hintergrundbild; erscheint auf allen Screens mit `welcome_enabled` (mehrere möglich), optional mit Ablaufdatum
|
- **Willkommensseite** (ohne Login erreichbar): bis zu 3 Kundenlogos via OpenAI + Brandfetch oder **manueller Upload mit automatischem Freistellen** (transparenter Hintergrund), pro Standort konfigurierbares Hintergrundbild; erscheint auf allen Screens mit `welcome_enabled` (mehrere möglich), optional mit Ablaufdatum
|
||||||
- Pro Screen eigene Playlist mit Drag-&-Drop-Reihenfolge
|
- Pro Screen eigene Playlist mit Drag-&-Drop-Reihenfolge
|
||||||
- Auto-Reload bei Playlist-Änderungen
|
- Auto-Reload bei Playlist-Änderungen
|
||||||
- Newsticker pro Screen
|
- Newsticker pro Screen
|
||||||
@@ -22,8 +22,8 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
|
|||||||
- **User-Verwaltung**: Mehrere User mit Rollen (Admin / Superuser / User), E-Mail als Login, Passwort-Hashing (scrypt), Berechtigungen pro Standort
|
- **User-Verwaltung**: Mehrere User mit Rollen (Admin / Superuser / User), E-Mail als Login, Passwort-Hashing (scrypt), Berechtigungen pro Standort
|
||||||
- **Passwort-Workflow**: First-Login-Änderung, Admin-Reset mit temporärem Passwort, Notfall-Login bei fehlendem Hash
|
- **Passwort-Workflow**: First-Login-Änderung, Admin-Reset mit temporärem Passwort, Notfall-Login bei fehlendem Hash
|
||||||
- **MFA (Multi-Faktor)**: Optionale TOTP-Authentifizierung (Google Authenticator, Authy) per User aktivierbar; QR-Code-Scan + Code-Verifikation, 8 Recovery-Codes, Deaktivierung mit Passwort-Bestätigung, **Trusted Device** (Gerät für 1–30 Tage speichern, überspringt MFA beim nächsten Login)
|
- **MFA (Multi-Faktor)**: Optionale TOTP-Authentifizierung (Google Authenticator, Authy) per User aktivierbar; QR-Code-Scan + Code-Verifikation, 8 Recovery-Codes, Deaktivierung mit Passwort-Bestätigung, **Trusted Device** (Gerät für 1–30 Tage speichern, überspringt MFA beim nächsten Login)
|
||||||
- **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 mit Seitennavigation
|
||||||
- **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 (bis zu 1000 Einträge) und im Dashboard seitenweise angezeigt (50 je Seite)
|
||||||
- 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
|
- **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.
|
||||||
@@ -207,7 +207,7 @@ Beides fehlte anfangs und machte die Fehlersuche unnötig schwer.
|
|||||||
| `GET /login` | Admin-Login |
|
| `GET /login` | Admin-Login |
|
||||||
| `GET /logout` | Ausloggen |
|
| `GET /logout` | Ausloggen |
|
||||||
| `GET /admin` | Redirect zum ersten konfigurierten Standort |
|
| `GET /admin` | Redirect zum ersten konfigurierten Standort |
|
||||||
| `GET /admin/dashboard` | Admin Dashboard (Statistiken + Verlauf) |
|
| `GET /admin/dashboard` | Admin Dashboard (Statistiken + Verlauf, `?page=<n>`) |
|
||||||
| `GET /admin/<site>` | Admin-Dashboard für einen Standort |
|
| `GET /admin/<site>` | Admin-Dashboard für einen Standort |
|
||||||
| `GET /admin/<site>/priority` | Priority-Playlist (separate Seite) |
|
| `GET /admin/<site>/priority` | Priority-Playlist (separate Seite) |
|
||||||
| `POST /admin/<site>/update/<screen>` | Allgemeine Screen-Einstellungen speichern (Intervall, Newsticker, Bilder/Videos, stay_on_first) |
|
| `POST /admin/<site>/update/<screen>` | Allgemeine Screen-Einstellungen speichern (Intervall, Newsticker, Bilder/Videos, stay_on_first) |
|
||||||
@@ -226,7 +226,7 @@ Beides fehlte anfangs und machte die Fehlersuche unnötig schwer.
|
|||||||
| `POST /admin/<site>/delete-background` | Hintergrundbild zurücksetzen |
|
| `POST /admin/<site>/delete-background` | Hintergrundbild zurücksetzen |
|
||||||
| `GET /player/<site>/<screen>` | Player-Ansicht |
|
| `GET /player/<site>/<screen>` | Player-Ansicht |
|
||||||
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme (für Auto-Reload) + Heartbeat (Online-Status) |
|
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme (für Auto-Reload) + Heartbeat (Online-Status) |
|
||||||
| `GET /willkommen?site=<site>` | Willkommensseite-Formular (GET + POST) |
|
| `GET /willkommen?site=<site>` | Willkommensseite-Formular (GET + POST) – **ohne Login** |
|
||||||
| `POST /api/customer` | API-Endpunkt für Willkommensseite (JSON) |
|
| `POST /api/customer` | API-Endpunkt für Willkommensseite (JSON) |
|
||||||
| `GET /change-password` | Passwort ändern (First-Login / nach Admin-Reset) |
|
| `GET /change-password` | Passwort ändern (First-Login / nach Admin-Reset) |
|
||||||
| `GET /admin/users` | User-Liste (Admin) |
|
| `GET /admin/users` | User-Liste (Admin) |
|
||||||
@@ -287,6 +287,8 @@ http://localhost:5005/admin/dashboard
|
|||||||
- 5 Statistik-Cards (Standorte, Screens, User, Admins, Superuser) mit ApexCharts-Sparkline + Trendanzeige
|
- 5 Statistik-Cards (Standorte, Screens, User, Admins, Superuser) mit ApexCharts-Sparkline + Trendanzeige
|
||||||
- Aktivitätsverlauf (History) als Liste mit Zeitstempel, Aktion und User
|
- Aktivitätsverlauf (History) als Liste mit Zeitstempel, Aktion und User
|
||||||
- Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden protokolliert
|
- Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden protokolliert
|
||||||
|
- **Seitennavigation**: 50 Einträge je Seite (`HISTORY_PER_PAGE`), Aufruf über `?page=<n>`; aufbewahrt werden 1000 Einträge (`HISTORY_MAX`)
|
||||||
|
- Kennzahlen und Sparklines beziehen sich immer auf die **jüngsten 30 Aktionen** und ändern sich beim Blättern bewusst nicht – sonst würden die Trendpfeile springen, obwohl sich nichts geändert hat
|
||||||
|
|
||||||
### Player-URL
|
### Player-URL
|
||||||
|
|
||||||
@@ -316,6 +318,8 @@ Beispiel:
|
|||||||
http://localhost:5005/willkommen?site=stuttgart
|
http://localhost:5005/willkommen?site=stuttgart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Ohne Login erreichbar:** Die Seite ist bewusst nicht durch `@login_required` geschützt, damit auch Mitarbeitende ohne Benutzerkonto eine Begrüßung erstellen können. Ein angemeldeter User sieht dabei weiterhin nur seine freigegebenen Standorte; ohne Anmeldung stehen alle Standorte zur Auswahl, ein unbekannter Standort führt zu 403. Im Aktivitätsverlauf erscheinen solche Einträge als „nicht angemeldet". Die programmatische Variante `POST /api/customer` bleibt angemeldet.
|
||||||
|
|
||||||
Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `welcome.html` wird in den Playlists aller Zielscreens vorne eingefügt. Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Fallback auf `static/wallpaper.png`).
|
Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `welcome.html` wird in den Playlists aller Zielscreens vorne eingefügt. Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Fallback auf `static/wallpaper.png`).
|
||||||
|
|
||||||
**Zielscreens:** Jeder Screen mit `welcome_enabled` (Schalter „Willkommensseite hier anzeigen" im Tab Einstellungen). Besitzt kein Screen des Standorts den Schlüssel, greift das alte Verhalten und die Seite geht nur in `lobby`.
|
**Zielscreens:** Jeder Screen mit `welcome_enabled` (Schalter „Willkommensseite hier anzeigen" im Tab Einstellungen). Besitzt kein Screen des Standorts den Schlüssel, greift das alte Verhalten und die Seite geht nur in `lobby`.
|
||||||
@@ -474,6 +478,8 @@ Liegt in `config.json` unter `sites.<site>.priority`, die Medien unter `media/<s
|
|||||||
|
|
||||||
Die Einträge laufen auf allen Screens **dieses** Standorts, eingestreut zwischen den normalen Playlist-Einträgen. Ein anderer Standort ist davon nicht betroffen.
|
Die Einträge laufen auf allen Screens **dieses** Standorts, eingestreut zwischen den normalen Playlist-Einträgen. Ein anderer Standort ist davon nicht betroffen.
|
||||||
|
|
||||||
|
**Vorschau in den Screen-Playlisten:** Ist Priority für den Standort aktiviert, erscheinen die Einträge zusätzlich über jeder Screen-Playlist – ausgegraut, mit rotem „Prio"-Badge und ohne Bedienelemente. Sie liegen bewusst in einer **eigenen `<ul>`**, damit das Speichern der Reihenfolge (`savePlaylist()`) sie nicht mit einsammelt und in die Screen-Playlist schreibt.
|
||||||
|
|
||||||
> Früher lag die Priority-Playlist global unter `config.priority` und wirkte auf *alle* Standorte. `get_priority_config()` übernimmt einen solchen Altbestand beim ersten Zugriff einmalig in den jeweiligen Standort.
|
> Früher lag die Priority-Playlist global unter `config.priority` und wirkte auf *alle* Standorte. `get_priority_config()` übernimmt einen solchen Altbestand beim ersten Zugriff einmalig in den jeweiligen Standort.
|
||||||
|
|
||||||
Die Ordnernamen `priority` und `logos` sind unter `media/<site>/` **reserviert** und werden bei der Screen-Erkennung übersprungen – sonst würden sie als Screens in der Oberfläche auftauchen.
|
Die Ordnernamen `priority` und `logos` sind unter `media/<site>/` **reserviert** und werden bei der Screen-Erkennung übersprungen – sonst würden sie als Screens in der Oberfläche auftauchen.
|
||||||
@@ -490,7 +496,7 @@ Ein Eintrag ist entweder ein String (Dateiname oder URL, Altbestand) oder ein Di
|
|||||||
{
|
{
|
||||||
"url": "https://example.com",
|
"url": "https://example.com",
|
||||||
"zoom": 1.0, // Zoom-Faktor im Player
|
"zoom": 1.0, // Zoom-Faktor im Player
|
||||||
"enabled": true, // Checkbox in der Playlist
|
"enabled": true, // Checkbox in der Playlist (nur der Schalter, nicht das Zeitfenster)
|
||||||
"passthrough_params": false, // Query-Parameter der Player-URL durchreichen
|
"passthrough_params": false, // Query-Parameter der Player-URL durchreichen
|
||||||
"schedule": { ... } // optional, siehe unten
|
"schedule": { ... } // optional, siehe unten
|
||||||
}
|
}
|
||||||
@@ -535,6 +541,9 @@ Optional pro Eintrag. Ohne `schedule` läuft der Eintrag immer (sofern `enabled`
|
|||||||
- Unlesbare oder unvollständige Zeitangaben blenden den Eintrag **nicht** aus – ein Konfigurationsfehler soll nicht dazu führen, dass stillschweigend nichts mehr läuft.
|
- 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.
|
- `_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.
|
- Der Aktiv-Zustand fliesst in `/playlist/<site>/<screen>/hash` ein, damit der Player beim Öffnen und Schliessen eines Zeitfensters von selbst neu lädt.
|
||||||
|
- **Checkbox und Zeitfenster sind getrennt:** Die Checkbox in der Playlist-Zeile zeigt ausschliesslich den gespeicherten Schalter `enabled`, nicht den momentanen Zeitfenster-Zustand. Sie bleibt also gesetzt, auch wenn ein zeitgesteuerter Eintrag gerade nicht läuft. Ob er gerade läuft, sagt das „Zeit"-Badge: blau = läuft, grau = ausserhalb des Fensters (Tooltip nennt den Grund). Abgespielt wird nur, was **beides** erfüllt – Haken gesetzt *und* im Zeitfenster.
|
||||||
|
|
||||||
|
> Bis Version 6.2.4 zeigte die Checkbox den ausgewerteten Zustand. Wurde die Playlist ausserhalb des Zeitfensters gespeichert, schrieb die Oberfläche `enabled: false` in die Konfiguration und der Eintrag war dauerhaft abgeschaltet. Der Fehler ist behoben, wirkt aber nicht rückwirkend: bereits betroffene Einträge müssen einmal von Hand wieder aktiviert werden.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -552,6 +561,7 @@ Optional pro Eintrag. Ohne `schedule` läuft der Eintrag immer (sofern `enabled`
|
|||||||
- `_in_validity_range(schedule, day)` – optionalen Gültigkeitszeitraum prüfen
|
- `_in_validity_range(schedule, day)` – optionalen Gültigkeitszeitraum prüfen
|
||||||
- `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips
|
- `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips
|
||||||
- `load_priority_files(site)` – Priority-Playlist eines Standorts + Dateien laden
|
- `load_priority_files(site)` – Priority-Playlist eines Standorts + Dateien laden
|
||||||
|
- `_load_history()` / `get_history(limit, offset)` / `get_history_count()` – Aktivitätsverlauf lesen, seitenweise ausschneiden, zählen
|
||||||
- `get_priority_config(cfg, site)` – Priority-Konfiguration des Standorts (übernimmt einmalig einen globalen Altbestand)
|
- `get_priority_config(cfg, site)` – Priority-Konfiguration des Standorts (übernimmt einmalig einen globalen Altbestand)
|
||||||
- `priority_dir(site)` – Pfad zu `media/<site>/priority/`
|
- `priority_dir(site)` – Pfad zu `media/<site>/priority/`
|
||||||
- `welcome_target_screens(config, site)` – Zielscreens der Willkommensseite (Fallback `lobby`)
|
- `welcome_target_screens(config, site)` – Zielscreens der Willkommensseite (Fallback `lobby`)
|
||||||
@@ -581,6 +591,7 @@ Wird über `localStorage("signage-theme")` persistiert. Umschalt-Button im Heade
|
|||||||
## Sicherheit
|
## Sicherheit
|
||||||
|
|
||||||
- Admin-Bereich per Flask-Login geschützt (E-Mail + Passwort)
|
- Admin-Bereich per Flask-Login geschützt (E-Mail + Passwort)
|
||||||
|
- **Ohne Login erreichbar** sind: Player und Medien (die Abspielgeräte melden sich nicht an) sowie das Formular der Willkommensseite. Letzteres legt Dateien an, schreibt in die Konfiguration und löst kostenpflichtige Logo-Abfragen aus – bei einer aus dem Internet erreichbaren Instanz empfiehlt sich eine Einschränkung auf das interne Netz im Reverse Proxy
|
||||||
- Optionale MFA (TOTP) per User aktivierbar (Google Authenticator, Authy)
|
- Optionale MFA (TOTP) per User aktivierbar (Google Authenticator, Authy)
|
||||||
- 8 Recovery-Codes pro User, einmalig bei Aktivierung angezeigt
|
- 8 Recovery-Codes pro User, einmalig bei Aktivierung angezeigt
|
||||||
- Passwörter gehasht (scrypt) in `users.json`
|
- Passwörter gehasht (scrypt) in `users.json`
|
||||||
|
|||||||
104
app.py
104
app.py
@@ -50,8 +50,10 @@ MEDIA_DIR = os.path.join(BASE_DIR, "media") # Medien-Dateien je Standort/S
|
|||||||
CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # Persistente Konfiguration
|
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
|
||||||
|
HISTORY_MAX = 1000 # so viele Einträge werden aufbewahrt
|
||||||
|
HISTORY_PER_PAGE = 50 # Einträge je Seite im Dashboard-Verlauf
|
||||||
|
|
||||||
APP_VERSION = "6.2.7"
|
APP_VERSION = "6.2.12"
|
||||||
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -183,24 +185,33 @@ def add_history_entry(action, detail, user_email):
|
|||||||
except (json.JSONDecodeError, OSError):
|
except (json.JSONDecodeError, OSError):
|
||||||
history = []
|
history = []
|
||||||
history.insert(0, entry) # Neueste zuerst
|
history.insert(0, entry) # Neueste zuerst
|
||||||
# Maximal 200 Einträge behalten
|
# Maximal HISTORY_MAX Einträge behalten
|
||||||
history = history[:200]
|
history = history[:HISTORY_MAX]
|
||||||
with open(HISTORY_FILE, "w") as f:
|
with open(HISTORY_FILE, "w") as f:
|
||||||
json.dump(history, f, indent=2)
|
json.dump(history, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def get_history(limit=50):
|
def _load_history():
|
||||||
"""Gibt die letzten history-Einträge zurück."""
|
"""Liest den kompletten Verlauf; bei defekter Datei eine leere Liste."""
|
||||||
if not os.path.exists(HISTORY_FILE):
|
if not os.path.exists(HISTORY_FILE):
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
with open(HISTORY_FILE) as f:
|
with open(HISTORY_FILE) as f:
|
||||||
history = json.load(f)
|
return json.load(f)
|
||||||
return history[:limit]
|
|
||||||
except (json.JSONDecodeError, OSError):
|
except (json.JSONDecodeError, OSError):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_history(limit=50, offset=0):
|
||||||
|
"""Gibt einen Ausschnitt des Verlaufs zurück (neueste zuerst)."""
|
||||||
|
return _load_history()[offset:offset + limit]
|
||||||
|
|
||||||
|
|
||||||
|
def get_history_count():
|
||||||
|
"""Anzahl aller gespeicherten Verlaufseintraege (fuer die Seitennavigation)."""
|
||||||
|
return len(_load_history())
|
||||||
|
|
||||||
|
|
||||||
def get_screen_config(cfg, site, screen):
|
def get_screen_config(cfg, site, screen):
|
||||||
"""Holt die Konfiguration für einen bestimmten Screen.
|
"""Holt die Konfiguration für einen bestimmten Screen.
|
||||||
Erzeugt automatisch fehlende Dict-Ebenen (sites → site → screens → screen)."""
|
Erzeugt automatisch fehlende Dict-Ebenen (sites → site → screens → screen)."""
|
||||||
@@ -942,7 +953,6 @@ def mfa_disable():
|
|||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@app.route("/willkommen", methods=["GET", "POST"])
|
@app.route("/willkommen", methods=["GET", "POST"])
|
||||||
@app.route("/customer", methods=["GET", "POST"])
|
@app.route("/customer", methods=["GET", "POST"])
|
||||||
@login_required
|
|
||||||
def add_customer():
|
def add_customer():
|
||||||
"""
|
"""
|
||||||
Formular für die Willkommensseite:
|
Formular für die Willkommensseite:
|
||||||
@@ -960,14 +970,21 @@ def add_customer():
|
|||||||
logo_hinweise = []
|
logo_hinweise = []
|
||||||
site = request.args.get("site") or request.form.get("site") or ""
|
site = request.args.get("site") or request.form.get("site") or ""
|
||||||
|
|
||||||
# Fallback auf ersten für den User zugänglichen Standort
|
# Diese Seite ist bewusst ohne Login erreichbar: Am Empfang soll eine
|
||||||
if not site:
|
# Begruessung auch ohne Benutzerkonto erstellt werden koennen. Ein
|
||||||
config = load_config()
|
# angemeldeter User sieht weiterhin nur seine freigegebenen Standorte.
|
||||||
sites = get_accessible_sites(config, current_user)
|
config = load_config()
|
||||||
site = sites[0] if sites else ""
|
if current_user.is_authenticated:
|
||||||
|
erlaubte_sites = get_accessible_sites(config, current_user)
|
||||||
|
else:
|
||||||
|
erlaubte_sites = get_site_list(config)
|
||||||
|
|
||||||
# Prüfen, ob der User Zugriff auf den Standort hat
|
# Fallback auf den ersten zugaenglichen Standort
|
||||||
if site and not current_user.can_access_site(site):
|
if not site:
|
||||||
|
site = erlaubte_sites[0] if erlaubte_sites else ""
|
||||||
|
|
||||||
|
# Zugriff auf den Standort pruefen
|
||||||
|
if site and site not in erlaubte_sites:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
remember_site(site)
|
remember_site(site)
|
||||||
@@ -1072,7 +1089,7 @@ def add_customer():
|
|||||||
"contacts": list(contacts)
|
"contacts": list(contacts)
|
||||||
}
|
}
|
||||||
save_config(config)
|
save_config(config)
|
||||||
add_history_entry("customer_added", f"Kunden ({', '.join(valid_names)}) zur Willkommensseite an Standort '{site}' hinzugefügt (Screens: {', '.join(targets)})", current_user.email)
|
add_history_entry("customer_added", f"Kunden ({', '.join(valid_names)}) zur Willkommensseite an Standort '{site}' hinzugefügt (Screens: {', '.join(targets)})", current_user.email if current_user.is_authenticated else "nicht angemeldet")
|
||||||
success = (f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt "
|
success = (f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt "
|
||||||
f"– angezeigt auf: {', '.join(targets)}")
|
f"– angezeigt auf: {', '.join(targets)}")
|
||||||
if valid_until:
|
if valid_until:
|
||||||
@@ -1191,17 +1208,41 @@ def api_add_customer():
|
|||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Medien ausliefern (statische Dateien)
|
# Medien ausliefern (statische Dateien)
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
# Traegt die URL ein ?v=<mtime>, darf der Player die Datei ein Jahr lang ohne
|
||||||
|
# Rueckfrage aus dem Cache nehmen: eine geaenderte Datei bekommt eine neue URL.
|
||||||
|
# Ohne ?v bleibt es beim Flask-Standard (no-cache + ETag), sonst saehe ein
|
||||||
|
# unversionierter Aufruf nach dem Ersetzen einer Datei weiter die alte Fassung.
|
||||||
|
MEDIA_CACHE_MAX_AGE = 365 * 24 * 3600
|
||||||
|
|
||||||
|
|
||||||
|
def media_version(path):
|
||||||
|
"""Liefert die mtime einer Datei als Versionskennung fuer ?v=, 0 wenn sie fehlt."""
|
||||||
|
try:
|
||||||
|
return int(os.path.getmtime(path))
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def send_media(directory, filename):
|
||||||
|
"""send_from_directory mit langem Cache, sofern die URL versioniert ist."""
|
||||||
|
if not request.args.get("v"):
|
||||||
|
return send_from_directory(directory, filename)
|
||||||
|
resp = send_from_directory(directory, filename, max_age=MEDIA_CACHE_MAX_AGE)
|
||||||
|
resp.cache_control.immutable = True
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@app.route("/media/<site>/priority/<path:filename>")
|
@app.route("/media/<site>/priority/<path:filename>")
|
||||||
def media_priority(site, filename):
|
def media_priority(site, filename):
|
||||||
"""Liefert die Priority-Medien eines Standorts aus media/<site>/priority/ aus."""
|
"""Liefert die Priority-Medien eines Standorts aus media/<site>/priority/ aus."""
|
||||||
return send_from_directory(priority_dir(site), filename)
|
return send_media(priority_dir(site), filename)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/media/<site>/<screen>/<path:filename>")
|
@app.route("/media/<site>/<screen>/<path:filename>")
|
||||||
def media(site, screen, filename):
|
def media(site, screen, filename):
|
||||||
"""Liefert Medien-Dateien für einen bestimmten Screen aus."""
|
"""Liefert Medien-Dateien für einen bestimmten Screen aus."""
|
||||||
path = os.path.join(MEDIA_DIR, site, screen)
|
path = os.path.join(MEDIA_DIR, site, screen)
|
||||||
return send_from_directory(path, filename)
|
return send_media(path, filename)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@@ -1262,7 +1303,8 @@ def player(site, screen):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if item_name and os.path.exists(os.path.join(folder, item_name)) and allowed_file(item_name):
|
if item_name and os.path.exists(os.path.join(folder, item_name)) and allowed_file(item_name):
|
||||||
normal_files.append({"kind": "file", "name": item_name, "enabled": True})
|
normal_files.append({"kind": "file", "name": item_name, "enabled": True,
|
||||||
|
"v": media_version(os.path.join(folder, item_name))})
|
||||||
|
|
||||||
existing_file_names = {
|
existing_file_names = {
|
||||||
item["name"] for item in normal_files if item["kind"] == "file"
|
item["name"] for item in normal_files if item["kind"] == "file"
|
||||||
@@ -1274,7 +1316,8 @@ def player(site, screen):
|
|||||||
if f in existing_file_names or f.startswith("._"):
|
if f in existing_file_names or f.startswith("._"):
|
||||||
continue
|
continue
|
||||||
if allowed_file(f):
|
if allowed_file(f):
|
||||||
normal_files.append({"kind": "file", "name": f})
|
normal_files.append({"kind": "file", "name": f,
|
||||||
|
"v": media_version(os.path.join(folder, f))})
|
||||||
|
|
||||||
# ─── Priority-Playlist (global) ───
|
# ─── Priority-Playlist (global) ───
|
||||||
priority_cfg = get_priority_config(config, site)
|
priority_cfg = get_priority_config(config, site)
|
||||||
@@ -1294,7 +1337,8 @@ def player(site, screen):
|
|||||||
|
|
||||||
file_path = os.path.join(priority_dir(site), item_name)
|
file_path = os.path.join(priority_dir(site), item_name)
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
prio_files.append({"kind": "file", "name": item_name, "enabled": True})
|
prio_files.append({"kind": "file", "name": item_name, "enabled": True,
|
||||||
|
"v": media_version(file_path)})
|
||||||
else:
|
else:
|
||||||
prio_files = []
|
prio_files = []
|
||||||
|
|
||||||
@@ -2074,8 +2118,19 @@ def admin_dashboard():
|
|||||||
admin_count = sum(1 for u in users_data.values() if u.get("role") == "admin")
|
admin_count = sum(1 for u in users_data.values() if u.get("role") == "admin")
|
||||||
superuser_count = sum(1 for u in users_data.values() if u.get("role") == "superuser")
|
superuser_count = sum(1 for u in users_data.values() if u.get("role") == "superuser")
|
||||||
|
|
||||||
|
# Kennzahlen und Sparklines bleiben bewusst am jüngsten Ausschnitt haengen,
|
||||||
|
# damit sie sich beim Blaettern im Verlauf nicht veraendern.
|
||||||
history = get_history(30)
|
history = get_history(30)
|
||||||
|
|
||||||
|
gesamt = get_history_count()
|
||||||
|
seiten = max(1, (gesamt + HISTORY_PER_PAGE - 1) // HISTORY_PER_PAGE)
|
||||||
|
try:
|
||||||
|
seite = int(request.args.get("page", 1))
|
||||||
|
except ValueError:
|
||||||
|
seite = 1
|
||||||
|
seite = min(max(seite, 1), seiten)
|
||||||
|
history_page = get_history(HISTORY_PER_PAGE, (seite - 1) * HISTORY_PER_PAGE)
|
||||||
|
|
||||||
# Sparkline-Daten aus dem Verlauf berechnen
|
# Sparkline-Daten aus dem Verlauf berechnen
|
||||||
def build_sparkline(entries, creates, deletes):
|
def build_sparkline(entries, creates, deletes):
|
||||||
seq = list(reversed(entries))
|
seq = list(reversed(entries))
|
||||||
@@ -2124,7 +2179,12 @@ def admin_dashboard():
|
|||||||
user_trend=user_trend,
|
user_trend=user_trend,
|
||||||
admin_trend=admin_trend,
|
admin_trend=admin_trend,
|
||||||
superuser_trend=superuser_trend,
|
superuser_trend=superuser_trend,
|
||||||
history=history,
|
history=history_page,
|
||||||
|
history_page=seite,
|
||||||
|
history_pages=seiten,
|
||||||
|
history_total=gesamt,
|
||||||
|
history_from=(seite - 1) * HISTORY_PER_PAGE + 1 if gesamt else 0,
|
||||||
|
history_to=min(seite * HISTORY_PER_PAGE, gesamt),
|
||||||
site_list=get_accessible_sites(cfg, current_user),
|
site_list=get_accessible_sites(cfg, current_user),
|
||||||
current_site=current_site_for(cfg, current_user),
|
current_site=current_site_for(cfg, current_user),
|
||||||
version=APP_VERSION,
|
version=APP_VERSION,
|
||||||
|
|||||||
BIN
docs/Simple-Signage-Ueberblick.pdf
Normal file
BIN
docs/Simple-Signage-Ueberblick.pdf
Normal file
Binary file not shown.
4
docs/cancom.svg
Executable file
4
docs/cancom.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- Created with xTool Creative Space (https://www.xtool.com/pages/software) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xcs="https://www.xtool.com/pages/software" version="1.1" preserveAspectRatio="xMinYMin meet" width="61.682394106284505mm" height="9.909010347638684mm" viewBox="136.55880294685778 209.2954948261807 61.682394106284505 9.909010347638684" xcs:version="2.7.22"><style></style>
|
||||||
|
<path transform="matrix(0.158038,0,0,0.158038,133.2558,163.322122)" stroke="none" fill="#ffffff" data-view-type="laser" d="M125.9,352.1h18.9L119,291.7h-16.2l-25,60.4h18.2l14.6-38.9L125.9,352.1z M323.6,322 c0,8.8-6.1,16.4-15.4,16.4c-9.1,0-15.4-7.6-15.4-16.4s6.1-16.4,15.2-16.4C317,305.6,323.6,313.4,323.6,322 M340.5,322.3 c0-17.4-13.4-31.3-32.3-31.3c-18.7,0-32.3,14.1-32.3,31.3c0,17.4,13.4,31.3,32.3,31.3C326.9,353.6,340.5,339.4,340.5,322.3 M230.4,322.5c0-8.8,6.1-16.4,14.9-16.4c3,0,5.8,0.8,8.1,2l6.6-13.4c-3-2-8.1-3.8-14.9-3.8c-18.2,0-31.6,14.1-31.6,31.3 c0,18.2,12.6,31.1,31.6,31.1c17.2,0,26-10.6,28.8-21l-13.6-5.6c-1.8,5.8-6.3,11.6-14.4,11.6C236.5,338.4,230.4,331.4,230.4,322.5 M38,322.5c0-8.8,6.1-16.4,14.9-16.4c3,0,5.8,0.8,8.1,2l6.6-13.4c-3-2-8.1-3.8-14.9-3.8c-18.4,0-31.8,14.1-31.8,31.3 c0,18.2,12.6,31.1,31.6,31.1c17.2,0,26-10.6,28.8-21l-13.6-5.6c-1.8,5.8-6.3,11.6-14.4,11.6C44,338.4,38,331.4,38,322.5 M191.8,352.1h14.4v-59.8H190v33.1l-25-33.1h-15.4v60.1h16.2v-34.1L191.8,352.1z M411.2,352.1v-59.8h-17.7l-14.4,23.2l-14.4-23.2 h-17.7v60.1h16.4V318l15.4,23.2h0.3l15.4-23.5v34.6L411.2,352.1L411.2,352.1z" fill-rule="nonzero"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
383
docs/onepager.html
Normal file
383
docs/onepager.html
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>CANCOM Simple Signage – Überblick</title>
|
||||||
|
<style>
|
||||||
|
@page { size: A4; margin: 12mm 13mm 11mm 13mm; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--rot: #DA002D;
|
||||||
|
--dunkel: #2b2f36;
|
||||||
|
--grau: #5b6472;
|
||||||
|
--hellgrau: #f2f4f7;
|
||||||
|
--linie: #dfe3e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0; padding: 0;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 9.1pt;
|
||||||
|
line-height: 1.42;
|
||||||
|
color: var(--dunkel);
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seite { page-break-after: always; }
|
||||||
|
.seite:last-child { page-break-after: auto; }
|
||||||
|
|
||||||
|
/* ── Kopf ── */
|
||||||
|
.kopf {
|
||||||
|
display: flex; align-items: center; gap: 10mm;
|
||||||
|
border-bottom: 2.5pt solid var(--rot);
|
||||||
|
padding-bottom: 3.5mm; margin-bottom: 5mm;
|
||||||
|
}
|
||||||
|
.logobox { background: var(--dunkel); padding: 2.5mm 4mm; border-radius: 1.2mm; }
|
||||||
|
.logobox img { height: 6.5mm; display: block; }
|
||||||
|
.kopf .titel { margin-left: auto; text-align: right; }
|
||||||
|
.kopf h1 { font-size: 16pt; margin: 0; letter-spacing: -0.2pt; }
|
||||||
|
.kopf .unter { font-size: 8.4pt; color: var(--grau); margin-top: 0.8mm; }
|
||||||
|
|
||||||
|
.kopf-klein {
|
||||||
|
display: flex; align-items: baseline;
|
||||||
|
border-bottom: 1.2pt solid var(--linie);
|
||||||
|
padding-bottom: 2.5mm; margin-bottom: 4.5mm;
|
||||||
|
}
|
||||||
|
.kopf-klein h2 { font-size: 12.5pt; margin: 0; color: var(--rot); }
|
||||||
|
.kopf-klein .seitezahl { margin-left: auto; font-size: 8pt; color: var(--grau); }
|
||||||
|
|
||||||
|
/* ── Einleitung ── */
|
||||||
|
.intro {
|
||||||
|
background: var(--hellgrau);
|
||||||
|
border-left: 3pt solid var(--rot);
|
||||||
|
padding: 3mm 4mm; margin-bottom: 5mm;
|
||||||
|
font-size: 9.4pt;
|
||||||
|
}
|
||||||
|
.intro strong { color: var(--rot); }
|
||||||
|
|
||||||
|
/* ── Raster ── */
|
||||||
|
.raster { display: grid; grid-template-columns: 1fr 1fr; gap: 4mm 6mm; }
|
||||||
|
.raster-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 3mm 5mm; }
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 9.6pt; margin: 0 0 1.2mm 0;
|
||||||
|
color: var(--dunkel);
|
||||||
|
display: flex; align-items: center; gap: 2mm;
|
||||||
|
}
|
||||||
|
h3::before {
|
||||||
|
content: ""; display: inline-block;
|
||||||
|
width: 3mm; height: 3mm; border-radius: 0.6mm;
|
||||||
|
background: var(--rot); flex: none;
|
||||||
|
}
|
||||||
|
h3.grau::before { background: var(--grau); }
|
||||||
|
|
||||||
|
p { margin: 0 0 1.6mm 0; }
|
||||||
|
.block { margin-bottom: 3.6mm; }
|
||||||
|
ul { margin: 0.5mm 0 0 0; padding-left: 4.2mm; }
|
||||||
|
li { margin-bottom: 0.7mm; }
|
||||||
|
.klein { font-size: 8.2pt; color: var(--grau); }
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
font-size: 8.2pt;
|
||||||
|
background: var(--hellgrau);
|
||||||
|
padding: 0.3mm 1mm; border-radius: 0.8mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Ablauf-Schritte ── */
|
||||||
|
.schritte { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3mm; margin-bottom: 5mm; }
|
||||||
|
.schritt {
|
||||||
|
border: 0.8pt solid var(--linie); border-radius: 1.5mm;
|
||||||
|
padding: 2.5mm 3mm; position: relative;
|
||||||
|
}
|
||||||
|
.schritt .nr {
|
||||||
|
display: inline-block; width: 5mm; height: 5mm; line-height: 5mm;
|
||||||
|
text-align: center; border-radius: 50%;
|
||||||
|
background: var(--rot); color: #fff;
|
||||||
|
font-size: 8pt; font-weight: bold; margin-bottom: 1.5mm;
|
||||||
|
}
|
||||||
|
.schritt b { display: block; font-size: 9pt; margin-bottom: 0.8mm; }
|
||||||
|
.schritt span { font-size: 8.2pt; color: var(--grau); line-height: 1.35; display: block; }
|
||||||
|
|
||||||
|
/* ── Tabelle ── */
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 8.5pt; }
|
||||||
|
th, td { text-align: left; padding: 1.4mm 2mm; border-bottom: 0.6pt solid var(--linie); vertical-align: top; }
|
||||||
|
th { background: var(--hellgrau); font-size: 8.2pt; }
|
||||||
|
td:first-child { white-space: nowrap; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ── Hinweiskasten ── */
|
||||||
|
.kasten {
|
||||||
|
border: 0.8pt solid var(--linie); border-radius: 1.5mm;
|
||||||
|
padding: 3mm 3.5mm; background: #fcfcfd;
|
||||||
|
}
|
||||||
|
.kasten h3 { margin-bottom: 1.8mm; }
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
border-left: 3pt solid #f59f00; background: #fff9e6;
|
||||||
|
padding: 2.5mm 3.5mm; border-radius: 0 1.5mm 1.5mm 0;
|
||||||
|
margin-bottom: 4mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block; padding: 0.2mm 1.4mm; border-radius: 0.8mm;
|
||||||
|
font-size: 7.6pt; font-weight: 600; vertical-align: 0.3mm;
|
||||||
|
}
|
||||||
|
.b-rot { background: var(--rot); color: #fff; }
|
||||||
|
.b-blau { background: #206bc4; color: #fff; }
|
||||||
|
.b-grau { background: #e4e7ec; color: #555e6d; }
|
||||||
|
.b-gruen { background: #2fb344; color: #fff; }
|
||||||
|
|
||||||
|
/* ── Fuß ── */
|
||||||
|
.fuss {
|
||||||
|
border-top: 0.6pt solid var(--linie);
|
||||||
|
margin-top: 4mm; padding-top: 1.5mm;
|
||||||
|
font-size: 7.4pt; color: var(--grau);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.fuss .rechts { margin-left: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ══════════════ VORDERSEITE ══════════════ -->
|
||||||
|
<div class="seite">
|
||||||
|
|
||||||
|
<div class="kopf">
|
||||||
|
<span class="logobox"><img src="cancom.svg" alt="CANCOM"></span>
|
||||||
|
<div class="titel">
|
||||||
|
<h1>Simple Signage</h1>
|
||||||
|
<div class="unter">Infobildschirme selbst befüllen – ohne IT-Kenntnisse</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="intro">
|
||||||
|
<strong>Was ist das?</strong> Simple Signage zeigt Bilder, Videos, Webseiten und Begrüßungen auf den
|
||||||
|
Info-Bildschirmen im Haus. Sie bestimmen im Browser, was wann auf welchem Bildschirm läuft – die
|
||||||
|
Änderung ist binnen Sekunden auf dem Schirm. Es wird nichts installiert: Sie brauchen nur einen Browser,
|
||||||
|
Ihre E-Mail-Adresse und Ihr Passwort.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="schritte">
|
||||||
|
<div class="schritt">
|
||||||
|
<span class="nr">1</span>
|
||||||
|
<b>Anmelden</b>
|
||||||
|
<span>Browser öffnen, E-Mail und Passwort eingeben. Sie sehen nur die Standorte, für die Sie freigeschaltet sind.</span>
|
||||||
|
</div>
|
||||||
|
<div class="schritt">
|
||||||
|
<span class="nr">2</span>
|
||||||
|
<b>Bildschirm wählen</b>
|
||||||
|
<span>Jeder Bildschirm („Screen“) hat eine eigene Kachel mit seiner Abspielliste.</span>
|
||||||
|
</div>
|
||||||
|
<div class="schritt">
|
||||||
|
<span class="nr">3</span>
|
||||||
|
<b>Inhalte einstellen</b>
|
||||||
|
<span>Bilder oder Videos hochladen, Webseiten als Link eintragen, Reihenfolge per Maus ziehen.</span>
|
||||||
|
</div>
|
||||||
|
<div class="schritt">
|
||||||
|
<span class="nr">4</span>
|
||||||
|
<b>Fertig</b>
|
||||||
|
<span>Es gibt keinen Speichern-Knopf für die Liste – der Bildschirm übernimmt die Änderung von selbst.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="raster">
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Standort und Bildschirm</h3>
|
||||||
|
<p>Ein <b>Standort</b> ist ein Haus oder eine Niederlassung, zum Beispiel Stuttgart. Darin liegen die
|
||||||
|
einzelnen <b>Bildschirme</b> – Empfang, Kantine, Flur. Oben im Kopf wechseln Sie den Standort.</p>
|
||||||
|
<p>Jeder Bildschirm hat seine eigene Abspielliste. Was Sie am Empfang ändern, betrifft die Kantine nicht.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Die Abspielliste (Playlist)</h3>
|
||||||
|
<p>Die Liste wird von oben nach unten durchlaufen und beginnt danach von vorn. Pro Eintrag haben Sie:</p>
|
||||||
|
<ul>
|
||||||
|
<li><b>Haken</b> – Eintrag läuft mit oder pausiert</li>
|
||||||
|
<li><b>Griff links</b> – mit der Maus an eine andere Stelle ziehen</li>
|
||||||
|
<li><b>Stift</b> – Einstellungen des Eintrags ändern</li>
|
||||||
|
<li><b>Papierkorb</b> – Eintrag und Datei entfernen</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Was Sie anzeigen können</h3>
|
||||||
|
<table>
|
||||||
|
<tr><td>Bilder</td><td>JPG und PNG, am besten in Bildschirmgröße (1920×1080)</td></tr>
|
||||||
|
<tr><td>Videos</td><td>MP4, laufen ohne Ton</td></tr>
|
||||||
|
<tr><td>Webseiten</td><td>Als Adresse eintragen, z. B. ein Dashboard oder Intranet</td></tr>
|
||||||
|
<tr><td>Begrüßung</td><td>Wird im Formular erzeugt, siehe Rückseite</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Zeitsteuerung</h3>
|
||||||
|
<p>Jeder Eintrag kann auf ein Zeitfenster gelegt werden – <b>einmalig</b> (von Datum bis Datum),
|
||||||
|
<b>täglich</b> oder <b>wöchentlich</b> an bestimmten Wochentagen. Außerhalb des Fensters wird er
|
||||||
|
einfach übersprungen.</p>
|
||||||
|
<p class="klein">Typisch: Das Kantinenmenü nur von 11 bis 14 Uhr, der Veranstaltungshinweis nur bis zum Termin.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Haken und Zeit sind zweierlei</h3>
|
||||||
|
<p>Der <b>Haken</b> ist Ihr Schalter: gesetzt heißt „soll laufen“. Er bleibt gesetzt, auch wenn gerade
|
||||||
|
keine Sendezeit ist.</p>
|
||||||
|
<p>Das <b>Zeit-Badge</b> sagt, ob es gerade so weit ist:
|
||||||
|
<span class="badge b-blau">Zeit</span> läuft jetzt ·
|
||||||
|
<span class="badge b-grau">Zeit</span> wartet auf sein Fenster.</p>
|
||||||
|
<p class="klein">Zu sehen ist ein Eintrag nur, wenn beides zutrifft: Haken gesetzt <i>und</i> im Zeitfenster.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Läuft der Bildschirm?</h3>
|
||||||
|
<p>Neben jeder Bildschirm-Kachel steht <span class="badge b-gruen">Online</span> oder
|
||||||
|
<span class="badge b-rot">Offline</span>. Grün heißt: Das Gerät hat sich in der letzten Minute gemeldet.</p>
|
||||||
|
<p class="klein">Bleibt es rot, ist meist das Gerät aus oder vom Netz getrennt – im Tab „Info“ stehen die
|
||||||
|
letzten Angaben dazu.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kasten" style="margin-top:1mm;">
|
||||||
|
<h3 class="grau">So sieht ein typischer Arbeitstag damit aus</h3>
|
||||||
|
<div class="raster-3" style="margin-top:1.5mm;">
|
||||||
|
<div>
|
||||||
|
<b>Morgens: Besuch kündigt sich an</b>
|
||||||
|
<p class="klein">Unter „Willkommen“ den Firmennamen eintragen, Datum bis wann, fertig. Der Empfangsbildschirm
|
||||||
|
begrüßt den Gast, danach verschwindet die Seite von selbst.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Mittags: Speiseplan</b>
|
||||||
|
<p class="klein">Das Plakat als Bild hochladen und auf täglich 11–14 Uhr stellen. Ab dann kümmert es sich
|
||||||
|
jeden Tag von allein.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Kurzfristig: Hinweis für alle</b>
|
||||||
|
<p class="klein">„Aufzug außer Betrieb“ in die Priority-Liste – der Hinweis läuft sofort auf allen
|
||||||
|
Bildschirmen des Standorts mit.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fuss">
|
||||||
|
<div>CANCOM Simple Signage – Kurzüberblick für Anwender</div>
|
||||||
|
<div class="rechts">Vorderseite · bitte wenden →</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════ RÜCKSEITE ══════════════ -->
|
||||||
|
<div class="seite">
|
||||||
|
|
||||||
|
<div class="kopf-klein">
|
||||||
|
<h2>Weitere Funktionen</h2>
|
||||||
|
<div class="seitezahl">CANCOM Simple Signage</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="raster">
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Begrüßungsseite für Besuch</h3>
|
||||||
|
<p>Über den Menüpunkt <b>Willkommen</b> tragen Sie bis zu drei Firmennamen ein. Das Logo wird automatisch
|
||||||
|
gesucht; finden Sie es nicht passend, laden Sie selbst eines hoch. Dazu je ein Feld für den
|
||||||
|
Ansprechpartner.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Die Seite erscheint ganz vorn auf allen Bildschirmen, die dafür eingestellt sind</li>
|
||||||
|
<li><b>Anzeigen bis:</b> Datum eintragen, dann verschwindet sie von selbst</li>
|
||||||
|
<li>Diese Seite können Sie auch <b>ohne Anmeldung</b> aufrufen – gedacht für den Empfang</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Priority-Liste: für alle gleichzeitig</h3>
|
||||||
|
<p>Was in die Priority-Liste eines Standorts kommt, läuft auf <b>allen</b> Bildschirmen dieses
|
||||||
|
Standorts mit – ohne dass Sie jede Liste einzeln anfassen. Gedacht für Ankündigungen, die alle
|
||||||
|
angehen.</p>
|
||||||
|
<p>In den normalen Listen sehen Sie diese Einträge grau hinterlegt mit dem Vermerk
|
||||||
|
<span class="badge b-rot">Prio</span>. Dort sind sie nur zur Ansicht – geändert wird im Menüpunkt
|
||||||
|
<b>Priority</b>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Knöpfe auf dem Bildschirm</h3>
|
||||||
|
<p>Ist der Bildschirm berührungsempfindlich, lässt sich ein Knopf einblenden, der eine Seite öffnet –
|
||||||
|
etwa ein Formular oder den Lageplan. Position frei wählbar, mit Zurück-Knopf.</p>
|
||||||
|
<p>Ebenso der <b>Digital Voice Agent</b>: ein Sprachassistent, den Besucher selbst starten können.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Laufschrift und Ansicht</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>Newsticker:</b> Laufschrift am unteren Rand, pro Bildschirm</li>
|
||||||
|
<li><b>Stehenbleiben:</b> Der Bildschirm zeigt dauerhaft nur den ersten Eintrag, statt zu wechseln</li>
|
||||||
|
<li><b>Anzeigedauer:</b> Wie lange ein Bild stehen bleibt, bevor das nächste kommt</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Ihr Zugang</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Beim ersten Anmelden vergeben Sie ein eigenes Passwort</li>
|
||||||
|
<li>Passwort vergessen? Ein Administrator setzt es zurück</li>
|
||||||
|
<li><b>Zwei-Faktor:</b> freiwillig einschaltbar – zusätzlich zum Passwort ein Zahlencode aus der
|
||||||
|
Authenticator-App. Ihr eigenes Gerät können Sie für bis zu 30 Tage merken lassen, dann entfällt
|
||||||
|
die Codeeingabe</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h3>Wer darf was</h3>
|
||||||
|
<table>
|
||||||
|
<tr><td>Anwender</td><td>Pflegt die Bildschirme der ihm zugewiesenen Standorte</td></tr>
|
||||||
|
<tr><td>Superuser</td><td>Pflegt alle Standorte</td></tr>
|
||||||
|
<tr><td>Administrator</td><td>Zusätzlich Benutzer, Standorte und Bildschirme verwalten</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="warn">
|
||||||
|
<b>Drei Dinge, die häufig für Verwirrung sorgen</b>
|
||||||
|
<ul style="margin-top:1.2mm;">
|
||||||
|
<li>Ein zeitgesteuerter Eintrag sieht außerhalb seines Fensters „untätig“ aus – das ist richtig so. Prüfen
|
||||||
|
Sie das graue Zeit-Badge, bevor Sie etwas ändern.</li>
|
||||||
|
<li>Priority-Einträge stehen in der normalen Liste nur zur Ansicht. Ändern lassen sie sich ausschließlich
|
||||||
|
im Menüpunkt Priority.</li>
|
||||||
|
<li>Die Bildschirme selbst melden sich nicht an. Wer die Adresse kennt, sieht den Inhalt – legen Sie dort
|
||||||
|
nichts Vertrauliches ab.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kasten">
|
||||||
|
<h3 class="grau">Wenn etwas nicht so aussieht wie erwartet</h3>
|
||||||
|
<div class="raster-3" style="margin-top:1.5mm;">
|
||||||
|
<div>
|
||||||
|
<b>Der Bildschirm zeigt Altes</b>
|
||||||
|
<p class="klein">Er prüft alle fünf Sekunden auf Änderungen. Dauert es länger, steht der Status
|
||||||
|
vermutlich auf Offline.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Ein Eintrag fehlt</b>
|
||||||
|
<p class="klein">Haken gesetzt? Zeitfenster aktiv? Beides muss zutreffen, damit etwas erscheint.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Das Video ruckelt</b>
|
||||||
|
<p class="klein">MP4 mit H.264 und höchstens 1920×1080 verwenden. Größere Dateien überfordern
|
||||||
|
die Abspielgeräte.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="klein" style="margin-top:2.5mm; margin-bottom:0;">
|
||||||
|
Ausführliche Erklärungen zu jeder Funktion finden Sie jederzeit in der Oberfläche unter
|
||||||
|
<b>Hilfe</b> – im Menü hinter Ihrer E-Mail-Adresse oben rechts.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fuss">
|
||||||
|
<div>CANCOM Simple Signage – Kurzüberblick für Anwender</div>
|
||||||
|
<div class="rechts">Rückseite</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -121,6 +121,9 @@
|
|||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title"><i class="ti ti-history me-1"></i>Verlauf</h3>
|
<h3 class="card-title"><i class="ti ti-history me-1"></i>Verlauf</h3>
|
||||||
|
{% if history_total %}
|
||||||
|
<span class="card-subtitle text-secondary small ms-auto">{{ history_from }}–{{ history_to }} von {{ history_total }}</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
@@ -171,6 +174,29 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if history_pages > 1 %}
|
||||||
|
<div class="card-footer d-flex align-items-center">
|
||||||
|
<p class="m-0 text-secondary small">Seite {{ history_page }} von {{ history_pages }}</p>
|
||||||
|
<ul class="pagination m-0 ms-auto">
|
||||||
|
<li class="page-item {% if history_page <= 1 %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="?page={{ history_page - 1 }}"><i class="ti ti-chevron-left"></i> zurück</a>
|
||||||
|
</li>
|
||||||
|
{# Nur ein Fenster um die aktuelle Seite zeigen, sonst wird die Leiste bei vielen Seiten zu lang #}
|
||||||
|
{% for p in range(1, history_pages + 1) %}
|
||||||
|
{% if p == 1 or p == history_pages or (p >= history_page - 2 and p <= history_page + 2) %}
|
||||||
|
<li class="page-item {% if p == history_page %}active{% endif %}">
|
||||||
|
<a class="page-link" href="?page={{ p }}">{{ p }}</a>
|
||||||
|
</li>
|
||||||
|
{% elif p == history_page - 3 or p == history_page + 3 %}
|
||||||
|
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<li class="page-item {% if history_page >= history_pages %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="?page={{ history_page + 1 }}">weiter <i class="ti ti-chevron-right"></i></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -104,8 +104,8 @@
|
|||||||
<li><strong>Medien hochladen:</strong> Im Tab "Medien" eine Datei auswählen und hochladen. Unterstützt werden Bilder (JPG, PNG) und Videos (MP4).</li>
|
<li><strong>Medien hochladen:</strong> Im Tab "Medien" eine Datei auswählen und hochladen. Unterstützt werden Bilder (JPG, PNG) und Videos (MP4).</li>
|
||||||
<li><strong>URL hinzufügen:</strong> Im Tab "Playlist" eine URL eingeben und optional den Zoom-Faktor anpassen.</li>
|
<li><strong>URL hinzufügen:</strong> Im Tab "Playlist" eine URL eingeben und optional den Zoom-Faktor anpassen.</li>
|
||||||
<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. Die Checkbox ist ein reiner Schalter und hat mit der Zeitsteuerung nichts zu tun: Bei einem zeitgesteuerten Eintrag bleibt der Haken gesetzt, auch wenn der Eintrag gerade nicht läuft. Ob er läuft, zeigt das Zeit-Badge – <span class="badge bg-info text-white">Zeit</span> blau = läuft gerade, <span class="badge bg-secondary-lt">Zeit</span> grau = ausserhalb des Zeitfensters. Abgespielt wird nur, was <em>beides</em> erfüllt: Haken gesetzt und im Zeitfenster.</li>
|
||||||
<li><strong>Löschen:</strong> Über den roten Papierkorb-Button.</li>
|
<li><strong>Löschen:</strong> Über den roten Papierkorb-Button. Die Datei wird dabei entfernt <em>und</em> der Eintrag aus der Playlist genommen.</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>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>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>
|
||||||
@@ -177,6 +177,7 @@
|
|||||||
<li>Priority muss für den Standort aktiviert sein (<code>config.json → sites.<standort>.priority.enabled</code>)</li>
|
<li>Priority muss für den Standort aktiviert sein (<code>config.json → sites.<standort>.priority.enabled</code>)</li>
|
||||||
<li>Die <a href="#help-zeitsteuerung">Zeitsteuerung</a> steht hier genauso zur Verfügung wie in den Screen-Playlists</li>
|
<li>Die <a href="#help-zeitsteuerung">Zeitsteuerung</a> steht hier genauso zur Verfügung wie in den Screen-Playlists</li>
|
||||||
<li>Jeder Standort hat seine <strong>eigene</strong> Priority-Playlist – Inhalte eines Standorts erscheinen nie auf den Screens eines anderen.</li>
|
<li>Jeder Standort hat seine <strong>eigene</strong> Priority-Playlist – Inhalte eines Standorts erscheinen nie auf den Screens eines anderen.</li>
|
||||||
|
<li><strong>Vorschau in den Screen-Playlisten:</strong> Die Priority-Einträge werden zusätzlich oberhalb jeder normalen Playlist angezeigt – ausgegraut und mit rotem <span class="badge bg-red text-red-fg">Prio</span>-Badge. Das ist eine <em>reine Anzeige</em>: Sie lässt sich dort nicht sortieren, bearbeiten oder löschen. So ist beim Bearbeiten einer Screen-Playlist sofort sichtbar, was dem Screen ohnehin noch vorweg läuft, ohne dass man in den anderen Menüpunkt wechseln muss. Geändert wird weiterhin ausschliesslich auf der Priority-Seite.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -248,6 +249,7 @@
|
|||||||
<div class="card-header"><h3 class="card-title" id="help-willkommen"><i class="ti ti-users me-1"></i>Willkommensseite</h3></div>
|
<div class="card-header"><h3 class="card-title" id="help-willkommen"><i class="ti ti-users me-1"></i>Willkommensseite</h3></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>Auf der Willkommensseite können bis zu 3 Kundennamen eingegeben werden. Die Logos werden automatisch via OpenAI + Brandfetch gesucht.</p>
|
<p>Auf der Willkommensseite können bis zu 3 Kundennamen eingegeben werden. Die Logos werden automatisch via OpenAI + Brandfetch gesucht.</p>
|
||||||
|
<p class="text-muted small"><i class="ti ti-lock-open me-1"></i><strong>Ohne Anmeldung nutzbar:</strong> Das Formular kann auch ohne Benutzerkonto aufgerufen werden – etwa am Empfang. Wer angemeldet ist, sieht weiterhin nur die eigenen Standorte; ohne Anmeldung stehen alle zur Auswahl. Im Aktivitätsverlauf erscheinen solche Einträge als „nicht angemeldet".</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Aufruf über <code>/willkommen?site=<standort></code> (oder Menüpunkt)</li>
|
<li>Aufruf über <code>/willkommen?site=<standort></code> (oder Menüpunkt)</li>
|
||||||
<li><strong>Zielscreens:</strong> Die generierte <code>welcome.html</code> wird automatisch an den Anfang der Playlist <em>jedes</em> Screens gesetzt, bei dem im Tab "Einstellungen" die Option <em>Willkommensseite hier anzeigen</em> aktiv ist. So lässt sich dieselbe Begrüßung auf mehreren Screens zeigen, ohne sie mehrfach anzulegen. Ist an einem Standort nichts eingestellt, geht sie wie bisher in die Lobby.</li>
|
<li><strong>Zielscreens:</strong> Die generierte <code>welcome.html</code> wird automatisch an den Anfang der Playlist <em>jedes</em> Screens gesetzt, bei dem im Tab "Einstellungen" die Option <em>Willkommensseite hier anzeigen</em> aktiv ist. So lässt sich dieselbe Begrüßung auf mehreren Screens zeigen, ohne sie mehrfach anzulegen. Ist an einem Standort nichts eingestellt, geht sie wie bisher in die Lobby.</li>
|
||||||
@@ -308,6 +310,7 @@
|
|||||||
<li><strong>Sparkline:</strong> Kleine Liniendiagramme zeigen den Verlauf der letzten Änderungen</li>
|
<li><strong>Sparkline:</strong> Kleine Liniendiagramme zeigen den Verlauf der letzten Änderungen</li>
|
||||||
<li><strong>Trend:</strong> Pfeil nach oben/unten mit Netto-Änderung aus den letzten 30 Aktionen</li>
|
<li><strong>Trend:</strong> Pfeil nach oben/unten mit Netto-Änderung aus den letzten 30 Aktionen</li>
|
||||||
<li><strong>Aktivitätsverlauf:</strong> Alle Erstell-, Lösch-, Änderungsaktionen sowie An-/Abmeldungen werden protokolliert</li>
|
<li><strong>Aktivitätsverlauf:</strong> Alle Erstell-, Lösch-, Änderungsaktionen sowie An-/Abmeldungen werden protokolliert</li>
|
||||||
|
<li><strong>Blättern:</strong> Der Verlauf zeigt 50 Einträge je Seite; über die Leiste unter der Liste lässt sich weiter zurückblättern. Aufbewahrt werden die letzten 1000 Einträge. Statistik-Cards, Sparklines und Trendpfeile beziehen sich dabei immer auf die jüngsten 30 Aktionen und ändern sich beim Blättern nicht.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -472,7 +472,8 @@ function playNext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Datei aus dem Medienverzeichnis
|
// Datei aus dem Medienverzeichnis
|
||||||
const src = `/media/${site}/${basePath}/${item.name}`;
|
// ?v=<mtime> erlaubt dem Browser, die Datei ohne Rueckfrage aus dem Cache zu nehmen
|
||||||
|
const src = `/media/${site}/${basePath}/${item.name}` + (item.v ? `?v=${item.v}` : "");
|
||||||
if (isVideo(item)) {
|
if (isVideo(item)) {
|
||||||
vid.style.display = "block";
|
vid.style.display = "block";
|
||||||
vid.src = src;
|
vid.src = src;
|
||||||
|
|||||||
Reference in New Issue
Block a user