From 2170f4a7b1f9702b6a8a68611596f7ae135c8a5a Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 12 Apr 2026 13:10:54 +0200 Subject: [PATCH] feat: Kalender-Ansicht aktualisiert sich live via SSE Backend: Neuer Event-Typ 'calendar' im Broadcaster. Wird bei Event-CRUD, Serien-Ausnahmen, Freigaben hinzufuegen/entfernen und beim Loeschen ganzer Kalender emittiert. Empfaenger: Eigentuemer + alle User mit CalendarShare auf dem jeweiligen Kalender. Frontend: CalendarView oeffnet beim Mount eine EventSource zu /api/sync/events und reloaded Kalenderliste + Events bei jedem 'calendar'-Event (300ms debounced). Damit sehen beteiligte Nutzer Aenderungen in praktisch Echtzeit - kein F5 mehr noetig. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/calendar.py | 23 +++++++++++++++++++++++ backend/app/services/events.py | 13 +++++++++++++ frontend/src/views/CalendarView.vue | 29 ++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/backend/app/api/calendar.py b/backend/app/api/calendar.py index 9ff6c69..903ab15 100644 --- a/backend/app/api/calendar.py +++ b/backend/app/api/calendar.py @@ -9,6 +9,11 @@ from app.api.auth import token_required from app.extensions import db, bcrypt from app.models.calendar import Calendar, CalendarEvent, CalendarShare from app.models.user import User +from app.services.events import notify_calendar_change + + +def _calendar_recipients(cal: Calendar): + return [s.shared_with_id for s in CalendarShare.query.filter_by(calendar_id=cal.id).all()] def _redact_if_private(event_dict: dict, is_owner: bool) -> dict: @@ -149,8 +154,12 @@ def delete_calendar(cal_id): if not cal or cal.owner_id != user.id: return jsonify({'error': 'Nicht gefunden oder keine Berechtigung'}), 404 + recipients = _calendar_recipients(cal) + owner_id = cal.owner_id + cal_id = cal.id db.session.delete(cal) db.session.commit() + notify_calendar_change(owner_id, cal_id, 'deleted', shared_with=recipients) return jsonify({'message': 'Kalender geloescht'}), 200 @@ -235,6 +244,8 @@ def create_event(cal_id): ) db.session.add(event) db.session.commit() + notify_calendar_change(cal.owner_id, cal.id, 'event', + shared_with=_calendar_recipients(cal)) return jsonify(event.to_dict()), 201 @@ -281,6 +292,8 @@ def update_event(event_id): ) event.updated_at = datetime.now(timezone.utc) db.session.commit() + notify_calendar_change(cal.owner_id, cal.id, 'event', + shared_with=_calendar_recipients(cal)) return jsonify(event.to_dict()), 200 @@ -369,8 +382,12 @@ def delete_event(event_id): if err: return err + cal = db.session.get(Calendar, event.calendar_id) db.session.delete(event) db.session.commit() + if cal: + notify_calendar_change(cal.owner_id, cal.id, 'event', + shared_with=_calendar_recipients(cal)) return jsonify({'message': 'Event geloescht'}), 200 @@ -418,6 +435,9 @@ def share_calendar(cal_id): except Exception: pass + notify_calendar_change(cal.owner_id, cal.id, 'share', + shared_with=[target.id, *_calendar_recipients(cal)]) + return jsonify({'message': f'Kalender mit {username} geteilt'}), 200 @@ -450,8 +470,11 @@ def remove_calendar_share(cal_id, share_id): if not share or share.calendar_id != cal_id: return jsonify({'error': 'Freigabe nicht gefunden'}), 404 + target_id = share.shared_with_id db.session.delete(share) db.session.commit() + notify_calendar_change(cal.owner_id, cal.id, 'share', + shared_with=[target_id, *_calendar_recipients(cal)]) return jsonify({'message': 'Freigabe entfernt'}), 200 diff --git a/backend/app/services/events.py b/backend/app/services/events.py index 92f95d5..6552de5 100644 --- a/backend/app/services/events.py +++ b/backend/app/services/events.py @@ -79,3 +79,16 @@ def notify_file_change(owner_id: int, file_id: int | None, change: str, 'change': change, # 'created' | 'updated' | 'deleted' | 'locked' | 'unlocked' 'file_id': file_id, }) + + +def notify_calendar_change(owner_id: int, calendar_id: int, change: str, + shared_with: Iterable[int] = ()) -> None: + """Emit a calendar-level change event (event added/changed/deleted or + share membership changed). Sent to owner + all users the calendar is + shared with.""" + recipients = [owner_id, *shared_with] + broadcaster.publish(recipients, { + 'type': 'calendar', + 'change': change, # 'event'|'share'|'deleted' + 'calendar_id': calendar_id, + }) diff --git a/frontend/src/views/CalendarView.vue b/frontend/src/views/CalendarView.vue index 7baa8ae..3983db9 100644 --- a/frontend/src/views/CalendarView.vue +++ b/frontend/src/views/CalendarView.vue @@ -260,7 +260,7 @@