Repository navigation
[Epic] Feature parity with interlinedlist.com — evaluation and tracking #44
Description
Feature parity with interlinedlist.com — tracking epic
Scope: everything the web app at https://interlinedlist.com does that the iOS client
does not, excluding billing/subscription management (never ships on iOS — App Store
Guideline 3.1.1) and the web-only surfaces listed under Out of scope below.
Evaluated: 2026-09-06, against backend ~/Codez/interlinedlist (Next.js, 228 API route
files / 296 verb+path pairs) and iOS origin/main @ 625a19b (121 Swift sources: 60 views,
19 services, 26 models; 104+ distinct verb+path call sites).
1 · Method
Four passes, cross-checked, backend source as ground truth:
- Endpoint diff — every
app/api/**/route.ts(verb + path + auth helper:
getCurrentUserOrSyncToken= Bearer-OK,getCurrentUser= session-only/mobile-blocked)
diffed mechanically against every"/api/…"string inAPIClient*.swift. - Surface diff — every web page (
app/**/page.tsx) and feature component
(components/{lists,documents,messages,messages-dm,organizations,settings,user,sharing, scheduled,notifications,follows,shared}) compared to the corresponding iOS view. - Dead-code sweep — every
APIClientfunction with zero call sites outside the
client itself. This is what the endpoint diff alone misses: an endpoint that is "consumed"
by a function no screen ever calls is a missing feature, not a covered one. It found
muteUser,trashDM,restoreDM,addOrganizationMember,joinOrganization,
organizations,refreshMessageMetadataand more. - Payload-key check — iOS request bodies vs. the fields the backend route actually
destructures. Found one silent data-loss bug (defaultVisibility→ ignored).
Prior analysis lives in work-consolidation.md; this
epic supersedes it for open work and corrects it in three places (see §5).
2 · Where iOS already is at parity ✅
Auth (email/password + OAuth: GitHub, LinkedIn, Twitter/X, Mastodon, Bluesky) · registration,
email verification, password reset, change email, sessions, delete account · feed with
compose (multi-image, video, tags, trending-tag strip, live link previews, scheduling,
cross-posting, post-as-organization), digs, replies/threads, edit, delete, search · lists
(CRUD, nesting via parentId, schema editor, rows, GitHub-backed lists with open/closed
filter, watchers, list connections, share links + email invites, lists shared with me,
public lists on profiles) · documents (CRUD, folders, markdown editor + toolbar, offline
sync with conflict merge, collaborators, share links + invites, templates, public reader) ·
DMs (inbox/sent/deleted folders, threads, send, image attachments, unread badge) ·
notifications (list, mark read, mark all read, delete, per-event channel preferences) ·
profiles (follow/unfollow, follow requests, followers/following, block, report, CSV exports:
messages/lists/list-data-rows/follows) · organizations (create, edit, delete, members, role
changes, post as org) · sharing (unified sharing screen for lists + documents) · AI writing
assistance (composer assistant, message series, article series, powered templates, powered
document — shipped in #43) · deep links (profile, /message/:id, /documents/:id, share
tokens) · push notifications.
3 · Out of scope (do not build)
- Billing / subscription / Stripe — Guideline 3.1.1. Subscriber-only features are hidden
for free users, never paywalled, never linked out, no price copy anywhere. - AI key entry / provider picker — AI is a subscriber entitlement on the app's own
server-side key. There are no per-user LLM keys. Gate onaiStatus().subscriberalone. - Dashboard + front-wall widget layouts, engagement stats, weather/transit/markets widgets,
admin console, blog + blog subscriptions,architecture-aggregates,/embed/*,/api-docs,
marketing pages (/features,/pricing,/products,/about). - Multi-account switching (
/api/auth/{accounts,switch,remove-account}) — session-only on
the backend; a Bearer client cannot use it. GET /api/users/lookup— unused by the web app too.- Realtime: there is no SSE/WebSocket anywhere in the product. Polling is the pattern.
4 · Workstreams
Nine independent lanes, sized so several can run concurrently. The index below is filled in
with the issue numbers.
| Lane | Theme | Primary files (ownership boundary) |
|---|---|---|
| W1 | Ship work already written | feat/create-from-and-github-depth branch |
| W2 | Moderation, privacy, account settings | SettingsView, EditProfileView, UserProfileView, FeedView menus, Models/User.swift |
| W3 | Direct messages | MessagesInboxView, DMThreadView, Models/DirectMessage.swift |
| W4 | Organizations | OrganizationsView, APIClient+OrganizationLinkedIn.swift |
| W5 | Documents | DocumentsView, APIClient+Documents*.swift |
| W6 | Lists | ListsView (ListDetailView), ListItemFormView |
| W7 | Deep links & permalinks | InterlinedListApp.swift, ILWebURL.swift |
| W8 | Papercuts | scattered — small, sequence them under one owner |
| W9 | Cross-device settings sync | new APIClient+AppSettings.swift |
5 · Corrections to work-consolidation.md
| It says | Correction |
|---|---|
| P6 — "batch reply counts … perf win for the feed" | Wrong. POST /api/messages/:id/reply-counts refreshes cross-post reply counts from Bluesky/Mastodon/LinkedIn/X for one message on its detail view (lib/crosspost/reply-counts.ts, 10-min cache). It is a feature gap, not a perf item. |
| "Documents shared with me — the web lacks it too. Parity holds." | Wrong. The web ships it: app/documents/layout.tsx calls getSharedDocuments(user.id) and renders SharedDocumentsList ("Shared with me"). It is server-rendered, which is why the endpoint diff missed it. Real gap, blocked on backend ask A11. |
"bare /lists/:id permalink — backend-limited (no owner in URL)" |
Stale. GET /api/lists/:id is Bearer-ready and authorizes by role (owner/manager/collaborator/watcher), so no owner username is needed. The same stale claim is repeated in a comment in InterlinedListApp.swift. |
6 · Working agreement (applies to every issue in this epic)
- Work in your own git worktree, with your own simulator UDID and your own
-derivedDataPath. Two worktrees testing on one UDID kill each other's runner and can
execute the other worktree's bundle (see the hazard note inCLAUDE.md). - New endpoints go in
APIClient+<Feature>.swift, never appended to theAPIClient.swift
class body. The HTTP seam inAPIClientTransport.swiftis deliberatelyinternalso that
per-feature extension files compile. - Pick the encoder per endpoint.
postCamel/putCamel/patchCamelfor the many
camelCase endpoints;post/put/patchfor snake_case. Check the neighbouring method
before adding one — and verify the key names the route actually destructures (see the
defaultVisibilitybug for what happens when they don't match). - Register every new
.swiftfile inproject.pbxproj(xcodeprojgem) — no synced groups. - SOLID/KISS house rules: no force-unwrap in production paths, no
DispatchQueue.main.async
(use@MainActor),#Previewin every view file,.accessibilityLabelon every
non-obvious control,privateinternals, comments only where the why is non-obvious. - A feature-endpoint 401 is not a logout — call
authState.handleUnauthorized(). - Verification is mandatory: build + full unit suite green (pinned UDID,
-parallel-testing-enabled NO,-skip-testing:InterlinedListTests/E2EReadOnlyTests)
before a PR is marked ready.
7 · Index
W1 · Ship work already written
- W1: Land Thread C — "Create from…" (G16) and GitHub label/assignee pickers (G18) #45 — Land Thread C: "Create from…" (G16) + GitHub label/assignee pickers (G18) — S, highest value/hour
Tier 0 · Broken shipped behaviour
- Bug: "Default to public" never persists — iOS sends defaultVisibility, the API reads defaultPubliclyVisible #46 — Bug: "Default to public" never persists (
defaultVisibilityvsdefaultPubliclyVisible) — XS - W7: Open the web canonical permalinks in-app (/user/:u/status/:id, list and document permalinks) #61 — Web permalinks don't open in-app;
/user/:u/status/:idopens the profile instead of the post — S–M (also W7)
W2 · Moderation, privacy, account settings
- Bug: "Default to public" never persists — iOS sends defaultVisibility, the API reads defaultPubliclyVisible #46 — the default-visibility bug (land first — it touches
updateUserSettings) - W2: Mute a user from the profile and post menus #47 — Mute a user from the profile and post menus — S
- W2: Private-account toggle in Settings (isPrivateAccount) #48 — Private-account toggle (
isPrivateAccount) — S - W2: View preferences — feed scope, page size, link previews, notification tray limit #49 — View preferences: feed scope, page size, link previews, tray limit — M
W3 · Direct messages
- W3: Trash and restore direct messages #50 — Trash and restore direct messages — S–M
- W3: Grouped DM inbox via GET /api/dm/conversations #51 — Grouped DM inbox via
GET /api/dm/conversations— M
W4 · Organizations
- W4: Add members to an organization; browse and join public organizations #52 — Add members; browse and join public organizations — M
- W4: Organization LinkedIn management (G19) #53 — Organization LinkedIn management (G19) — M (verify the backend flag first)
W5 · Documents
- W5: "Shared with me" documents (needs a backend route — A11) #54 — "Shared with me" documents — M, blocked on A11 (Backend asks blocking iOS parity — A8, A9, A11, A12 (+ A10 docs) #66)
- W5: Rename a document folder #55 — Rename a document folder — XS
- W5: Live document presence — "N people viewing" (G11) #56 — Live document presence, "N people viewing" (G11) — M
- W5: Seed the default document templates (P8) #57 — Seed the default document templates (P8) — XS
W6 · Lists
- W6: List contributors (P7) #58 — List contributors (P7) — S
- W6: Schedule a message from a list row #59 — Schedule a message from a list row — S
- W6: Sort, filter and search rows inside a list #60 — Sort, filter and search rows inside a list — M
W7 · Deep links
- W7: Open the web canonical permalinks in-app (/user/:u/status/:id, list and document permalinks) #61 — Open the web canonical permalinks in-app — S–M
W8 · Papercuts
- W8: Cross-post reply counts on a message detail (P6, re-scoped) #62 — Cross-post reply counts on a message detail (P6, re-scoped) — S
- W8: Identity status for Bluesky, Mastodon and GitHub (P5) #63 — Identity status for Bluesky, Mastodon, GitHub (P5) — S
- W8: Sweep APIClient for endpoints no screen calls #64 — Sweep
APIClientfor endpoints no screen calls — S, run last
W9 · Infrastructure (not web parity)
- W9: Cross-device app-settings sync (G21) #65 — Cross-device app-settings sync (G21) — M
Backend
- Backend asks blocking iOS parity — A8, A9, A11, A12 (+ A10 docs) #66 — Backend asks: A8 (Bearer on profile-wall messages), A9 (Bearer on invite acceptance), A11 (documents shared-with-me route), A12 (docs), A10 (AI doc fix)
Suggested order
- W1: Land Thread C — "Create from…" (G16) and GitHub label/assignee pickers (G18) #45 (land what's built) and Bug: "Default to public" never persists — iOS sends defaultVisibility, the API reads defaultPubliclyVisible #46 (data-loss bug) in parallel — both are small and both are
already-broken-or-already-written. - Backend asks blocking iOS parity — A8, A9, A11, A12 (+ A10 docs) #66 filed against the backend immediately, because W5: "Shared with me" documents (needs a backend route — A11) #54 cannot start without A11 and
W7: Open the web canonical permalinks in-app (/user/:u/status/:id, list and document permalinks) #61's dig-state fix wants A8. - Then the lanes in parallel: W2 (W2: Mute a user from the profile and post menus #47→W2: Private-account toggle in Settings (isPrivateAccount) #48→W2: View preferences — feed scope, page size, link previews, notification tray limit #49, one owner, shared files), W3, W4, W5, W6, W7.
- W8: Sweep APIClient for endpoints no screen calls #64 last — the sweep is only meaningful once the wiring issues have landed.
Activity
docs: mark G15/G16/G18 shipped and point open work at epic #44
Adron commentedon Sep 15, 2026
Batch update — 2026-09-15
Worked the open issues in dependency order. 11 PRs open, one issue closed as blocked, one
confirmed still blocked, and 9 new issues filed from what the work turned up.
PRs by issue
mainupdateUserSettingsmainmainmainmainmainmainUsermainMerge order for the stack: #75 → #81 → #82 → #89. The other seven are independent of each other
and of the stack.
Closed / still blocked
{"orgScopesEnabled":false}, so the org LinkedIn screen would ship with a connect action thatcannot complete. Reopen when the flag flips.
shared-with-meroute, no?scope=sharedhandling,and
getSharedDocumentsstill has exactly one server-side caller.getCurrentUserOrSyncToken. A11, A12 and A10 remain.New issues filed
Found by the payload-key sweep #46 required:
PATCH /api/messages/{id}is the reschedule route andno route anywhere accepts a content edit. The web has no edit surface either.
providerin the body, the routereads the query string.
change-email/requestaccepts apasswordand never verifies it.Found while reviewing light/dark/system theme support:
Split out of #64 rather than wiring untested paths inside a hygiene sweep:
Note on repo state
My local
devwas 36 commits behindorigin/main, which made #45/#47/#50/#52/#55/#58/#61/#63 lookunimplemented on a first pass. They are all correctly in
main— no roll-up hazard this time. Allwork above is cut from
origin/main.Adron commentedon Sep 16, 2026
Final batch state — 2026-09-16
18 PRs open. Every issue from the working list is either PR'd and closed, or commented with the
specific decision blocking it.
PRs, by merge chain
Awaiting a decision, not code
on either client. The cross-post question (re-push / remote-edit / go stale) probably decides the
design, and there is no
editedAtcolumn.password hash.
getSharedDocumentsalready returns the shape), with one openquestion about pagination convention. Half of A10 appears already done.
/identities/verifyreturns 200 "success" for unrecognised providers without checking.AccentColoris configured correctly but not honoured;.tint()is an unexplainedworkaround, and it does not cross into sheets.
What the work turned up beyond the original list
Nine new issues, most from bugs that were green in CI:
refreshMessageMetadatadecoded a shape the route never sends, returning[]forever.Its test passed because the fixture was invented to match the wrong assumption.
FeedViewrenders from a private working copy; in-place updates were invisible anddeletes never reached the persisted cache.
The recurring shape is worth naming: four separate defects were invisible because the test asserted
a contract nobody had checked against the route.
/ios-reviewnow carries the payload-key check anda zero-call-site sweep, but fixtures written from Swift types rather than route source remain the gap.
Caveat on all of it
mainhas none of this. Fourteen-plus issues are closed with their code entirely in open PRs, andthe stacks want merging in order. Known conflict surfaces:
project.pbxprojandAPIClientProfileTests.swift(#75 and #97 both add tests near the same point).Nothing in these PRs has been verified against a live account — every PR states its own unverified
criteria explicitly.