feat(dashboard+api): card-based dashboard, camelCase API, UTC fixes

Dashboard: new Syncfusion card layout, global stats, filters, health bars, active event display, client details, bulk restart, 15s auto-refresh, manual refresh toasts
API: standardized responses to camelCase; added serializers.py and updated events endpoints
Time: ensured UTC storage; frontend appends 'Z' for parsing and displays local time
Docs: updated copilot-instructions.md, README.md, TECH-CHANGELOG.md
Program Info: bumped to 2025.1.0-alpha.12 with user-facing changelog
BREAKING: external API consumers must migrate field names from PascalCase to camelCase.
This commit is contained in:
RobbStarkAustria
2025-11-27 20:30:00 +00:00
parent 452ba3033b
commit 6dcf93f0dd
13 changed files with 1282 additions and 350 deletions

View File

@@ -1,6 +1,7 @@
from flask import Blueprint, request, jsonify
from server.permissions import editor_or_higher
from server.database import Session
from server.serializers import dict_to_camel_case, dict_to_snake_case
from models.models import Event, EventMedia, MediaType, EventException, SystemSetting
from datetime import datetime, timezone, timedelta
from sqlalchemy import and_
@@ -95,28 +96,29 @@ def get_events():
recurrence_exception = ','.join(tokens)
base_payload = {
"Id": str(e.id),
"GroupId": e.group_id,
"Subject": e.title,
"Description": getattr(e, 'description', None),
"StartTime": e.start.isoformat() if e.start else None,
"EndTime": e.end.isoformat() if e.end else None,
"IsAllDay": False,
"MediaId": e.event_media_id,
"Type": e.event_type.value if e.event_type else None, # <-- Enum zu String!
"Icon": get_icon_for_type(e.event_type.value if e.event_type else None),
"id": str(e.id),
"group_id": e.group_id,
"subject": e.title,
"description": getattr(e, 'description', None),
"start_time": e.start.isoformat() if e.start else None,
"end_time": e.end.isoformat() if e.end else None,
"is_all_day": False,
"media_id": e.event_media_id,
"type": e.event_type.value if e.event_type else None,
"icon": get_icon_for_type(e.event_type.value if e.event_type else None),
# Recurrence metadata
"RecurrenceRule": e.recurrence_rule,
"RecurrenceEnd": e.recurrence_end.isoformat() if e.recurrence_end else None,
"RecurrenceException": recurrence_exception,
"SkipHolidays": bool(getattr(e, 'skip_holidays', False)),
"recurrence_rule": e.recurrence_rule,
"recurrence_end": e.recurrence_end.isoformat() if e.recurrence_end else None,
"recurrence_exception": recurrence_exception,
"skip_holidays": bool(getattr(e, 'skip_holidays', False)),
}
result.append(base_payload)
# No need to emit synthetic override events anymore since detached occurrences
# are now real Event rows that will be returned in the main query
session.close()
return jsonify(result)
# Convert all keys to camelCase for frontend
return jsonify(dict_to_camel_case(result))
@events_bp.route("/<event_id>", methods=["GET"]) # get single event
@@ -126,32 +128,32 @@ def get_event(event_id):
event = session.query(Event).filter_by(id=event_id).first()
if not event:
return jsonify({"error": "Termin nicht gefunden"}), 404
# Convert event to dictionary with all necessary fields
event_dict = {
"Id": str(event.id),
"Subject": event.title,
"StartTime": event.start.isoformat() if event.start else None,
"EndTime": event.end.isoformat() if event.end else None,
"Description": event.description,
"Type": event.event_type.value if event.event_type else "presentation",
"IsAllDay": False, # Assuming events are not all-day by default
"MediaId": str(event.event_media_id) if event.event_media_id else None,
"SlideshowInterval": event.slideshow_interval,
"PageProgress": event.page_progress,
"AutoProgress": event.auto_progress,
"WebsiteUrl": event.event_media.url if event.event_media and hasattr(event.event_media, 'url') else None,
"id": str(event.id),
"subject": event.title,
"start_time": event.start.isoformat() if event.start else None,
"end_time": event.end.isoformat() if event.end else None,
"description": event.description,
"type": event.event_type.value if event.event_type else "presentation",
"is_all_day": False, # Assuming events are not all-day by default
"media_id": str(event.event_media_id) if event.event_media_id else None,
"slideshow_interval": event.slideshow_interval,
"page_progress": event.page_progress,
"auto_progress": event.auto_progress,
"website_url": event.event_media.url if event.event_media and hasattr(event.event_media, 'url') else None,
# Video-specific fields
"Autoplay": event.autoplay,
"Loop": event.loop,
"Volume": event.volume,
"Muted": event.muted,
"RecurrenceRule": event.recurrence_rule,
"RecurrenceEnd": event.recurrence_end.isoformat() if event.recurrence_end else None,
"SkipHolidays": event.skip_holidays,
"Icon": get_icon_for_type(event.event_type.value if event.event_type else "presentation"),
"autoplay": event.autoplay,
"loop": event.loop,
"volume": event.volume,
"muted": event.muted,
"recurrence_rule": event.recurrence_rule,
"recurrence_end": event.recurrence_end.isoformat() if event.recurrence_end else None,
"skip_holidays": event.skip_holidays,
"icon": get_icon_for_type(event.event_type.value if event.event_type else "presentation"),
}
return jsonify(dict_to_camel_case(event_dict))
return jsonify(event_dict)
except Exception as e:
return jsonify({"error": f"Fehler beim Laden des Termins: {str(e)}"}), 500