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
Acknowledged changes are deleted from the database, so the endpoint always returns exactly the set of un-acknowledged changes. 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 |
Sixteen 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) registrationssync_tasks: authoritative task mapping between Trello cards and EKReminderspending_changes: queue of changes waiting to be applied by a clientdevices: registered iOS/macOS devices and their APNs tokensconflicts: unresolved field-level conflicts surfaced for user resolutionsync_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)