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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user