Multi-Device-Sync + Teilen/Import + Google-Kalender-Reservierungen - #27
Merged
Conversation
Pre-existing Pylance reportArgumentType warnings on .grid(**kw) because Pylance inferred kw as dict[str, int | str] from the heterogeneous dict literal and could not narrow to grid_configure's per-parameter types.
Bei show_weekend=False blieben die Spalten 5 und 6 mit weight=1 konfiguriert und absorbierten den vom Header/Footer geforderten Extra-Platz — sichtbar als leerer Streifen rechts neben Fr. _get_inactive_grid setzt jetzt weight=0 für nicht sichtbare Spalten, gleiche Behandlung im _refresh- Resize-Pfad fuer den neuen Inactive-Frame.
Das email-Setting (UI-Label "Absender") war toter Code: kein Modul außer dem Settings-Dialog selbst hat es jemals gelesen. send_email() in mail.py setzt keinen From:-Header, der Absender wird zwingend vom OAuth-Token über userId='me' bestimmt. Ein gesetzter Wert konnte den Sender also gar nicht beeinflussen. - DEFAULTS-Key entfernt - Settings-Dialog-Zeile (label + entry + updates-Eintrag) entfernt - Tests, die email als generisches String-Setting nutzten, auf recipient umgestellt; neuer Regressionstest, der dokumentiert dass ein altes email-Feld in settings.json still ignoriert wird
Header() assignment auf message subject und tk.Tk.report_callback_exception mit pyright ignores; in theme.py _LabelButton-Subklasse und _ToggleColors TypedDict für getypte Attribute/Kwargs, width-Default 0 statt None. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Spec für Google-Drive-basierten Multi-Device-Sync (appDataFolder, LWW pro Eintrag, Konflikt-Objekte mit User-Resolution). Plus known-limitations.md mit Hinweis auf fehlende Tombstone-GC. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Truncation per _truncate reicht; wraplength entfernen, damit der gekürzte Name nicht zusätzlich umgebrochen wird. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5-phase plan: storage/settings foundation, sync engine, Drive client, UI wiring, robustness polish. TDD with frequent commits per task. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Re-add the existence guard in delete() so calling it on a never-saved date is a no-op rather than persisting a phantom tombstone. Add _REQUIRED_ENTRY_KEYS constant and validation in apply_merge() so malformed sync entries are rejected at the boundary instead of corrupting _data silently. Tighten test_delete_nonexistent to assert no phantom entry leaks; add two new tests for apply_merge validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tence Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
reconcile_reservations re-reads the store right before apply_reconciled and rebases reservations added/changed during its network phase, so a concurrent save (e.g. the startup reconcile overlapping a user save) is not lost by the blind replace. Also guards the settings-dialog calendar-id persistence behind a loaded calendar list. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
„Monat senden" und „Teilen…" zeigten bei fehlender Internetverbindung den rohen Python-Traceback im Fehler-Popup. Neuer Helfer `mail.is_offline_error` erkennt Netzwerkfehler anhand der Exception-Kette; der Sendepfad zeigt dann „Keine Internetverbindung" statt der kryptischen Detail-Ausgabe. Echte Fehler behalten den Traceback (CLAUDE.md); der Trace landet ohnehin weiter im Logfile. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eine Reservierung aendert den Zelltyp nicht mehr - sie wird immer nur als kleiner violetter Eck-Punkt auf die ohnehin gebaute Zelle gelegt. Nur-Reservierungs-Tage sehen damit aus wie ein leerer Tag (bzw. Feiertag) mit Punkt statt einer violetten Vollzelle. - _build_reservation_cell entfernt - Eck-Punkt als Canvas-Oval statt kaum sichtbarem Text-Bullet - _empty_hover faerbt den Punkt beim Hover mit - Feiertags-Tooltip geht bei Ist-Zeit+Reservierung+Feiertag nicht mehr verloren (zweite Tooltip-Zeile) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bei deaktiviertem Google-Kalender-Sync (gcal_enabled=false) wurden Reservierungen trotzdem im Kalender als Eck-Punkt gerendert und waren per Linksklick im Tages-Dialog setzbar. Neuer Helper _reservations_active() gated Grid-Rendering und Dialog-Aufruf einheitlich auf gcal_enabled. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Der Teilen-Dialog verwaltet den Empfänger bereits vollständig (vorbefülltes Feld + "Als Standard speichern"-Checkbox). Das separate Settings-Feld war eine zweite, verwirrende Pflegestelle für dieselbe Adresse (share_recipient). Der settings-Key bleibt erhalten, nur das Dialog-Feld entfällt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Das per dialog.after() verzögerte except-Lambda griff frei auf die except-Variable `e` zu. Python 3 löscht die am Ende des except-Blocks (impliziter del), sodass beim verzögerten Ausführen ein NameError statt der gedachten Fehlermeldung "Kalenderliste nicht verfügbar" auftrat. e/tb werden jetzt als Default-Argumente ans Lambda gebunden. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Xveyn
force-pushed
the
feat/multi-device-sync
branch
from
May 21, 2026 13:32
2f365f8 to
3da0010
Compare
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Der 1.13.0-Block (Reservierungen, Google-Kalender, Offline-Meldung) wandert in den 1.12.0-Block — master ist bei 1.11.0, es gab kein 1.13.0-Release. Dabei zwei veraltete Beschreibungen korrigiert: Reservierungen sind ein violetter Eck-Punkt (keine violetten Vollzellen), und das entfernte Feld „Teilen mit:" wird nicht mehr genannt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`wm_maxsize()` liefert auf Windows nur die Arbeitsfläche des Primärmonitors, sodass das Clamping einen über `parent` geöffneten Dialog wieder auf den ersten Bildschirm gezogen hat — auch wenn das Hauptfenster auf Monitor 2 saß. Neuer Helper `_parent_workarea(parent)`: ermittelt auf Windows die Arbeitsfläche des Monitors, auf dem der Parent liegt, über `MonitorFromPoint` mit dem Bildschirm-Mittelpunkt des Parent. Tk's `winfo_id()` liefert auf Windows den Client-HWND (nicht den WM-Frame) und ist als Lookup-Anker weniger robust; `MonitorFromPoint` arbeitet direkt auf den Bildschirmkoordinaten aus `winfo_rootx/y`. Explizite ctypes-Signaturen für `MonitorFromPoint` und `GetMonitorInfoW`: ohne `restype` ist der Default `c_int` (32 Bit), und ein HMONITOR auf 64-Bit-Windows kann darüber liegen — der Handle wäre dann truncated und `GetMonitorInfo` schlüge still fehl, sodass der Fallback auf den Primärmonitor zurückgriff (genau der bisherige Bug). macOS/Linux: unverändert auf `wm_maxsize()` als Fallback, weil Multi-Monitor-Workarea dort nicht ohne externe Lib aus Tk abrufbar ist. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Der Settings-Dialog ist mit der Sync- und Google-Kalender-Sektion so hoch geworden, dass auf 1080p-Monitoren der Speichern-Button unter den Bildschirmrand rutschen konnte — selbst flush an die Bildschirmkante gerückt. Beide Section-Header werden klickbar mit ▶/▼-Indikator. „Mail-Vorlage" startet eingeklappt (Block wird selten geändert, spart am meisten Höhe); „Gmail-Zugangsdaten" bleibt default ausgeklappt, da Empfänger/Name/ Bundesland in derselben Section liegen und häufig sichtbar gebraucht werden. Helper `_section_header(title, row, top_pad)`: erzeugt Header + Toggle. Caller hängt jedes zur Section gehörende Widget an die zurückgegebene `widgets`-Liste an. Beim Collapse wird pro Widget gemerkt, ob es zuletzt im Grid lag — damit eine ohnehin eingeklappte Sub-Section (z.B. „Standardzeiten:") beim Re-Expand der Eltern-Section nicht versehentlich aufgeht. Wir nutzen `winfo_manager() == "grid"` statt `winfo_ismapped()`, weil letzteres vor dem ersten Window-Mapping False liefert. Beim initialen `mv_toggle()` (Default-Einklappen vor `center_dialog_on_parent`) hätte ismapped sonst alle Widgets als „nicht sichtbar" eingestuft, kein grid_remove() ausgeführt — Header-Pfeil ging auf ▶, der Inhalt blieb trotzdem sichtbar, und der User hätte zweimal klicken müssen, bis Header und Inhalt sync sind. `winfo_manager()` ist mapping-unabhängig. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wenn `ZEITERFASSUNG_DATA_DIR` gesetzt ist, wird der base_path daher übernommen — vor der Frozen/Script-Verzweigung. Use case: Dev-Setups, in denen `python -m src.main` aus dem Repo denselben Daten-Ordner nutzen soll wie die installierte App. Hard- oder Symlinks scheitern an dieser Stelle, weil alle Stores (`storage`, `settings`, `reservations`, `conflicts_store`) per `os.replace(tmp, target)` atomar speichern und dabei den Verzeichniseintrag durch eine neue Datei ersetzen — der Link bricht beim ersten Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
appDataFolder, Scopedrive.appdata, non-sensitive). Pull beim App-Start, Push manuell oder beim Schließen. Konflikte werden als First-Class-Objekte gehalten und vom User manuell aufgelöst (A/B/eigener Wert).1.11.1enthalten, hier mit gebündelt da auf demselben Branch): Wochenenden im Kalender optional ausblendbar.🏷️ Release-Label:
release:minorBitte vor dem Merge
release:minoram PR setzen — neue Features ohne Breaking Changes. Version steht schon auf1.12.0insrc/version.py. Ohne Label triggert der Release-Workflow nicht und es wird kein Tag/Installer-Release erzeugt. (Fork-User hat keine Permissions zum Label-Setzen.)Was neu ist
Sync-Architektur
src/sync.py— pure Sync-Engine mit LWW-Merge pro Eintrag und pro Whitelist-Setting, Tombstones, Konflikt-Idempotenz, Resolution-Propagationsrc/drive.py— Drive-API-Wrapper (OAuth mit kombinierten Scopesgmail.send+userinfo.email+drive.appdata, find/download/upload)src/conflicts_store.py— lokaleconflicts.json-Persistenzsrc/dialogs/conflicts_dialog.py— Modal zur User-ResolutionGoogle-Kalender-Reservierungen
src/gcal.py— Google-Calendar-API-Wrapper (Event-CRUD, Kalenderliste, OAuth mit Scope-Union; Google-Imports lazy, damit die CI ohnerequirements.txtdurchläuft)src/reservations.py—ReservationStore, lokalereservations.json-Persistenz (eigenständiges Konzept, nicht Teil der Drive-Sync)src/reservations_sync.py— puremerge_reservations(LWW, ohne Konflikt-Dialog) +reconcile_reservations-Orchestrator (pull → merge → push)last_calendar_sync_at) unterscheidet lokale Löschungen von Remote-Neuanlagen — keine Konflikt-Dialogegcal_enabledwird durch alle Service-Getter (mail.py/drive.py) gereicht, damit das geteiltetoken.jsonalle Scopes behältTeilen / Import
src/share.py+src/dialogs/share_dialog.py— Einträge als JSON-Anhang per Gmail teilen; Empfänger direkt im Dialog eingebbar, optional als Standard speicherbarsrc/dialogs/import_dialog.py— Import mit Zeitraum-Filter und Konflikt-ModiUI
Mail-Pipeline
userinfo.emailaus dem Token gezogen; „Aktualisieren"-Button im Settings-Dialog für Token-Reconsentmail.is_offline_errorerkennt Netzwerkfehler über die Exception-Kette. Andere Fehler behalten die Detail-Ausgabe (CLAUDE.md), der Trace bleibt im LogfileStorage / Settings
modified_at,device_id,deletedals Tombstone)zeiterfassung.jsonauf neues Format (idempotent, mtime-basiert)_synced_metafür Per-Field-Tracking der Whitelist-Keysgcal_enabled/gcal_calendar_id/last_calendar_sync_at;gcal_calendar_idreist über die Settings-Sync mit, damit alle Geräte denselben Kalender treffenDoku
README.md— neue Sektionen „Multi-Device-Sync einrichten" unduserinfo.email-Scope-Hinweisdocs/superpowers/specs/2026-05-14-multi-device-sync-design.md— vollständige Specdocs/superpowers/plans/2026-05-14-multi-device-sync.md— Implementierungsplandocs/superpowers/specs/2026-05-20-google-calendar-reservierungen-design.md+docs/superpowers/plans/2026-05-20-google-calendar-reservierungen.md— Spec & Plan der Kalender-Anbindungdocs/known-limitations.md— Tombstone-GC bewusst ausgelassen, YAGNICHANGELOG.md— vollständige Feature-/Change-ListeTests / CI
test_sync.py,test_drive.py,test_conflicts_store.py,test_storage_migration.py, Erweiterungen intest_storage.py/test_settings.py/test_mail.py)test_gcal.py,test_reservations.py,test_reservations_sync.pymail.is_offline_errorintest_mail.pygoogle-api-python-client,google-auth,google-auth-oauthlibTrade-offs
etagentfernt und dasIf-Match-Pattern lässt sich mitgoogleapiclientnicht sauber bedienen. Konflikterkennung läuft stattdessen pro Eintrag übermodified_at, was für ein 1-Datei-pro-User-Sync mit niedrigem Push-Volumen ausreichend ist.docs/known-limitations.md). Größenordnung Kilobyte pro Jahr bei normalem Gebrauch.Test plan
pytest— 317 passed, 1 skippedpython build.py+ Install via Datei-Tausch in%LOCALAPPDATA%\Programs\Zeiterfassung\(Stand Multi-Device-Sync)drive.appdata→ Push → File erscheint imappDataFolder(per Drive-API verifiziert)Aktivierung im Test
Default: Sync und Google-Kalender-Abgleich sind deaktiviert. Bestehende User merken nichts. Wer testen will: Settings öffnen → „Mit Google Drive synchronisieren" bzw. „Reservierungen mit Google Kalender abgleichen" anhaken → einmalig OAuth-Reconsent (siehe README für Cloud-Console-Setup mit
drive.appdata-,userinfo.email- und Calendar-Scopes).🤖 Generated with Claude Code