DailyRoundup

DailyRoundup app icon and name over a gradient background

DailyRoundup

An iOS and macOS app that syncs tasks from Trello into Apple Reminders. A self-hosted sync broker running on any always-on Linux host receives Trello webhook events and queues changes for the app to pull.

Key features:

Privacy Policy Terms of Service

Table of Contents

Usage

iOS App

  1. Open the app and tap the gear icon to open Settings.
  2. Enter the server URL and tap Connect to Server… to link your account (see Connecting Trello for full setup details).
  3. Tap Create Synced List… to create a linked Trello list and Reminders list pair.
  4. Create or update cards in the Trello list. The next time you open DailyRoundup, it pulls all pending changes and applies them to Reminders automatically.

The main view shows all synced reminder lists and items, with sync status and any unresolved conflicts displayed at the top. The sync button shows a health badge (green checkmark or red exclamation). Settings are accessible via the gear icon in the toolbar.

macOS App

On macOS, DailyRoundup runs as a regular dock app with the same single-view layout as iOS — synced lists and tasks with inline sync status. A menu bar icon provides quick access to sync status, pending change count, conflict count, a Sync Now button, and Settings. The app syncs continuously in the background — it triggers a sync whenever Reminders changes, on a 60-second fallback timer, and immediately after the Mac wakes from sleep.

roundup-server

Start the server for development:

cd roundup-server
python -m dailyroundup.app

Start with gunicorn for production:

gunicorn "dailyroundup.app:create_app()" --bind 0.0.0.0:5000 --workers 2

Installation and Configuration

App (iOS / macOS)

Requirements: Xcode 26+, iOS 26+, macOS 26+, an Apple Developer account

  1. Clone the repository:

    git clone git@github.com:dcwalker/DailyRoundup.git
    cd DailyRoundup
    
  2. Copy the Xcode config sample and add your development team ID:

    cp Local.xcconfig.sample Local.xcconfig
    # Edit Local.xcconfig and set DEVELOPMENT_TEAM to your Apple Developer Team ID
    
  3. Open the project in Xcode and run on a device or simulator:

    open DailyRoundup.xcodeproj
    
  4. On first launch, grant Reminders and Notifications access when prompted.

  5. Tap the gear icon, enter your server URL, and tap Connect to Server… to complete setup (see Connecting Trello).

roundup-server

Requirements: Python 3.12+

  1. Clone the repository and change into this directory:

    cd roundup-server
    
  2. Install dependencies:

    pip install -r requirements.txt
    
  3. Copy env.sample to .env and fill in all required values:

    cp env.sample .env
    $EDITOR .env
    
  4. Run database migrations:

    python migrate.py
    
  5. Start the server (see Usage).

Running Behind a Reverse Proxy (HTTPS)

The server must be reachable over HTTPS for Trello webhooks to fire. Use nginx or Caddy as a TLS-terminating reverse proxy in front of gunicorn.

Configuring APNs Push Notifications

Silent background-sync pushes are optional. The server starts and operates normally without them — devices fall back to polling whenever the app is opened. To enable near-real-time sync (Trello change → silent push → Reminders update within seconds):

  1. Generate an APNs key in the Apple Developer portal:
    • Go to Certificates, Identifiers & Profiles → Keys
    • Create a new key with the Apple Push Notifications service (APNs) capability enabled
    • Download the .p8 file (it can only be downloaded once; store it securely on the server)
  2. Note your credentials:
    • Key ID: the 10-character identifier shown on the key detail page
    • Team ID: the 10-character identifier shown in the top-right of the portal
  3. Set the environment variables in your .env file (see Environment Variables):
    • APNS_KEY_ID and APNS_TEAM_ID from the values above
    • APNS_BUNDLE_ID: the app’s bundle ID (e.g. dev.dcwalker.DailyRoundup)
    • APNS_PRIVATE_KEY: the full contents of the .p8 file, including the -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- lines
  4. Set APNS_USE_SANDBOX=1 if testing with a development or Ad Hoc build. Omit it (or set to 0) for App Store and TestFlight builds.

  5. Restart the server so it picks up the new variables.

If any of the four required variables (APNS_KEY_ID, APNS_TEAM_ID, APNS_BUNDLE_ID, APNS_PRIVATE_KEY) are absent, APNs calls are silently skipped and a DEBUG-level log message is emitted.

Connecting Trello

The app connects to Trello via OAuth during initial setup. You will need a Trello API Key and Webhook Secret before you begin.

  1. Get a Trello API Key from the Trello Power-Ups admin page. Create a new Power-Up (or use an existing one) and copy the API Key from its API Key tab.

  2. Generate a Webhook Secret — any long random string. The server uses this to verify HMAC-SHA1 signatures on incoming Trello webhook events. You can generate one with:

    openssl rand -hex 32
    
  3. In the app, tap the gear icon to open Settings, then tap Connect to Server…. Fill in:
    • Server URL: your roundup-server’s HTTPS URL
    • Bootstrap Token: the DAILYROUNDUP_BOOTSTRAP_TOKEN value from your server’s .env file
    • Trello API Key: the key from step 1
    • Webhook Secret: the secret from step 2
  4. Tap Connect. The app creates your account on the server and immediately opens a Trello OAuth page in an in-app browser. Sign in to Trello and tap Allow to grant DailyRoundup read/write access. The OAuth token is sent back to the app automatically and persisted on the server.

  5. Once connected, the bootstrap token is no longer needed — the app stores a permanent per-account auth token in iCloud Keychain.

If you already have a permanent auth token (e.g. from a previous setup or another device), expand I already have a token in the connect sheet and paste the token directly. This skips the bootstrap token and OAuth steps.

To re-authorize Trello (e.g. if you revoked access), tap Authorize Trello in the Trello Configuration section of Settings. This opens the same OAuth flow and updates the token on the server.

For a detailed sequence diagram showing every API call during setup and what the server stores at each step, see Setup Flow Details.

Operations

Health Monitoring

The server exposes a GET /health endpoint that returns 200 OK when the service is running. Check it from the server or any machine with network access:

curl https://<your-server>/health

For account-scoped stats (number of synced lists and registered devices), use the authenticated status endpoint:

curl -H "Authorization: Bearer <auth-token>" https://<your-server>/dailyroundup/status

The healthcheck.py script in roundup-server/ performs a more thorough post-deployment check: it polls GET /health until the service responds, validates APNs JWT signing, and verifies Trello API connectivity for all configured accounts. It is run automatically by the Deploy code to sync server GitHub Actions workflow after each deployment.

Alerting

GitHub Actions sends a notification when the Deploy code to sync server or Upload to App Store Connect workflow fails. No automated alerting is configured for runtime server errors; monitor the service by watching logs (see Server Logs) or polling GET /health.

Common Issues

App shows “Authentication failed” after entering server URL

The auth token in Settings is invalid or missing. Tap Connect to Server… and enter your bootstrap token to create a new account. If you already have a permanent token, expand “I already have a token” and paste it directly.

Trello changes are not appearing in Reminders

  1. Verify the server is reachable: tap the sync button (arrow icon) to trigger a sync and check the status badge.
  2. Confirm a Webhook Secret is configured in Settings → Trello Configuration. The server rejects all webhook events from accounts without one.
  3. Check the server logs for 403 responses or no trello_webhook_secret configured warnings.

Webhook events returning 403

The Webhook Secret in the iOS app does not match the value Trello is signing requests with. Update the Webhook Secret in Settings → Trello Configuration to match, then save.

APNs push notifications not delivering

Check that all four APNS_* environment variables are set in .env and the server has been restarted. The server logs a DEBUG message on startup if any are missing. Confirm APNS_USE_SANDBOX matches the build type (set to 1 for development builds, omit for App Store/TestFlight).

Sync stopped after moving to a new device

iCloud Keychain syncs the auth token automatically, but may take a few minutes. If the token does not appear, use “I already have a token” in Settings to paste it from your previous device.

Server Logs

The server logs to stdout/stderr, captured by systemd’s journal. Logs are written in this format:

2026-02-24T14:32:01 INFO [dailyroundup.webhook] Recorded 'create' change for Trello card abc123

Fields: YYYY-MM-DDTHH:MM:SS LEVEL [logger.name] message

Reading logs requires SSH access to the server and the journalctl tool (included with systemd). Replace <username> with the system user running the service:

# Stream live logs
journalctl -u dailyroundup@<username> -f

# Last 100 lines
journalctl -u dailyroundup@<username> -n 100

# Logs from the past hour
journalctl -u dailyroundup@<username> --since "1 hour ago"

# Errors only
journalctl -u dailyroundup@<username> -p err

# Save today's logs to a file
journalctl -u dailyroundup@<username> --since today > ~/dailyroundup-$(date +%F).log

Technical Details

System Architecture

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 /changes/ack| 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

Webhook Ingest Flow

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

Foreground Sync Flow

Each time the app comes to the foreground, SyncManager pages through all pending changes, applies them to Reminders, and acknowledges each one:

sequenceDiagram
    participant App as DailyRoundup
    participant Server as roundup-server
    participant EK as EventKit
    participant iCKV as iCloud KV

    App->>App: scenePhase == .active
    App->>App: guard !isSyncing

    loop Until has_more == false
        App->>Server: GET /dailyroundup/changes
        Note right of Pi: after_id for pagination
        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

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.

Sync Internals

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.

App Module Structure

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
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

roundup-server Module Structure

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

Database Schema

Fourteen tables store all sync state:

Environment Variables

Variable Required Description
DAILYROUNDUP_BOOTSTRAP_TOKEN Yes (initial setup) One-time token used to create the first account via POST /dailyroundup/account. Once an account is created, the permanent per-account token is stored in iCloud Keychain and this variable is no longer needed.
DAILYROUNDUP_WEBHOOK_URL Yes Public HTTPS URL for the Trello webhook callback
DAILYROUNDUP_DB_PATH No Path to the SQLite database file (default: dailyroundup.db)
DAILYROUNDUP_HOST No Development server bind host (default: 0.0.0.0)
DAILYROUNDUP_PORT No Development server port (default: 5000)
FLASK_DEBUG No Set to 1 for Flask debug mode (development only)
APNS_KEY_ID No* 10-character APNs key ID from the Apple Developer portal
APNS_TEAM_ID No* 10-character Team ID from the Apple Developer portal
APNS_BUNDLE_ID No* iOS/macOS app bundle ID (e.g. dev.dcwalker.DailyRoundup)
APNS_PRIVATE_KEY No* Full PEM contents of the .p8 file, including BEGIN/END PRIVATE KEY lines
APNS_USE_SANDBOX No Set to 1 to target the APNs sandbox; omit for production
HEALTHCHECK_URL No Base URL for the post-deployment health check script (default: http://127.0.0.1:5000)
HEALTHCHECK_TIMEOUT No Seconds to wait for the server to become ready during the health check (default: 30)
HEALTHCHECK_TOKEN No Per-account bearer token for the GET /dailyroundup/status check in healthcheck.py; if unset, that check is skipped

* All four APNS_* variables must be set together to enable push notifications. If any are absent, APNs is disabled and the server logs a DEBUG message. See Configuring APNs Push Notifications for setup steps.

Trello credentials (trello_api_key, trello_token, trello_board_id) and the Webhook Secret (trello_webhook_secret) are stored per-account in the database and configured via the app’s Settings screen after account creation.

API Reference

POST /dailyroundup/account is protected by the DAILYROUNDUP_BOOTSTRAP_TOKEN environment variable. All other endpoints except GET /health and the Trello webhook endpoints require an Authorization header with the per-account bearer token returned when the account was created:

Authorization: Bearer <account-token>

See roundup-server/openapi.yaml for the full OpenAPI specification.

Method Path Description
GET /health Health check
GET /dailyroundup/status Account-scoped operational stats (sync list count, registered device count)
POST /dailyroundup/account Create a new account (requires bootstrap token)
GET /dailyroundup/account Get current account details
PUT /dailyroundup/account Update account settings (Trello credentials, webhook secret)
GET /dailyroundup/lists List all synced list pairs for this account
POST /dailyroundup/lists Create a new synced list pair
POST /dailyroundup/lists/merge Merge an existing Trello list and Reminders calendar into a synced pair
PATCH /dailyroundup/lists/{sync_list_id} Update list metadata
DELETE /dailyroundup/lists/{sync_list_id} Remove a synced list pair
POST /dailyroundup/lists/{sync_list_id}/reset Reset all sync state for a list
GET /dailyroundup/trello_boards List all Trello boards on the configured account
GET /dailyroundup/trello_lists List all Trello lists on the configured board
GET /dailyroundup/trello_cards List all cards in a Trello list
GET /dailyroundup/trello_test Test Trello API connectivity
GET /dailyroundup/changes Return pending Trello-side changes
POST /dailyroundup/changes/{change_id}/ack Acknowledge an applied change
POST /dailyroundup/sync Accept and apply Reminders-side changes
GET /dailyroundup/sync/status Return per-list sync statistics
GET /dailyroundup/state Full state dump for initial sync or recovery
GET /dailyroundup/card_metadata Fetch labels, attachments, checklists, and custom fields for cards
POST /dailyroundup/devices Register a device APNs token
POST /dailyroundup/reconcile Reconcile sync_tasks against actual Trello cards; emits delete changes for cards no longer present
GET /dailyroundup/conflicts List all unresolved conflicts
POST /dailyroundup/conflicts/{conflict_id}/resolve Resolve a conflict
POST /dailyroundup/attachments/move-urls Batch-move URL link attachments into card descriptions and delete the attachments from Trello
DELETE /dailyroundup/account Delete account and all associated data
GET /dailyroundup/webhook/trello Trello webhook verification endpoint
POST /dailyroundup/webhook/trello Receive Trello webhook events

Permissions

Permission Access granted Required for
Reminders (EventKit) Read and write all reminder lists and items Displaying, creating, and updating reminders from Trello
iCloud Key-Value Store Read and write app-specific KV pairs in iCloud Syncing server URL, sync list IDs, and last sync timestamp across devices
iCloud Keychain Read and write a single Keychain item Storing the auth token securely across devices
Push Notifications (APNs) Receive remote notifications and notification actions Registering device tokens and delivering conflict-resolution alerts

Design Guidelines

The app follows Apple’s Human Interface Guidelines on both iOS and macOS. There are no custom colors, typefaces, or animation overrides — all controls use standard SwiftUI system components so the app adapts automatically to light/dark mode, Dynamic Type, and accessibility settings. On macOS, Forms use .formStyle(.grouped) for consistent grouped-section layout. New UI should use system-provided components and avoid hardcoded colors or custom interaction patterns unless a standard component cannot fulfill the requirement.

Documentation

Contributing

See CONTRIBUTING.md for coding standards, commit message format, and documentation guidelines. See AGENTS.md for AI-agent-specific directives.