Files
infoscreen/.github/copilot-instructions.md
RobbStarkAustria 3487d33a2f feat: improve scheduler recurrence, DB config, and docs
- Broaden scheduler query window to next N days for proper recurring event expansion (scheduler.py)
- Update DB connection logic for consistent .env loading and fallback (database.py)
- Harden timezone handling and logging in scheduler and DB utils
- Stop auto-deactivating recurring events before recurrence_end (API/events)
- Update documentation to reflect new scheduler, API, and logging behavior
2025-10-18 06:18:06 +00:00

18 KiB
Raw Blame History

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.

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 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.
    • Nginx: Reverse proxy routes /api/* and /screenshots/* to API; everything else to dashboard (nginx.conf).

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.
    • Listener also creates its own engine for writes to clients.
  • 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.

  • 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)

  • 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.

  • 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).

  • Times are stored as timezone-aware; treat comparisons in UTC (see scheduler and routes/events).

  • 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 keyvalue 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

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.

  • 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 todays 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 Syncfusions native lower-right recurrence badge.

  • 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 “Profil” and “Abmelden”.
    • Implemented with Syncfusion DropDownButton (@syncfusion/ej2-react-splitbuttons).
    • “Abmelden” navigates to /logout; the page invokes backend logout and redirects to /login.
  • Settings page (dashboard/src/settings.tsx):

    • Structure: Syncfusion TabComponent with role-gated tabs
      • 📅 Academic Calendar (all users)
        • School Holidays: CSV/TXT import and list
        • Academic Periods: 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
        • Other event types (presentation, website, video, 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.
  • 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

  • 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.
  • 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.
  • 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, and
    4. Return JSON-safe values (serialize enums and datetimes).
  • 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.

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 ≤ 810 bullets; summarize or group micro-changes
  • JSON hygiene: valid JSON, no trailing commas, dont edit historical entries except typos