See Process - BACKLOG and CHANGELOG management
Work items use the same prefixes as CHANGELOG.md, plus these backlog-only ones:
| Prefix | Meaning |
|---|---|
| OPS | Operational issue (hosting, external service, infrastructure) |
| FTR | Future feature, not yet scheduled to a release |
See CHANGELOG.md for the full shared list (ADD, ARC, BLD, BUG, CFG, CHG, DEC, DELETE, DOC, FIX, GUI, INF, MOD, NOTE, REL, SEC, TST, UI).
Every prefixed work item gets an ID, with one exception: items in ## 2. New work items stay ID-free while parked there, however there may be ID numbers in that section if work items are moved there by Andrew or Claude. Claude Code usually assigns the ID only when moving an item into CHANGELOG's WIP, see that section for the workflow. This became mandatory alongside CHANGELOG's v0.8.78 build 448 release, and was backfilled across all existing prefixed items in this file at the same time. Unprefixed sub-bullets (e.g. under Codebase cleanup) may also stay ID-free too, since there's no prefix to attach an ID to.
IDs are shared with CHANGELOG.md: one number line across both files, so an ID always points to exactly one item no matter which file it lives in.
Format: ID-NNN XXX:, where XXX is the item's prefix code, placed at the start of the bullet before the prefix.
To assign a new ID, search both BACKLOG.md and CHANGELOG.md for the highest existing ID-NNN and use the next number.
Search again immediately before each new ID, even within a batch, and write the item into the file before using its number anywhere else.
Once an ID is given, it does not change even if the item moves section (e.g. from Unconfirmed BUGs to a scheduled section once confirmed). Closed items retain their ID number.
<none>
- ID-135 FTR: DEVICES can clear offline devices from its list. It stays clear of DHCP reservations. Pinning a device reserves its address in
dhcp_staticlist, unpinning leaves the reservation, and the app never removes one (Andrew's decision, 2026-09-28, merged from ID-153). Removing one restarts the whole network, and can break a pin silently at a later lease renewal (ARCHITECTURE 6.3, 6.8.3). Anyone who wants to can do it in the web interface. - ID-305 FTR: move stock to
mailsend-gov1.0.13 and add-printCertsto a failed send's diagnostics. v1.0.13 (2026-09-29, upstream issue #76) prints the server's certificate while sending, not only with-info, and adds-verbose.-verifyCertalready applied to sending in v1.0.12: itssendMailsetsInsecureSkipVerify: !VerifyCert. So the alerts have been verifying the SMTP server's certificate all along, and this is diagnostics only. The upstream ChangeLog agrees there's "no need to update unless you need certificate details". The work: new pins and SHA-256s for both architectures inbinary_installer.dartfrom the published checksums, recomputed on hardware; the archive member names; offering the update to routers that already have v1.0.12; and a run of TEST EMAIL and a deliberate failure on stock. - ID-127 FTR: ship our own
curlbesidejqandmailsend-go, if ASUS tighten their wrapper further. Stock's/usr/sbin/curlsilently refuses a caller withcrondin its ancestry, and any URL whose host is an IP address (ARCHITECTURE 2, rows 6 and 7). Both are worked around: the script detaches from cron, and every request uses a hostname with--resolve.BinaryInstallercould ship one, but a TLS stack of our own makes its CVEs this project's job, sits outside the dependency scanning, and asks users to trust far more than a JSON parser. Revisit only if a firmware update breaks row 6 or 7. Raised 2026-09-19. - ID-024 FTR: localise into French, Spanish and Latin American Spanish, then decide on more. Translate the Play listing by hand: Google's automatic translations break its character limits.
- ID-257 FTR: pilot: manual tests as Flutter integration tests. The real app, on the emulator, taps through a flow, checks the screen, and checks the real router over its own SSH connection. Start with groups that need no PIA login (SET, ROUTER LOG, ROUTER RESOLVER STATUS, ROUTER DNS ROUTING), and judge it before going further. Needs a router the test can reset to a known state, and credentials kept out of the repo. Stays by hand: anything physical, emails arriving, the password manager, Play installs and purchases. Parked 2026-09-28, unrun (Andrew's decision): the
integration_testpackage putsandroidx.test,junitandguavainto the debug build, which broke the Gradle lockfile; locking them moved RevenueCat in the debug build and broughtjunitandguavafindings into the OSV scan (ID-058). The plan is.claude/plans/plan_integration-test-pilot.md. To resume: lock the new libraries or exempt the debug build from locking, then follow the plan.
- ID-386 BUG?: the small home screen widget cuts its summary line short at its own default size. Seen 2026-10-13 on the emulator (Pixel 7 Pro, Android 17, Nova Launcher) while making ID-380's screenshot: dropped at 2×1, the size
widget_compact_info.xmlasks for (targetCellWidth="2",minWidth="110dp"), it read "2 tunnels u…" for "2 tunnels up · 6 pinned"; widened by a cell, it fits. Not yet seen on a phone - Andrew's had been widened - and launchers size cells differently, so whether it needs a wider default, a shorter line, or nothing depends on more than this one launcher.
Early thinking, with measurements: .claude/plans/plan_firmware-abstraction.md.
Important
Do not start the firmware abstraction until stock support has soaked in release. It touches every path stock support just landed on, and a regression here would be indistinguishable from a stock-support bug - the same reasoning that gave SSH connection reuse its own build number.
- Map codebase
- By file and by function (done 2026-09-01, now in
CONTEXT.md) - Identify redundant or duplicated code
- By file and by function (done 2026-09-01, now in
- Optimise and simplify
- nvram statements
- Duplication of variables
- Complexity reduction/reduce lines of code
- audit large/long/complex source files
- audit naming of source code files
- Abstract firmware from Manage and watchdog. Measured 2026-09-05: 27
isStockFirmwarebranches across six files, 21 of them inrouter_slot_service.dart(14) androuter_watchdog.dart(7). Both stock bugs found this session were "the stock branch does not match the Merlin branch's intent".- Create functions to act on classes of activities
- Split out to functions
- Model on
buildWatchdogScript, which already resolves firmware once and substitutes the differences (__KILLSW__,__MAILHDR__,__MAILCMD__) rather than branching at runtime - Take it in slices, each on its own build: start/stop first, then slot reading, then cron persistence, then delete
- Largest files, measured 2026-09-05:
router_watchdog.dart1,554 lines,router_slot_service.dart842,slot_modal.dart767. The email layout inrouter_watchdog.dart(buildEmailBody,RouterEmailFacts, the section constants) is self-contained and would move out with no behaviour change. - ID-041 CHG: fix the Sonar Cloud scan's findings, now 88, in six batches from safest to riskiest. Triaged 2026-09-15 at 35 and put on hold with the rest of this section; analysed again 2026-10-09. A plan, approved by Andrew, before any code: this is that plan, waiting on his three decisions at the end.
- Where it stands, 2026-10-09: 88 open, 50 of them newer than the triage, mostly the rewrite's new code. All code smells; none a bug or a vulnerability. CI's SonarCloud check passes, since the quality gate judges new code only. Cognitive complexity (S3776, limit 15): 38 functions, 571 points over. Nested ternaries (S3358): 29. Duplicated literals (S1192): 14. Too many parameters (S107): 2. Missing
const(S7112, S3962): 5. Re-read the list withhttps://sonarcloud.io/api/issues/search?componentKeys=ExponentiallyDigital_cfg-pia-wg&resolved=false&ps=500before starting; it moves with every build. - The worst functions:
- DEVICES:
_apply84,_tunnelWarnings32 (lib/widgets/device_assignment_screen.dart), andDeviceAssignmentService.apply31 with 13 parameters: APPLY, the most production-critical path. - Router DNS:
buildDnsRouting67 anddnsReportText55 (lib/router_dns.dart), both pure functions, so their output can be pinned by tests before anything changes. - Watchdog and slots:
uninstallFromRouter53,deployWatchdog45,validate30 (lib/router_watchdog.dart);createConfigToSlot37,fetchSlots30,enableSlot26 (lib/router_slot_service.dart). Full of stock-or-Merlin branches the firmware abstraction above would restructure anyway. - Screens and dialogs:
_slotList46,_buttons36,_enableManage28 (lib/widgets/slot_modal.dart);_save36,_dohPicker26 (lib/watchdog_dialog.dart);_onConnect35 (lib/widgets/router_slots_screen.dart);_secureStartupand_maxActiveVpns30 each (lib/screens/settings_screen.dart);slotParamErrors28 (lib/input_checks.dart).
- DEVICES:
- The duplicated strings, judged:
"Exception: ", 24 times across four screens, is real duplication, the same error-tidying code copied around, and one shared helper fixes all four findings. The DNS file paths (/etc/dnsmasq.conf,/tmp/resolv.dnsmasq,/etc/resolv.conf) are shared facts that belong in constants, as does the DEVICES label"Default connection". The router commands (nvram commit24 times over three files,nvram get vpnc_clientlist,nvram get vpnc_max_conn,date +%s) arguably read better written out than as constants, and could instead be marked "won't fix" in SonarCloud with that reason. - The plan, one build per step:
- The quick, safe batch, about 20 issues: the five
constfixes, the error-tidying helper, the DNS path and label constants, and the router commands as Andrew decides. - Router DNS's two pure functions: tests that pin their output first, then split them, changing nothing they produce. Two of the four worst.
- The 29 nested ternaries, mechanical; the 8 in the watchdog script builder checked against the generated script text.
- DEVICES' APPLY:
_apply,_tunnelWarningsandapply, its 13 parameters into one request object. Pieces moved into functions of their own and nothing else changed; the tests that pin APPLY's exact order, "THE DEFAULT CONNECTION SEQUENCE IS EXACT" among them, are the safety net. - The screens and dialogs, one file per build.
- The watchdog and slot service functions: left to the firmware abstraction, and scored again after it.
- The quick, safe batch, about 20 issues: the five
- For every step: behaviour pinned by a test before the change, functions only split, never reworked, the full suite, and the SonarCloud count read again afterwards.
- Decisions for Andrew: start now, or keep holding (steps 1 to 3 carry little risk; 4 onward touch paths real users depend on); the router commands, constants or "won't fix" with a reason; and whether step 6 waits for the firmware abstraction.
- Where it stands, 2026-10-09: 88 open, 50 of them newer than the triage, mostly the rewrite's new code. All code smells; none a bug or a vulnerability. CI's SonarCloud check passes, since the quality gate judges new code only. Cognitive complexity (S3776, limit 15): 38 functions, 571 points over. Nested ternaries (S3358): 29. Duplicated literals (S1192): 14. Too many parameters (S107): 2. Missing
Parked - US$99/year Apple Developer Program against an unknown iOS user base. Early thinking, and what actually blocks it: .claude/plans/plan_ios-port.md.
-
The router half ports for free:
dartssh2is pure Dart, and the watchdog script runs on the router, which does not care what phone deployed it. -
The platform hardening does not.
FLAG_SECUREhas no iOS equivalent - screenshots cannot be blocked, only the task-switcher snapshot can be covered.README.mdandSECURITY.mdstate that guarantee unconditionally today and would need to state it per-platform. -
The silent clipboard clear is an Android method channel (
ClipboardManager.clearPrimaryClip()); iOS needsUIPasteboard.general.items = []or the 60-second auto-clear regresses to the system copy popup that 403 removed. -
Non-code costs: a Mac or hosted runner for signing, a materially stricter App Store review, and a release pipeline (
release.yml, SBOM, Gradle lockfiles) that is Android-shaped throughout. -
Extend RevenueCat to use Apple Store.
-
ID-029 DOC: phrase the hardening claims in
README.mdandSECURITY.mdas "on Android" rather than absolutely, so an iOS build cannot quietly make them untrue. -
ID-375 BLD: post the agreed comment on flutter_launcher_icons pull request #549, and retire
tool/launcher_icons.dartonce a release carries the fix. Agreed 2026-10-09, Andrew's call, but not posted: theghtoken here can't comment outside the organisation ("Resource not accessible by personal access token"). Post it from Andrew's own login, or aftergh auth refresh -s public_repo. Once a flutter_launcher_icons release changes onlyASSETCATALOG_COMPILER_APPICON_NAME, the wrapper (ID-374) can go, andscripts/build.ps1,scripts/build.sh, the quality and release workflows, BUILDING, CONTRIBUTING and the note inpubspec.yamlcan run the tool directly again. The comment:Still happening on 0.14.4, and not only with flavors or App Clips: plain `ios: true` triggers it too. With `ios: true` and no flavor, `createIcons` ends with `changeIosLauncherIcon('AppIcon', flavor)`, which sets every line containing `ASSETCATALOG` in the build configurations to the icon name. In our `ios/Runner.xcodeproj/project.pbxproj` (Flutter 3.47.5), two of the three ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; lines became ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; even though the icon set's name hadn't changed. This PR's one-line fix still works, but it no longer applies as is: the check has moved to around line 340 of `lib/ios.dart`. The equivalent there is `line.contains('ASSETCATALOG_COMPILER_APPICON_NAME')` in place of `line.contains('ASSETCATALOG')`. For now we run the tool through a small wrapper that puts the other lines back afterwards.
Andrew adds new items here as he finds them, unsequenced, unprioritised, and often without an ID-NNN - items in this section may not have an ID until they're moved into CHANGELOG.
Triage prompt for Claude Code:
When asked to triage BACKLOG new work, work through it in this order:
- Check for duplicates. For each item in this section, check whether it duplicates or overlaps with anything already in CHANGELOG's Pending, WIP, or Implemented history, or an existing BACKLOG item elsewhere in this file. Flag any overlap found rather than silently merging or dropping it.
- Add a unique ID to any work item below that does not already have one. Assign each item its
ID-NNN. Search both CHANGELOG.md and BACKLOG.md for the highest existing ID and increment from there. - Sequence into batches. Group the work items into logical implementation batches, following the same descending-importance ordering rule CHANGELOG already uses. Batches should reflect what makes sense to build and test together, not just priority order.
- Flag anything unclear. Before moving anything, list any item where the requirement, scope, or expected behaviour needs clarification from Andrew. Wait for answers before proceeding on those specific items; the rest of the triage can continue.
- Move agreed items to WIP. Once Andrew confirms the batching, move the agreed work items into CHANGELOG's
### 1.2. WIPsection. Take into account what's already in WIP, resequencing existing items if the combined set changes the intended build order. Remove moved items from this section. - Do not start implementing. Stop after the move and wait for Andrew to separately say to begin implementation.
- Reference work items in conversation. When discussing work items, include the ID number and a very brief sentence that describes that item at the beginning of conversation.
<none>