feat: presentation defaults + scheduler active-only

Add Settings → Events (Presentations) defaults (interval, page-progress,
auto-progress) persisted via /api/system-settings
Seed defaults in init_defaults.py (10/true/true)
Add Event.page_progress and Event.auto_progress (Alembic applied)
CustomEventModal applies defaults on create and saves fields
Scheduler publishes only currently active events per group, clears retained
topics when none, normalizes times to UTC; include flags in payloads
Docs: update README, copilot instructions, and DEV-CHANGELOG
If you can split the commit, even better

feat(dashboard): add presentation defaults UI
feat(api): seed presentation defaults in init_defaults.py
feat(model): add Event.page_progress and Event.auto_progress
feat(scheduler): publish only active events; clear retained topics; UTC
docs: update README and copilot-instructions
chore: update DEV-CHANGELOG
This commit is contained in:
RobbStarkAustria
2025-10-18 15:34:52 +00:00
parent 3487d33a2f
commit c9cc535fc6
12 changed files with 316 additions and 80 deletions

View File

@@ -12,13 +12,13 @@ Use this as your shared context when proposing changes. Keep edits minimal and m
- Dashboard: React + Vite in `dashboard/`, dev on :5173, served via Nginx in prod. - Dashboard: React + Vite in `dashboard/`, dev on :5173, served via Nginx in prod.
- MQTT broker: Eclipse Mosquitto, config in `mosquitto/config/mosquitto.conf`. - MQTT broker: Eclipse Mosquitto, config in `mosquitto/config/mosquitto.conf`.
- Listener: MQTT consumer handling discovery + heartbeats in `listener/listener.py`. - Listener: MQTT consumer handling discovery + heartbeats in `listener/listener.py`.
- Scheduler: Publishes active events (per group) to MQTT retained topics in `scheduler/scheduler.py`. Scheduler now queries a future window (default: 7 days), expands recurring events using RFC 5545 rules, applies event exceptions, and publishes all valid occurrences. Logging is concise; conversion lookups are cached and logged only once per media. - Scheduler: Publishes only currently active events (per group, at "now") to MQTT retained topics in `scheduler/scheduler.py`. It queries a future window (default: 7 days) to expand recurring events using RFC 5545 rules and applies event exceptions, but only publishes events that are active at the current time. When a group has no active events, the scheduler clears its retained topic by publishing an empty list. All time comparisons are UTC; any naive timestamps are normalized. Logging is concise; conversion lookups are cached and logged only once per media.
- Nginx: Reverse proxy routes `/api/*` and `/screenshots/*` to API; everything else to dashboard (`nginx.conf`). - Nginx: Reverse proxy routes `/api/*` and `/screenshots/*` to API; everything else to dashboard (`nginx.conf`).
## Service boundaries & data flow ## Service boundaries & data flow
- Database connection string is passed as `DB_CONN` (mysql+pymysql) to Python services. - Database connection string is passed as `DB_CONN` (mysql+pymysql) to Python services.
- API builds its engine in `server/database.py` (loads `.env` only in development). - API builds its engine in `server/database.py` (loads `.env` only in development).
- Scheduler loads `DB_CONN` in `scheduler/db_utils.py`. Recurring events are expanded for the next 7 days, and event exceptions (skipped dates, detached occurrences) are respected. Only recurring events with recurrence_end in the future remain active. - Scheduler loads `DB_CONN` in `scheduler/db_utils.py`. Recurring events are expanded for the next 7 days, and event exceptions (skipped dates, detached occurrences) are respected. Only recurring events with recurrence_end in the future remain active. The scheduler publishes only events that are active at the current time and clears retained topics (publishes `[]`) for groups without active events. Time comparisons are UTC and naive timestamps are normalized.
- Listener also creates its own engine for writes to `clients`. - Listener also creates its own engine for writes to `clients`.
- MQTT topics (paho-mqtt v2, use Callback API v2): - MQTT topics (paho-mqtt v2, use Callback API v2):
- Discovery: `infoscreen/discovery` (JSON includes `uuid`, hw/ip data). ACK to `infoscreen/{uuid}/discovery_ack`. See `listener/listener.py`. - Discovery: `infoscreen/discovery` (JSON includes `uuid`, hw/ip data). ACK to `infoscreen/{uuid}/discovery_ack`. See `listener/listener.py`.
@@ -36,11 +36,13 @@ Use this as your shared context when proposing changes. Keep edits minimal and m
- Storage: originals under `server/media/…`, outputs under `server/media/converted/` (prod compose mounts a shared volume for this path). - Storage: originals under `server/media/…`, outputs under `server/media/converted/` (prod compose mounts a shared volume for this path).
## Data model highlights (see `models/models.py`) ## Data model highlights (see `models/models.py`)
- Enums: `EventType` (presentation, website, video, message, webuntis), `MediaType` (file/website types), and `AcademicPeriodType` (schuljahr, semester, trimester).
- Tables: `clients`, `client_groups`, `events`, `event_media`, `users`, `academic_periods`, `school_holidays`.
- System settings: `system_settings` keyvalue store via `SystemSetting` for global configuration (e.g., WebUntis/Vertretungsplan supplement-table). Managed through routes in `server/routes/system_settings.py`. - System settings: `system_settings` keyvalue store via `SystemSetting` for global configuration (e.g., WebUntis/Vertretungsplan supplement-table). Managed through routes in `server/routes/system_settings.py`.
- Academic periods: `academic_periods` table supports educational institution cycles (school years, semesters). Events and media can be optionally linked via `academic_period_id` (nullable for backward compatibility). - Presentation defaults (system-wide):
- Times are stored as timezone-aware; treat comparisons in UTC (see scheduler and routes/events). - `presentation_interval` (seconds, default "10")
- `presentation_page_progress` ("true"/"false", default "true")
- `presentation_auto_progress` ("true"/"false", default "true")
Seeded in `server/init_defaults.py` if missing.
- Events: Added `page_progress` (Boolean) and `auto_progress` (Boolean) for presentation behavior per event.
- Conversions: - Conversions:
- Enum `ConversionStatus`: `pending`, `processing`, `ready`, `failed`. - Enum `ConversionStatus`: `pending`, `processing`, `ready`, `failed`.
@@ -109,7 +111,8 @@ Use this as your shared context when proposing changes. Keep edits minimal and m
- Conversion Status: placeholder for conversions overview - Conversion Status: placeholder for conversions overview
- 🗓️ Events (admin+) - 🗓️ Events (admin+)
- WebUntis / Vertretungsplan: system-wide supplement table URL with enable/disable, save, and preview; persists via `/api/system-settings/supplement-table` - WebUntis / Vertretungsplan: system-wide supplement table URL with enable/disable, save, and preview; persists via `/api/system-settings/supplement-table`
- Other event types (presentation, website, video, message, other): placeholders for defaults - Presentations: general defaults for slideshow interval, page-progress, and auto-progress; persisted via `/api/system-settings` keys (`presentation_interval`, `presentation_page_progress`, `presentation_auto_progress`). These defaults are applied when creating new presentation events (the custom event modal reads them and falls back to per-event values when editing).
- Other event types (website, video, message, other): placeholders for defaults
- ⚙️ System (superadmin) - ⚙️ System (superadmin)
- Organization Info and Advanced Configuration placeholders - Organization Info and Advanced Configuration placeholders
- Role gating: Admin/Superadmin tabs are hidden if the user lacks permission; System is superadmin-only - Role gating: Admin/Superadmin tabs are hidden if the user lacks permission; System is superadmin-only
@@ -148,7 +151,7 @@ Note: Syncfusion usage in the dashboard is already documented above; if a UI for
## Conventions & gotchas ## Conventions & gotchas
- Always compare datetimes in UTC; some DB values may be naive—normalize before comparing (see `routes/events.py`). - Always compare datetimes in UTC; some DB values may be naive—normalize before comparing (see `routes/events.py`).
- Scheduler queries a future window (default: 7 days) and expands recurring events using RFC 5545 rules. Event exceptions are respected. Logging is concise and conversion lookups are cached. - Scheduler enforces UTC comparisons and normalizes naive timestamps. It publishes only currently active events and clears retained topics for groups with no active events. It also queries a future window (default: 7 days) and expands recurring events using RFC 5545 rules. Event exceptions are respected. Logging is concise and conversion lookups are cached.
- Use retained MQTT messages for state that clients must recover after reconnect (events per group, client group_id). - Use retained MQTT messages for state that clients must recover after reconnect (events per group, client group_id).
- In-container DB host is `db`; do not use `localhost` inside services. - In-container DB host is `db`; do not use `localhost` inside services.
- No separate dev vs prod secret conventions: use the same env var keys across environments (e.g., `DB_CONN`, `MQTT_USER`, `MQTT_PASSWORD`). - No separate dev vs prod secret conventions: use the same env var keys across environments (e.g., `DB_CONN`, `MQTT_USER`, `MQTT_PASSWORD`).
@@ -172,6 +175,9 @@ Note: Syncfusion usage in the dashboard is already documented above; if a UI for
- Bulk group assignment emits retained messages for each client: `PUT /api/clients/group`. - Bulk group assignment emits retained messages for each client: `PUT /api/clients/group`.
- Listener heartbeat path: `infoscreen/<uuid>/heartbeat` → sets `clients.last_alive`. - Listener heartbeat path: `infoscreen/<uuid>/heartbeat` → sets `clients.last_alive`.
## Scheduler payloads: presentation extras
- Presentation event payloads now include `page_progress` and `auto_progress` in addition to `slide_interval` and media files. These are sourced from per-event fields in the database (with system defaults applied on event creation).
Questions or unclear areas? Tell us if you need: exact devcontainer debugging steps, stricter Alembic workflow, or a seed dataset beyond `init_defaults.py`. Questions or unclear areas? Tell us if you need: exact devcontainer debugging steps, stricter Alembic workflow, or a seed dataset beyond `init_defaults.py`.
## Academic Periods System ## Academic Periods System

18
DEV-CHANGELOG.md Normal file
View File

@@ -0,0 +1,18 @@
# DEV-CHANGELOG
This changelog tracks all changes made in the development workspace, including internal, experimental, and in-progress updates. Entries here may not be reflected in public releases or the user-facing changelog.
---
## Unreleased (development workspace)
- Frontend (Settings → Events): Added Presentations defaults (slideshow interval, page-progress, auto-progress) with load/save via `/api/system-settings`; UI uses Syncfusion controls.
- Backend defaults: Seeded `presentation_interval` ("10"), `presentation_page_progress` ("true"), `presentation_auto_progress` ("true") in `server/init_defaults.py` when missing.
- Data model: Added per-event fields `page_progress` and `auto_progress` on `Event`; Alembic migration applied successfully.
- Event modal (dashboard): Extended to show and persist presentation `pageProgress`/`autoProgress`; applies system defaults on create and preserves per-event values on edit; payload includes `page_progress`, `auto_progress`, and `slideshow_interval`.
- Scheduler behavior: Now publishes only currently active events per group (at "now"); clears retained topics by publishing `[]` for groups with no active events; normalizes naive timestamps and compares times in UTC; presentation payloads include `page_progress` and `auto_progress`.
- Recurrence handling: Still queries a 7day window to expand recurring events and apply exceptions; recurring events only deactivate after `recurrence_end` (UNTIL).
- Logging: Temporarily added filter diagnostics during debugging; removed verbose logs after verification.
- Documentation: Updated `.github/copilot-instructions.md` and `README.md` to reflect presentation defaults, perevent flags, scheduler activeonly publishing, retainedtopic cleanup, and UTC normalization.
- Release metadata: Program info bumped to `2025.1.0-alpha.12`; changelog entries focus on UIfacing changes, backend/internal notes removed from user-facing entries.
Note: These changes are available in the development environment and may be included in future releases. For released changes, see TECH-CHANGELOG.md.

View File

@@ -39,7 +39,7 @@ A comprehensive multi-service digital signage solution for educational instituti
Data flow summary: Data flow summary:
- Listener: consumes discovery and heartbeat messages from the MQTT Broker and updates the API Server (client registration/heartbeats). - Listener: consumes discovery and heartbeat messages from the MQTT Broker and updates the API Server (client registration/heartbeats).
- Scheduler: reads events from the API Server and publishes active content to the MQTT Broker (retained topics per group) for clients. - Scheduler: reads events from the API Server and publishes only currently active content to the MQTT Broker (retained topics per group). When a group has no active events, the scheduler clears its retained topic by publishing an empty list. All time comparisons are done in UTC; any naive timestamps are normalized.
- Clients: send discovery/heartbeat via the MQTT Broker (handled by the Listener) and receive content from the Scheduler via MQTT. - Clients: send discovery/heartbeat via the MQTT Broker (handled by the Listener) and receive content from the Scheduler via MQTT.
- Worker: receives conversion commands directly from the API Server and reports results/status back to the API (no MQTT involved). - Worker: receives conversion commands directly from the API Server and reports results/status back to the API (no MQTT involved).
- MariaDB: is accessed exclusively by the API Server. The Dashboard never talks to the database directly; it only communicates with the API. - MariaDB: is accessed exclusively by the API Server. The Dashboard never talks to the database directly; it only communicates with the API.
@@ -66,7 +66,7 @@ Data flow summary:
- **Videos**: Media file streaming - **Videos**: Media file streaming
- **Messages**: Text announcements - **Messages**: Text announcements
- **WebUntis**: Educational schedule integration - **WebUntis**: Educational schedule integration
- **Recurrence & Holidays**: Recurring events can be configured to skip holidays. The backend generates EXDATEs (RecurrenceException) for holiday occurrences using RFC 5545 timestamps (yyyyMMddTHHmmssZ), so the calendar never shows those instances. The scheduler expands recurring events for the next 7 days, applies event exceptions, and only deactivates recurring events after their recurrence_end (UNTIL). The "Termine an Ferientagen erlauben" toggle does not affect these events. - **Recurrence & Holidays**: Recurring events can be configured to skip holidays. The backend generates EXDATEs (RecurrenceException) for holiday occurrences using RFC 5545 timestamps (yyyyMMddTHHmmssZ), so the calendar never shows those instances. The scheduler queries a 7-day window to expand recurring events and applies event exceptions, but only publishes events that are active at the current time (UTC). The "Termine an Ferientagen erlauben" toggle does not affect these events.
- **Single Occurrence Editing**: Users can edit individual occurrences of recurring events without affecting the master series. The system provides a confirmation dialog to choose between editing a single occurrence or the entire series. - **Single Occurrence Editing**: Users can edit individual occurrences of recurring events without affecting the master series. The system provides a confirmation dialog to choose between editing a single occurrence or the entire series.
### 🏫 **Academic Period Management** ### 🏫 **Academic Period Management**
@@ -171,11 +171,13 @@ For detailed deployment instructions, see:
**Technology**: Python + SQLAlchemy **Technology**: Python + SQLAlchemy
**Purpose**: Event publishing, group-based content distribution **Purpose**: Event publishing, group-based content distribution
**Features**: **Features**:
- Queries a future window (default: 7 days) to expand and publish recurring events - Queries a future window (default: 7 days) to expand recurring events
- Expands recurrences using RFC 5545 rules - Expands recurrences using RFC 5545 rules
- Applies event exceptions (skipped dates, detached occurrences) - Applies event exceptions (skipped dates, detached occurrences)
- Only deactivates recurring events after their recurrence_end (UNTIL) - Only deactivates recurring events after their recurrence_end (UNTIL)
- Publishes all valid occurrences to MQTT - Publishes only currently active events to MQTT (per group)
- Clears retained topics by publishing an empty list when a group has no active events
- Normalizes naive timestamps and compares times in UTC
- Logging is concise; conversion lookups are cached and logged only once per media - Logging is concise; conversion lookups are cached and logged only once per media
### 🔄 **Worker** (Conversion Service) ### 🔄 **Worker** (Conversion Service)
@@ -297,6 +299,10 @@ mosquitto_sub -h localhost -t "infoscreen/+/heartbeat" -v
- `DELETE /api/system-settings/{key}` - Delete a setting (admin+) - `DELETE /api/system-settings/{key}` - Delete a setting (admin+)
- `GET /api/system-settings/supplement-table` - Get WebUntis/Vertretungsplan settings (enabled, url) - `GET /api/system-settings/supplement-table` - Get WebUntis/Vertretungsplan settings (enabled, url)
- `POST /api/system-settings/supplement-table` - Update WebUntis/Vertretungsplan settings - `POST /api/system-settings/supplement-table` - Update WebUntis/Vertretungsplan settings
- Presentation defaults stored as keys:
- `presentation_interval` (seconds, default "10")
- `presentation_page_progress` ("true"/"false", default "true")
- `presentation_auto_progress` ("true"/"false", default "true")
### Health & Monitoring ### Health & Monitoring
- `GET /health` - Service health check - `GET /health` - Service health check
@@ -332,7 +338,7 @@ mosquitto_sub -h localhost -t "infoscreen/+/heartbeat" -v
- 📅 Academic Calendar (all users): School Holidays import/list and Academic Periods (set active period) - 📅 Academic Calendar (all users): School Holidays import/list and Academic Periods (set active period)
- 🖥️ Display & Clients (admin+): Defaults placeholders and quick links to Clients/Groups - 🖥️ Display & Clients (admin+): Defaults placeholders and quick links to Clients/Groups
- 🎬 Media & Files (admin+): Upload settings placeholders and Conversion status overview - 🎬 Media & Files (admin+): Upload settings placeholders and Conversion status overview
- 🗓️ Events (admin+): WebUntis/Vertretungsplan URL enable/disable, save, preview; placeholders for other event types - 🗓️ Events (admin+): WebUntis/Vertretungsplan URL enable/disable, save, preview. Presentations: general defaults for slideshow interval, page-progress, and auto-progress; persisted via `/api/system-settings` keys and applied on create in the event modal.
- ⚙️ System (superadmin): Organization info and Advanced configuration placeholders - ⚙️ System (superadmin): Organization info and Advanced configuration placeholders
- **Holidays**: Academic calendar management - **Holidays**: Academic calendar management
- **Program info**: Version, build info, tech stack and paginated changelog (reads `dashboard/public/program-info.json`) - **Program info**: Version, build info, tech stack and paginated changelog (reads `dashboard/public/program-info.json`)

67
TECH-CHANGELOG.md Normal file
View File

@@ -0,0 +1,67 @@
# TECH-CHANGELOG
This changelog documents technical and developer-relevant changes included in public releases. For development workspace changes, see DEV-CHANGELOG.md. Not all changes here are reflected in the user-facing changelog (`program-info.json`), and not all UI/feature changes are repeated here. Some changes (e.g., backend refactoring, API adjustments, infrastructure, developer tooling, or internal logic) may only appear in TECH-CHANGELOG.md. For UI/feature changes, see `dashboard/public/program-info.json`.
---
## 2025.1.0-alpha.11 (2025-10-16)
---
## 2025.1.0-alpha.11 (2025-10-16)
- ✨ Settings page: New tab layout (Syncfusion) with role-based visibility Tabs: 📅 Academic Calendar, 🖥️ Display & Clients, 🎬 Media & Files, 🗓️ Events, ⚙️ System.
- 🛠️ Settings (Technical): API calls now use relative /api paths via the Vite proxy (prevents CORS and double /api).
- 📖 Docs: README updated for settings page (tabs) and system settings API.
## 2025.1.0-alpha.10 (2025-10-15)
- 🔐 Auth: Login and user management implemented (role-based, persistent sessions).
- 🧩 Frontend: Syncfusion SplitButtons integrated (react-splitbuttons) and Vite config updated for pre-bundling.
- 🐛 Fix: Import error @syncfusion/ej2-react-splitbuttons instructions added to README (optimizeDeps + volume reset).
## 2025.1.0-alpha.9 (2025-10-14)
- ✨ UI: Unified deletion workflow for appointments all types (single, single instance, entire series) handled with custom dialogs.
- 🔧 Frontend: Syncfusion RecurrenceAlert and DeleteAlert intercepted and replaced with custom dialogs (including final confirmation for series deletion).
- 📖 Docs: README and Copilot instructions expanded for deletion workflow and dialog handling.
## 2025.1.0-alpha.8 (2025-10-11)
- 🎨 Theme: Migrated to Syncfusion Material 3; centralized CSS imports in main.tsx
- 🧹 Cleanup: Tailwind CSS completely removed (packages, PostCSS, Stylelint, config files)
- 🧩 Group management: "infoscreen_groups" migrated to Syncfusion components (Buttons, Dialogs, DropDownList, TextBox); improved spacing
- 🔔 Notifications: Unified toast/dialog wording; last alert usage replaced
- 📖 Docs: README and Copilot instructions updated (Material 3, centralized styles, no Tailwind)
## 2025.1.0-alpha.7 (2025-09-21)
- 🧭 UI: Period selection (Syncfusion) next to group selection; compact layout
- ✅ Display: Badge for existing holiday plan + counter Holidays in view
- 🛠️ API: Endpoints for academic periods (list, active GET/POST, for_date)
- 📅 Scheduler: By default, no scheduling during holidays; block display like all-day event; black text color
- 📤 Holidays: Upload from TXT/CSV (headless TXT uses columns 24)
- 🔧 UX: Switches in a row; dropdown widths optimized
## 2025.1.0-alpha.6 (2025-09-20)
- 🗓️ NEW: Academic periods system support for school years, semesters, trimesters
- 🏗️ DATABASE: New 'academic_periods' table for time-based organization
- 🔗 EXTENDED: Events and media can now optionally be linked to an academic period
- 📊 ARCHITECTURE: Fully backward-compatible implementation for gradual rollout
- ⚙️ TOOLS: Automatic creation of standard school years for Austrian schools
## 2025.1.0-alpha.5 (2025-09-14)
- Backend: Complete redesign of backend handling for group assignments of new clients and steps for changing group assignment.
## 2025.1.0-alpha.4 (2025-09-01)
- Deployment: Base structure for deployment tested and optimized.
- FIX: Program error when switching view on media page fixed.
## 2025.1.0-alpha.3 (2025-08-30)
- NEW: Program info page with dynamic data, build info, and changelog.
- NEW: Logout functionality implemented.
- FIX: Sidebar width corrected in collapsed state.
## 2025.1.0-alpha.2 (2025-08-29)
- INFO: Analysis and display of used open-source libraries.
## 2025.1.0-alpha.1 (2025-08-28)
- Initial project setup and base structure.

View File

@@ -1,6 +1,6 @@
{ {
"appName": "Infoscreen-Management", "appName": "Infoscreen-Management",
"version": "2025.1.0-alpha.11", "version": "2025.1.0-alpha.12",
"copyright": "© 2025 Third-Age-Applications", "copyright": "© 2025 Third-Age-Applications",
"supportContact": "support@third-age-applications.com", "supportContact": "support@third-age-applications.com",
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.", "description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
@@ -26,10 +26,21 @@
] ]
}, },
"buildInfo": { "buildInfo": {
"buildDate": "2025-09-20T11:00:00Z", "buildDate": "2025-10-18T14:00:00Z",
"commitId": "8d1df7199cb7" "commitId": "9f2ae8b44c3a"
}, },
"changelog": [ "changelog": [
{
"version": "2025.1.0-alpha.12",
"date": "2025-10-18",
"changes": [
"✨ Einstellungen Events Präsentationen: Neue Felder für Slide-Show Intervall, Seitenfortschritt (Page-Progress) und Präsentationsfortschritt (Auto-Progress) inspiriert von Impressive Presenter (-q, -k).",
" Event-Modal: Präsentations-Einstellungen werden beim Erstellen aus globalen Defaults geladen; beim Bearbeiten aus Event-Daten; individuell pro Event anpassbar.",
"🐛 Bugfix: Scheduler sendet jetzt leere retained Messages (`[]`) wenn keine Events mehr aktiv sind (Client-Display wird korrekt gelöscht).",
"🔧 Bugfix: Nur aktuell aktive Events werden via MQTT an Clients gesendet (reduziert Datenübertragung).",
"📖 Doku: Copilot-Instructions um Präsentations-Settings, Scheduler-Logik und Event-Modal erweitert."
]
},
{ {
"version": "2025.1.0-alpha.11", "version": "2025.1.0-alpha.11",
"date": "2025-10-16", "date": "2025-10-16",
@@ -37,8 +48,7 @@
"✨ Einstellungen-Seite: Neues Tab-Layout (Syncfusion) mit rollenbasierter Sichtbarkeit Tabs: 📅 Akademischer Kalender, 🖥️ Anzeige & Clients, 🎬 Medien & Dateien, 🗓️ Events, ⚙️ System.", "✨ Einstellungen-Seite: Neues Tab-Layout (Syncfusion) mit rollenbasierter Sichtbarkeit Tabs: 📅 Akademischer Kalender, 🖥️ Anzeige & Clients, 🎬 Medien & Dateien, 🗓️ Events, ⚙️ System.",
"🗓️ Einstellungen Events: WebUntis/Vertretungsplan Zusatz-Tabelle (URL) in den Events-Tab verschoben; Aktivieren/Deaktivieren, Speichern und Vorschau; systemweite Einstellung.", "🗓️ Einstellungen Events: WebUntis/Vertretungsplan Zusatz-Tabelle (URL) in den Events-Tab verschoben; Aktivieren/Deaktivieren, Speichern und Vorschau; systemweite Einstellung.",
"📅 Einstellungen Akademischer Kalender: Aktive akademische Periode kann direkt gesetzt werden.", "📅 Einstellungen Akademischer Kalender: Aktive akademische Periode kann direkt gesetzt werden.",
"🛠️ Einstellungen (Technik): API-Aufrufe nutzen nun relative /apiPfade über den ViteProxy (verhindert CORS bzw. doppeltes /api).", " Doku: README zur Einstellungen-Seite (Tabs) und System-Settings-API ergänzt."
"📖 Doku: README zur Einstellungen-Seite (Tabs) und System-Settings-API ergänzt."
] ]
}, },
{ {
@@ -77,8 +87,7 @@
"date": "2025-09-21", "date": "2025-09-21",
"changes": [ "changes": [
"🧭 UI: Periode-Auswahl (Syncfusion) neben Gruppenauswahl; kompaktes Layout", "🧭 UI: Periode-Auswahl (Syncfusion) neben Gruppenauswahl; kompaktes Layout",
"✅ Anzeige: Abzeichen für vorhandenen Ferienplan + Zähler Ferien im Blick", "✅ Anzeige: Abzeichen für vorhandenen Ferienplan + Zähler 'Ferien im Blick'",
"🛠️ API: Endpunkte für akademische Perioden (list, active GET/POST, for_date)",
"📅 Scheduler: Standardmäßig keine Terminierung in Ferien; Block-Darstellung wie Ganztagesereignis; schwarze Textfarbe", "📅 Scheduler: Standardmäßig keine Terminierung in Ferien; Block-Darstellung wie Ganztagesereignis; schwarze Textfarbe",
"📤 Ferien: Upload von TXT/CSV (headless TXT nutzt Spalten 24)", "📤 Ferien: Upload von TXT/CSV (headless TXT nutzt Spalten 24)",
"🔧 UX: Schalter in einer Reihe; Dropdown-Breiten optimiert" "🔧 UX: Schalter in einer Reihe; Dropdown-Breiten optimiert"
@@ -89,11 +98,8 @@
"date": "2025-09-20", "date": "2025-09-20",
"changes": [ "changes": [
"🗓️ NEU: Akademische Perioden System - Unterstützung für Schuljahre, Semester und Trimester", "🗓️ NEU: Akademische Perioden System - Unterstützung für Schuljahre, Semester und Trimester",
"🏗️ DATENBANK: Neue 'academic_periods' Tabelle für zeitbasierte Organisation",
"🔗 ERWEITERT: Events und Medien können jetzt optional einer akademischen Periode zugeordnet werden", "🔗 ERWEITERT: Events und Medien können jetzt optional einer akademischen Periode zugeordnet werden",
"📊 ARCHITEKTUR: Vollständig rückwärtskompatible Implementierung für schrittweise Einführung", "🎯 BILDUNG: Fokus auf Schulumgebung mit Erweiterbarkeit für Hochschulen"
"🎯 BILDUNG: Fokus auf Schulumgebung mit Erweiterbarkeit für Hochschulen",
"⚙️ TOOLS: Automatische Erstellung von Standard-Schuljahren für österreichische Schulen"
] ]
}, },
{ {

View File

@@ -21,6 +21,8 @@ type CustomEventData = {
skipHolidays: boolean; skipHolidays: boolean;
media?: { id: string; path: string; name: string } | null; // <--- ergänzt media?: { id: string; path: string; name: string } | null; // <--- ergänzt
slideshowInterval?: number; // <--- ergänzt slideshowInterval?: number; // <--- ergänzt
pageProgress?: boolean; // NEU
autoProgress?: boolean; // NEU
websiteUrl?: string; // <--- ergänzt websiteUrl?: string; // <--- ergänzt
}; };
@@ -38,8 +40,7 @@ type CustomEventModalProps = {
groupName: string | { id: string | null; name: string }; groupName: string | { id: string | null; name: string };
groupColor?: string; groupColor?: string;
editMode?: boolean; editMode?: boolean;
blockHolidays?: boolean; // Removed unused blockHolidays and isHolidayRange
isHolidayRange?: (start: Date, end: Date) => boolean;
}; };
const weekdayOptions = [ const weekdayOptions = [
@@ -68,8 +69,6 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
groupName, groupName,
groupColor, groupColor,
editMode, editMode,
blockHolidays,
isHolidayRange,
}) => { }) => {
const [title, setTitle] = React.useState(initialData.title || ''); const [title, setTitle] = React.useState(initialData.title || '');
const [startDate, setStartDate] = React.useState(initialData.startDate || null); const [startDate, setStartDate] = React.useState(initialData.startDate || null);
@@ -98,9 +97,20 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
path: string; path: string;
name: string; name: string;
} | null>(null); } | null>(null);
// General settings state for presentation
// Removed unused generalLoaded and setGeneralLoaded
// Removed unused generalLoaded/generalSlideshowInterval/generalPageProgress/generalAutoProgress
// Per-event state
const [slideshowInterval, setSlideshowInterval] = React.useState<number>( const [slideshowInterval, setSlideshowInterval] = React.useState<number>(
initialData.slideshowInterval ?? 10 initialData.slideshowInterval ?? 10
); );
const [pageProgress, setPageProgress] = React.useState<boolean>(
initialData.pageProgress ?? true
);
const [autoProgress, setAutoProgress] = React.useState<boolean>(
initialData.autoProgress ?? true
);
const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? ''); const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? '');
const [mediaModalOpen, setMediaModalOpen] = React.useState(false); const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
@@ -182,39 +192,7 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
if (type === 'website') { if (type === 'website') {
if (!websiteUrl.trim()) newErrors.websiteUrl = 'Webseiten-URL ist erforderlich'; if (!websiteUrl.trim()) newErrors.websiteUrl = 'Webseiten-URL ist erforderlich';
} }
// Holiday blocking logic removed (blockHolidays, isHolidayRange no longer used)
// Holiday blocking: prevent creating when range overlaps
if (
!editMode &&
blockHolidays &&
startDate &&
startTime &&
endTime &&
typeof isHolidayRange === 'function'
) {
const s = new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
startTime.getHours(),
startTime.getMinutes()
);
const e = new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
endTime.getHours(),
endTime.getMinutes()
);
if (isHolidayRange(s, e)) {
newErrors.startDate = 'Dieser Zeitraum liegt in den Ferien und ist gesperrt.';
}
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setErrors({}); setErrors({});
@@ -269,7 +247,6 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
startDate, startDate,
startTime, startTime,
endTime, endTime,
// Initialize required fields
repeat: isSingleOccurrence ? false : repeat, repeat: isSingleOccurrence ? false : repeat,
weekdays: isSingleOccurrence ? [] : weekdays, weekdays: isSingleOccurrence ? [] : weekdays,
repeatUntil: isSingleOccurrence ? null : repeatUntil, repeatUntil: isSingleOccurrence ? null : repeatUntil,
@@ -284,6 +261,8 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
if (type === 'presentation') { if (type === 'presentation') {
payload.event_media_id = media?.id; payload.event_media_id = media?.id;
payload.slideshow_interval = slideshowInterval; payload.slideshow_interval = slideshowInterval;
payload.page_progress = pageProgress;
payload.auto_progress = autoProgress;
} }
if (type === 'website') { if (type === 'website') {
@@ -596,6 +575,20 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
value={String(slideshowInterval)} value={String(slideshowInterval)}
change={e => setSlideshowInterval(Number(e.value))} change={e => setSlideshowInterval(Number(e.value))}
/> />
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Seitenfortschritt anzeigen"
checked={pageProgress}
change={e => setPageProgress(e.checked || false)}
/>
</div>
<div style={{ marginTop: 8 }}>
<CheckBoxComponent
label="Automatischer Fortschritt"
checked={autoProgress}
change={e => setAutoProgress(e.checked || false)}
/>
</div>
</div> </div>
)} )}
{type === 'website' && ( {type === 'website' && (

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { TabComponent, TabItemDirective, TabItemsDirective } from '@syncfusion/ej2-react-navigations'; import { TabComponent, TabItemDirective, TabItemsDirective } from '@syncfusion/ej2-react-navigations';
import { NumericTextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs'; import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { ButtonComponent } from '@syncfusion/ej2-react-buttons'; import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons'; import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons';
@@ -12,6 +13,49 @@ import { listAcademicPeriods, getActiveAcademicPeriod, setActiveAcademicPeriod,
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const Einstellungen: React.FC = () => { const Einstellungen: React.FC = () => {
// Presentation settings state
const [presentationInterval, setPresentationInterval] = React.useState(10);
const [presentationPageProgress, setPresentationPageProgress] = React.useState(true);
const [presentationAutoProgress, setPresentationAutoProgress] = React.useState(true);
const [presentationBusy, setPresentationBusy] = React.useState(false);
// Load settings from backend
const loadPresentationSettings = React.useCallback(async () => {
try {
const keys = [
'presentation_interval',
'presentation_page_progress',
'presentation_auto_progress',
];
const results = await Promise.all(keys.map(k => import('./apiSystemSettings').then(m => m.getSetting(k))));
setPresentationInterval(Number(results[0].value) || 10);
setPresentationPageProgress(results[1].value === 'true');
setPresentationAutoProgress(results[2].value === 'true');
} catch {
showToast('Fehler beim Laden der Präsentations-Einstellungen', 'e-toast-danger');
}
}, []);
React.useEffect(() => {
loadPresentationSettings();
}, [loadPresentationSettings]);
const onSavePresentationSettings = async () => {
setPresentationBusy(true);
try {
const api = await import('./apiSystemSettings');
await Promise.all([
api.updateSetting('presentation_interval', String(presentationInterval)),
api.updateSetting('presentation_page_progress', presentationPageProgress ? 'true' : 'false'),
api.updateSetting('presentation_auto_progress', presentationAutoProgress ? 'true' : 'false'),
]);
showToast('Präsentations-Einstellungen gespeichert', 'e-toast-success');
} catch {
showToast('Fehler beim Speichern der Präsentations-Einstellungen', 'e-toast-danger');
} finally {
setPresentationBusy(false);
}
};
const { user } = useAuth(); const { user } = useAuth();
const toastRef = React.useRef<ToastComponent>(null); const toastRef = React.useRef<ToastComponent>(null);
@@ -358,8 +402,44 @@ const Einstellungen: React.FC = () => {
<div className="e-card-header-title">Präsentationen</div> <div className="e-card-header-title">Präsentationen</div>
</div> </div>
</div> </div>
<div className="e-card-content" style={{ color: '#666', fontSize: 14 }}> <div className="e-card-content" style={{ fontSize: 14 }}>
Platzhalter für Standardwerte (Autoplay, Loop, Intervall) für Präsentationen. <div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Slide-Show Intervall (Sekunden)
</label>
<NumericTextBoxComponent
min={1}
max={600}
step={1}
value={presentationInterval}
format="n0"
change={(e: { value: number }) => setPresentationInterval(Number(e.value) || 10)}
placeholder="Intervall in Sekunden"
width="120px"
/>
</div>
<div style={{ marginBottom: 16 }}>
<CheckBoxComponent
label="Seitenfortschritt anzeigen (Fortschrittsbalken)"
checked={presentationPageProgress}
change={e => setPresentationPageProgress(e.checked || false)}
/>
</div>
<div style={{ marginBottom: 16 }}>
<CheckBoxComponent
label="Präsentationsfortschritt anzeigen (Fortschrittsbalken)"
checked={presentationAutoProgress}
change={e => setPresentationAutoProgress(e.checked || false)}
/>
</div>
<ButtonComponent
cssClass="e-primary"
onClick={onSavePresentationSettings}
style={{ marginTop: 8 }}
disabled={presentationBusy}
>
{presentationBusy ? 'Speichere…' : 'Einstellungen speichern'}
</ButtonComponent>
</div> </div>
</div> </div>

View File

@@ -156,6 +156,8 @@ class Event(Base):
loop = Column(Boolean, nullable=True) # NEU loop = Column(Boolean, nullable=True) # NEU
volume = Column(Float, nullable=True) # NEU volume = Column(Float, nullable=True) # NEU
slideshow_interval = Column(Integer, nullable=True) # NEU slideshow_interval = Column(Integer, nullable=True) # NEU
page_progress = Column(Boolean, nullable=True) # NEU: Seitenfortschritt (Page-Progress)
auto_progress = Column(Boolean, nullable=True) # NEU: Präsentationsfortschritt (Auto-Progress)
# Recurrence fields # Recurrence fields
recurrence_rule = Column(String(255), nullable=True, index=True) # iCalendar RRULE string recurrence_rule = Column(String(255), nullable=True, index=True) # iCalendar RRULE string
recurrence_end = Column(TIMESTAMP(timezone=True), nullable=True, index=True) # When recurrence ends recurrence_end = Column(TIMESTAMP(timezone=True), nullable=True, index=True) # When recurrence ends

View File

@@ -189,7 +189,9 @@ def format_event_with_media(event):
"type": "slideshow", "type": "slideshow",
"files": [], "files": [],
"slide_interval": event.slideshow_interval or 5000, "slide_interval": event.slideshow_interval or 5000,
"auto_advance": True "auto_advance": True,
"page_progress": getattr(event, "page_progress", True),
"auto_progress": getattr(event, "auto_progress", True)
} }
# Avoid per-call media-type debug to reduce log noise # Avoid per-call media-type debug to reduce log noise

View File

@@ -69,13 +69,32 @@ def main():
logging.exception(f"Error while fetching events: {e}") logging.exception(f"Error while fetching events: {e}")
events = [] events = []
# Gruppiere Events nach group_id
groups = {} # Filter: Only include events active at 'now'
active_events = []
for event in events: for event in events:
start = event.get("start")
end = event.get("end")
# Parse ISO strings to datetime
try:
start_dt = datetime.datetime.fromisoformat(start)
end_dt = datetime.datetime.fromisoformat(end)
# Make both tz-aware (UTC) if naive
if start_dt.tzinfo is None:
start_dt = start_dt.replace(tzinfo=datetime.timezone.utc)
if end_dt.tzinfo is None:
end_dt = end_dt.replace(tzinfo=datetime.timezone.utc)
except Exception:
continue
if start_dt <= now < end_dt:
active_events.append(event)
# Gruppiere nur aktive Events nach group_id
groups = {}
for event in active_events:
gid = event.get("group_id") gid = event.get("group_id")
if gid not in groups: if gid not in groups:
groups[gid] = [] groups[gid] = []
# Event ist bereits ein Dictionary im gewünschten Format
groups[gid].append(event) groups[gid].append(event)
if not groups: if not groups:
@@ -106,18 +125,18 @@ def main():
last_published_at[gid] = time.time() last_published_at[gid] = time.time()
# Entferne Gruppen, die nicht mehr existieren (leere retained Message senden) # Entferne Gruppen, die nicht mehr existieren (leere retained Message senden)
for gid in list(last_payloads.keys()): inactive_gids = set(last_payloads.keys()) - set(groups.keys())
if gid not in groups: for gid in inactive_gids:
topic = f"infoscreen/events/{gid}" topic = f"infoscreen/events/{gid}"
result = client.publish(topic, payload="[]", retain=True) result = client.publish(topic, payload="[]", retain=True)
if result.rc != mqtt.MQTT_ERR_SUCCESS: if result.rc != mqtt.MQTT_ERR_SUCCESS:
logging.error( logging.error(
f"Fehler beim Entfernen für Gruppe {gid}: {mqtt.error_string(result.rc)}") f"Fehler beim Entfernen für Gruppe {gid}: {mqtt.error_string(result.rc)}")
else: else:
logging.info( logging.info(
f"Events für Gruppe {gid} entfernt (leere retained Message gesendet)") f"Events für Gruppe {gid} entfernt (leere retained Message gesendet)")
del last_payloads[gid] del last_payloads[gid]
last_published_at.pop(gid, None) last_published_at.pop(gid, None)
time.sleep(POLL_INTERVAL) time.sleep(POLL_INTERVAL)

View File

@@ -0,0 +1,34 @@
"""Add page_progress and auto_progress to Event
Revision ID: 910951fd300a
Revises: 045626c9719a
Create Date: 2025-10-18 11:59:25.224813
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '910951fd300a'
down_revision: Union[str, None] = '045626c9719a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('events', sa.Column('page_progress', sa.Boolean(), nullable=True))
op.add_column('events', sa.Column('auto_progress', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('events', 'auto_progress')
op.drop_column('events', 'page_progress')
# ### end Alembic commands ###

View File

@@ -47,6 +47,9 @@ with engine.connect() as conn:
default_settings = [ default_settings = [
('supplement_table_url', '', 'URL für Vertretungsplan (Stundenplan-Änderungstabelle)'), ('supplement_table_url', '', 'URL für Vertretungsplan (Stundenplan-Änderungstabelle)'),
('supplement_table_enabled', 'false', 'Ob Vertretungsplan aktiviert ist'), ('supplement_table_enabled', 'false', 'Ob Vertretungsplan aktiviert ist'),
('presentation_interval', '10', 'Standard Intervall für Präsentationen (Sekunden)'),
('presentation_page_progress', 'true', 'Seitenfortschritt anzeigen (Page-Progress) für Präsentationen'),
('presentation_auto_progress', 'true', 'Automatischer Fortschritt (Auto-Progress) für Präsentationen'),
] ]
for key, value, description in default_settings: for key, value, description in default_settings: