interlinedlist-android
94 rows · Updated
| Issue # | Title | Body | State | Labels | Assignees | Link | Created | Updated |
|---|---|---|---|---|---|---|---|---|
| 128 | Host `/.well-known/assetlinks.json` — App Links verification currently fails for every https deep link | Found while implementing #89. **Pre-existing, and it silently disables a feature that already shipped.** ## The problem `https://interlinedlist.com/.well-known/assetlinks.json` returns **404** (verified). `app/src/main/AndroidManifest.xml` declares an `autoVerify="true"` intent filter covering: - `/lists/shared/…` and `/documents/shared/…` (share links) - `/reset-password` - `/verify-email` - `/verify-email-change`, `/undo-email-change` (#72) - `/` (the tag feed, #29) - `/api/blog/subscribe/confirm`, `/api/blog/unsubscribe` (#89) **Android App Links verification requires the site to host a Digital Asset Links file naming the app's package and signing-certificate fingerprints.** Without it, verification fails, and on Android 12+ the system will **not** auto-open the app for any of those URLs — the user gets a browser, or at best a disambiguation dialog they have to opt into via Settings → Default apps → Opening links. So every `https://` deep link the app has shipped so far is effectively inert on modern Android. The `interlinedlist://` custom-scheme equivalents work regardless, which is why this has not been noticed. ## The fix (server side) Host at `https://interlinedlist.com/.well-known/assetlinks.json`, served as `application/json` over HTTPS with no redirect: ```json [{ "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.interlinedlist.android", "sha256_cert_fingerprints": [ "<debug keystore SHA-256>", "<release keystore SHA-256>", "<Play App Signing SHA-256, if Play re-signs>" ] } }] ``` **All three fingerprints matter.** If Play App Signing is enabled, the certificate users actually get is Play's, not the upload key — omitting it means verification passes in testing and fails in production. Confirm the `package_name` against the release `applicationId` (the debug build uses a `.debug` suffix, which needs its own entry or its own file if both are to verify). ## Verifying afterwards - `adb shell pm get-app-links com.interlinedlist.android` should report `verified` per host. - Google's checker: `https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://interlinedlist.com&relation=delegate_permission/common.handle_all_urls` ## Related - **#39** (OAuth) offers an App Link redirect as its preferred option — that option depends entirely on this file existing. - #29 notes the `/` filter claims plain homepage links; that decision is worth revisiting at the same time. | open | P1 | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/128 | Sep 16, 2026, 9:38 PM | Sep 16, 2026, 9:38 PM |
| 124 | Lists: the freshness poll does not pick up rows another person inserts | Follow-up from #53 (PR #123), which built the collaborative freshness poll. ## The gap `POST /api/lists/{id}/data/versions` is a **version-diff** endpoint: the client sends `rowVersions` for the rows it is holding, and the server answers with `changed` (those rows, if stale) and `deleted`. By construction it can only report on rows the client **already knows about**. A row that another collaborator *inserts* has no version for us to quote, so it never appears in `changed` and the grid does not learn about it. #123 appends defensively if a changed row arrives that we were not holding, but insertions do not arrive at all. **Effect:** two people editing the same list see each other's *edits* and *deletions* live, but not each other's *new rows*, until someone pulls to refresh. ## Options 1. **Client-side:** have the poll periodically (much less often than the 10s beat — say every Nth quiet beat) re-read `GET /api/lists/{id}/data` page 1 and merge in unseen ids. Cheap to build, but it is exactly the whole-table refetch the versions endpoint exists to avoid. 2. **Server-side (better):** have the versions response include an `inserted[]` array, or a list-level row-count/high-water mark the client can compare against cheaply. Worth asking the owner whether the first-party web grid has this problem too, or whether it solves it some other way. Option 2 is the right fix if the endpoint can grow it; option 1 is the fallback. ## Context `version` is now plumbed through `RowDto`/`ListRow` by #123, which is also the prerequisite for the other half of collaborative editing — `If-Match` + `{ changes, expect }` per-field deltas and `409 version_conflict` handling — which remains unimplemented. | open | P2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/124 | Sep 16, 2026, 9:26 PM | Sep 16, 2026, 9:26 PM |
| 120 | Bug: an unrecognised `customerStatus` locks a paying subscriber out of subscriber features | Found while implementing #58 (list invites), which fixed it on the lists side. `:feature:documents` still has it. ## The bug `CustomerStatus` in `:core:model` maps an unrecognised `customerStatus` string to `UNKNOWN`, whose `isSubscriber` is `false`. `:feature:documents`'s `currentUserIsSubscriber()` treats that as **"not a subscriber"** — so if the server ever returns a status the app does not know (a future `subscriber:lifetime`, a renamed tier, anything), a **paying owner is locked out of their own invite form without the server ever being asked**. The gate is client-side, so there is no 403 to recover from; the feature simply is not offered. ## The fix `:feature:lists` (PR #119) treats `UNKNOWN` as **unreadable** rather than as *free*, and fails **open** — the server stays the real gate, which it always was. It has a regression test, `an unrecognised customer status is treated as unknown, not free`. Apply the same one-line change to `:feature:documents`'s `currentUserIsSubscriber()` and add the equivalent test. ## Worth checking at the same time Any other client-side subscriber gate in the codebase. `:core:materialize` (#11) and `:feature:ai` (#4) both deliberately fail open already, so they are fine — but a grep for `isSubscriber` before closing this would be worthwhile. ## Why it was not fixed in #58's PR Scope — `:feature:documents` had parallel work in flight at the time. | open | P2area:documents | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/120 | Sep 16, 2026, 9:07 PM | Sep 16, 2026, 9:07 PM |
| 104 | Consolidate the two writers of account preferences behind one owner | Follow-up from #33 and #19, both of which needed the same account preference and deliberately did not create a feature→feature dependency to get it. ## Current state Two independent paths now read and write `PATCH /api/user/update`: 1. **`:feature:profile` → `SettingsRepository`** (from #33) — models all fifteen fields, backs the Settings screen, `@Singleton` with a process-scoped in-memory cache. 2. **`:core:network` → `ViewingPreferenceStore`** (from #19) — a narrow accessor for `viewingPreference` so `:feature:messages` can drive the feed switcher. Each has its own in-flight state and neither knows about the other's writes. **User-visible symptom:** change the view preference in Settings, and an already-composed feed switcher still shows the old value until the feed screen is re-entered. ## Why it happened No feature module in this repo depends on another — that is a deliberate convention that keeps modules conflict-free for parallel work. `:feature:messages` therefore could not read `:feature:profile`'s repository, and duplicating a narrow accessor in `:core:network` was the smaller evil at the time. ## What to do Promote a single account-preferences owner — a `:core:preferences` (or similar) module holding the `UserSettings` model, the partial-PATCH call and one observable cache — and have **both** `:feature:profile`'s Settings screen and `:feature:messages`'s feed switcher consume it. Then delete `ViewingPreferenceStore` and fold `SettingsRepository` onto the shared owner. Best done once the Settings epic (#31) has finished landing, so it is not fighting five parallel branches. | open | P2 | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/104 | Sep 16, 2026, 8:16 PM | Sep 16, 2026, 8:16 PM |
| 101 | Bug: Android sends the wrong collaborator role vocabulary — every collaborator is silently added as read-only | Found while implementing #59 (document email invites), then **confirmed against the live API**. ## The bug `CollaboratorRole` in `feature/documents/.../domain/Collaborator.kt` sends `viewer` / `editor` / `admin`: ```kotlin enum class CollaboratorRole(val apiValue: String, val label: String) { VIEWER("viewer", "Viewer"), EDITOR("editor", "Editor"), ADMIN("admin", "Admin"); ``` The server's vocabulary is **`watcher` / `collaborator` / `manager`**. From the live API, on `POST /api/lists/{id}/invites`: ``` {"error":"Invalid role. Must be watcher, collaborator, or manager","code":"bad_request"} ``` — returned for `role: "viewer"` as well as for a nonsense value. ## Why it is worse than a rejected request `POST /api/documents/{id}/collaborators` **does not validate the role.** A live POST with `role: "zzz"` returned `201 {"collaborating": true}`, and the resulting collaborator came back from `GET /api/documents/{id}/collaborators` as: ```json { "role": "watcher", "user": { "username": "…" } } ``` So an unrecognised role silently falls back to `watcher`, the **read-only** level. **Consequence:** every collaborator added from the Android app is granted read-only access no matter which role the user picked. Choosing "Editor" or "Admin" appears to work — the request succeeds, the row appears — and the person silently cannot edit. There is no error to notice. The read path masks it too: `CollaboratorRole.fromApi` maps anything unrecognised to `VIEWER`, so a real `collaborator` or `manager` role coming back from the server also renders as "Viewer". ## Fix - Change the `apiValue`s to `watcher` / `collaborator` / `manager`. - Update `fromApi` to map the real values (keeping a tolerant fallback is fine, but it must recognise the three real ones first). - Check `PUT /api/documents/{id}/collaborators/{userId}` for the same problem. - Check whether `:feature:lists` contributors have the equivalent issue. - Add a test that pins the three wire values, so this cannot regress silently. `/help/api/documents` and `/help/api/sharing` both document `watcher`/`collaborator`/`manager`. #59 uses the correct values for its separate `InviteRole`; the collaborator path was left alone as out of scope for that issue. ## Note Confirming this required one live write (adding a collaborator to a document on the test account). It was removed immediately afterwards and the collaborator list verified back to empty. | open | P1area:documents | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/101 | Sep 16, 2026, 8:14 PM | Sep 16, 2026, 8:14 PM |
| 97 | Bug: a newly created message lands mid-feed instead of at the top | Found while implementing #22 (cursor pagination). Pre-existing, unrelated to that change, so filed rather than fixed there. ## The bug `DefaultMessagesRepository.createMessage` inserts the new post at `maxFeedOrder() - 1`. The feed sorts by `feedOrder ASC`. With a cached feed of more than two rows, `maxFeedOrder() - 1` is **not** smaller than every other row's order — so the new message lands somewhere in the middle of the feed instead of at the top. ## Why the tests do not catch it The existing coverage exercises a single-row feed, where `maxFeedOrder() - 1` happens to be correct. The bug only shows up with three or more cached rows. ## Fix Add a `minFeedOrder()` query to `MessageDao` and insert at `minFeedOrder() - 1`. Extend the test to a feed of at least three rows so the regression is actually covered. Reported by the agent implementing #22; left out of that PR because it sits in composer code that #19 and #20 will be touching. | closed | P2area:messages | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/97 | Sep 16, 2026, 7:58 PM | Sep 16, 2026, 9:20 PM |
| 94 | Thread the API error `code` through to typed `AppError`s (replaces string-sniffing) | Discovered while implementing #33, and it affects every feature module — filing it rather than smuggling a repo-wide refactor into an unrelated PR. ## The problem Every InterlinedList API error comes back as: ```json { "error": "<human message>", "code": "<snake_case_code>" } ``` The app throws the `code` away. `ErrorDto` in `:core:network` models only `{ "error": ... }`, and `safeApiCall` maps **HTTP status → `AppError`**, with the `AppError` subclasses in `:core:common` carrying just a `message`. Two concrete costs: 1. **Distinct failures collapse.** `:feature:ai` had to define its own module-local `AiError`/`AiResult` (see #4 / PR #92) purely because the shared `AppError` cannot tell `quota_exceeded` from `rate_limited` — both are HTTP 429 — nor `no_provider_configured` from a generic conflict. Every future module with meaningful error codes will face the same fork. 2. **`safeApiCall` currently string-sniffs.** It decides `SubscriptionRequired` with a `message.contains("subscription")` heuristic. That is a user-visible behaviour hanging off English prose in a server response — it breaks the day the copy is reworded. ## What to do - Add `code` to `ErrorDto`. - Widen `AppError` (or add a typed `code` alongside the message) so call sites can branch on the server's own vocabulary. - Replace the `contains("subscription")` heuristic with the real code. - Fold `:feature:ai`'s module-local `AiError` back onto the shared type once it can express `quota_exceeded` / `rate_limited` (+ `Retry-After`) / `no_provider_configured`. ## Why it is filed rather than done It touches every `AppError` construction site across all feature modules, so it needs a quiet moment with no parallel branches in flight — otherwise it conflicts with everything. Best done after the current parity wave merges. **Observed codes so far:** `bad_request` (rejected `viewingPreference`), `unauthorized` (`GET /api/auth/accounts` to a bearer token), plus `quota_exceeded`, `rate_limited` and `no_provider_configured` documented at `https://interlinedlist.com/help/api/ai-integration`. | open | P2 | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/94 | Sep 16, 2026, 7:53 PM | Sep 16, 2026, 7:53 PM |
| 89 | Blog: subscribe and unsubscribe from post emails | Part of #87 — Blog: read posts in-app and manage the email subscription - Subscribe with `POST /api/blog/subscribe { email }`, defaulting to the signed-in account's address, and explain the **double opt-in** — the user must confirm from the email before anything arrives. - Handle the confirmation link (`GET /api/blog/subscribe/confirm`) as a deep link. - Offer unsubscribe (`POST /api/blog/unsubscribe`), and handle the human-facing unsubscribe link from an email footer as a deep link too. **Tests:** subscribe round-trip; confirm and unsubscribe deep-link parsing. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP3area:blog | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/89 | Sep 15, 2026, 11:01 AM | Sep 16, 2026, 9:35 PM |
| 88 | Blog: reach the blog from the app | Part of #87 — Blog: read posts in-app and manage the email subscription No public JSON endpoint exists for blog content, so this is a routing decision rather than a data-layer one. - Simplest honest option: a Blog entry in the Account hub that opens `/blog` in a **Custom Tab**, themed to match the app. - If in-app rendering is wanted, it needs a server-side listing endpoint — raise that with the owner rather than scraping HTML. - Deep-link blog URLs so a shared post opens sensibly from Android. **Tests:** entry point routes; deep link handled. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP3area:blog | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/88 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 9:12 PM |
| 87 | Blog: read posts in-app and manage the email subscription | ## What the web app has A public **Blog** (`/blog`) of product updates and long-form writing, with an email subscription using double opt-in and one-click unsubscribe. `/help/blog` documents post states, writing, scheduling, publishing and unpublishing — but the authoring endpoints are all under `/api/admin/blog/*` and are **admin-only**, so authoring is out of scope for this client. ## Unused API surface - `POST /api/blog/subscribe` (`{ email }`) - `GET /api/blog/subscribe/confirm` — double opt-in confirmation from the email - `GET|POST /api/blog/unsubscribe` — human-facing link and the RFC-8058 one-click target ## The constraint There is **no public blog-listing endpoint** in the OpenAPI spec — the blog is server-rendered. Reading it in-app therefore means a Custom Tab or a lightweight scrape, not a JSON feed. Decide which before building. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | epicparityP3area:blog | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/87 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 9:09 PM |
| 86 | DMs: explain who can be messaged when the recipient list is empty | Part of #84 — Direct Messages parity gaps `/help/direct-messages` → *Who you can message*: DMs are constrained to a defined set of people (mutual follows in practice). `GET /api/dm/recipients` returns that set. When it is empty — which is the normal state for a new account — the Android picker shows nothing and reads as broken. - Replace the empty picker with an explanation of the rule and a route to find and follow people. **Tests:** empty-recipients state renders the explanation, not a blank list. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP3area:dm | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/86 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 7:55 PM |
| 85 | DMs: build the inbox from `GET /api/dm/conversations` | Part of #84 — Direct Messages parity gaps The endpoint returns **one row per conversation**, grouped by `pairKey`, newest activity first — exactly what an inbox should render. `InboxViewModel` currently pages `GET /api/dm`, a flat message list, and groups client-side. - Switch the inbox to `/api/dm/conversations`, keeping the existing cursor-paging approach. - Keep `GET /api/dm` for whatever still needs the flat list (if nothing does, drop it). - Verify the unread counts line up with `GET /api/dm/unread-count`. **Tests:** conversations parse; inbox renders one row per conversation; unread counts agree. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP2area:dm | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/85 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 7:46 PM |
| 84 | Direct Messages parity gaps | ## Where this stands `:feature:directmessages` is in good shape — inbox with cursor paging, send, thread by username, `/updates` polling, read, trash, restore, recipients and image upload, with an unread badge. It is one of the more complete modules in the app. ## Gaps - **`GET /api/dm/conversations`** is unused — *"One row per conversation (grouped by pairKey), newest activity first."* The inbox is currently built from `GET /api/dm`, which is a message list, not a conversation list. On the web the user sees conversations; on Android they see messages. - `/help/direct-messages` documents **who you can message** — recipients are constrained (the test account can DM only a mutual follow). Android surfaces the recipient list but not the *rule*, so an empty picker looks like a bug. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | epicparityP2area:dm | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/84 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 7:55 PM |
| 83 | Organizations: roles and public/private visibility surfaced correctly | Part of #80 — Organizations parity gaps `/help/organizations` documents roles and public-vs-private organizations. The Android member management should reflect what each role may actually do, and the detail screen should show visibility. - Audit the member screen against the documented roles; hide actions the current user's role does not permit rather than failing the call. - Show and (for permitted roles) edit organization visibility. **Tests:** per-role action visibility. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP3area:organizations | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/83 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 8:54 PM |
| 82 | Organizations: post as an organization (`organizationId` on compose) | Part of #80 — Organizations parity gaps `POST /api/messages` accepts `organizationId`. The Android composer always posts as the user. - Add an author picker (self / each organization the user may post for) when the user belongs to at least one, and hide it entirely otherwise. - Show organization authorship on the message card so it is unambiguous who posted. **Tests:** request carries `organizationId`; card renders org authorship. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:messagesarea:organizations | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/82 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 81 | Organizations: join and leave | Part of #80 — Organizations parity gaps `/help/organizations` covers *Joining organizations* (browse public organizations and join) and *Leaving an organization*. Android can browse and create but not join or leave. - Determine the real contract for joining — `POST /api/user/organizations` is the likely candidate but is undocumented in the spec; capture the live request before implementing. - Leave, with a confirmation, and correct behaviour for the last remaining owner. **Tests:** join/leave round-trips; last-owner guard. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP3area:organizations | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/81 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 8:35 PM |
| 80 | Organizations parity gaps | ## Where this stands `:feature:organizations` covers list, create, detail, members (add/update/remove) and user search. `/help/organizations` documents three things that have no Android equivalent: **joining** an organization, **leaving** one, and **posting as an organization**. ## Gaps - `POST /api/user/organizations` is unused — confirm its semantics (join? create-under-user?) against the live API; the spec gives no summary. - Leaving an organization has no Android affordance. - `POST /api/messages` accepts **`organizationId`**, which the composer never sends — so an Android user cannot post as their organization. - LinkedIn company pages are covered in the social-linking epic. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | epicparityP3area:organizations | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/80 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 79 | App settings: an Applications screen to manage devices | Part of #76 — Companion-app settings sync — register Android under Settings → Applications The Android-side equivalent of the web's Applications section: list the machines registered for this app, with - **Set as main workstation** (only one at a time; the first ever registered gets it automatically), - **Rename** (`PATCH …/devices/{deviceId}`), - **Remove** (`DELETE …/devices/{deviceId}` — deletes that machine's per-device settings; removing the main workstation auto-promotes the most recently used), - **View** the stored settings, **Copy to shared**, and **Delete shared settings**. **Tests:** promote/rename/remove round-trips; auto-promotion after removing the main workstation. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:settings | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/79 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 78 | App settings: sync shared and per-device preferences | Part of #76 — Companion-app settings sync — register Android under Settings → Applications - **Shared** settings (`GET|PUT /api/user/app-settings/{appKey}`) follow the account everywhere. - **Per-device** settings (`GET|PUT …/devices/{deviceId}/settings`) stay pinned to one device. - Decide which Android preferences belong to which bucket — theme and feed defaults are plausibly shared; sync cadence and notification behaviour are per-device. - Last-write-wins is fine here; do not build conflict resolution the server does not model. - Offline changes must survive and sync later, consistent with the app's offline-first stance. **Tests:** shared vs per-device routing; offline change syncs on reconnect. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:settings | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/78 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 77 | App settings: register the Android device and adopt the bootstrap settings | Part of #76 — Companion-app settings sync — register Android under Settings → Applications - Register on first sign-in: `POST /api/user/app-settings/{appKey}/devices` with a human-readable device name (reuse the `deviceLabel` already sent on `sync-token`, so the same device reads consistently in both Sessions and Applications). - On a brand-new install, seed from `GET /api/user/app-settings/{appKey}/bootstrap?deviceId=` — the web's stated behaviour is that a new machine starts from the **main workstation's** settings. - Deregister on sign-out / account deletion. **Tests:** register → device appears; bootstrap seeds a fresh install. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP3area:settings | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/77 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 9:53 PM |
| 76 | Companion-app settings sync — register Android under Settings → Applications | ## What the web app has `/help/app-settings` documents a whole subsystem: companion apps save their preferences to the user's account so they travel between machines. Settings → **Applications** lists each app, the machines signed in on it, and the settings stored — with a **main workstation** whose settings seed any brand-new machine, plus rename, remove, view, *copy to shared* and *delete shared*. Ten endpoints back it, none of them used by Android: `GET|PUT|DELETE /api/user/app-settings/{appKey}` · `GET /api/user/app-settings/{appKey}/bootstrap?deviceId=` · `GET|POST /api/user/app-settings/{appKey}/devices` · `PATCH|DELETE /api/user/app-settings/{appKey}/devices/{deviceId}` · `GET|PUT /api/user/app-settings/{appKey}/devices/{deviceId}/settings` ## Why it matters here The Android app **is** a companion app. Registering it would mean a user's device-local preferences (theme, feed defaults, the "add another after saving" toggle, sync cadence) follow them to a new phone instead of resetting — and would make the app visible in the web's Applications section alongside the Mac app. ## Worth confirming first Agree the `appKey` for the Android client with the owner before building anything. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | epicparityP3area:settings | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/76 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 75 | Account: multi-account switching — confirm it is web-only and record the decision | Part of #71 — Account lifecycle & security completeness The web has an account switcher: `GET /api/auth/accounts`, `POST /api/auth/switch`, `POST /api/auth/remove-account`, `POST /api/auth/{provider}/link`. A previous live check found `/api/auth/accounts` returning `Unauthorized` to a valid bearer token — these are cookie-session flows. **Re-verify** against the current API. - If still cookie-only → close as out-of-scope with the evidence recorded, so it stops resurfacing in every parity review. - If bearer-reachable → file the implementation work (the `:core:datastore` session store assumes a single account today, so this would not be a small change). **Done when:** the decision is recorded with evidence. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | P3area:account | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/75 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 74 | Account: subscription status and the route to manage it (billing stays on the web) | Part of #71 — Account lifecycle & security completeness Billing was deliberately removed from Android — subscriptions are handled on the web — and that decision stands. But the *consequences* of subscription state are everywhere: lists, documents, media, scheduling, cross-posting and all AI features are Subscriber-gated. - Show the current subscription state (from `customerStatus` on the user object) in the Account hub. - When a subscriber-gated action is blocked, say so specifically and route the user to manage the subscription **on the web**, rather than failing opaquely. - Refresh `GET /api/user` on return so a subscription started on the web takes effect without a sign-out. **Note:** Play policy makes in-app Stripe checkout the wrong call here; this issue is deliberately scoped to *status and routing*, not payment. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP2area:account | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/74 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 73 | Account: surface account status, posting restrictions and the appeal path | Part of #71 — Account lifecycle & security completeness `/help/account` documents **Account status**, *why posting is limited on a new account*, and **appealing a restriction or suspension**. Android shows none of this — a restricted user just sees posts fail. - Read the account status from the user payload and show it in the Account hub. - When posting is limited, explain **why** at the point of failure rather than surfacing a raw error. - Link the appeal path. **Tests:** restricted-account state renders; composer shows the explanation instead of a generic error. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP2area:account | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/73 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 72 | Account: complete the email-change flow (verify and undo) | Part of #71 — Account lifecycle & security completeness `POST /api/user/change-email/request` is wired, but the two endpoints that finish the flow are not: - **`POST /api/auth/verify-email-change`** — confirm the new address from the emailed link. - **`POST /api/auth/undo-email-change`** — the safety valve if the change was not the user's doing. Today an Android user can start an email change and then has to finish it on the web. - Deep-link handlers for both emailed links, plus an in-app pending-change state showing the address awaiting verification with resend and cancel. **Tests:** deep-link parse for each; pending state renders and clears. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP2area:account | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/72 | Sep 15, 2026, 11:00 AM | Sep 16, 2026, 8:30 PM |
| 71 | Account lifecycle & security completeness | ## Where this stands `:feature:auth` and `:feature:profile` already cover register, forgot/reset password, verify email, send-verification-email, active sessions (with "This device" labelling and revoke), connected-account listing, change-email request, account deletion, blocks/mutes and reports. The remaining gaps are the tail of the email-change flow, the account-status surfaces the web explains at `/help/account`, and the deliberate absence of billing. ## Unused API surface - `POST /api/auth/verify-email-change` - `POST /api/auth/undo-email-change` - `POST /api/auth/login` / `POST /api/auth/logout` (the app uses `sync-token`; confirm no behaviour is lost) - `GET /api/auth/accounts`, `POST /api/auth/switch`, `POST /api/auth/remove-account`, `POST /api/auth/{provider}/link` — previously confirmed cookie-session-only --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | epicparityP2area:account | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/71 | Sep 15, 2026, 11:00 AM | Sep 15, 2026, 11:00 AM |
| 70 | Android home-screen widgets (Glance) for the same data | Part of #67 — Widgets, location and weather Not API parity — a platform-native expression of the same feature. The web's widgets live on a dashboard; Android's natural home for them is the launcher. - Glance-based app widgets for a subset — unread DMs/notifications, next scheduled post, and whichever data widgets prove useful. - Update via the existing WorkManager infrastructure; do not add a second polling scheme. **Scope check:** only worth doing once the in-app widget surface exists and the owner confirms the data matters on mobile. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | enhancementP3area:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/70 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 69 | Widgets: weather and location tied to the profile's coordinates | Part of #67 — Widgets, location and weather `GET /api/weather` and `GET /api/location`, driven by the account's `latitude`/`longitude` (set in the Settings epic) or the device's current location. - Must degrade cleanly with no location permission and no stored coordinates. **Tests:** parse; no-location fallback. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:settingsarea:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/69 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 68 | Widgets: in-app widget surface (news, markets, bike-share, transit) | Part of #67 — Widgets, location and weather - Fetch and render the four widget feeds. - Confirm which require authentication and which are public, and confirm the response shapes live — none of them are modelled in the app today. - Transit needs the user's location for `GET /api/widgets/transit/stops`. **Tests:** parse each feed; graceful empty/error states (a dead third-party feed must not break the screen). --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/68 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 67 | Widgets, location and weather | ## What the web app has The web dashboard/front wall hosts data widgets, and Settings has a **Location and weather widgets** section gated behind a location permission: | Endpoint | | |---|---| | `GET /api/widgets/news` | Top Hacker News headlines | | `GET /api/widgets/markets` | Crypto watchlist quotes | | `GET /api/widgets/bike-share` | Bike-share availability | | `GET /api/widgets/transit` | Normalized transit data | | `GET /api/widgets/transit/stops` | Nearby stops | | `GET /api/weather` | Weather | | `GET /api/location` | Location | `/help/getting-started` also mentions a **Clock** page — a full-screen analog clock for the user's timezone. ## What Android has None of it. A previous review scoped widgets out as a web-dashboard concern; the transit widget is new since then, and the whole set is a natural fit for **Android home-screen widgets**, which would be a genuine platform advantage rather than a port. ## Worth deciding before building These are the least "parity"-shaped items in the backlog — they are dashboard furniture on the web. Confirm with the owner whether they matter on mobile at all before investing. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | epicparityP3area:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/67 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 66 | Dashboard: layout persistence (`dashboard-layout` / `front-wall-layout`) — verify scope first | Part of #62 — Dashboard & data-management surface `GET|PUT /api/user/dashboard-layout` and `GET|PUT /api/user/front-wall-layout` persist the web's arrangeable tile layouts. **This issue is a decision, not a build.** Re-check both against the live API with a bearer token. Then decide: - If they are cookie-gated web surfaces → close as out-of-scope and note it on the epic. - If they are bearer-reachable → decide whether a phone should honour a layout designed for a wide screen. Adopting it verbatim is likely the wrong call; ignoring it entirely means a user who rearranges their dashboard on the web sees no effect on mobile. **Done when:** the decision is recorded with evidence, and any follow-up work is filed. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/66 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 65 | Dashboard: engagement stats (`GET /api/user/engagement`) | Part of #62 — Dashboard & data-management surface *"Aggregate dig/push engagement received on the current user's own messages."* **Verify bearer access first** — this endpoint previously returned `Unauthorized` to a valid bearer token, suggesting it is cookie-gated. If it is genuinely web-only, close this issue as out-of-scope and record that on the epic. If it is reachable: show the aggregate on the dashboard, and consider per-message counts on the own-messages list. **Tests:** parse the aggregate payload; empty state. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/65 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 64 | Dashboard: a hub gathering scheduled posts, exports, settings, profile summary and the lists tree | Part of #62 — Dashboard & data-management surface The web Dashboard's tiles: **Scheduled posts**, **Exports**, **Settings**, **Profile Information** (username, display name, avatar, bio, with an edit link) and a **lists tree** for quick access. Every one of these screens already exists in the Android app — they are just scattered. This issue is about giving them one coherent entry point, not rebuilding them. - Decide placement deliberately: a sixth bottom-nav tab is probably wrong on a phone; folding it into the Account tab is likely better. **Tests:** Compose test that each tile routes to its existing destination. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP2area:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/64 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 63 | Dashboard: own-messages view with management actions | Part of #62 — Dashboard & data-management surface The core of the Dashboard: the user's own messages, separate from the public feed, with the management actions attached (edit, delete, share, create-from, select-mode for bulk actions). - Backed by the `onlyMine` query parameter on `GET /api/messages` (see the feed epic). - A table on the web; on a phone this should be a dense list, not a literal table. - This is also where multi-select for **Create from…** naturally lives. **Tests:** query carries `onlyMine`; actions operate on the right message. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP2area:messagesarea:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/63 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 62 | Dashboard & data-management surface | ## What the web app has `/help/getting-started` describes two distinct surfaces: - **Home** — the main feed (own messages plus all public messages), with the composer and the lists tree in the sidebar. - **Dashboard** — *only the user's own messages*, in a table view, plus **Data Management** (Exports, Settings), a **profile summary**, a **Scheduled posts** tile and a **lists tree**. The web also persists per-user layouts for the dashboard and the "front wall" (`GET|PUT /api/user/dashboard-layout`, `GET|PUT /api/user/front-wall-layout`) and exposes `GET /api/user/engagement` — *"aggregate dig/push engagement received on the current user's own messages."* ## What Android has Bottom-nav tabs — Messages, DMs, Lists, Documents, Account — and an Account hub. There is no own-content management surface: no own-messages view, no engagement stats, no single place that gathers scheduled posts, exports and settings. ## A caveat worth resolving first An earlier live check found `/api/user/dashboard-layout`, `/api/user/front-wall-layout` and `/api/user/engagement` returning `Unauthorized` to a **valid bearer token** — they appeared to be cookie/session-gated web surfaces. The spec has since grown; **re-verify each against the live API with a bearer token before building**, and close the ones that are genuinely web-only as out-of-scope. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | epicparityP2area:dashboard | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/62 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 61 | Invites: "Email this person" opt-out when granting access directly | Part of #57 — Email invites for lists and documents (invite someone who has no account yet) Web behaviour: when someone is added to a list (or their role changed) they get an in-app notification **and, by default, an email**. An **Email this person** checkbox can be unticked to skip the email; the in-app notification still goes out. Whether the email actually arrives also depends on that person's own *Sharing* notification settings. Android's manage-access flow has no such control, so it always takes the default. - Add the checkbox to the add-collaborator / change-role flow and send whatever flag the API expects — **confirm the field name against the live request before implementing**, as it is not in the OpenAPI spec. **Tests:** flag reaches the request in both states. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/61 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 60 | Invites: accept an invite via deep link (landing, sign-in, claim) | Part of #57 — Email invites for lists and documents (invite someone who has no account yet) The receiving half, and the one with the most edge cases. - App Link intent filters for the invite URLs → resolve with `GET /api/{lists,documents}/invite/{token}` and show a landing screen with what is being shared and at what role. - Signed out: route through login/register and **return to the invite** afterwards. - Signed in as the wrong or an unverified address: say so plainly — the invite is bound to the invited address and requires it to be **verified**. - Claim with `POST /api/{lists,documents}/invite/{token}`, then open the list/document. - Revoked / expired / invalid → the friendly "no longer valid" state, never a raw error. **Tests:** each of the four states (valid, wrong account, unverified, invalid); return-to-invite after sign-in. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP2area:listsarea:documents | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/60 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 59 | Invites: send and revoke document invites | Part of #57 — Email invites for lists and documents (invite someone who has no account yet) The document equivalent of the list invite flow, on the existing document access/collaborators screen. - `POST /api/documents/{id}/invites`, `GET /api/documents/{id}/invites`, `DELETE /api/documents/{id}/invites/{token}`. - Sits alongside the existing collaborator roles (viewer / editor / admin) rather than replacing them. **Tests:** send/list/revoke round-trips. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP2area:documents | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/59 | Sep 15, 2026, 10:59 AM | Sep 16, 2026, 8:15 PM |
| 58 | Invites: send and revoke list invites | Part of #57 — Email invites for lists and documents (invite someone who has no account yet) - **Invite by email** form in the list access section: email + role (Read-only / Edit / Admin) → `POST /api/lists/{id}/invites`. - **Pending invites** list from `GET /api/lists/{id}/invites` showing email, role, status (Pending → Accepted) and expiry. - **Revoke** via `DELETE /api/lists/{id}/invites/{token}`; the link must stop working immediately. - Revoking is free even when a subscription has lapsed — do not gate it behind the subscriber check that guards sending. - Roles map to the API names already used in this module: Read-only = `watcher`, Edit = `collaborator`, Admin = `manager`. **Tests:** send/list/revoke round-trips; revoke ungated by subscription. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/58 | Sep 15, 2026, 10:59 AM | Sep 16, 2026, 9:07 PM |
| 57 | Email invites for lists and documents (invite someone who has no account yet) | ## What the web app has A list or document owner can invite a person **by email address** — someone who may not have an InterlinedList account at all — and the list stays **private** the whole time. The invitee gets an *Accept invite* link; they must be signed in with **that same verified email** to accept, so a forwarded link does not work for anyone else. Pending invites are listed with role, status and expiry, and can be revoked (which kills the link immediately). A revoked, expired or invalid link shows a friendly "no longer valid" page. Sending invites is a Subscriber feature; **accepting is always free**, and so is revoking. ## What Android has Share **links** (lists and documents) and collaborator management, but no email invites at all. ## Unused API surface | Endpoint | | |---|---| | `GET\|POST /api/lists/{id}/invites` | list / send | | `DELETE /api/lists/{id}/invites/{token}` | revoke | | `GET\|POST /api/lists/invite/{token}` | resolve / claim | | `GET\|POST /api/documents/{id}/invites` | list / send | | `DELETE /api/documents/{id}/invites/{token}` | revoke | | `GET\|POST /api/documents/invite/{token}` | resolve / claim | Reference: `/help/lists` → *Inviting someone by email*. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | epicparityP2area:listsarea:documents | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/57 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 56 | Lists: read-only row data for a shared list (`GET /api/lists/shared/{token}/data`) | Part of #49 — Lists parity gaps: GitHub-backed lists, saved views, view modes and collaborative editing `SharedListScreen` resolves `GET /api/lists/shared/{token}` but never fetches the rows through **`GET /api/lists/shared/{token}/data`** — *"Token-authorized, READ-ONLY row data for a shared list."* A viewer opening a share link therefore sees the list's shell without its contents. **Tests:** token → rows render; unauthorised/revoked token → the friendly invalid state. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/56 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 55 | Lists: "Add another after saving" batch row entry | Part of #49 — Lists parity gaps: GitHub-backed lists, saved views, view modes and collaborative editing Web behaviour (`/help/lists` → *Entering several rows in a row*): an **Add another after saving** checkbox next to Add Row keeps the user on the form after each save — the row is saved, the form empties, a running count is shown, and focus returns to the first field. The checkbox **remembers the choice across lists and across visits**. If a save is rejected (e.g. a required field is empty) nothing is cleared, so the typed values survive. This is small but it is the difference between entering thirty rows and giving up. - Add the checkbox to `RowEditor`, persisted in `:core:datastore`. - Keep focus management correct with the software keyboard up. - Preserve form state on a rejected save. **Tests:** repeated-save flow; persistence across screens; values retained on validation error. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP3area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/55 | Sep 15, 2026, 10:59 AM | Sep 15, 2026, 10:59 AM |
| 54 | Lists: creation options the API accepts but the app drops (`parentId`, `folderId`, `messageId`, `initialRows`, `metadata`) | Part of #49 — Lists parity gaps: GitHub-backed lists, saved views, view modes and collaborative editing `CreateListRequest` sends only `title, description, schema, isPublic`. - **`parentId`** — child lists form a tree with breadcrumb navigation on the web; Android cannot set a parent at creation. - **`messageId`** — *"Create list from message"*: the list-plus icon on any message creates a list using that message as the description, with a suggested name. (Related to, but distinct from, the Create-from epic.) - **`initialRows`** — starter rows at creation, which Powered Templates depends on. - **`metadata`** / **`source`** — pass-through fields. - Also expose the **From Template** tab the web offers on list creation. There is no list-template endpoint in the OpenAPI spec, so confirm with the owner where those templates come from before building. **Tests:** each field reaches the request; parent breadcrumb navigation. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/54 | Sep 15, 2026, 10:59 AM | Sep 16, 2026, 8:05 PM |
| 53 | Lists: collaborative grid freshness poll and presence (`POST /api/lists/{id}/data/versions`) | Part of #49 — Lists parity gaps: GitHub-backed lists, saved views, view modes and collaborative editing Unused API: `POST /api/lists/{id}/data/versions` — *"Combined freshness poll and presence heartbeat for the grid"* — taking an optional `focusedRowId`. Without it, two people editing the same list on Android silently clobber each other's rows. - Heartbeat while a list is open, sending the focused row. - Use the freshness response to refresh changed rows rather than refetching the whole table. - Show who else is present, mirroring the presence indicator `:feature:documents` already has. **Tests:** heartbeat lifecycle (starts on open, stops on leave); stale-row refresh. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/53 | Sep 15, 2026, 10:58 AM | Sep 16, 2026, 9:26 PM |
| 52 | Lists: card, grid and ERD view modes | Part of #49 — Lists parity gaps: GitHub-backed lists, saved views, view modes and collaborative editing The features page advertises viewing list data **as cards, a grid, or an ERD diagram**, and lists can be nested and connected as a graph. Android renders a single table-ish layout. - **Cards** — the mode that actually suits a phone; probably the sensible Android default. - **Grid** — the dense spreadsheet view; needs horizontal scrolling and frozen first column to be usable on a phone. - **ERD** — the relationship diagram across connected lists; `:feature:lists` already models connections (`GET /api/lists/connections`), so the data is there. Each mode should be selectable per view (see the saved-views sub-issue) rather than being a global toggle. **Tests:** Compose tests per mode; wide-table horizontal scrolling. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/52 | Sep 15, 2026, 10:58 AM | Sep 16, 2026, 8:25 PM |
| 51 | Lists: saved views (shared and personal, default, fork-to-personal) | Part of #49 — Lists parity gaps: GitHub-backed lists, saved views, view modes and collaborative editing Unused API: `GET /api/lists/{id}/views` — *"Every shared view on the list, plus this user's own personal views"* — with `POST` to create (`name`, `scope`, `config`, `isDefault`), `PUT` to update, `DELETE` to remove, and `POST /api/lists/{id}/views/{viewId}` to **fork a shared view into a personal copy** (*"the escape hatch"* when someone else's shared view does not suit). Android has no concept of views at all, so a list always renders one way. - View switcher on the list detail screen listing shared and personal views. - Create / rename / delete / set-default, respecting `scope`. - Fork a shared view to a personal copy. - Confirm the `config` shape against the live API before modelling it — the spec types it loosely. **Tests:** view list parse; create/update/delete round-trips; fork produces a personal copy. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP1area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/51 | Sep 15, 2026, 10:58 AM | Sep 16, 2026, 8:25 PM |
| 50 | Lists: GitHub-backed lists (create from a repo, refresh, repo link and private-repo tag) | Part of #49 — Lists parity gaps: GitHub-backed lists, saved views, view modes and collaborative editing Web behaviour (`/help/lists` → *GitHub-backed List*): a list can be created from a GitHub repository's issues. Rows map to issues (title, body, labels, assignees, state) — adding a row creates an issue, editing updates it, deleting closes it. The schema is fixed; only the parent list is editable. **Refresh from GitHub** pulls the latest issues. Android has a `:feature:integrations` GitHub section (repos, issues, create/comment) but cannot create or operate a GitHub-backed list. - Repo picker on list creation, sending `githubRepo` / `githubSource`; requires GitHub linked with the **Issues** scope. - Link the repository under the list title, with the **Private repo** tag — the tag describes the *repository's* visibility on GitHub, not the list's, and warns that an invited collaborator may hit a sign-in or "not found" page. Visibility is re-read on each sync. - **Refresh from GitHub** action (`POST /api/lists/{id}/refresh` is already wired). - Row edits must map to the issue operations, and the schema editor must be locked to parent-only for these lists. - `GET /api/github/orgs` and `GET /api/github/repos/{owner}/{repo}/next-issue-number` are also unused and belong here. **Tests:** create with `githubRepo`; schema editor locked; refresh updates rows. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | closed | parityP1area:listsarea:integrations | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/50 | Sep 15, 2026, 10:58 AM | Sep 16, 2026, 8:52 PM |
| 49 | Lists parity gaps: GitHub-backed lists, saved views, view modes and collaborative editing | ## Where this stands `:feature:lists` covers list CRUD, the schema editor, rows, folders, watchers, connections, contributors, share links, shared-with-me and search. Four substantial web capabilities have no Android equivalent, and list creation drops several fields the API accepts. ## Verified gaps `CreateListRequest` sends `title, description, schema, isPublic`. `POST /api/lists` also accepts **`parentId`**, **`messageId`**, **`metadata`**, **`source`**, **`githubRepo`**, **`githubSource`** and **`initialRows`**. Unused endpoints: `GET|POST /api/lists/{id}/views`, `PUT|DELETE|POST /api/lists/{id}/views/{viewId}`, `POST /api/lists/{id}/data/versions`, `GET /api/lists/shared/{token}/data`. Reference: `/help/lists`, and the features page's *"view data as cards, a grid, or an ERD diagram"*. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | epicparityP1area:lists | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/49 | Sep 15, 2026, 10:58 AM | Sep 15, 2026, 10:58 AM |
| 48 | Push: retire or demote the polling worker once FCM is live | Part of #44 — Push notifications: real device push (FCM) to replace the WorkManager poll Once FCM delivers, the poll should stop being the primary path. - Keep it as a low-frequency reconciliation pass (it also repairs the unread count after missed messages), or remove it entirely — decide with measurements, not by assumption. - Make sure the two paths cannot both notify for the same item (the persisted last-seen marker is the natural coordination point). - Whatever is decided, the battery cost of the current interval should not survive unchanged. **Tests:** no duplicate notifications with both paths enabled. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | P2area:notifications | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/48 | Sep 15, 2026, 10:58 AM | Sep 15, 2026, 10:58 AM |
| 47 | Push: handle FCM messages — channels from notification preferences, deep links on tap | Part of #44 — Push notifications: real device push (FCM) to replace the WorkManager poll - A `FirebaseMessagingService` that raises notifications through the **existing** channel and deep-link plumbing built for the poll (`il_mentions` et al., grouped + capped, last-seen marker) — this issue should reuse that code, not duplicate it. - Route each notification to its category channel per the account's notification-preferences `push` settings. - Tap → the same deep-link destinations the poll notifications already use. - Handle foreground vs background delivery, and de-duplicate against anything the poll may have already shown during the transition. **Tests:** payload → channel mapping; tap → route; no duplicate notification when both paths see the same item. --- _Filed from the parity review of `feature/parity-wip` @ `14d7795` against interlinedlist.com (OpenAPI `/api/openapi.json`, 233 paths) and the public help centre._ | open | parityP1area:notifications | Empty | https://github.com/CompositeCode/interlinedlist-android/issues/47 | Sep 15, 2026, 10:58 AM | Sep 15, 2026, 10:58 AM |