graph TB
subgraph Trello
TC[Cards & Lists]
end
subgraph Server[roundup-server]
WH[webhook.py<br/>Webhook receiver]
API[api.py<br/>REST endpoints]
TS[trello_sync.py<br/>Trello API operations]
DB[(SQLite<br/>sync_lists · sync_tasks<br/>pending_changes · devices · conflicts)]
end
subgraph App[DailyRoundup · iOS / macOS]
SM[SyncManager]
SAC[SyncServerAPIClient]
EKS[EventKitStore]
SS[SettingsStore]
ST[(SyncTask<br/>SwiftData)]
end
subgraph Apple
Rem[Reminders]
iCKV[iCloud KV]
KC[iCloud Keychain]
APNs[Apple Push Notification service]
end
TC -->|Webhook POST| WH
WH -->|Insert pending_change| DB
API <-->|Query / update| DB
TS -->|REST API calls| TC
API -->|Trigger| TS
SAC -->|GET /changes| API
SAC -->|POST /sync → 202 + poll| API
SAC -->|POST /devices| API
SM --> SAC
SM <--> EKS
SM <--> ST
EKS <--> Rem
SS <--> iCKV
SS <--> KC
API -->|Conflict alerts| APNs
APNs -->|Push notifications| App
When a card is created or modified in Trello, the server receives a webhook and queues a pending change:
sequenceDiagram
participant T as Trello
participant WH as webhook.py
participant DB as SQLite
T->>WH: POST /dailyroundup/webhook/trello
WH->>WH: Validate HMAC-SHA1 signature
WH->>WH: Filter to handled action types<br/>(createCard, updateCard, deleteCard, …)
WH->>DB: Look up sync_list by trello_list_id
opt createCard (webhook payload is minimal)
WH->>T: GET /cards/{id}?fields=all&customFieldItems=true
T-->>WH: Full card (desc, due, start, custom fields, …)
end
WH->>DB: Insert pending_change<br/>(change_type, changed_fields, trello_card_id)
WH-->>T: 200 OK
Each time the app comes to the foreground, SyncManager pulls pending Trello-side changes and pushes Reminders-side changes. The outbound push uses an async background job pattern:
sequenceDiagram
participant App as DailyRoundup
participant Server as roundup-server
participant EK as EventKit
App->>App: scenePhase == .active
App->>App: guard !isSyncing
loop Pull: until has_more == false
App->>Server: GET /dailyroundup/changes
Server-->>App: { changes, has_more, server_time }
loop For each pending change
App->>EK: syncCreate / syncUpdate / syncComplete / syncDelete
EK-->>App: EKReminder identifier
App->>Server: POST /dailyroundup/changes/{id}/ack
App->>App: Update SyncTask in SwiftData
end
end
App->>Server: POST /dailyroundup/sync (Reminders changes)
Server-->>App: 202 { job_id }
loop Poll: until status != running
App->>Server: GET /dailyroundup/sync/jobs/{job_id}
Server-->>App: { status, processed, total, results }
end
Each device is served the changes it has not acknowledged itself, so the endpoint always returns exactly that device’s un-applied set. A change is deleted once no registered device is still owed it: for create and move that is the first ack, since both are answered by materializing a Reminder — EventKit cannot move one between calendars, so a move deletes the source and builds a new one — and two devices acting on either would produce two Reminders for a single card; for every other change type it is the last device’s ack. A device counts as owed a change when it belongs to the account, polled within the seven-day retention window, and was registered before the change was queued — a device registered afterwards builds its copy from GET /dailyroundup/state, which already reflects it. A row nobody has read within that window is pruned; see Pending Change Delivery for what a device past it converges through instead. Pagination uses after_id (the id of the last change received) when has_more is true.
The outbound sync job runs in a background thread on the server. If a job’s updated_at has not progressed for 60 seconds (e.g. the worker thread died during a gunicorn restart), the polling endpoint automatically marks it as error so the client stops polling.
Bidirectional sync involves field mapping, change detection via hashing, conflict resolution, and echo-loop prevention across four independent mechanisms. For full details on synced fields, create vs update processing, custom field (Priority) handling, conflict detection and resolution, and echo-loop prevention, see Sync Internals.
| File | Purpose |
|---|---|
DailyRoundupApp.swift |
App entry point; constructs SettingsStore, EventKitStore, SyncManager |
ContentView.swift |
Root view; triggers sync on foreground |
EventKitView.swift |
Main view: synced lists, tasks, sync status, conflicts, and EventKitStore (EventKit wrapper) |
SettingsView.swift |
Settings sheet and SettingsStore (iCloud KV + Keychain) |
SyncListView.swift |
Unified sheet for all four list-pairing modes: create new, import from Trello, import from Reminders, and merge existing lists |
GoogleTasksImportView.swift |
Multi-step sheet for importing tasks from Google Tasks into a synced list via OAuth 2.0 + PKCE |
GoogleTasksClient.swift |
Google Tasks API client: OAuth token management, Keychain persistence, task list and task CRUD operations |
SyncManager.swift |
Pull/apply/acknowledge sync orchestrator |
SyncServerAPIClient.swift |
URLSession REST client for the sync server API |
SyncTask.swift |
SwiftData model caching the Trello↔Reminders ID mapping |
KeychainHelper.swift |
iCloud Keychain wrapper for auth token storage |
HTTPRetryHelper.swift |
Generic async retry with exponential backoff for transient HTTP errors |
MacAppDelegate.swift |
(macOS) NSApplicationDelegate: APNs token capture, silent push sync triggers, and conflict notification action handling |
MacSyncCoordinator.swift |
(macOS) Persistent background sync via EventKit observation, 60-second fallback timer, App Nap prevention, and wake-from-sleep handling |
MenuBarView.swift |
(macOS) Menu bar dropdown showing sync status, pending change count, conflict count, and Sync Now action |
MacOSShims.swift |
(macOS) No-op stubs for iOS-only SwiftUI modifiers, allowing shared views to compile on macOS |
SettingsStore manages all app preferences using a tiered storage strategy based on whether a setting should be shared across devices or remain device-specific.
Most preferences sync across devices via NSUbiquitousKeyValueStore with a UserDefaults write-through cache. The cache ensures values persist across TestFlight and App Store updates even if iCloud KV hasn’t delivered data yet on first launch. Synced settings include: server URL, synced list pairs, show completed tasks filter, collapsed list sections, move URL links to description, Google import delete mode, Google import mappings, Google account emails, and notify on sync errors.
On write, values are saved to both iCloud KV and the UserDefaults cache. On launch, init() prefers the iCloud KV value and falls back to the cache. refreshFromiCloudKV() re-reads values on app foreground, and observeExternalChanges() listens for NSUbiquitousKeyValueStore.didChangeExternallyNotification to pick up changes pushed from other devices in real time.
Auth token and Trello API key are stored in iCloud Keychain via KeychainHelper, syncing securely across devices without appearing in iCloud KV.
Some settings are intentionally device-specific:
| Setting | Why device-local |
|---|---|
deviceId |
Stable per-device UUID sent in API calls — each device needs its own identity |
localCalendarIds |
Maps sync_list_id → local EKCalendar.calendarIdentifier. Apple assigns different calendar identifiers on each device for the same Reminders list, so these mappings are inherently device-specific. When a synced list first appears on a new device (via iCloud KV), the sync engine resolves or creates the local Reminders calendar and stores the mapping here. |
confirmedSyncListIds |
Tracks which synced lists have completed at least one successful sync on this device. Guards auto-delete: only lists confirmed-synced locally can be auto-deleted when their calendar disappears. |
The synced list configuration (syncedLists) is shared across all devices via iCloud KV, telling every device what to sync. Each device independently resolves where to sync locally:
syncedLists array to a new device.localCalendarIds for a cached EKCalendar identifier.EKCalendar.calendarIdentifier is stored in localCalendarIds for future launches.This separation ensures that adding a synced list on one device automatically propagates the configuration, while each device maintains its own mapping to the local Reminders calendar system.
| File | Purpose |
|---|---|
dailyroundup/app.py |
Application factory and development entry point |
dailyroundup/api.py |
Flask REST endpoints |
dailyroundup/webhook.py |
Trello webhook receiver |
dailyroundup/trello_sync.py |
Trello API operations |
dailyroundup/db.py |
SQLite database layer |
dailyroundup/models.py |
Data model dataclasses |
dailyroundup/trello_client.py |
Trello HTTP client with auth, retry, and rate limiting |
dailyroundup/notifications.py |
APNs push notification support |
dailyroundup/echo_loop.py |
SQLite-backed fingerprint store for webhook echo-loop prevention |
dailyroundup/log_filter.py |
Logging filter that redacts secrets |
dailyroundup/scheduler.py |
Background daemon driving the per-list “move cards by date” actions |
dailyroundup/card_actions.py |
Scheduling arithmetic for those card actions |
dailyroundup/live_activities.py |
Live Activity push payloads and lifecycle |
dailyroundup/sync_lease.py |
Lease renewal helper shared by the background job runners |
dailyroundup/http_status.py |
Shared “is this status transient?” predicate for the retry loops |
dailyroundup/timeutil.py |
UTC timestamp primitives shared by every module that puts a timestamp on the wire |
dailyroundup/colors.py |
Trello colour name to hex resolution |
dailyroundup/metrics.py |
DogStatsD counters, gauges, and histograms |
dailyroundup/calendar_source.py |
iCal and Google Calendar sources, normalized into events |
dailyroundup/calendar_tags.py |
Source identity and per-field change detection for synced events |
dailyroundup/google_calendar.py |
Google Calendar authorization and API access |
dailyroundup/calendar_sync.py |
Calendar sync engine: copy resolution, change detection, writing, and the cache-rebuild and tag-backfill repairs |
dailyroundup/calendar_api.py |
REST endpoints for configuring calendar syncing |
dailyroundup/calendar_scheduler.py |
Background daemon driving calendar syncing on a cadence |
dailyroundup/calendar_schedule.py |
Whether a calendar sync is due: window, cadence, and last-run arithmetic |
dailyroundup/calendar_lease.py |
Per-account calendar lease, held for the length of an operation |
dailyroundup/link_mirror.py |
Two-way mirror between a card’s URL link attachments and its description |
Thirty tables store all sync state:
accounts: per-account credentials (hashed auth token, Trello API key/token, webhook secret)sync_lists: paired (Trello list, Reminders list) registrations, at most one per Trello listsync_tasks: authoritative task mapping between Trello cards and EKReminderspending_changes: queue of changes waiting to be applied — Trello-origin rows by a client, device-origin reorder_list rows by the server’s schedulerchange_deliveries: which devices have acknowledged each Trello-origin pending change, so one device’s ack no longer retires it for the restdevices: registered iOS/macOS devices and their APNs tokensconflicts: unresolved field-level conflicts surfaced for user resolutiondevice_applied_values: per task field, the value the server last applied from a device, so an older push baseline is not a conflict when Trello never changed the fieldsync_attachments: attachment metadata for Trello card attachments synced to reminder notessync_custom_fields: per-list priority custom field mappings (Trello custom field option IDs to priority names)sync_locations: cached Trello card location and geofence data per sync tasksync_lease: singleton lease row used to prevent overlapping background sync runstrello_card_labels: cached Trello card label associations for displaysync_checklists: Trello card checklists synced for displaysync_checklist_items: individual checklist items with completion statustrello_card_custom_field_values: cached Trello custom field values per cardbackground_jobs: async job tracking for long-running operations (sync, URL attachment migration)potential_duplicates: candidate duplicate records awaiting user resolution (merge or keep both)card_action_index: per-card due and remind timestamps driving the card-action schedulerlive_activity_tokens: per-device push-to-start and update tokens for Live Activitiesauto_created_labels: provenance record of every Trello label this app created itselfapplied_data_migrations: markers for one-time data migrations that have completedcalendar_google_accounts: Google accounts authorized on-device for calendar syncing, and their refresh tokenscalendar_sources: configured calendar sources and the Google Calendar each writes intocalendar_event_mappings: rebuildable cache of source-event to destination-event correspondencecalendar_source_event_state: what each source event’s fields last said, for the before-and-after on a notification cardcalendar_sync_lease: per-account lease preventing overlapping calendar sync runslink_mirror_state: per card, the set of link URLs as of the last successful mirror pass, so the next one can tell a newly added link from a just-deleted oneecho_fingerprints: short-lived fingerprints of this server’s own Trello writes, so the webhook each provokes is recognised and dropped rather than applied backseen_action_ids: Trello action IDs already processed, so a redelivered webhook is not applied twiceThe last two are created by echo_loop.configure() at startup rather than by the schema in db.py, in the same SQLite file so every gunicorn worker shares them.