DB/model Add Conversion model + ConversionStatus enum (pending, processing, ready, failed) Alembic migrations: create conversions table, indexes, unique (source_event_media_id, target_format, file_hash), and NOT NULL on file_hash API Enqueue on upload (ppt|pptx|odp) in routes/eventmedia.py: compute sha256, upsert Conversion, enqueue job New routes: POST /api/conversions/<media_id>/pdf — ensure/enqueue conversion GET /api/conversions/<media_id>/status — latest status/details GET /api/files/converted/<path> — serve converted PDFs Register conversions blueprint in wsgi Worker server/worker.py: convert_event_media_to_pdf Calls Gotenberg /forms/libreoffice/convert, writes to server/media/converted/ Updates Conversion status, timestamps, error messages Fix media root resolution to /server/media Prefer function enqueue over string path; expose server.worker in package init for RQ string compatibility Queue/infra server/task_queue.py: RQ queue helper (REDIS_URL, default redis://redis:6379/0) docker-compose: Add redis and gotenberg services Add worker service (rq worker conversions) Pass REDIS_URL and GOTENBERG_URL to server/worker Mount shared media volume in prod for API/worker parity docker-compose.override: Add dev redis/gotenberg/worker services Ensure PYTHONPATH + working_dir allow importing server.worker Use rq CLI instead of python -m rq for worker Dashboard dev: run as appropriate user/root and pre-create/chown caches to avoid EACCES Dashboard dev UX Vite: set cacheDir .vite to avoid EACCES in node_modules Disable Node inspector by default to avoid port conflicts Docs Update copilot-instructions.md with conversion system: flow, services, env vars, endpoints, storage paths, and data model
9.5 KiB
Copilot instructions for infoscreen_2025
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. - Nginx: Reverse proxy routes
/api/*and/screenshots/*to API; everything else to dashboard (nginx.conf).
- API: Flask + SQLAlchemy (MariaDB), in
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.envonly in development). - Scheduler loads
DB_CONNinscheduler/db_utils.py. - Listener also creates its own engine for writes to
clients.
- API builds its engine in
-
MQTT topics (paho-mqtt v2, use Callback API v2):
- Discovery:
infoscreen/discovery(JSON includesuuid, hw/ip data). ACK toinfoscreen/{uuid}/discovery_ack. Seelistener/listener.py. - Heartbeat:
infoscreen/{uuid}/heartbeatupdatesClient.last_alive(UTC). - Event lists (retained):
infoscreen/events/{group_id}fromscheduler/scheduler.py. - Per-client group assignment (retained):
infoscreen/{uuid}/group_idviaserver/mqtt_helper.py.
- Discovery:
-
Screenshots: server-side folders
server/received_screenshots/andserver/screenshots/; Nginx exposes/screenshots/{uuid}.jpgviaserver/wsgi.pyroute. -
Presentation conversion (PPT/PPTX/ODP → PDF):
- Trigger: on upload in
server/routes/eventmedia.pyfor media typesppt|pptx|odp(compute sha256, upsertConversion, enqueue job). - Worker: RQ worker runs
server.worker.convert_event_media_to_pdf, calls Gotenberg LibreOffice endpoint, writes toserver/media/converted/. - Services: Redis (queue) and Gotenberg added in compose; worker service consumes the
conversionsqueue. - Env:
REDIS_URL(defaultredis://redis:6379/0),GOTENBERG_URL(defaulthttp://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 underserver/media/converted/(prod compose mounts a shared volume for this path).
- Trigger: on upload in
Data model highlights (see models/models.py)
-
Enums:
EventType(presentation, website, video, message, webuntis),MediaType(file/website types), andAcademicPeriodType(schuljahr, semester, trimester). -
Tables:
clients,client_groups,events,event_media,users,academic_periods,school_holidays. -
Academic periods:
academic_periodstable supports educational institution cycles (school years, semesters). Events and media can be optionally linked viaacademic_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.idondelete 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).
- Enum
API patterns
- Blueprints live in
server/routes/*and are registered inserver/wsgi.pywith/api/...prefixes. - Session usage: instantiate
Session()per request, commit when mutating, and alwayssession.close()before returning. - Examples:
- Clients:
server/routes/clients.pyincludes bulk group updates and MQTT sync (publish_multiple_client_groups). - Groups:
server/routes/groups.pycomputes “alive” using a grace period that varies byENV. - Events:
server/routes/events.pyserializes enum values to strings and normalizes times to UTC. - Media:
server/routes/eventmedia.pyimplements a simple file manager API rooted atserver/media/. - Academic periods:
server/routes/academic_periods.pyexposes:GET /api/academic_periods— list all periodsGET /api/academic_periods/active— currently active periodPOST /api/academic_periods/active— set active period (deactivates others)GET /api/academic_periods/for_date?date=YYYY-MM-DD— period covering given date
- Clients:
Frontend patterns (dashboard)
- Vite React app; proxies
/apiand/screenshotsto API in dev (vite.config.ts). - Uses Syncfusion components; Vite config pre-bundles specific packages to avoid alias issues.
- Environment:
VITE_API_URLprovided at build/run; in dev compose, proxy handles/apiso local fetches can use relative/api/...paths. - 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)
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.devwith debugpy on 5678, Flask appwsgi:appon :8000. - Dashboard (dev):
dashboard/Dockerfile.devexposes :5173 and waits for API viadashboard/wait-for-backend.sh. - Mosquitto: allows anonymous in dev; WebSocket on :9001.
- API (dev):
- 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 headandserver/init_defaults.pybefore gunicorn.- Use
server/init_academic_periods.pyto populate default Austrian school years after migration.
- Use
Production
docker-compose.prod.ymluses 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 —
developmentorproduction; in development,server/database.pyloads.env. - MQTT_BROKER_HOST, MQTT_BROKER_PORT — Defaults
mqttand1883; 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
/apitoserver:8000. - HEARTBEAT_GRACE_PERIOD_DEV / HEARTBEAT_GRACE_PERIOD_PROD — Groups “alive” window (defaults ~15s dev / 180s prod).
- REFRESH_SECONDS — Optional scheduler republish interval;
0disables periodic refresh.
Conventions & gotchas
- Always compare datetimes in UTC; some DB values may be naive—normalize before comparing (see
routes/events.py). - 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 uselocalhostinside 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:
- Create a Blueprint in
server/routes/..., - Register it in
server/wsgi.py, - Manage
Session()lifecycle, and - Return JSON-safe values (serialize enums and datetimes).
- Create a Blueprint in
- When extending media types, update
MediaTypeand any logic ineventmediaand 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).
Quick examples
- Add client description persists to DB and publishes group via MQTT: see
PUT /api/clients/<uuid>/descriptioninroutes/clients.py. - Bulk group assignment emits retained messages for each client:
PUT /api/clients/group. - Listener heartbeat path:
infoscreen/<uuid>/heartbeat→ setsclients.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_idfor better organization and filtering. - Constraints: Only one period can be active at a time; use
init_academic_periods.pyfor 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 explicitacademic_period_idonschool_holidayscan be added later if tighter association is required.