- Update copilot-instructions.md with user model, API routes, and frontend patterns - Update README.md with RBAC details, user management API, and security sections - Add user management technical documentation to TECH-CHANGELOG.md - Bump version to 2025.1.0-alpha.13 with user management changelog entries
367 lines
34 KiB
Markdown
367 lines
34 KiB
Markdown
# Copilot instructions for infoscreen_2025
|
||
|
||
# Purpose
|
||
These instructions tell Copilot Chat how to reason about this codebase.
|
||
Prefer explanations and refactors that align with these structures.
|
||
|
||
Use this as your shared context when proposing changes. Keep edits minimal and match existing patterns referenced below.
|
||
|
||
## TL;DR
|
||
Small multi-service digital signage app (Flask API, React dashboard, MQTT scheduler). Edit `server/` for API logic, `scheduler/` for event publishing, and `dashboard/` for UI. If you're asking Copilot for changes, prefer focused prompts that include the target file(s) and the desired behavior.
|
||
|
||
### How to ask Copilot
|
||
- "Add a new route `GET /api/events/summary` that returns counts per event_type — implement in `server/routes/events.py`."
|
||
- "Create an Alembic migration to add `duration` and `resolution` to `event_media` and update upload handler to populate them."
|
||
- "Refactor `scheduler/db_utils.py` to prefer precomputed EventMedia metadata and fall back to a HEAD probe."
|
||
- "Add an ffprobe-based worker that extracts duration/resolution/bitrate and stores them on `EventMedia`."
|
||
|
||
Keep docs synced with code. When you change services/MQTT/API/UTC/env or dev/prod run steps, update this file in the same commit (see `AI-INSTRUCTIONS-MAINTENANCE.md`).
|
||
|
||
### When not to change
|
||
- Avoid editing generated assets under `dashboard/dist/` and compiled bundles. Don't modify files produced by CI or Docker builds (unless intentionally updating build outputs).
|
||
|
||
### Contact / owner
|
||
- Primary maintainer: RobbStarkAustria (owner). For architecture questions, ping the repo owner or open an issue and tag `@RobbStarkAustria`.
|
||
|
||
### Important files (quick jump targets)
|
||
- `scheduler/db_utils.py` — event formatting and scheduler-facing logic
|
||
- `scheduler/scheduler.py` — scheduler main loop and MQTT publisher
|
||
- `server/routes/eventmedia.py` — file uploads, streaming endpoint
|
||
- `server/routes/events.py` — event CRUD and recurrence handling
|
||
- `dashboard/src/components/CustomEventModal.tsx` — event creation UI
|
||
- `dashboard/src/media.tsx` — FileManager / upload settings
|
||
- `dashboard/src/settings.tsx` — settings UI (nested tabs; system defaults for presentations and videos)
|
||
|
||
|
||
|
||
## Big picture
|
||
- Multi-service app orchestrated by Docker Compose.
|
||
- API: Flask + SQLAlchemy (MariaDB), in `server/` exposed on :8000 (health: `/health`).
|
||
- Dashboard: React + Vite in `dashboard/`, dev on :5173, served via Nginx in prod.
|
||
- MQTT broker: Eclipse Mosquitto, config in `mosquitto/config/mosquitto.conf`.
|
||
- Listener: MQTT consumer handling discovery, heartbeats, and dashboard screenshot uploads in `listener/listener.py`.
|
||
- 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`).
|
||
|
||
- Dev Container (hygiene): UI-only `Dev Containers` extension runs on host UI via `remote.extensionKind`; do not install it in-container. Dashboard installs use `npm ci`; shell aliases in `postStartCommand` are appended idempotently.
|
||
|
||
### Screenshot retention
|
||
- Screenshots sent via dashboard MQTT are stored in `server/screenshots/`.
|
||
- For each client, only the latest and last 20 timestamped screenshots are kept; older files are deleted automatically on each upload.
|
||
|
||
## Recent changes since last commit
|
||
|
||
### Latest (November 2025)
|
||
|
||
- **API Naming Convention Standardization (camelCase)**:
|
||
- Backend: Created `server/serializers.py` with `dict_to_camel_case()` utility for consistent JSON serialization
|
||
- Events API: `GET /api/events` and `GET /api/events/<id>` now return camelCase fields (`id`, `subject`, `startTime`, `endTime`, `type`, `groupId`, etc.) instead of PascalCase
|
||
- Frontend: Dashboard and appointments page updated to consume camelCase API responses
|
||
- Appointments page maintains internal PascalCase for Syncfusion scheduler compatibility with automatic mapping from API responses
|
||
- **Breaking**: External API consumers must update field names from PascalCase to camelCase
|
||
|
||
- **UTC Time Handling**:
|
||
- Database stores all timestamps in UTC (naive timestamps normalized by backend)
|
||
- API returns ISO strings without 'Z' suffix: `"2025-11-27T20:03:00"`
|
||
- Frontend: Dashboard and appointments automatically append 'Z' to parse as UTC and display in user's local timezone
|
||
- Time formatting functions use `toLocaleTimeString('de-DE')` for German locale display
|
||
- All time comparisons use UTC; `new Date().toISOString()` sends UTC back to API
|
||
- API returns ISO strings without `Z`; frontend must append `Z` before parsing to ensure UTC
|
||
|
||
- **Dashboard Enhancements**:
|
||
- New card-based design for Raumgruppen (room groups) with Syncfusion components
|
||
- Global statistics summary: total infoscreens, online/offline counts, warning groups
|
||
- Filter buttons: All, Online, Offline, Warnings with dynamic counts
|
||
- Active event display per group: shows currently playing content with type icon, title, date, and time
|
||
- Health visualization with color-coded progress bars per group
|
||
- Expandable client details with last alive timestamps
|
||
- Bulk restart functionality for offline clients per group
|
||
- Manual refresh button with toast notifications
|
||
- 15-second auto-refresh interval
|
||
|
||
### Earlier changes
|
||
|
||
- Scheduler: when formatting video events the scheduler now performs a best-effort HEAD probe of the streaming URL and includes basic metadata in the emitted payload (mime_type, size, accept_ranges). Placeholders for richer metadata (duration, resolution, bitrate, qualities, thumbnails, checksum) are included for later population by a background worker.
|
||
- Streaming endpoint: a range-capable streaming endpoint was added at `/api/eventmedia/stream/<media_id>/<filename>` that supports byte-range requests (206 Partial Content) to enable seeking from clients.
|
||
- Event model & API: `Event` gained video-related fields (`event_media_id`, `autoplay`, `loop`, `volume`) and the API accepts and persists these when creating/updating video events.
|
||
- Dashboard: UI updated to allow selecting uploaded videos for events and to specify autoplay/loop/volume. File upload settings were increased (maxFileSize raised) and the client now validates video duration (max 10 minutes) before upload.
|
||
- FileManager: uploads compute basic metadata and enqueue conversions for office formats as before; video uploads now surface size and are streamable via the new endpoint.
|
||
|
||
- Event model & API (new): Added `muted` (Boolean) for video events; create/update and GET endpoints accept, persist, and return `muted` alongside `autoplay`, `loop`, and `volume`.
|
||
- Dashboard — Settings: Settings page refactored to nested tabs; added Events → Videos defaults (autoplay, loop, volume, mute) backed by system settings keys (`video_autoplay`, `video_loop`, `video_volume`, `video_muted`).
|
||
- Dashboard — Events UI: CustomEventModal now exposes per-event video `muted` and initializes all video fields from system defaults when creating a new event.
|
||
- Dashboard — Academic Calendar: Merged “School Holidays Import” and “List” into a single “📥 Import & Liste” tab; nested tab selection is persisted with controlled `selectedItem` state to avoid jumps.
|
||
|
||
Note: these edits are intentionally backwards-compatible — if the probe fails, the scheduler still emits the stream URL and the client should fallback to a direct play attempt or request richer metadata when available.
|
||
|
||
Backend rework notes (no version bump):
|
||
- Dev container hygiene: UI-only Remote Containers; reproducible dashboard installs (`npm ci`); idempotent shell aliases.
|
||
- Serialization consistency: snake_case internal → camelCase external via `server/serializers.py` for all JSON.
|
||
- UTC normalization across routes/scheduler; enums and datetimes serialize consistently.
|
||
|
||
## Service boundaries & data flow
|
||
- 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).
|
||
- 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`.
|
||
- Scheduler queries a future window (default: 7 days) to expand recurring events using RFC 5545 rules, applies event exceptions (skipped dates, detached occurrences), and publishes only events that are active at the current time (UTC). When a group has no active events, the scheduler clears its retained topic by publishing an empty list. Time comparisons are UTC; naive timestamps are normalized. Logging is concise; conversion lookups are cached and logged only once per media.
|
||
- 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`.
|
||
- Heartbeat: `infoscreen/{uuid}/heartbeat` updates `Client.last_alive` (UTC).
|
||
- Event lists (retained): `infoscreen/events/{group_id}` from `scheduler/scheduler.py`.
|
||
- Per-client group assignment (retained): `infoscreen/{uuid}/group_id` via `server/mqtt_helper.py`.
|
||
- Screenshots: server-side folders `server/received_screenshots/` and `server/screenshots/`; Nginx exposes `/screenshots/{uuid}.jpg` via `server/wsgi.py` route.
|
||
|
||
- Dev Container guidance: If extensions reappear inside the container, remove UI-only extensions from `devcontainer.json` `extensions` and map them in `remote.extensionKind` as `"ui"`.
|
||
|
||
- Presentation conversion (PPT/PPTX/ODP → PDF):
|
||
- Trigger: on upload in `server/routes/eventmedia.py` for media types `ppt|pptx|odp` (compute sha256, upsert `Conversion`, enqueue job).
|
||
- Worker: RQ worker runs `server.worker.convert_event_media_to_pdf`, calls Gotenberg LibreOffice endpoint, writes to `server/media/converted/`.
|
||
- Services: Redis (queue) and Gotenberg added in compose; worker service consumes the `conversions` queue.
|
||
- Env: `REDIS_URL` (default `redis://redis:6379/0`), `GOTENBERG_URL` (default `http://gotenberg:3000`).
|
||
- Endpoints: `POST /api/conversions/<media_id>/pdf` (ensure/enqueue), `GET /api/conversions/<media_id>/status`, `GET /api/files/converted/<path>` (serve PDFs).
|
||
- 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`)
|
||
- User model: Includes 7 new audit/security fields (migration: `4f0b8a3e5c20_add_user_audit_fields.py`):
|
||
- `last_login_at`, `last_password_change_at`: TIMESTAMP (UTC) tracking for auth events
|
||
- `failed_login_attempts`, `last_failed_login_at`: Security monitoring for brute-force detection
|
||
- `locked_until`: TIMESTAMP placeholder for account lockout (infrastructure in place, not yet enforced)
|
||
- `deactivated_at`, `deactivated_by`: Soft-delete audit trail (FK self-reference); soft deactivation is the default, hard delete superadmin-only
|
||
- Role hierarchy (privilege escalation enforced): `user` < `editor` < `admin` < `superadmin`
|
||
- System settings: `system_settings` key–value store via `SystemSetting` for global configuration (e.g., WebUntis/Vertretungsplan supplement-table). Managed through routes in `server/routes/system_settings.py`.
|
||
- Presentation defaults (system-wide):
|
||
- `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.
|
||
- Video defaults (system-wide):
|
||
- `video_autoplay` ("true"/"false", default "true")
|
||
- `video_loop` ("true"/"false", default "true")
|
||
- `video_volume` (0.0–1.0, default "0.8")
|
||
- `video_muted` ("true"/"false", default "false")
|
||
Used as initial values when creating new video events; editable per event.
|
||
- Events: Added `page_progress` (Boolean) and `auto_progress` (Boolean) for presentation behavior per event.
|
||
- Event (video fields): `event_media_id`, `autoplay`, `loop`, `volume`, `muted`.
|
||
- WebUntis URL: WebUntis uses the existing Vertretungsplan/Supplement-Table URL (`supplement_table_url`). There is no separate `webuntis_url` setting; use `GET/POST /api/system-settings/supplement-table`.
|
||
|
||
- Conversions:
|
||
- Enum `ConversionStatus`: `pending`, `processing`, `ready`, `failed`.
|
||
- Table `conversions`: `id`, `source_event_media_id` (FK→`event_media.id` ondelete CASCADE), `target_format`, `target_path`, `status`, `file_hash` (sha256), `started_at`, `completed_at`, `error_message`.
|
||
- Indexes: `(source_event_media_id, target_format)`, `(status, target_format)`; Unique: `(source_event_media_id, target_format, file_hash)`.
|
||
|
||
## API patterns
|
||
- Blueprints live in `server/routes/*` and are registered in `server/wsgi.py` with `/api/...` prefixes.
|
||
- Session usage: instantiate `Session()` per request, commit when mutating, and always `session.close()` before returning.
|
||
- Examples:
|
||
- Clients: `server/routes/clients.py` includes bulk group updates and MQTT sync (`publish_multiple_client_groups`).
|
||
- Groups: `server/routes/groups.py` computes “alive” using a grace period that varies by `ENV`.
|
||
- Events: `server/routes/events.py` serializes enum values to strings and normalizes times to UTC. Recurring events are only deactivated after their recurrence_end (UNTIL); non-recurring events deactivate after their end time. Event exceptions are respected and rendered in scheduler output.
|
||
- Media: `server/routes/eventmedia.py` implements a simple file manager API rooted at `server/media/`.
|
||
- System settings: `server/routes/system_settings.py` exposes key–value CRUD (`/api/system-settings`) and a convenience endpoint for WebUntis/Vertretungsplan supplement-table: `GET/POST /api/system-settings/supplement-table` (admin+).
|
||
- Academic periods: `server/routes/academic_periods.py` exposes:
|
||
- `GET /api/academic_periods` — list all periods
|
||
- `GET /api/academic_periods/active` — currently active period
|
||
- `POST /api/academic_periods/active` — set active period (deactivates others)
|
||
- `GET /api/academic_periods/for_date?date=YYYY-MM-DD` — period covering given date
|
||
- User management: `server/routes/users.py` exposes comprehensive CRUD for users (admin+):
|
||
- `GET /api/users` — list all users (role-filtered: admin sees user/editor/admin, superadmin sees all); includes audit fields in camelCase (lastLoginAt, lastPasswordChangeAt, failedLoginAttempts, deactivatedAt, deactivatedBy)
|
||
- `POST /api/users` — create user with username, password (min 6 chars), role, and status; admin cannot create superadmin; initializes audit fields
|
||
- `GET /api/users/<id>` — get detailed user record with all audit fields
|
||
- `PUT /api/users/<id>` — update user (cannot change own role/status; admin cannot modify superadmin accounts)
|
||
- `PUT /api/users/<id>/password` — admin password reset (requires backend check to reject self-reset for consistency)
|
||
- `DELETE /api/users/<id>` — hard delete (superadmin only, with self-deletion check)
|
||
- Auth routes (`server/routes/auth.py`): Enhanced to track login events (sets `last_login_at`, resets `failed_login_attempts` on success; increments `failed_login_attempts` and `last_failed_login_at` on failure). Self-service password change via `PUT /api/auth/change-password` requires current password verification.
|
||
|
||
Documentation maintenance: keep this file aligned with real patterns; update when routes/session/UTC rules change. Avoid long prose; link exact paths.
|
||
|
||
## Frontend patterns (dashboard)
|
||
- Vite React app; proxies `/api` and `/screenshots` to API in dev (`vite.config.ts`).
|
||
- Uses Syncfusion components; Vite config pre-bundles specific packages to avoid alias issues.
|
||
- Environment: `VITE_API_URL` provided at build/run; in dev compose, proxy handles `/api` so local fetches can use relative `/api/...` paths.
|
||
- **API Response Format**: All API endpoints return camelCase JSON (e.g., `startTime`, `endTime`, `groupId`). Frontend consumes camelCase directly.
|
||
- **UTC Time Parsing**: API returns ISO strings without 'Z' suffix. Frontend appends 'Z' before parsing to ensure UTC interpretation: `const utcString = dateStr.endsWith('Z') ? dateStr : dateStr + 'Z'; new Date(utcString);`. Display uses `toLocaleTimeString('de-DE')` for German format.
|
||
|
||
- Dev Container: When adding frontend deps, prefer `npm ci` and, if using named volumes, recreate dashboard `node_modules` volume so installs occur inside the container.
|
||
- Theming: Syncfusion Material 3 theme is used. All component CSS is imported centrally in `dashboard/src/main.tsx` (base, navigations, buttons, inputs, dropdowns, popups, kanban, grids, schedule, filemanager, notifications, layouts, lists, calendars, splitbuttons, icons). Tailwind CSS has been removed.
|
||
- Scheduler (appointments page): top bar includes Group and Academic Period selectors (Syncfusion DropDownList). Selecting a period calls `POST /api/academic_periods/active`, moves the calendar to today’s month/day within the period year, and refreshes a right-aligned indicator row showing:
|
||
- Holidays present in the current view (count)
|
||
- Period label (display_name or name) with a badge indicating whether any holidays exist in that period (overlap check)
|
||
|
||
- Recurrence & holidays (latest):
|
||
- Backend stores holiday skips in `EventException` and emits `RecurrenceException` (EXDATE) for master events in `GET /api/events`. EXDATE tokens are formatted in RFC 5545 compact form (`yyyyMMddTHHmmssZ`) and correspond to each occurrence start time (UTC). Syncfusion uses these to exclude holiday instances reliably.
|
||
- Frontend lets Syncfusion handle all recurrence patterns natively (no client-side expansion). Scheduler field mappings include `recurrenceID`, `recurrenceRule`, and `recurrenceException` so series and edited occurrences are recognized correctly.
|
||
- Event deletion: All event types (single, single-in-series, entire series) are handled with custom dialogs. The frontend intercepts Syncfusion's built-in RecurrenceAlert and DeleteAlert popups to provide a unified, user-friendly deletion flow:
|
||
- Single (non-recurring) event: deleted directly after confirmation.
|
||
- Single occurrence of a recurring series: user can delete just that instance.
|
||
- Entire recurring series: user can delete all occurrences after a final custom confirmation dialog.
|
||
- Detached occurrences (edited/broken out): treated as single events.
|
||
- Single occurrence editing: Users can detach individual occurrences from recurring series. The frontend hooks `actionComplete`/`onActionCompleted` with `requestType='eventChanged'` to persist changes: it calls `POST /api/events/<id>/occurrences/<date>/detach` for single-occurrence edits and `PUT /api/events/<id>` for series or single events as appropriate. The backend creates `EventException` and a standalone `Event` without modifying the master beyond EXDATEs.
|
||
- UI: Events with `SkipHolidays` render a TentTree icon next to the main event icon. The custom recurrence icon in the header was removed; rely on Syncfusion’s native lower-right recurrence badge.
|
||
- Website & WebUntis: Both event types display a website. WebUntis reads its URL from the system `supplement_table_url` and does not provide a per-event URL field.
|
||
|
||
- Program info page (`dashboard/src/programminfo.tsx`):
|
||
- Loads data from `dashboard/public/program-info.json` (app name, version, build info, tech stack, changelog).
|
||
- Uses Syncfusion card classes (`e-card`, `e-card-header`, `e-card-title`, `e-card-content`) for consistent styling.
|
||
- Changelog is paginated with `PagerComponent` (from `@syncfusion/ej2-react-grids`), default page size 5; adjust `pageSize` or add a selector as needed.
|
||
|
||
- Groups page (`dashboard/src/infoscreen_groups.tsx`):
|
||
- Migrated to Syncfusion inputs and popups: Buttons, TextBox, DropDownList, Dialog; Kanban remains for drag/drop.
|
||
- Unified toast/dialog wording; replaced legacy alerts with toasts; spacing handled via inline styles to avoid Tailwind dependency.
|
||
|
||
- Header user menu (top-right):
|
||
- Shows current username and role; click opens a menu with "Passwort ändern" (lock icon), "Profil", and "Abmelden".
|
||
- Implemented with Syncfusion DropDownButton (`@syncfusion/ej2-react-splitbuttons`).
|
||
- "Passwort ändern": Opens self-service password change dialog (available to all authenticated users); requires current password verification, new password min 6 chars, must match confirm field; calls `PUT /api/auth/change-password`
|
||
- "Abmelden" navigates to `/logout`; the page invokes backend logout and redirects to `/login`.
|
||
|
||
- User management page (`dashboard/src/users.tsx`):
|
||
- Full CRUD interface for managing users (admin+ only in menu); accessible via "Benutzer" sidebar entry
|
||
- Syncfusion GridComponent: 20 per page (configurable), sortable columns (ID, username, role), custom action button template with role-based visibility
|
||
- Statistics cards: total users, active (non-deactivated), inactive (deactivated) counts
|
||
- Dialogs: Create (username/password/role/status), Edit (with self-edit protections), Password Reset (admin only, no current password required), Delete (superadmin only, self-check), Details (read-only audit info with formatted timestamps)
|
||
- Role badges: Color-coded display (user: gray, editor: blue, admin: green, superadmin: red)
|
||
- Audit information displayed: last login, password change, last failed login, deactivation timestamps and deactivating user
|
||
- Role-based permissions (enforced backend + frontend):
|
||
- Admin: can manage user/editor/admin roles (not superadmin); soft-deactivate only; cannot see/edit superadmin accounts
|
||
- Superadmin: can manage all roles including other superadmins; can permanently hard-delete users
|
||
- Security rules enforced: cannot change own role, cannot deactivate own account, cannot delete self, cannot reset own password via admin route (must use self-service)
|
||
- API client in `dashboard/src/apiUsers.ts` for all user operations (listUsers, getUser, createUser, updateUser, resetUserPassword, deleteUser)
|
||
- Menu visibility: "Benutzer" menu item only visible to admin+ (role-gated in App.tsx)
|
||
|
||
- Settings page (`dashboard/src/settings.tsx`):
|
||
- Structure: Syncfusion TabComponent with role-gated tabs
|
||
- 📅 Academic Calendar (all users)
|
||
- 📥 Import & Liste: CSV/TXT import and list combined
|
||
- 🗂️ Perioden: select and set active period (uses `/api/academic_periods` routes)
|
||
- 🖥️ Display & Clients (admin+)
|
||
- Default Settings: placeholders for heartbeat, screenshots, defaults
|
||
- Client Configuration: quick links to Clients and Groups pages
|
||
- 🎬 Media & Files (admin+)
|
||
- Upload Settings: placeholders for limits and types
|
||
- Conversion Status: placeholder for conversions overview
|
||
- 🗓️ Events (admin+)
|
||
- WebUntis / Vertretungsplan: system-wide supplement table URL with enable/disable, save, and preview; persists via `/api/system-settings/supplement-table`
|
||
- 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).
|
||
- Videos: system-wide defaults for `autoplay`, `loop`, `volume`, and `muted`; persisted via `/api/system-settings` keys (`video_autoplay`, `video_loop`, `video_volume`, `video_muted`). These defaults are applied when creating new video events (the custom event modal reads them and falls back to per-event values when editing).
|
||
- Other event types (website, message, other): placeholders for defaults
|
||
- ⚙️ System (superadmin)
|
||
- Organization Info and Advanced Configuration placeholders
|
||
- Role gating: Admin/Superadmin tabs are hidden if the user lacks permission; System is superadmin-only
|
||
- API clients use relative `/api/...` URLs so Vite dev proxy handles requests without CORS issues. The settings UI calls are centralized in `dashboard/src/apiSystemSettings.ts`.
|
||
- Nested tabs: implemented as controlled components using `selectedItem` with stateful handlers to prevent sub-tab resets during updates.
|
||
|
||
- Dashboard page (`dashboard/src/dashboard.tsx`):
|
||
- Card-based overview of all Raumgruppen (room groups) with real-time status monitoring
|
||
- Global statistics: total infoscreens, online/offline counts, warning groups
|
||
- Filter buttons: All / Online / Offline / Warnings with dynamic counts
|
||
- Per-group cards show:
|
||
- Currently active event (title, type, date/time in local timezone)
|
||
- Health bar with online/offline ratio and color-coded status
|
||
- Expandable client list with last alive timestamps
|
||
- Bulk restart button for offline clients
|
||
- Uses Syncfusion ButtonComponent, ToastComponent, and card CSS classes
|
||
- Auto-refresh every 15 seconds; manual refresh button available
|
||
- "Nicht zugeordnet" group always appears last in sorted list
|
||
|
||
- User dropdown technical notes:
|
||
- Dependencies: `@syncfusion/ej2-react-splitbuttons` and `@syncfusion/ej2-splitbuttons` must be installed.
|
||
- Vite: add both to `optimizeDeps.include` in `vite.config.ts` to avoid import-analysis errors.
|
||
- Dev containers: when `node_modules` is a named volume, recreate the dashboard node_modules volume after adding dependencies so `npm ci` runs inside the container.
|
||
|
||
Note: Syncfusion usage in the dashboard is already documented above; if a UI for conversion status/downloads is added later, link its routes and components here.
|
||
|
||
## Local development
|
||
- Compose: development is `docker-compose.yml` + `docker-compose.override.yml`.
|
||
- API (dev): `server/Dockerfile.dev` with debugpy on 5678, Flask app `wsgi:app` on :8000.
|
||
- Dashboard (dev): `dashboard/Dockerfile.dev` exposes :5173 and waits for API via `dashboard/wait-for-backend.sh`.
|
||
- Mosquitto: allows anonymous in dev; WebSocket on :9001.
|
||
- Common env vars: `DB_CONN`, `DB_USER`, `DB_PASSWORD`, `DB_HOST=db`, `DB_NAME`, `ENV`, `MQTT_USER`, `MQTT_PASSWORD`.
|
||
- Alembic: prod compose runs `alembic ... upgrade head` and `server/init_defaults.py` before gunicorn.
|
||
- Local dev: prefer `python server/initialize_database.py` for one-shot setup (migrations + defaults + academic periods).
|
||
- Defaults: `server/init_defaults.py` seeds initial system settings like `supplement_table_url` and `supplement_table_enabled` if missing.
|
||
- `server/init_academic_periods.py` remains available to (re)seed school years.
|
||
|
||
## Production
|
||
- `docker-compose.prod.yml` uses prebuilt images (`ghcr.io/robbstarkaustria/*`).
|
||
- Nginx serves dashboard and proxies API; TLS certs expected in `certs/` and mounted to `/etc/nginx/certs`.
|
||
|
||
## Environment variables (reference)
|
||
- DB_CONN — Preferred DB URL for services. Example: `mysql+pymysql://${DB_USER}:${DB_PASSWORD}@db/${DB_NAME}`
|
||
- DB_USER, DB_PASSWORD, DB_NAME, DB_HOST — Used to assemble DB_CONN in dev if missing; inside containers `DB_HOST=db`.
|
||
- ENV — `development` or `production`; in development, `server/database.py` loads `.env`.
|
||
- MQTT_BROKER_HOST, MQTT_BROKER_PORT — Defaults `mqtt` and `1883`; MQTT_USER/MQTT_PASSWORD optional (dev often anonymous per Mosquitto config).
|
||
- VITE_API_URL — Dashboard build-time base URL (prod); in dev the Vite proxy serves `/api` to `server:8000`.
|
||
- HEARTBEAT_GRACE_PERIOD_DEV / HEARTBEAT_GRACE_PERIOD_PROD — Groups "alive" window (defaults 180s dev / 170s prod). Clients send heartbeats every ~65s; grace periods allow 2 missed heartbeats plus safety margin.
|
||
- REFRESH_SECONDS — Optional scheduler republish interval; `0` disables periodic refresh.
|
||
|
||
## Conventions & gotchas
|
||
- **Datetime Handling**:
|
||
- Always compare datetimes in UTC; some DB values may be naive—normalize before comparing (see `routes/events.py`).
|
||
- Database stores timestamps in UTC (naive datetimes are normalized to UTC by backend)
|
||
- API returns ISO strings **without** 'Z' suffix: `"2025-11-27T20:03:00"`
|
||
- Frontend **must** append 'Z' before parsing: `const utcStr = dateStr.endsWith('Z') ? dateStr : dateStr + 'Z'; new Date(utcStr);`
|
||
- Display in local timezone using `toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })`
|
||
- When sending to API, use `date.toISOString()` which includes 'Z' and is UTC
|
||
- Frontend must append `Z` to API strings before parsing; backend compares in UTC and returns ISO without `Z`.
|
||
- **JSON Naming Convention**:
|
||
- Backend uses snake_case internally (Python convention)
|
||
- API returns camelCase JSON (web standard): `startTime`, `endTime`, `groupId`, etc.
|
||
- Use `dict_to_camel_case()` from `server/serializers.py` before `jsonify()`
|
||
- Frontend consumes camelCase directly; Syncfusion scheduler maintains internal PascalCase with field mappings
|
||
- 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).
|
||
- Clients should parse `event_type` and then read the corresponding nested payload (`presentation`, `website`, `video`, etc.). `website` and `webuntis` use the same nested `website` payload with `type: browser` and a `url`. Video events include `autoplay`, `loop`, `volume`, and `muted`.
|
||
- 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`).
|
||
- When adding a new route:
|
||
1) Create a Blueprint in `server/routes/...`,
|
||
2) Register it in `server/wsgi.py`,
|
||
3) Manage `Session()` lifecycle,
|
||
4) Return JSON-safe values (serialize enums and datetimes), and
|
||
5) Use `dict_to_camel_case()` for camelCase JSON responses
|
||
|
||
Docs maintenance guardrails (solo-friendly): Update this file alongside code changes (services/MQTT/API/UTC/env). Keep it concise (20–50 lines per section). Never include secrets.
|
||
- When extending media types, update `MediaType` and any logic in `eventmedia` and dashboard that depends on it.
|
||
- Academic periods: Events/media can be optionally associated with periods for educational organization. Only one period should be active at a time (`is_active=True`).
|
||
- Initialization scripts: legacy DB init scripts were removed; use Alembic and `initialize_database.py` going forward.
|
||
|
||
### Recurrence & holidays: conventions
|
||
- Do not pre-expand recurrences on the backend. Always send master events with `RecurrenceRule` + `RecurrenceException`.
|
||
- Ensure EXDATE tokens are RFC 5545 timestamps (`yyyyMMddTHHmmssZ`) matching the occurrence start time (UTC) so Syncfusion can exclude them natively.
|
||
- When `skip_holidays` or recurrence changes, regenerate `EventException` rows so `RecurrenceException` stays in sync.
|
||
- Single occurrence detach: Use `POST /api/events/<id>/occurrences/<date>/detach` to create standalone events and add EXDATE entries without modifying master events. The frontend persists edits via `actionComplete` (`requestType='eventChanged'`).
|
||
|
||
## Quick examples
|
||
- Add client description persists to DB and publishes group via MQTT: see `PUT /api/clients/<uuid>/description` in `routes/clients.py`.
|
||
- Bulk group assignment emits retained messages for each client: `PUT /api/clients/group`.
|
||
- 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).
|
||
|
||
## Scheduler payloads: website & webuntis
|
||
- For both `website` and `webuntis`, the scheduler emits a nested `website` object:
|
||
- `{ "type": "browser", "url": "https://..." }`
|
||
- The `event_type` remains `website` or `webuntis`. Clients should treat both identically for rendering.
|
||
- The WebUntis URL is set at event creation by reading the system `supplement_table_url`.
|
||
|
||
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
|
||
- **Purpose**: Organize events and media by educational cycles (school years, semesters, trimesters).
|
||
- **Design**: Fully backward compatible - existing events/media continue to work without period assignment.
|
||
- **Usage**: New events/media can optionally reference `academic_period_id` for better organization and filtering.
|
||
- **Constraints**: Only one period can be active at a time; use `init_academic_periods.py` for Austrian school year setup.
|
||
- **UI Integration**: The dashboard highlights the currently selected period and whether a holiday plan exists within that date range. Holiday linkage currently uses date overlap with `school_holidays`; an explicit `academic_period_id` on `school_holidays` can be added later if tighter association is required.
|
||
|
||
## Changelog Style Guide (Program info)
|
||
|
||
- Source: `dashboard/public/program-info.json`; newest entry first
|
||
- Fields per release: `version`, `date` (YYYY-MM-DD), `changes` (array of short bullets)
|
||
- Tone: concise, user-facing; German wording; area prefixes allowed (e.g., “UI: …”, “API: …”)
|
||
- Categories via emoji or words: Added (🆕/✨), Changed (🛠️), Fixed (✅/🐛), Removed (🗑️), Security (🔒), Deprecated (⚠️)
|
||
- Breaking changes must be prefixed with `BREAKING:`
|
||
- Keep ≤ 8–10 bullets; summarize or group micro-changes
|
||
- JSON hygiene: valid JSON, no trailing commas, don’t edit historical entries except typos
|