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) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-12 13:10:54 +02:00
parent ce4faedd88
commit 2170f4a7b1
3 changed files with 64 additions and 1 deletions
+23
View File
@@ -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