diff --git a/OPEN_ISSUES_TRIAGE.md b/OPEN_ISSUES_TRIAGE.md
index 3603e9df8..01b759699 100644
--- a/OPEN_ISSUES_TRIAGE.md
+++ b/OPEN_ISSUES_TRIAGE.md
@@ -3,6 +3,22 @@
**Generated:** 2026-07-11 · **Reviewed against:** `CLAUDE.md` design philosophy + `TODO.md` standing constraints
**Method:** Every item below was traced into the actual code (Java + native C + Rust gotatun). Line numbers are from the tree at/around `master` (`a9f060a`) — re-verify before editing, code moves.
+## Status (live)
+
+Draft PRs are being generated by agents. This table is the running checklist — ✅ = draft PR open.
+
+| WI | Issue(s) | State | PR |
+|----|----------|-------|----|
+| WI-1 | #327 pause auto-resume | ✅ draft PR open | [#601](https://github.com/TrackerControl/tracker-control-android/pull/601) |
+| WI-2 | #576 dead toggle | ✅ draft PR open | [#602](https://github.com/TrackerControl/tracker-control-android/pull/602) |
+| WI-3 | #478 PMTUD MSS clamp | 🚧 in progress | — |
+| WI-4 | #339 IPv6 DNS | 🚧 in progress | — |
+| WI-5 | #526/#331 self-disable | 🚧 in progress | — |
+| WI-6 | #554/#561/#358 transparency | 🚧 in progress | — |
+| WI-7+8 | #586/#575/#551/#560/#559 UI polish | 🚧 in progress | — |
+| WI-9 | #405 memory footprint | 🚧 in progress | — |
+| WI-11 | general self-check / diagnostics (new) | 🚧 investigate + draft | — |
+
## How to use this document
Each **work item (WI-n)** in Sections A/B is self-contained: one agent can be handed a single WI and act on it without re-reading the whole review. Each carries a **Verdict**, the **root cause with file:line**, a **concrete fix**, and a **risk/scope** note.
@@ -20,16 +36,18 @@ Verdict vocabulary:
## Section A — Ready-to-implement fixes (Fixable-aligned)
-### WI-1 · Pause auto-resumes after a manual resume, dropping connections
+### WI-1 · Pause auto-resumes after a manual resume, dropping connections — ✅ Draft PR [#601](https://github.com/TrackerControl/tracker-control-android/pull/601)
- **Issue:** [#327](https://github.com/TrackerControl/tracker-control-android/issues/327)
+- **Status:** Draft PR #601 open. Fix in `ActivityMain.onActivityResult` (REQUEST_VPN branch): cancel the `INTENT_ON` pause alarm before `ServiceSinkhole.start(...)`, matching `WidgetAdmin`'s PendingIntent (the Quick-Settings tile `ServiceTileMain` already did this — confirms the main-UI path was the gap). `compileGithubDebugJavaWithJavac` green. Needs on-device pause→resume verification.
- **Verdict:** Fixable-aligned. Smallest clean win on the board; no packet-path risk.
- **Symptom:** User pauses TC for 10 min from the notification, then manually resumes from the app. Later the original pause timer still fires — TC vibrates, a second foreground/VPN re-init happens mid-session, and in-flight connections drop (one reporter lost a payment).
- **Root cause:** A pause schedules an `INTENT_ON` alarm `pause` minutes out in `WidgetAdmin.java:84-90`. That alarm is only cancelled inside `WidgetAdmin.onReceive` for its three broadcast actions (`WidgetAdmin.java:57-61`). The **main-UI resume path does not go through that code** — `ActivityMain.java:282-355` sets `enabled=true` and calls `ServiceSinkhole.start(...)` directly, never calling `am.cancel(...)`. The orphaned alarm survives and re-fires.
- **Fix:** In the UI resume path (`ActivityMain`), cancel the pending `INTENT_ON` pause alarm (mirror the cancel in `WidgetAdmin.java:57-61`), or make the alarm handler a no-op when TC is already enabled (idempotent resume).
- **Risk/scope:** Tiny, UI/lifecycle only. No native or DNS change.
-### WI-2 · Some tracker domains silently won't toggle ALLOWED→BLOCKED (regression in 2026.04.03)
+### WI-2 · Some tracker domains silently won't toggle ALLOWED→BLOCKED (regression in 2026.04.03) — ✅ Draft PR [#602](https://github.com/TrackerControl/tracker-control-android/pull/602)
- **Issue:** [#576](https://github.com/TrackerControl/tracker-control-android/issues/576) (confirmed by 3+ users: Android 11 rooted, Android 16, F-Droid)
+- **Status:** Draft PR #602 open. UI-only fix in `TrackersListAdapter`: override `isEnabled(position)`/`areAllItemsEnabled()` so ambiguous shared-IP rows are non-interactive at the ListView level (standard idiom), plus a defensive no-op guard + informative toast, and a new `allowed_shared_ip` string ("Allowed — shared IP; enable Strict mode to block"). Runtime blocking semantics untouched. `compileGithubDebugJavaWithJavac` green. Needs visual confirmation.
- **Verdict:** Fixable-with-tension leaning aligned — the **runtime** behaviour is correct-by-design; the **UI** lies to the user. Fix the UI, not the blocking logic.
- **Symptom:** In an app's tracker list (Standard mode), tapping certain domains (e.g. an Amazon ad domain under gasbuddy) does nothing — the row won't flip to BLOCKED — while other rows in the same enabled section toggle fine.
- **Root cause:** The per-row status is recomputed in `TrackersListAdapter.java:305-318`. For domains where `t.isAllowedInStandardMode()` is true, it force-pins the label back to ALLOWED in non-strict mode:
@@ -118,6 +136,14 @@ Verdict vocabulary:
- **Analysis:** No literal ~2.5 min timer exists anywhere. Two *continuous* CPU loops were added by the gotatun migration and run **only when remote egress is enabled**: gotatun's 250 ms `handle_timers` tokio loop (`gotatun-0.7.2 device/mod.rs:500-502`, spawned at `:219`) and TC's 1 s `WgConnectivityMonitor` poll calling `tunnel.stats()` over JNI (`WgConnectivityMonitor.kt:226`, `LOOP_SLEEP_MS=1000`). The ~2.5 min *radio* cadence is emergent WG re-handshake (gotatun `noise/timers.rs:30-31`, `REKEY_AFTER_TIME=120s`/`REJECT_AFTER_TIME=180s`) because screen-off drops persistent-keepalive (`WgConfig.kt:31-35`) — this is protocol-optimal and intentionally not "fixed" with a keepalive (would add its own wakeups). The restart-backoff defeat noted historically is **already fixed** (`restartAttempts` now resets on observed rx, `WgEgress.kt` `onRxAdvanced`).
- **Fix:** When the tunnel is idle **and** screen-off, back off the 1 s `WgConnectivityMonitor` poll (extend its existing `onSuspended`/doze handling to e.g. 10–30 s) and consider a slower/adaptive gotatun tick. No change for default (no-egress) users — they have no such loop.
+### WI-11 · General self-check / diagnostics (strategic — addresses the un-triageable-report problem)
+- **Motivation:** "Apps don't connect" / "battery drain" reports are un-actionable because reporters never provide the deciding fact (Private DNS/DoT on? always-on VPN? blocking mode? remote egress? IPv6-only carrier?). Rather than chase each report, give the app an **on-demand self-check** that captures those facts itself and produces a plain-language verdict + shareable report.
+- **Verdict:** Fixable-aligned (diagnostic, not configuration — fits the philosophy). Report-first core is low-risk (no packet-path, no background pollers).
+- **Design constraints (maintainer steer):** Battery is *surfaced, not re-investigated* — surface TC's own wakelock/CPU/wakeup stats + the mis-attribution explanation (see C-1); do not re-derive. Connectivity diagnostics stay in the **non-WireGuard** path (the gotatun egress is already well-worked). On-demand only (battery). All data on-device; user chooses to share.
+- **Heritage note:** NetGuard originally had diagnostic/network-info/log-export code; the maintainer believes it was **removed** from TC and is unsure it was ever sufficient for TC's DNS/tracker model. First step is git-history archaeology to recover it and assess the gap.
+- **Signals the TC check must surface** (from Sections B/C + the connectivity map): Private DNS/DoT active (#348), IPv6-only DNS discarded (#339), always-on/onRevoke state (#526/#331), blocking mode + Strict shared-IP overblock (#297), system-apps/DoH/remote-egress toggles, tun-up / DNS-resolving / route sanity, and the battery attribution stats.
+- **Status:** 🚧 Opus agent investigating + drafting PR (report-first core; active health-check/auto-recovery deferred as documented follow-up).
+
---
## Section C — Unfixable-by-construction (respond to reporter; do NOT add a knob)
diff --git a/app/src/main/java/eu/faircode/netguard/AdapterLog.java b/app/src/main/java/eu/faircode/netguard/AdapterLog.java
index de5b76810..861cb219a 100644
--- a/app/src/main/java/eu/faircode/netguard/AdapterLog.java
+++ b/app/src/main/java/eu/faircode/netguard/AdapterLog.java
@@ -169,24 +169,32 @@ public void bindView(final View view, final Context context, final Cursor cursor
tvTime.setText(new SimpleDateFormat("HH:mm:ss").format(time));
// Show connection type
- if (connection <= 0)
+ String connectionDescription;
+ if (connection <= 0) {
ivConnection.setImageResource(allowed > 0 ? R.drawable.host_allowed : R.drawable.host_blocked);
- else {
+ connectionDescription = context.getString(allowed > 0 ? R.string.log_connection_allowed : R.string.log_connection_blocked);
+ } else {
+ String networkType = context.getString(connection == 1 ? R.string.log_connection_wifi : R.string.log_connection_other);
+ String allowedState = context.getString(allowed > 0 ? R.string.log_connection_allowed : R.string.log_connection_blocked);
+ connectionDescription = networkType + ", " + allowedState;
if (allowed > 0)
ivConnection.setImageResource(connection == 1 ? R.drawable.wifi_on : R.drawable.other_on);
else
ivConnection.setImageResource(connection == 1 ? R.drawable.wifi_off : R.drawable.other_off);
}
+ ivConnection.setContentDescription(connectionDescription);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
Drawable wrap = DrawableCompat.wrap(ivConnection.getDrawable());
DrawableCompat.setTint(wrap, allowed > 0 ? colorOn : colorOff);
}
// Show if screen on
- if (interactive <= 0)
+ if (interactive <= 0) {
ivInteractive.setImageDrawable(null);
- else {
+ ivInteractive.setContentDescription(null);
+ } else {
ivInteractive.setImageResource(R.drawable.screen_on);
+ ivInteractive.setContentDescription(context.getString(R.string.log_screen_on));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
Drawable wrap = DrawableCompat.wrap(ivInteractive.getDrawable());
DrawableCompat.setTint(wrap, colorOn);
@@ -233,9 +241,9 @@ public void bindView(final View view, final Context context, final Cursor cursor
} catch (PackageManager.NameNotFoundException ignored) {
}
- if (info == null)
+ if (info == null) {
ivIcon.setImageDrawable(null);
- else {
+ } else {
if (info.icon <= 0)
ivIcon.setImageResource(android.R.drawable.sym_def_app_icon);
else {
diff --git a/app/src/main/java/net/kollnig/missioncontrol/ActivityBlocklists.java b/app/src/main/java/net/kollnig/missioncontrol/ActivityBlocklists.java
index 9ee4694e0..c2cc506d1 100644
--- a/app/src/main/java/net/kollnig/missioncontrol/ActivityBlocklists.java
+++ b/app/src/main/java/net/kollnig/missioncontrol/ActivityBlocklists.java
@@ -155,6 +155,8 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.textError.setVisibility(View.VISIBLE);
}
+ holder.switchEnabled.setContentDescription(
+ context.getString(R.string.blocklist_enable_description, item.url));
holder.switchEnabled.setChecked(item.enabled);
holder.switchEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
item.enabled = isChecked;
diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml
index 5c4ddd59d..85106224d 100644
--- a/app/src/main/res/layout/about.xml
+++ b/app/src/main/res/layout/about.xml
@@ -18,6 +18,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
+ android:importantForAccessibility="no"
android:src="@drawable/ic_rocket_colour" />
diff --git a/app/src/main/res/layout/item_insights_header.xml b/app/src/main/res/layout/item_insights_header.xml
index 8a8b28a3d..49be3e2f5 100644
--- a/app/src/main/res/layout/item_insights_header.xml
+++ b/app/src/main/res/layout/item_insights_header.xml
@@ -71,11 +71,12 @@
diff --git a/app/src/main/res/layout/item_timeline_entry.xml b/app/src/main/res/layout/item_timeline_entry.xml
index 349897146..5ac4a02d9 100644
--- a/app/src/main/res/layout/item_timeline_entry.xml
+++ b/app/src/main/res/layout/item_timeline_entry.xml
@@ -21,6 +21,7 @@
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
+ android:importantForAccessibility="no"
tools:src="@android:drawable/sym_def_app_icon" />
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/vpn_enabled_switch_description" />
diff --git a/app/src/main/res/layout/list_item_trackers.xml b/app/src/main/res/layout/list_item_trackers.xml
index 6dfaf94ec..d0fa63717 100644
--- a/app/src/main/res/layout/list_item_trackers.xml
+++ b/app/src/main/res/layout/list_item_trackers.xml
@@ -20,6 +20,7 @@
android:id="@+id/root_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:accessibilityHeading="true"
android:text="@string/tracker_name"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
diff --git a/app/src/main/res/layout/log.xml b/app/src/main/res/layout/log.xml
index 1f0b7ca1c..e6fa14c4c 100644
--- a/app/src/main/res/layout/log.xml
+++ b/app/src/main/res/layout/log.xml
@@ -20,6 +20,7 @@
diff --git a/app/src/main/res/layout/sure.xml b/app/src/main/res/layout/sure.xml
index 268e6f7b6..97da43bdf 100644
--- a/app/src/main/res/layout/sure.xml
+++ b/app/src/main/res/layout/sure.xml
@@ -13,6 +13,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
+ android:importantForAccessibility="no"
android:src="@drawable/ic_rocket_colour" />
Invalid URL
Failed to apply host files
Last update: %s
+
+
+ Tracker blocking
+ VPN
+ Delete blocklist
+ Enable blocklist %s
+ Allowed
+ Blocked
+ Wi-Fi
+ Other network
+ Screen on
+ App icon