If you don’t see your problem here, the support bundle has more context: Settings → Admin → Deployment Diagnostics → Copy support bundle. Paste it into a GitHub issue.

App Store labels stopped parsing

Symptoms: New apps import without error but show no privacy categories. Existing apps that re-sync get a snapshot with category_count = 0. Most likely cause: Apple changed the HTML shape of the App Store page. The parser has a four-layer fallback chain (shelfMapping.privacyTypes.items → privacyHeader.seeAllAction.pageData.shelves → generic pageData.shelves → extractFromShoebox for the historical Ember/FastBoot shoebox-media-api-cache-apps shape), but Apple has shipped breaking changes several times — they may do it again. Diagnose:
If neither marker is present, Apple changed the page envelope altogether. If a marker is present but category_count is still 0 after a re-sync, one of the layouts changed shape internally. Fix: Open an issue with the support bundle and the app’s bundle ID. The parser change is usually a 5–30 line update in lib/scraper.ts once we have a known-broken example. The four-fallback chain absorbs most schema drift without code changes; intervention is only required when a wholly new envelope ships. Symptoms: App page shows labels but the developer privacy-policy link doesn’t render and AI summaries don’t trigger. Cause: Apple’s “Developer’s Privacy Policy” aria-label sometimes uses a curly apostrophe (') instead of a straight one ('). The regex handles both, but bespoke locale-specific labels may not match. Diagnose:
If privacyPolicyUrl is NULL and the app’s App Store page does have a policy link, that’s the apostrophe (or another locale-specific aria-label) bug. Workaround: Add the policy URL manually in Settings → Apps → <app> → Edit policy URL, or open an issue with the App Store URL so the regex can be widened.

Docker container exits immediately

Symptoms: docker compose up brings the container up, then it dies with exit code 1 within a few seconds. Diagnose:
The most common error says the database file can’t be opened (unable to open database file). Fix: With the default Compose file the database is in a Docker-managed volume, which the container’s user can always write. The error comes from the bind-mount file (docker-compose.bind-mount.yml) when ./data on the host isn’t owned by the container’s user, audit (UID 100, GID 101). Docker creates a missing bind source as root, so create it first:
If you’re running on SELinux (e.g. Fedora), add :Z to the volume mount in docker-compose.bind-mount.yml so the host directory gets the right context. If docker compose up stops before starting anything with required variable AUDITOR_ADMIN_TOKEN is missing a value, add a token to .env first (see Installation → Docker).

/api/ready returns not_ready

Diagnose:
A ready instance answers 200 with {"status":"ready"}. Two things are checked: SQLite reachability and data-directory writability. The checks object names which failed. If the process was force-killed, don’t delete the -wal or -shm file it left behind. SQLite replays the WAL the next time it opens the database, so deleting it discards every transaction that was committed to it but not yet checkpointed. A lock held by a process that has died is released by the OS on its own.

AI summaries never appear

Symptoms: AI provider is configured, the test in Settings → AI → Test connection passes, but summaries on app detail pages stay empty. Diagnose: Turn on Record AI prompts and responses, then open the AI debug log under Settings → Admin → Developer Options. Recent calls show provider, model, prompt size (chars), response status, and timing. For local models, the providerLikelyNeedsChunking flag automatically splits documents over ~12 KB into chunks. If your model has a larger context window, you can disable this in lib/ai-config.ts (source builds only).

Reverse-proxy CSRF rejection

Symptoms: Self-hosted behind Caddy/Traefik/Nginx; reads work but every mutation (POST, PUT, DELETE) returns 403 with {"error":"Cross-origin mutation rejected"}. Cause: privacytracker enforces a same-origin CSRF check on every destructive route by comparing Origin against Host. If your proxy rewrites either header, the check fails. Fix: Forward the original Host header. Sample Caddy snippet:
Sample Traefik label:
Complete Compose stacks ship in the main repo under deploy/caddy/ and deploy/traefik/ — start from those if you have a checkout. If reads fail too, with 400 {"error":"Host not allowed"}, this is a different check: the Host allowlist, which defaults to loopback only. Add your proxy’s hostname to PRIVACYTRACKER_ALLOWED_HOSTS — see Configuration → Network exposure.

Wayback import: “skipped, no capture”

Symptoms: POST /api/wayback/import-all finishes with most quarters reporting skipped_no_capture. Cause: archive.org has no capture for that App Store URL within the ±42-day tolerance window around the quarter target. This is normal for niche apps and recent quarters. What the importer does about it: When a quarter has nothing in tolerance, the importer fires a submitToWaybackSaveNow(app.url) request fire-and-forget so a future run can pick it up. Each app submits at most once per run; the count shows up as snapshotsRequested in the result. Fix: Re-run the import in 1-2 weeks. Wayback typically takes a few days to process Save-Page-Now requests, and the importer will pick up newly-archived captures on the next pass.

Migration error on startup

Symptoms: App boots into an error screen reading “Migration step <name> failed: …” with an Attempt counter and a Try again button. Diagnose: The error message names the failing step. The six steps run in order and each is idempotent:
  1. schema_check — verify feature_flag_overrides and the other new tables exist
  2. user_intent_migration — map old user_intent to new focus keys
  3. notification_prefs_absorb — flatten the old JSON blob into per-type rows
  4. callout_rename — drop stale override rows for renamed keys
  5. quarantine_check — flag overrides whose keys are unknown to this version
  6. focus_goal_rename — move flag.focus.goal.understand / .declutter onto .monitor / .cleanup
Fix: Tap Try again — most failures are transient (file lock, slow disk). Up to 3 retries are offered; after that, the screen surfaces a Reset DB option that wipes data/privacy.db and routes you to onboarding. Take a backup first if you have anything you don’t want to lose:

The app’s layout looks wrong after I changed focus or a flag

Symptoms: After adjusting your focus or toggling something under Settings → Developer Options → Feature flags, a surface is missing, stuck collapsed, or shows the wrong controls. Cause: A feature-flag override — or the combination of your current audience + goals — is resolving to something you didn’t intend. Overrides are explicit and sticky; they persist until you clear them. Fix — least drastic first:
  1. Reset one flag. In Settings → Developer Options → Feature flags, find the flag and clear its override. It falls back to the computed default for your current focus.
  2. Flip the kill-switch. If several flags are tangled, set flag.devopts.feature_flag_system.enabled to off on the same Developer Options screen. This collapses every flag to its hard default without touching your focus, apps, or notes. Turning it back on re-engages your audience/goals and any overrides — no data is lost either way.
If the layout is still wrong after the kill-switch, it isn’t a flag problem — grab the support bundle (Settings → Admin → Deployment Diagnostics) and open an issue.

Sync stuck “running”

Symptoms: The TaskCenter or Settings → Sync shows a sync running for hours; GET /api/tasks/active reports running: true but no progress. Cause: A previous run was killed mid-flight and the sync_running mutex never cleared. The boot-time resume probe handles this on next process restart by either resuming the queue (if state is intact) or healing the lock (if state is gone) — but if you’re still on the same boot, the lock stays held.
TaskCenter resumed sync

TaskCenter showing a resumed App Store sync

Fix: Restart the process. On boot, instrumentation.ts runs three staggered resume checks (8s / 10s / 12s) — for each of sync_running, wayback_import_running, and policy_sync_running, it does one of:
  • Nothing pending → no-op
  • Mutex held but no queue → clear the lock and raise a *_stale_cleared notification
  • Queue pending → notify and resume with initiator: 'resume'
Notification bell resume alert

Resume notification in the bell

If a restart doesn’t help, you can manually clear the lock:
Then restart. This is safe — the worst case is one app’s worth of duplicate work on the next run, which the per-target dedup absorbs.

429 from App Store

Symptoms: Bulk sync logs report partial: rateLimited and stop early. Cause: Apple rate-limited the IP. The runner is tuned to bail cleanly on the first 429, record a partial activity row, and clear state + mutex so the next 30-minute scheduler tick can retry fresh. Fix: Wait 30+ minutes and the scheduled tick will retry automatically. If you hit 429 repeatedly, switch Settings → Sync → Schedule from daily to weekly, or stagger your apps across multiple manual runs. daily is the fastest cadence there is — the 30-minute figure is the ticker interval, not a schedule option. If the 429s keep coming, set the schedule to manual. A rate-limited exit deliberately doesn’t stamp last_auto_sync, so the run stays “due” and the ticker retries every 30 minutes regardless of whether you picked daily or weekly. Only manual stops that loop.

Where to ask for help