
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 |
Each list section header carries a quick-add button that opens the preset picker; choosing a preset creates that task in the list whose header was tapped. Presets are created by opening a task and choosing Save as Preset, which seeds the title, notes, priority, and location — including whether it reminds you on arrival or on leaving — from that task and opens an edit sheet for the start, due, and “Remind me” offsets. Each date is either a duration counted from the moment the preset is used (“in 2 hr”) or a clock time on a chosen day (“Friday at 3:00 PM”), and a clock time that has already passed rolls forward to its next occurrence rather than creating an overdue task. An offset left unset means the created task carries no such date. The picker filters by title as you type, and presets are reordered by dragging after tapping Reorder, or one place at a time from a row’s context menu; the library is stored in iCloud Key-Value Store so it follows you between devices, with the most recent write winning for the whole library. Because a preset deliberately produces identical tasks, a task created from one is never held for duplicate review.
The main view shows all synced reminder lists and items, with sync status displayed below the navigation title and any unresolved conflicts or potential duplicates listed as tappable rows. Tapping a task, conflict, or duplicate row opens its details in a stackable slide-up view (the same presentation style as Settings); task completion is still toggled by tapping the row’s checkbox. Pull down on the list to trigger a manual sync. Settings are accessible via the gear icon in the toolbar.
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.
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 1 --timeout 60
--workers 1 is load-bearing, not a capacity choice (issue #393). Background
jobs run as in-process threads, startup recovery in create_app sweeps every
job still running at boot, and calendar syncing holds a per-account lease that
the same sweep clears. Under more than one worker, a restarting worker would
flip live jobs in its siblings to error and release leases they are still
working under, which is how two calendar runs end up on the same calendars at
once.
Requirements: Xcode 26+, iOS 26+, macOS 26+, watchOS 26+ (for the Apple Watch app), an Apple Developer account
Clone the repository:
git clone git@github.com:dcwalker/DailyRoundup.git
cd DailyRoundup
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
Open the project in Xcode and run on a device or simulator:
open DailyRoundup.xcodeproj
On first launch, grant Reminders and Notifications access when prompted.
Tap the gear icon, enter your server URL, and tap Connect to Server… to complete setup (see Connecting Trello).
Requirements: Python 3.12 — the exact version named in roundup-server/.python-version, which is what CI installs and what the deployed server runs. See Python version.
Clone the repository and change into this directory:
cd roundup-server
Install dependencies:
pip install -r requirements.txt
Copy env.sample to .env and fill in all required values:
cp env.sample .env
$EDITOR .env
Run database migrations:
python migrate.py
Start the server (see Usage).
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.
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):
.p8 file (it can only be downloaded once; store it securely on the server).env file (see Environment Variables):
APNS_KEY_ID and APNS_TEAM_ID from the values aboveAPNS_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----- linesSet 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.
The app connects to Trello via OAuth during initial setup. You will need a Trello API Key and Webhook Secret before you begin.
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.
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
DAILYROUNDUP_BOOTSTRAP_TOKEN value from your server’s .env fileTap 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.
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.
Health monitoring via GET /health and GET /dailyroundup/status, optional Datadog usage metrics over DogStatsD, GitHub Actions alerting on deployment failures, common troubleshooting scenarios (auth errors, missing webhooks, APNs configuration), and server log access via journalctl. See Operations for full details.
A background daemon thread inside roundup-server (issue #334) that acts on cards by date, without the app running:
due - offset, applied when the due date changes rather than on a schedule, so a hand-edited value survives.sync_lists; a NULL destination means “keep the card here”. The accounts columns still govern the board’s unsynced lists.It is event-driven off a card_action_index table, acquires the global sync_lease before acting, and runs reconciliation passes on startup and daily to correct drift from missed webhook events. Configuration lives on accounts and per-list columns, set via PUT /dailyroundup/account and PATCH /dailyroundup/lists/{id}. Set DAILYROUNDUP_SCHEDULER_ENABLED=0 to disable it.
See Card Action Scheduler for the full behaviour, including the ordering tie-breaks, the Live Activity window, and the reorder-drain stand-down.
Calendar syncing runs on its own scheduler thread, separate from the card-action
one, and takes a per-account lease rather than the server-wide sync_lease.
An account is synced when three things hold: calendar_sync_enabled is on, the
wall-clock window is open in the account’s calendar_sync_timezone, and
calendar_sync_interval_minutes has elapsed since calendar_last_run_at. An
account that has never run is due as soon as its window opens.
calendar_sync_timezone is also the zone every time on a notification card is
rendered in, named on the card so the reading is unambiguous. Feeds commonly
publish UTC, so a card left in the feed’s own zone can be out by hours and by a
day.
When a source fails, the failure is written to its last_error and reported
once — again on the next cycle only if it changes, or returns after a success.
Where it is reported depends on whether the user can do anything about it. A
Google grant that has lapsed sends a push notification to the account’s
registered devices, because only its owner can renew it and a source can sit
broken overnight otherwise. Every other failure raises a ⚠️ … calendar sync
failed card on the account’s Trello notification list instead: a feed
returning 500 is worth finding later, not being woken for.
One lapsed grant sends one push, not one per source riding on it, and is not pushed again until it has synced successfully and lapsed afresh. That record is held in memory, so a restart or a redeploy re-pushes every grant still lapsed at the time — once each, on the next run.
When the push cannot be delivered at all — no APNS_* credentials on the
server, no device registered to the account, every registered device reporting
that it may not show alerts, or APNs rejecting every token — the grant falls
back to the card path like any other failure, which means the account’s Trello
notification list if one is chosen and last_error alone if not.
Devices report whether they may show alerts when they register, because APNs accepts an alert for a device that will silently drop it and a push nobody can see must not suppress the card. That answer is refreshed whenever the app is brought to the foreground, so permission turned off in iOS Settings is noticed the next time the app is opened.
calendar_sync_enabled defaults to off, and that default is load-bearing.
Events synced by the old Sync Assistant carry cca tags; until the migration
has rewritten them to dr, a first sync would create every event afresh and
double the calendar — and those orphans cannot be reclaimed, because a
re-created source gets a new identity. Configure the sources, run the
migration, verify, and only then switch syncing on.
A window whose start is later than its end runs across midnight (22:00–06:00). Both ends on the same minute is rejected: it is empty, not “always”.
The scheduler defers to anything started from the app — a repair or a source removal takes the same lease — because that is somebody waiting at a screen.
System architecture diagrams, webhook ingest and foreground sync flow sequences, module structure tables for the iOS/macOS app and roundup-server, and the thirty-table database schema. See Technical Details for full details.
| 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) |
DAILYROUNDUP_MAX_CONTENT_LENGTH |
No | Maximum request body size in bytes (default: 8388608, i.e. 8 MB) |
DAILYROUNDUP_SCHEDULER_ENABLED |
No | Set to 0 to disable the card-action scheduler (default: enabled). See Card Action Scheduler. |
DAILYROUNDUP_DRAIN_TIMEOUT |
No | Seconds the deploy-drain step (drain.py) waits for in-flight sync jobs to finish before a restart (default: 30). |
DAILYROUNDUP_GOOGLE_CLIENT_ID |
No† | OAuth client ID the app authorizes Google accounts with. Required for calendar syncing: a refresh token is bound to the client that obtained it, so the server must present the same client ID when exchanging it for access tokens. |
DAILYROUNDUP_GOOGLE_TOKEN_ENDPOINT |
No | Google’s OAuth 2.0 token endpoint (default: https://oauth2.googleapis.com/token) |
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_SOCKET |
No | Path to the app’s Unix domain socket for healthcheck.py (issue #478). Set by the deploy workflow; when set, checks connect over this socket instead of TCP and HEALTHCHECK_URL is ignored |
HEALTHCHECK_URL |
No | Base URL for the post-deployment health check script (default: http://127.0.0.1:5000). Only used when HEALTHCHECK_SOCKET is unset — e.g. for a manual local run |
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 |
DD_AGENT_HOST |
No | Datadog Agent host for DogStatsD usage metrics. When set, the server emits the counters described in Usage Metrics; when unset, metric emission is disabled and no socket is opened |
DD_DOGSTATSD_PORT |
No | DogStatsD UDP port (default: 8125); only used when DD_AGENT_HOST is set |
† Required only for calendar syncing. Without it the server starts normally and every other feature works; calendar syncing cannot refresh Google access tokens and reports a configuration error.
* 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.
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, calendar sync enable/cadence/window and its Trello notification list) |
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 |
PATCH |
/dailyroundup/lists/reorder |
Reassign display order for the account’s synced lists (queues the Trello writes; makes no Trello calls itself) |
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, each carrying the sync_list_id it is already paired with, or null when it is available to import |
GET |
/dailyroundup/trello_cards |
List all cards in a Trello list |
GET |
/dailyroundup/trello_test |
Test Trello API connectivity |
GET |
/dailyroundup/label_colors |
List available Trello label colors with resolved hex values |
GET |
/dailyroundup/changes |
Return pending Trello-side changes |
POST |
/dailyroundup/changes/{change_id}/ack |
Acknowledge an applied change |
POST |
/dailyroundup/sync |
Accept Reminders-side changes (returns 202 with job_id for async polling) |
GET |
/dailyroundup/sync/jobs/{job_id} |
Poll for sync job status and results |
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, custom fields, and current position for cards |
PATCH |
/dailyroundup/tasks/{task_id}/reminders-item-id |
Update a sync task’s reminders_item_id after client-side content matching |
POST |
/dailyroundup/sync-nudge |
Ask the account’s devices to sync, and start a server-side reconcile (Apple Watch) |
GET |
/dailyroundup/tasks/{task_id} |
Fetch a single task, including its description (Apple Watch) |
POST |
/dailyroundup/tasks/{task_id}/complete |
Mark a task complete (Apple Watch) |
POST |
/dailyroundup/tasks/{task_id}/clear-due |
Clear a task’s due date (Apple Watch) |
POST |
/dailyroundup/tasks/{task_id}/clear-remind |
Clear a task’s Remind-me date (Apple Watch or iPhone) |
POST |
/dailyroundup/devices |
Register a device APNs token |
GET |
/dailyroundup/devices/{device_id} |
Fetch a device’s stored Live Activity and pinned-card preferences |
POST |
/dailyroundup/devices/{device_id}/push-to-start-token |
Register a device’s Live Activity push-to-start token |
DELETE |
/dailyroundup/devices/{device_id}/push-to-start-token |
Drop a device’s Live Activity push-to-start token |
POST |
/dailyroundup/devices/{device_id}/activity-tokens |
Register the per-activity update token for a started Live Activity |
POST |
/dailyroundup/tasks/{task_id}/activity-ended |
Report that a Live Activity for a task has ended on-device |
POST |
/dailyroundup/reconcile |
Reconcile sync_tasks against actual Trello cards (open and archived); emits delete changes for cards that were permanently deleted |
GET |
/dailyroundup/potential-duplicates |
List unresolved potential duplicates |
POST |
/dailyroundup/potential-duplicates/{dup_id}/resolve |
Resolve a potential duplicate (merge or keep_both) |
GET |
/dailyroundup/conflicts |
List all unresolved conflicts |
POST |
/dailyroundup/conflicts/{conflict_id}/resolve |
Resolve a conflict |
POST |
/dailyroundup/conflicts/resolve-batch |
Resolve multiple conflicts belonging to the same sync task in one call |
POST |
/dailyroundup/attachments/move-urls |
Bring existing cards’ URL links into agreement across attachments and description, as a background job. Named for the move it used to perform; since the two-way mirror it copies rather than moves. An attachment is deleted only when its link was previously mirrored and has since been removed from the description, so a card’s first pass can only add |
GET |
/dailyroundup/attachments/move-urls/{job_id} |
Poll for move-URL-attachments job status and results |
POST |
/dailyroundup/tasks/complete-archived |
Batch-complete tasks whose Trello card is already archived |
GET |
/dailyroundup/tasks/complete-archived/{job_id} |
Poll for complete-archived job status and results |
POST |
/dailyroundup/tasks/sync-label-brackets |
Batch-add or strip [LabelName] tokens across Reminder titles and Trello card names |
GET |
/dailyroundup/tasks/sync-label-brackets/{job_id} |
Poll for sync-label-brackets job status and results |
GET |
/dailyroundup/calendar/google-accounts |
List the Google accounts authorized for calendar syncing |
POST |
/dailyroundup/calendar/google-accounts |
Store a refresh token from the app’s own OAuth flow; reauthorizing clears the Reconnect flag |
DELETE |
/dailyroundup/calendar/google-accounts/{id} |
Remove a Google account and the sources that depend on it. Their events are kept — and become unreachable, since this destroys the identity that finds them, so delete each source’s events first if they should go |
GET |
/dailyroundup/calendar/google-accounts/{id}/calendars |
List that account’s writable calendars, for the destination picker |
GET |
/dailyroundup/calendar/sources |
List the configured calendar sources |
POST |
/dailyroundup/calendar/sources |
Configure a new calendar source |
PATCH |
/dailyroundup/calendar/sources/{id} |
Change a source’s options (source_type, source_url, target_calendar_id, and google_account_id are fixed at creation) |
DELETE |
/dailyroundup/calendar/sources/{id} |
Remove a source; delete_events=true also deletes its events, as a background job |
POST |
/dailyroundup/calendar/sources/{id}/rebuild-cache |
Start a cache rebuild for one source |
POST |
/dailyroundup/calendar/sources/{id}/backfill-tags |
Start a tag backfill for one source |
GET |
/dailyroundup/calendar/jobs/{job_id} |
Poll a calendar removal or repair job |
POST |
/dailyroundup/metrics |
Ingest a batch of client telemetry |
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 |
| 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 and calendar sync failures that need reconnecting |
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. Markdown rendering is the one place with its own metrics: MarkdownText sets block spacing, list indents, and a code-block background, all derived from system text styles and semantic colors, and per-platform so the watch reads correctly.
Task descriptions are Markdown. Any new surface that displays one read-only must render it with MarkdownText, and any surface that can only carry a String, a notification body above all, must pass it through MarkdownDescription.plainText(_:). Neither the raw source nor its syntax should reach the user. Editors are the exception: they edit the original Markdown.
.xcstrings catalogs are used and how to add new stringsSee CONTRIBUTING.md for coding standards, commit message format, and documentation guidelines. See AGENTS.md for AI-agent-specific directives.
roundup-server/.python-version names the Python the server is built and
tested on. It is the only place that choice is made, and four things read it:
| Reader | What it uses the version for |
|---|---|
test-server.yml, lint.yml, sonarcloud.yml, convention-checks.yml |
setup-python’s python-version-file, so every workflow installs it |
deploy.yml |
Rebuilds the server’s virtualenv when its interpreter no longer matches |
roundup-server/ruff.toml |
target-version, which decides the spellings ruff will rewrite code into |
roundup-server/sonar-project.properties |
sonar.python.version, the grammar SonarCloud parses with |
The workflows and deploy.yml read the file itself. The rest — ruff.toml, the sonar properties, and this
README — restate the version because they cannot read it, so
scripts/check-python-version.sh — the Check Python version declarations
job in Convention checks — fails the build when one of them, or this
README’s requirement line above, disagrees with the file. The workflows are checked the other way round: each
must name the file, and none may pin a literal version, so adding a version matrix means changing that check
too.
The file holds a major.minor version and nothing else — 3.12, not pyenv’s usual 3.12.4. That is the
granularity everything here works at: the interpreter lookup, the server’s venvs/pyX.Y directories, and the
agreement between CI and the server. Patch level is deliberately not pinned.
Moving the version means editing that one file. The deploy converges the server on its own: it provisions the
interpreter if the host does not already have a usable one — Debian packages only one Python per release, so
apt cannot supply an arbitrary version — and rebuilds the server’s virtualenv against it. The deploy
documentation covers where that interpreter comes from,
how the switch is made, and how to roll it back.
Two limits are worth knowing. The agreement is only as good as the last successful deploy — between merging a
change to this file and the deploy that acts on it, CI is ahead of the server, and nothing reports the deployed
interpreter back to CI. And it is a major.minor agreement: the runner installs the newest 3.12.x it has while
the host builds from whatever python3.12 it has, so patch-level differences between them stay invisible.
Both languages are linted in CI by the Lint workflow, and both must pass before a pull request can merge.
| Language | Tool | Configuration | Run locally |
|---|---|---|---|
| Python | ruff | roundup-server/ruff.toml |
cd roundup-server && ruff check . |
| Swift | SwiftLint | .swiftlint.yml |
swiftlint lint --strict |
Both configurations list their rules explicitly rather than starting from the tool’s defaults, so the tree passes with no suppressions carried forward. Each file ends with the rule families deliberately not adopted yet and the reason for each; adopting one of those is a change of its own rather than something to fold into an unrelated pull request.
ruff check --fix and swiftlint --fix apply the mechanical corrections. Review what they change before committing: an automatic fix removed a re-exported import during this configuration’s own rollout, which the tests caught.
SonarCloud analyses the two halves of the codebase as two separate projects, on different schedules and different runners. Neither scan sees the other’s sources, so a green check on one says nothing about the other.
| Project | Covers | Runs on | Job |
|---|---|---|---|
dcwalker_DailyRoundup_server |
roundup-server/dailyroundup and its tests |
Every push to main, every pull request, manual dispatch |
Analyze (Python) |
dcwalker_DailyRoundup |
DailyRoundup/ — the main app target only |
Manual only (Actions → Run workflow) | Analyze (Swift) |
Analyze (Python) is the only one of the two that reports on a pull request, and SonarCloud posts its own check there named [DailyRoundup (server)] SonarCloud Code Analysis. That name is easy to read as whole-repository coverage. It is not: it covers the server, and no Swift code is in its scope.
The Swift scan covers the main app target only. RoundupWidgets/, RoundupWatchWidgets/, and RoundupWatch Watch App/ — about 420 lines — are outside sonar.sources and are analysed by neither project.
No Swift change is gated by static analysis. Running the Swift scan automatically was considered and rejected on cost (issue #595): it needs a macOS runner for the Xcode build and takes 25–35 minutes, those minutes bill at ten times the Linux rate on a private repository, and at the observed 4.3 merges per day a push-to-main trigger would cost roughly $200–280 per month. For about half the codebase on a single-maintainer project, that was not judged worth it.
Run it by hand from Actions → Run workflow before a release, or after a substantial Swift change. A manual run analyses both projects.
To get the Xcode build on a pull request beforehand, apply the check-build label. The Build & Test workflow picks it up and removes it again as the run starts, and then builds only if the pull request actually touched DailyRoundup/, DailyRoundupTests/, DailyRoundupUITests/, or DailyRoundup.xcodeproj/ — so labelling a server-only pull request consumes the label without producing a build.
A manual job is only as current as its last run. Check the analysis date on the Swift project before treating its result as describing the code in front of you.