Skip to content

interlinedlist-windows-app

126 rows · Updated
interlinedlist-windows-app
Issue #TitleBodyStateLabelsAssigneesLinkCreatedUpdated
179Lists: `CreateListAsync` sends 2 of the 11 fields `POST /api/lists` accepts — `initialRows` would remove an N+1## The gap `POST /api/lists` accepts **eleven** fields. `CreateListAsync` sends **two** (`title`, `isPublic`): ```csharp public async Task CreateListAsync(string title, bool isPublic, …) => … new { title, isPublic } … ``` Found by #73's probing (PR #175), which needed `source` + `githubRepo` and discovered the rest. The spec's request properties include `title`, `description`, `isPublic`, `parentId`, `folderId`, `schema`, `source`, `githubRepo`, `githubSource`, `initialRows`, `messageId`. ## Why `initialRows` in particular matters Three features currently create a list and then add rows one at a time: - **Powered Templates** (#174) — an AI `powered_template` artifact returns a full `dsl` *and* `rows`; today that's 1 create + N row POSTs. - **Materialize** (#136) — `listConfig.includeData` seeds rows from a source, so the server already does this server-side; but a client building a list from data hits the same N+1. - Any import path. `initialRows` collapses that to one request. With no transaction across N+1 requests, a partial failure today leaves a list with some of its rows and no clean way to report that. ## The other fields already have consumers waiting | Field | Wanted by | |---|---| | `description` | #18 (schema builder already collects one) | | `parentId` | #64 parent/child hierarchy + breadcrumbs | | `folderId` | #63 list folders | | `schema` | #17/#18 — **already used** via a separate overload on PR #143 | | `source` + `githubRepo` | #73 GitHub-backed lists — **used** on PR #175 | | `messageId` | provenance when a list is created from a message (Materialize) | So this isn't speculative: most of these are being added piecemeal by whichever issue needs them. Consolidating `CreateListAsync` into one options object would stop that. ## Verified behaviour worth keeping - `source: "github"` **without** `githubRepo` → `400 "githubRepo is required for GitHub-backed lists (format: owner/repo)"`, and **creates nothing** (verified — no orphan list). - Create returns `{message, data:{…}}`, not `{list:{…}}`. ## Acceptance criteria - [ ] `CreateListAsync` takes an options object covering the accepted fields rather than growing positional parameters - [ ] `initialRows` supported, and the callers above collapsed from N+1 to one request - [ ] `initialRows` response shape verified live on a throwaway list before being typed strictly - [ ] Existing two-argument callers keep working (overload or optional parameters) ## Note Filed from a review finding, not the parity audit. Low urgency — the current path works, it's just chatty and is being extended one field at a time by four different issues.openenhancementparityP2area:listsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/179Sep 16, 2026, 10:18 PMSep 16, 2026, 10:18 PM
171Settings: "Save profile" has never worked — it sends `theme: null` and gets HTTP 500## The Save Profile button returns 500 every time Found while reviewing #46's work (fixed there — filed for visibility, since a shipped-broken button deserves a tracked identity). `SettingsViewModel` calls: ```csharp await _session.Api.UpdateProfileAsync(DisplayName, Bio, IsPrivateAccount); ``` `UpdateProfileAsync` has a 4th parameter defaulting to `null`: ```csharp public Task UpdateProfileAsync(string? displayName, string? bio, bool? isPrivateAccount = null, string? theme = null, …) => SendVoidAsync(HttpMethod.Patch, "api/user/update", new { displayName, bio, isPrivateAccount, theme }, ct); ``` `JsonSerializerDefaults.Web` **does not drop nulls**, so the body always carries `"theme": null` — and the server rejects an explicit null. ## Reproduced live (2026-09-16) ``` A) exactly what Save profile sends today: {"displayName":"…","bio":"…","isPrivateAccount":false,"theme":null} -> HTTP 500 {"error":"Internal server error","code":"internal_error"} B) identical body with the null key removed: {"displayName":"…","bio":"…","isPrivateAccount":false} -> HTTP 200 {"message":"User updated successfully","user":{…}} ``` So editing your display name, bio or private-account flag from this app has **never** worked. The `catch (InterlinedApiException)` shows the message, so it isn't silent — it just always fails. ## Root cause worth generalising Any writer that builds a PATCH body from an **anonymous object with optional parameters** will ship nulls for the ones the caller omitted. On this API an explicit `null` is a **500, not a no-op**, so every such writer is a latent 500. PATCH bodies must be built **sparsely** — only the fields actually being changed. ## Fix Already implemented in **#167** (`issue-46-honor-preferences`): both profile writers build a sparse dictionary instead of an anonymous object. ## Acceptance criteria - [x] `UpdateProfileAsync` omits unset fields rather than sending null — #167 - [ ] Any other `PATCH`/`PUT` writer built from an anonymous object with optional params audited for the same defect - [ ] Save profile verified working end to end against a live account ## Related live findings from the same work - The OpenAPI **request** schema for `/api/user/update` types every field as `"string"`. That's wrong — native JSON numbers and booleans are correct (the *response* schema uses native types). - `viewingPreference` is enum-validated; the server names the values in its 400: **`my_messages`, `all_messages`, `followers_only`, `following_only`**. - `theme` is **not** validated — it accepted and stored `"zzz_bogus"`. - Live ranges from 400s: `maxMessageLength` 1–10000, `messagesPerPage` 10–30, `notificationTrayLimit` 10–40. - The PATCH 200's `user` is **narrower** than `GET /api/user` (no `accountStatus`/`cleared`/`pendingEmail`/`isAdministrator`) — deserializing it into `CurrentUser` would blank those fields, so callers must re-fetch.closedbugP0area:settingsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/171Sep 16, 2026, 9:47 PMSep 16, 2026, 9:47 PM
165Settings: add "revoke all other sessions" — the test account holds 1,276 standing sync-tokens## Why this is worth a control, not just a list Settings already lists API sessions and revokes them **one at a time**. That's unusable at the scale this actually reaches. The `.env` test account right now: ``` total sessions: 1276 1251 labelled "CLI" 24 created today (probes, agents, harnesses) 1 isCurrent ``` `CLAUDE.md` treats `session.dat` as a **standing credential**, and every one of those 1,276 is a live bearer token that never expires. There is no bulk revoke, so cleaning up means 1,275 individual clicks. ## Constraints found while probing (#114, #126) - **`POST /api/auth/logout` does nothing for a bearer client.** It returns `200 {"message":"Logged out successfully","remaining":0}` even with **no `Authorization` header** (`x-auth-type: "none"`, `security: []`); the token still works afterward. `remaining` counts *cookie* sessions. So it is not a bulk-revoke. - **`DELETE /api/user/sessions/{id}` is the only real revoke** (`204`, after which the token `401`s and its row disappears). - **A session cannot revoke itself** — `400 {"error":"cannot_revoke_current_session"}`. Exactly one row reports `isCurrent: true`. So the client has to loop `DELETE` over every non-current id. That's the feature. ## The label is the useful part `deviceLabel` is what makes this tractable, and the API assigns **`CLI`** when `POST /api/auth/sync-token` is called without one. That's why 1,251 rows are indistinguishable from each other and from ad-hoc tooling — I could only safely clean up the 14 tokens that carried a distinctive label, and had to leave 10 `CLI`-labelled ones I'd probably created myself alone rather than risk revoking real `il-sync` use. Two things follow: 1. ~~This app should send a meaningful `deviceLabel`~~ — **it already does** (`Environment.MachineName`), so its own tokens are identifiable. The unlabelled pile is not this client's doing. 2. The UI should **group by `deviceLabel`** and show `createdAt`/`lastUsedAt`, so "revoke everything that isn't this machine" is a decision the user can actually make. ## Acceptance criteria - [ ] "Revoke all other sessions" in Settings, looping `DELETE /api/user/sessions/{id}` over every row where `isCurrent` is false - [ ] Skips the current session (it would `400`) and reports progress rather than freezing — 1,275 sequential deletes is minutes of work - [ ] Partial failure is reported and doesn't abort the rest - [ ] Sessions grouped by `deviceLabel`, with `lastUsedAt` shown, so a stale pile is legible - [ ] Per-label revoke ("revoke all 1,251 `CLI` sessions") as the middle ground between one and all - [ ] Confirm in place — no blocking native dialog (`CLAUDE.md` dispatcher rule) - [x] ~~`RequestSyncTokenAsync` sends a real `deviceLabel`~~ — **already done**: `SessionService` passes `Environment.MachineName`. The 1,251 `CLI` rows are from the `il-sync` CLI and bare API calls, not this app. ## Note Filed because I found the need while cleaning up after my own probing, not from the parity audit. The API-session list itself already exists — this is the missing control on top of it.closedenhancementP2area:settingsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/165Sep 16, 2026, 9:24 PMSep 16, 2026, 9:29 PM
160People: "Mutual connections" is silently dead — `/mutual` returns counts, not a user array## The feature renders nothing, and fails silently `the-gaps.md` records "Mutual connections on a profile (People) — chips that open that user" as shipped in session 6. **It has never displayed anything.** Found by the contract-test suite (#126 / PR #159) and confirmed independently. ## Why `GET /api/follow/{userId}/mutual` returns **counts**, not users: ``` GET /api/follow/c65092fa-…/mutual -> {"mutualFollowers":0,"mutualFollowing":0} GET /api/follow/15e3d575-…/mutual -> {"mutualFollowers":1,"mutualFollowing":1} GET /api/follow/15e3d575-…/mutual?otherUserId=c65092fa-… -> {"mutualFollowers":1,"mutualFollowing":1} ``` But the client asks for an array under a `mutual` key: ```csharp public Task<List<FollowUser>> GetMutualAsync(string userId, …) => GetUserArrayAsync($"api/follow/{userId}/mutual", "mutual", ct); ``` …and `GetUserArrayAsync` is deliberately tolerant: ```csharp return json.TryGetProperty(property, out var arr) && arr.ValueKind == JsonValueKind.Array ? arr.Deserialize<List<FollowUser>>(JsonOptions) ?? new() : new(); // <-- silently empty ``` So it returns an empty list, `HasMutuals` is false, the chips section collapses, and **nothing indicates a problem**. That tolerance is right for a genuinely optional array — it's exactly what hides this. ## The API does not support the built feature I checked for an alternative. The only mutual endpoint is this one, and both its parameter forms (`userId` alone, and `userId` + the spec-documented `otherUserId`) return the same counts object. **There is no endpoint that lists mutual users**, so clickable chips cannot be built as designed. ## Fix - Model the real response (`{mutualFollowers, mutualFollowing}`) and change `GetMutualAsync`'s return type — its current signature is a promise the API can't keep. - Replace the chips with the counts, which is what's actually available. - Record on the method that no user-list endpoint exists, so nobody reinstates the chips. ## Wider point worth acting on This is the second bug of the same shape in two days: a tolerant "absent array → empty list" read turning an API mismatch into a silently missing feature. The other was #144 (`ListDataRow.ListId`), which at least threw. Worth auditing the other `GetUserArrayAsync` call sites — `followers`, `following`, `requests` — against live payloads; `followers` and `following` are confirmed correct (#95), `requests` is unverified. ## Acceptance criteria - [ ] `GetMutualAsync` returns the real shape; no caller can expect a user list - [ ] Profile shows mutual counts - [ ] Verified against a live payload, not by eye - [ ] The remaining `GetUserArrayAsync` key (`requests`) checked against live dataclosedbugparityP1area:peopleEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/160Sep 16, 2026, 8:59 PMSep 16, 2026, 9:01 PM
144Lists: viewing any list that has rows throws — `ListDataRow.ListId` is `required` but the API doesn't send it## Severity **Any list with at least one row fails to load.** `GET /api/lists/{id}/data` rows do not contain `listId`, but `Models/ListDataRow.ListId` is declared `required`, so `System.Text.Json` throws before the rows reach the view. Found while verifying a side-note from #17's service work. Not previously in the backlog. ## Proof The live row shape (captured 2026-09-16 from a throwaway list, since the test account's only list has zero rows): ```json {"rows":[{"id":"679fbb8b-…", "rowData":{"probe":"value"}, "version":1, "createdAt":"2026-09-16T20:12:48.970Z", "updatedAt":"2026-09-16T20:12:48.970Z", "createdByUser":{"id":"…","username":"messenger","displayName":"…","avatar":"…"}, "lastEditedByUser":null}], "pagination":{"total":1,"limit":20,"offset":0,"hasMore":false}} ``` Row keys are exactly: `createdAt, createdByUser, id, lastEditedByUser, rowData, updatedAt, version`. **No `listId`.** Deserializing that with the current model in a `net10.0` harness: ``` THROWS — this is the bug: JsonException: JSON deserialization for type 'InterlinedList.Models.ListDataRow' was missing required properties including: 'listId'. ``` ## Why it wasn't caught The test account's single list has **no rows**, so every manual verification of the Lists view exercised the empty path. `the-gaps.md` records list row edit/delete as shipped in session 2 — so either the API dropped `listId` from row payloads since then, or it was never present and the feature was verified against a create response (which *does* include `listId`) rather than a read. Note the asymmetry that makes this easy to miss: `POST /api/lists/{id}/data` **does** return `listId` in its `{message, data:{…}}` envelope. Only the read path omits it. ## Fix - `ListDataRow.ListId` must not be `required` — the caller already knows the list id it asked for. - While in there, model the fields the payload actually carries and the app currently drops: `version` (needed for optimistic concurrency on row edits), `createdByUser`, `lastEditedByUser`, `rowNumber`. ## Acceptance criteria - [ ] A list with rows loads without throwing. - [ ] Verified by deserializing a captured live payload, not by eye. - [ ] `version`, `createdByUser`, `lastEditedByUser` modelled. - [ ] Ties into #21 (typed row editor) and #70 (server-side row paging), both of which read rows.closedbugparityP0area:listsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/144Sep 16, 2026, 8:13 PMSep 16, 2026, 9:22 PM
128Platform: push notifications — decide whether a Windows client can use `/api/push/*`## Endpoints - `POST /api/push/register` — *"register a device token for push notifications"* - `DELETE /api/push/unregister` — *"remove a device token on logout"* `the-gaps.md` excludes these as *"mobile"*, and `/help/settings` describes push in terms of the iOS app. But `GET /api/user/notification-preferences` returns a **`push` channel on every event** (live-verified), which the app currently renders as a toggle it cannot honor. ## Decision needed Whether the server's push transport can address a Windows client at all. Two paths: 1. If the token format is APNs/FCM-specific, Windows (WNS) cannot participate → mark it mobile-only and stop rendering a `push` toggle the app cannot deliver on. 2. If the transport is generic, register a WNS channel and deliver real toast notifications. ## Acceptance criteria - [ ] Established (from the spec or by probing `POST /api/push/register`) whether a non-APNs/FCM token is accepted. - [ ] Finding recorded in `CLAUDE.md`. - [ ] If mobile-only: the notification-preferences UI stops offering a `push` toggle it cannot honor, or labels it as applying to the mobile app. - [ ] If viable: WNS channel registration on sign-in, `DELETE /api/push/unregister` on sign-out. closedparityP3area:platformEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/128Sep 15, 2026, 5:30 PMSep 16, 2026, 7:53 PM
127Platform: audit the `Message`, `ListSummary` and `CurrentUser` wire types against live payloads## Problem Three core models have drifted materially behind the API. Each gap is called out in its own feature epic, but the drift itself is one piece of work and is worth doing in one pass. ### `Models/Message.cs` — missing 9 live fields Live `GET /api/messages` returns `linkMetadata`, `crossPostUrls`, `scheduledCrossPostConfig`, `replyCounts`, `repliesCheckedAt`, `pushedMessageId`, `pushedMessage`, `viewerHasMuted`, `_count` — none are modelled. ### `Models/ListSummary.cs` — missing 8 live fields Live payload adds `userId`, `messageId`, `parentId`, `folderId`, `metadata`, `source`, `githubRepo`, `githubRepoPrivate`. ### `Models/CurrentUser.cs` — missing 14 live fields `accountStatus`, `cleared`, `messagesPerPage`, `viewingPreference`, `showPreviews`, `notificationTrayLimit`, `showAdvancedPostSettings`, `latitude`, `longitude`, `pendingEmail`, `dashboardLayout`, `frontWallLayout`, `githubDefaultRepo`, `stripeCustomerId`. ### `Models/NotificationPreference.cs` — missing the `email` channel See the dedicated bug issue in the Settings epic. ## Acceptance criteria - [ ] All four models reconciled against live payloads captured from the test account. - [ ] Opaque fields (`metadata`, `dashboardLayout`, `frontWallLayout`, app-settings `settings`) kept as raw JSON so they round-trip. - [ ] Every field is either consumed by a feature or explicitly noted as modelled-for-round-trip — no silent dead weight. - [ ] A short note in `CLAUDE.md` on how to re-capture payloads when the API moves. closedparityP1area:platformEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/127Sep 15, 2026, 5:30 PMSep 16, 2026, 7:41 PM
126Platform: a live contract-test suite for the endpoints the app depends on## Why Every stale constraint this audit found existed because a one-off manual probe was written into a doc and never re-run. The API moved (244 → 304 operations) and nothing told the repo. `.env` already anticipates this: it documents `INTERLINEDLIST_EMAIL` / `INTERLINEDLIST_PASSWORD` / `INTERLINEDLIST_API_BASE_URL` for *"the env-gated `contract-tests.yml` workflow"* and even gives the `gh secret set` commands — **but no such workflow exists in `.github/workflows/`**, and the comment refers to `InterlinedKitTests/ContractTests.swift`, a path from a different (Apple) repo. ## Scope A test project that signs in with the `.env` account and asserts, for each endpoint the app calls: the status code, the auth model (bearer accepted), and the presence of the fields the app's models require. ## Design constraints - **Read-only by default.** Writes only against throwaway objects the suite creates and deletes, never shared test infrastructure — the caution already recorded in `CLAUDE.md` about `CreateOrganizationAsync` and `RemoveIdentityAsync`. - **Env-gated:** with no credentials the suite skips and the build still passes (the pattern `.env` describes). - Must not run on every PR — the API is a live shared system. ## Acceptance criteria - [ ] A contract-test project alongside `InterlinedList.Sync.Core.Tests` (which already runs on macOS/CI). - [ ] A real `.github/workflows/contract-tests.yml`, env-gated, `workflow_dispatch` + scheduled — not on every push. - [ ] Coverage of every endpoint in `InterlinedApiClient*.cs`, asserting auth model and required fields. - [ ] A drift report naming endpoints whose shape changed. - [ ] `.env`'s comments corrected to point at this repo's actual paths. closedparityP1area:platformEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/126Sep 15, 2026, 5:30 PMSep 16, 2026, 8:47 PM
125Platform: shared client plumbing — cursor paging, limits cache, error codes## Why Several epics need the same three pieces. Building them once keeps every domain partial consistent — the same rationale as the Phase-0 work in `the-gaps.md`. ## 1. Cursor pagination helper `GET /api/messages`, `GET /api/dm/conversations` and others return `nextCursor`. The spec is explicit: *"Pass back the `nextCursor` from the previous response verbatim — never construct, parse, or modify one."* - [ ] `Models/Pagination.cs` gains `NextCursor`. - [ ] A shared paged-fetch helper that carries an opaque cursor, with `offset` as the fallback only where no cursor is offered. ## 2. `GET /api/limits` cache - [ ] Fetched once per session and exposed on a shared service, so composer counters and file pickers validate against real server limits (image `maxBytes`/`maxPixels`/`acceptedFormats`, video `maxBytes`/`acceptedFormats`, `message.maxContentLength`). ## 3. Machine-readable error codes Every error body carries a stable `code` (`not_found`, `bad_request`, `unauthorized`, `subscription_required`, `quota_exceeded`, `rate_limited`, `version_conflict`, `payload_too_large`, `invalid_input`, `invalid_ai_output`, `no_provider_configured`, `provider_error`, ...). - [ ] `InterlinedApiException` carries `Code` alongside the status and message. - [ ] `429` responses expose `Retry-After` and the `RateLimit-*` headers to callers. - [ ] A shared helper turns `402`/`403 subscription_required` into one reusable "upgrade needed" state, so every gated feature gets the same treatment instead of each inventing one. ## Acceptance criteria - [ ] All three land in `Services/InterlinedApiClient.cs` (or a small shared file) with no behaviour change to existing callers. - [ ] At least one existing caller is migrated to each, proving the shape. closedparityP0area:platformEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/125Sep 15, 2026, 5:29 PMSep 16, 2026, 7:36 PM
124Platform: correct the stale constraints in `CLAUDE.md` and refresh `the-gaps.md`## Why this is first `CLAUDE.md`'s "load-bearing constraints" section is treated as authoritative by anyone (human or agent) working in this repo. Six of its claims are now wrong, and each one currently reads as a reason **not** to build something that is in fact buildable. ## Corrections to make 1. **`GET /api/user/engagement`** — documented as `401` with bearer; live `200`. 2. **`GET`/`PUT /api/user/dashboard-layout`** — documented as `401` with bearer; live `200`. Same for `/api/user/front-wall-layout`. 3. **List schema DSL** — documented as *"only partially reverse engineered ... not implemented"*; now fully published at `/help/api/lists-dsl` (12 field types, validation rules, conditional visibility, both `PUT` body shapes). 4. **Materialize** — documented as *"a single opaque `source` string"*; now fully published at `/help/api/create-from` (5 source kinds, 4 targets, full config objects). 5. **GitHub** — documented as *"every `/api/github/*` call returns 'GitHub account not linked'"*; `repos` and `orgs` return `200 []`. 6. **LinkedIn per-page targeting** — listed as blocked; `GET /api/linkedin/targets` returns `200`. Also add the **new domains** the file does not mention at all: `/api/ai/*`, `/api/tags/*`, `/api/user/app-settings/*`, list saved-views, list/document email invites, `/api/lists/{id}/data/versions`, `/api/limits`, `/api/link-metadata`, the transit widget. ## Also worth recording - `GET /api/messages` now documents **cursor** pagination (`cursor` / `nextCursor`); `offset` still works but is no longer a documented parameter. - The confirmed-cookie-only list: `/api/auth/accounts`, `/api/auth/switch`, `/api/auth/remove-account`, `/api/auth/send-verification-email`, `/api/stripe/*`, `/api/architecture-aggregates/*`, `/api/admin/*`. ## Acceptance criteria - [ ] All six claims corrected, each with its probe date. - [ ] New domains listed in the API-integration section. - [ ] `the-gaps.md` re-baselined against the current spec (its "effective feature parity reached" conclusion from 2026-08-01 no longer holds — the product moved). - [ ] The pattern noted explicitly: **the spec's declared `security` is not authoritative — probe each endpoint** (`engagement` and `dashboard-layout` declare bearer-or-cookie and previously behaved as cookie-only; the reverse is now true). closeddocumentationparityP0area:platformEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/124Sep 15, 2026, 5:29 PMSep 16, 2026, 7:31 PM
123Epic: Platform work this parity effort depends on## Summary Cross-cutting work that several feature epics need, plus documentation corrections this audit turned up. **Do the first two sub-issues before starting the feature epics** — the stale constraints in `CLAUDE.md` are actively misleading, and several epics need the same client plumbing. ## What this audit found (live probes, 2026-09-15) The app implements **115 of 259** in-scope API operations. Six documented blockers in `CLAUDE.md` / `the-gaps.md` are **stale**: | Claim in the repo | Reality (2026-09-15) | |---|---| | `GET /api/user/engagement` is 401 with bearer | **200** | | `GET`/`PUT /api/user/dashboard-layout` is 401 with bearer | **200** (and `front-wall-layout` too) | | List schema DSL *"only partially reverse engineered"* | **fully documented** at `/help/api/lists-dsl` | | Materialize is *"a single opaque `source` string"* | **fully documented** at `/help/api/create-from` | | Every `/api/github/*` call returns "GitHub account not linked" | `repos`/`orgs` return **200 `[]`** | | LinkedIn per-page targeting blocked by the auth model | `GET /api/linkedin/targets` → **200** | Genuinely still cookie-only: `/api/auth/accounts`, `/api/auth/switch`, `/api/auth/remove-account`, `/api/auth/send-verification-email`, `/api/stripe/*`, `/api/architecture-aggregates/*`, all of `/api/admin/*`. The API has also grown since `the-gaps.md` was written: **189 paths / 244 operations → 233 paths / 304 operations**, adding the AI, Tags and Application-Settings domains and list saved-views, invites and grid-presence. ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. closedepicparityP0area:platformEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/123Sep 15, 2026, 5:29 PMSep 24, 2026, 2:06 AM
122Blog: subscribe and unsubscribe from the newsletter## Endpoints - `POST /api/blog/subscribe` — `{email}`, double opt-in (a confirmation email follows). - `GET /api/blog/subscribe/confirm` — the public confirmation link; likely belongs in the browser, not the app. - `GET`/`POST /api/blog/unsubscribe` — the `POST` form is the RFC-8058 one-click target. ## Acceptance criteria - [ ] `SubscribeToBlogAsync(email)` and the unsubscribe counterpart; shapes verified live (use a throwaway address — do **not** subscribe the test account's real inbox). - [ ] A subscribe control in Settings, pre-filled with the account email, stating that a confirmation email follows. - [ ] Unsubscribe available in the same place. - [ ] Confirmation links open in the OS browser. closedparityP3area:blogEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/122Sep 15, 2026, 5:29 PMSep 16, 2026, 9:50 PM
121Blog: decide between an in-app reader and a browser handoff## Decision needed before building The four public `/api/blog/*` endpoints are **subscription management only** — subscribe, confirm, unsubscribe. There is no public *post-listing* or *post-content* endpoint outside `/api/admin/blog*`, which is admin-only and cookie-authed. So an in-app blog **reader** has no API to read from. Two honest options: 1. **Browser handoff** (recommended) — a "Blog" entry that opens `https://interlinedlist.com/blog` in the OS browser, exactly like the existing OAuth and "Manage account on the web" handoffs. 2. **Wait for a public read endpoint** and revisit. ## Acceptance criteria - [ ] Confirm there is no public blog-read endpoint (re-check the live spec at build time). - [ ] Implement the browser handoff, or record the decision to defer. - [ ] Do not scrape the website's HTML as a substitute for an API. closedparityP3area:blogEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/121Sep 15, 2026, 5:29 PMSep 16, 2026, 9:50 PM
120Epic: Blog reading and subscription## Summary **Blog** is a documented product feature with its own help page (`/help/blog`) and a nav entry on the website. The app has no blog surface. ## Endpoints (non-admin) | Method | Path | Purpose | |---|---|---| | `POST` | `/api/blog/subscribe` | `{email}` — double opt-in | | `GET` | `/api/blog/subscribe/confirm` | public confirmation link from the email | | `GET` | `/api/blog/unsubscribe` | human-facing unsubscribe link | | `POST` | `/api/blog/unsubscribe` | RFC-8058 one-click unsubscribe target | Authoring lives under `/api/admin/blog*` and is **out of scope** (admin-only). ## Priority note Lowest priority in this parity effort: it is a read-and-subscribe surface that the OS browser already serves well. Filed because the user asked for every feature, with the explicit recommendation to consider the browser-handoff option below. ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. closedepicparityP3area:blogEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/120Sep 15, 2026, 5:29 PMSep 24, 2026, 2:06 AM
119Widgets: weather and location from the profile coordinates## Live verification (2026-09-15) ``` GET /api/weather -> 400 {"error":"Latitude and longitude are required","code":"bad_request"} GET /api/location -> 400 {"error":"Latitude and longitude are required","code":"bad_request"} ``` Both are bearer-reachable and simply need coordinates. ## Source of coordinates `GET /api/user` returns `latitude`/`longitude` — the profile location (see the Settings epic). The app must **not** attempt OS geolocation; the web's browser-permission flow has no desktop equivalent, and the documented fallback is a stored profile location. ## Documented behaviour (`/help/settings`) A Location widget (city, state, timezone) and a Weather widget (current conditions, high/low). Weather comes from the U.S. National Weather Service (`api.weather.gov`) and is **cached server-side for 30 minutes** — so the client should not poll aggressively. ## Acceptance criteria - [ ] Both service methods take lat/long from `CurrentUser`. - [ ] With no profile location set, the widgets prompt to set one in Settings instead of showing an error. - [ ] Refresh no more often than the 30-minute server cache warrants. - [ ] Depends on the profile-location work in the Settings epic. closedparityP3area:widgetsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/119Sep 15, 2026, 5:29 PMSep 16, 2026, 8:41 PM
118Widgets: news, markets, transit and bike-share## Endpoints | Path | Content | |---|---| | `GET /api/widgets/news` | top Hacker News headlines — **live-verified `200`** with `{items:[{title,url,points}]}` | | `GET /api/widgets/markets` | crypto watchlist quotes | | `GET /api/widgets/transit` | normalized transit info | | `GET /api/widgets/transit/stops` | nearby stops (needs location) | | `GET /api/widgets/bike-share` | bike-share availability | All bearer-reachable. ## Acceptance criteria - [ ] Service methods + models for all five. - [ ] Widgets render in the existing right rail, each collapsible and individually refreshable. - [ ] Location-dependent widgets (transit stops, bike-share) explain themselves when no profile location is set rather than erroring. - [ ] A widget failure degrades to a quiet placeholder — one flaky upstream must never break the shell. - [ ] Reasonable caching so the rail does not hammer the endpoints on every nav switch. closedparityP3area:widgetsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/118Sep 15, 2026, 5:29 PMSep 16, 2026, 8:36 PM
117Dashboard: read and persist dashboard / front-wall layout## Endpoints (bearer-verified 2026-09-15) - `GET`/`PUT /api/user/dashboard-layout` → `200 {"layout":null}` when unset - `GET`/`PUT /api/user/front-wall-layout` → `200 {"layout":null}` - Both also arrive inline on `GET /api/user` as `dashboardLayout` / `frontWallLayout` ## Acceptance criteria - [ ] Service methods for both, with the layout kept as **raw JSON** — it is a web-oriented structure and must round-trip untouched. - [ ] `CurrentUser` gains `DashboardLayout` and `FrontWallLayout`. - [ ] A `null` layout is a valid "not configured" state, not an error. - [ ] The app does **not** invent its own layout schema; if it renders a dashboard it interprets the server's, and writes back only what it understands. - [ ] `CLAUDE.md` corrected to remove the 401 claim. openparityP3area:widgetsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/117Sep 15, 2026, 5:29 PMSep 15, 2026, 5:29 PM
116Epic: Dashboard, widgets and utility endpoints (no longer cookie-blocked)## Summary The web product has a configurable **dashboard** and **front wall**, plus a set of sidebar widgets. The app has none, and `CLAUDE.md` records the layout endpoints as cookie-session-only. ## Status correction — both layout endpoints now answer the bearer token `CLAUDE.md` states: *"`GET /api/user/engagement` and `GET/PUT /api/user/dashboard-layout` return 401 with a valid bearer token"*. **Re-probed live 2026-09-15:** ``` GET /api/user/dashboard-layout -> 200 {"layout":null} GET /api/user/front-wall-layout -> 200 {"layout":null} GET /api/user/engagement -> 200 {"totalDigs":24,"totalPushes":6,"recent":[...]} ``` `GET /api/user` also returns `dashboardLayout` and `frontWallLayout` inline. **This constraint in `CLAUDE.md` is stale and must be corrected.** ## Endpoints | Method | Path | Live result | |---|---|---| | `GET`/`PUT` | `/api/user/dashboard-layout` | `200 {"layout":null}` | | `GET`/`PUT` | `/api/user/front-wall-layout` | `200 {"layout":null}` | | `GET` | `/api/widgets/news` | `200` (Hacker News headlines) | | `GET` | `/api/widgets/markets` | crypto watchlist quotes | | `GET` | `/api/widgets/transit` · `/transit/stops` | transit + nearby stops | | `GET` | `/api/widgets/bike-share` | bike-share availability | | `GET` | `/api/weather` · `/api/location` | `400` until lat/long supplied | ## Priority note This is genuinely lower value for a desktop client than the social and data pillars — it is filed for completeness and because the blocking claim turned out to be wrong. ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. openepicparityP3area:widgetsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/116Sep 15, 2026, 5:29 PMSep 15, 2026, 5:29 PM
115Auth: document multi-account switching as auth-model-blocked## Finding Multi-account support (`GET /api/auth/accounts`, `POST /api/auth/switch`, `POST /api/auth/remove-account`) declares **`cookieAuth`** in the live spec, and the live probe confirms it: ``` GET /api/auth/accounts -> 401 {"error":"Unauthorized","code":"unauthorized"} ``` A bearer sync-token client cannot obtain a cookie session, so this is **structurally unavailable** — the same category as Stripe checkout/portal. ## However A native client could offer its own multi-account support **without** these endpoints: hold several sync-tokens locally and switch which one `InterlinedApiClient` uses. That is a client-side feature, not an API one — worth a decision rather than an assumption. ## Acceptance criteria - [ ] `CLAUDE.md` records the cookie-only status of these three endpoints with the 2026-09-15 probe result. - [ ] A decision recorded: either implement client-side multi-token switching (which would mean extending `CredentialStore` from one `session.dat` to a keyed store, and coordinating with `InterlinedList.Sync`, which reads that exact file) or declare multi-account out of scope. - [ ] No UI ships that calls these endpoints with a bearer token. openquestionparityblockedP3area:authEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/115Sep 15, 2026, 5:29 PMSep 16, 2026, 7:53 PM
114Auth: real logout (`POST /api/auth/logout`)## Gap Sign-out currently clears the local DPAPI `session.dat` (`Services/CredentialStore.cs`) and returns to the login window via `Navigator.OnLoggedOut`. `POST /api/auth/logout` is never called, so the **sync-token is never invalidated server-side** — it keeps working for anyone holding it. This matters because `CLAUDE.md` treats `%LocalAppData%\InterlinedList\session.dat` as a standing credential, and the test account had accumulated **502** stale tokens. ## Interaction with the sync utility `InterlinedList.Sync` reads the **same** `session.dat` (`DpapiCredentialSource`). Signing out of the main app must not leave the tray utility running on a token the user believes is dead. ## Acceptance criteria - [ ] Sign-out calls `POST /api/auth/logout` (shape verified live) before clearing local state. - [ ] The local token is cleared even if the call fails — logging out must never be blocked by the network. - [ ] Verified that the token is actually rejected afterward, or the session disappears from `GET /api/user/sessions`. - [ ] The tray utility notices the signed-out state rather than syncing on a dead token. - [ ] `CLAUDE.md` updated with what logout actually does to a sync-token. closedparityP2area:authEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/114Sep 15, 2026, 5:29 PMSep 16, 2026, 8:37 PM
113Auth: email verification and email-change confirmation flows## Endpoints - `POST /api/auth/verify-email` — complete verification from an emailed token. - `POST /api/auth/verify-email-change` — confirm a new address. - `POST /api/auth/undo-email-change` — reverse a change. Verification is load-bearing (`/help/account`): you must verify before posting messages or attaching media, and it is the fastest way off the `new` account status. **Note:** `POST /api/auth/send-verification-email` (resend) is **`cookieAuth`-only** — see the Settings epic for the handoff. ## Acceptance criteria - [ ] All three implemented; shapes verified live. - [ ] A token can be entered in the app (paste from the email), so verification does not require the website. - [ ] `pendingEmail` from `GET /api/user` drives the confirm/undo affordances. - [ ] Success re-reads `GET /api/user` so `emailVerified` and `accountStatus` update immediately. closedparityP2area:authEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/113Sep 15, 2026, 5:29 PMSep 16, 2026, 8:35 PM
112Auth: complete the forgot-password loop with `reset-password`## Gap `Services/InterlinedApiClient.Auth.cs` implements `POST /api/auth/forgot-password` (request a reset link) but **not** `POST /api/auth/reset-password`. So the app can start a reset and then has nowhere to finish it — the user must go to the website. ## Acceptance criteria - [ ] `ResetPasswordAsync(token, newPassword)`; request shape verified live. - [ ] `LoginWindow` gains a reset-completion mode accepting the emailed token and a new password. - [ ] On success, sign in and carry on; on an invalid or expired token, say which. - [ ] Server validation messages surface inline, not as a generic failure. closedparityP2area:authEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/112Sep 15, 2026, 5:29 PMSep 16, 2026, 8:28 PM
111Epic: Auth flows — reset, verify, logout, multi-account## Summary `LoginWindow` does sync-token sign-in, register and forgot-password. Twenty-three `/api/auth/*` operations are unimplemented — most are OAuth provider legs (covered in the Cross-posting epic), but several core flows are missing and some are genuinely cookie-only. ## Unimplemented core flows | Method | Path | Auth reality | |---|---|---| | `POST` | `/api/auth/reset-password` | bearer/cookie per spec | | `POST` | `/api/auth/verify-email` | bearer/cookie per spec | | `POST` | `/api/auth/verify-email-change` | bearer/cookie per spec | | `POST` | `/api/auth/undo-email-change` | bearer/cookie per spec | | `POST` | `/api/auth/logout` | bearer/cookie per spec | | `POST` | `/api/auth/login` | cookie-session login — not this client's path | | `GET` | `/api/auth/accounts` | **`cookieAuth` only** | | `POST` | `/api/auth/switch` | **`cookieAuth` only** | | `POST` | `/api/auth/remove-account` | **`cookieAuth` only** | | `POST` | `/api/auth/send-verification-email` | **`cookieAuth` only** | ## Live verification (2026-09-15) ``` GET /api/auth/accounts -> 401 {"error":"Unauthorized","code":"unauthorized"} ``` Confirms the cookie-only group. A bearer-token client structurally cannot obtain a cookie session, so **multi-account switching is auth-model-blocked** — the correct outcome is to document it, not to build a failing button. ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. openepicparityP2area:authEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/111Sep 15, 2026, 5:29 PMSep 15, 2026, 5:29 PM
110Cross-posting: `POST /api/auth/{provider}/link` and OAuth-based sign-in## Two unimplemented pieces **1. `POST /api/auth/{provider}/link`** — the generic link endpoint, unimplemented. The app links via browser OAuth handoff only. **2. Sign in with a linked account.** Per `/help/account`: *"Once linked, you can sign in with GitHub, Mastodon, and Bluesky."* `LoginWindow` offers email/password, register and forgot-password only. ## Constraint `CLAUDE.md`: **no WebView2** — OAuth opens the OS default browser and the user returns. For *sign-in* that is awkward, because the callback lands in a browser session while this client needs a **bearer sync-token** from `POST /api/auth/sync-token`. ## Acceptance criteria - [ ] Establish live whether an OAuth callback can yield a sync-token for a native client, or whether it only establishes a cookie session. - [ ] If only a cookie session: **do not ship a sign-in button that cannot work** — record the finding in `CLAUDE.md` and close this with that conclusion. - [ ] If a token is obtainable: an OAuth sign-in path in `LoginWindow`, reusing the existing browser-handoff pattern. - [ ] `POST /api/auth/{provider}/link` implemented if it improves on the current handoff. openparityP3area:linkedinarea:authEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/110Sep 15, 2026, 5:29 PMSep 15, 2026, 5:29 PM
109Cross-posting: identity verification and reconnect reminders## Two gaps **1. `POST /api/user/identities/verify`** — unimplemented. `ConnectedAccountsViewModel` reads link state from `GET /api/user/identities` (correctly — per `CLAUDE.md`, `/api/auth/{provider}/status` reports *server* configuration, not per-user link state). This endpoint presumably re-validates a stored credential; confirm what it does live before wiring it. **2. Integration reconnect reminders.** Per `/help/settings` there is an **Integration reconnect reminders** setting, on by default: *"InterlinedList reminds you when one of your cross-posting connections (such as LinkedIn) is expiring or needs reconnecting, so your cross-posts don't quietly stop."* The app shows no such warning and does not expose the setting. ## Acceptance criteria - [ ] Behaviour of `identities/verify` established live and implemented if it is meaningful for a native client. - [ ] An expiring/needs-reconnect credential is flagged in Connected Accounts with a reconnect action (browser handoff). - [ ] The reminders preference is exposed wherever it lives (`/api/user/update` or notification preferences — confirm which). - [ ] A cross-post that fails because a credential lapsed says so, rather than failing silently. openparityP3area:settingsarea:linkedinEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/109Sep 15, 2026, 5:28 PMSep 15, 2026, 5:28 PM
108LinkedIn: sync pages`POST /api/linkedin/sync-pages` refreshes the LinkedIn pages available to the account — the prerequisite for `GET /api/linkedin/targets` returning anything. ## Acceptance criteria - [ ] Service method; response shape verified live before being typed strictly. - [ ] A "Sync LinkedIn pages" action in Connected Accounts, followed by a re-read of targets. - [ ] Reports plainly when LinkedIn is not linked, rather than showing a generic failure. closedparityP3area:linkedinEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/108Sep 15, 2026, 5:28 PMSep 16, 2026, 8:41 PM
107LinkedIn: link-as-first-comment option`POST /api/messages` accepts **`linkedInLinkAsFirstComment`** — *"post the link as the first comment instead of in the body. Only meaningful when the content contains a URL."* A well-known LinkedIn reach tactic, and a one-field change on a path the app already uses. ## Acceptance criteria - [ ] `PostMessageAsync` accepts the flag. - [ ] A composer toggle that appears **only** when the draft contains a URL and LinkedIn is a selected destination. - [ ] Also passed through by the AI Message Series `crossPost` object (AI epic). openparityP3area:messagesarea:linkedinEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/107Sep 15, 2026, 5:28 PMSep 15, 2026, 5:28 PM
106LinkedIn: per-destination posting targets in the composer## Scope Let the user choose **which LinkedIn destinations** a post goes to, instead of a single on/off toggle. ## API - `GET /api/linkedin/targets` — available destinations (live-verified `200 {"targets":[]}`). - `GET`/`PUT /api/linkedin/posting-targets` — the user's saved default selection. - `POST /api/messages` takes `linkedInTargets: [{kind: "personal"} | {kind: "orgPage", pageId}]`. **Prefer `linkedInTargets` (plural).** The spec marks `linkedInTarget` as *"superseded singular form, still accepted for older clients"* — do not build on it. ## Acceptance criteria - [ ] Service methods for targets and posting-targets. - [ ] Composer advanced panel lists personal + org-page destinations with individual toggles. - [ ] Selection is sent as `linkedInTargets`; the legacy singular is never sent. - [ ] Saved defaults from `GET /api/linkedin/posting-targets` pre-select, and `PUT` persists a change. - [ ] Empty targets degrade to the personal destination without an error. - [ ] Subscriber-gated with the upgrade-needed state. openparityP2area:messagesarea:linkedinEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/106Sep 15, 2026, 5:28 PMSep 15, 2026, 5:28 PM
105Epic: Cross-platform syndication depth (LinkedIn targeting, reconnect, verify)## Summary Connected Accounts and compose-time cross-post toggles work. The remaining gap is **LinkedIn per-destination targeting** — which `CLAUDE.md` still records as blocked. ## Status correction `CLAUDE.md` lists *"LinkedIn per-page posting targets"* as not built and once recorded `/api/linkedin/targets` as 401-walled. **Live, 2026-09-15:** ``` GET /api/linkedin/targets -> 200 {"targets":[]} ``` Bearer-reachable; empty because the test account has no LinkedIn pages. Buildable against the empty state. ## Unimplemented endpoints | Method | Path | |---|---| | `GET` | `/api/linkedin/targets` | | `GET`/`PUT` | `/api/linkedin/posting-targets` | | `POST` | `/api/linkedin/sync-pages` | | `POST` | `/api/user/identities/verify` | | `POST` | `/api/auth/{provider}/link` | ## Compose fields not sent `POST /api/messages` accepts `linkedInTargets[]` (`{kind:"personal"}` or `{kind:"orgPage", pageId}`), the superseded singular `linkedInTarget`, and **`linkedInLinkAsFirstComment`**. `PostMessageAsync` sends only the `crossPostToLinkedIn` boolean. ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. openepicparityP2area:linkedinEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/105Sep 15, 2026, 5:28 PMSep 15, 2026, 5:28 PM
104Organizations: LinkedIn organization integration## Endpoints - `GET /api/organizations/{id}/linkedin/status` - `POST /api/organizations/{id}/linkedin/sync-pages` - `PUT /api/organizations/{id}/linkedin/assignments` - `DELETE /api/organizations/{id}/linkedin/credential` - plus `GET /api/auth/linkedin/org-authorize` and `/org-callback` for the OAuth leg This is how an organization's LinkedIn **pages** become available as posting targets — the org-level counterpart to the per-user LinkedIn targeting in the Cross-posting epic. ## Acceptance criteria - [ ] All four org LinkedIn endpoints implemented; shapes verified live (the test account has no org LinkedIn credential — expect empty state). - [ ] An org detail shows its LinkedIn link state, with connect (browser handoff) and disconnect. - [ ] Sync-pages refreshes available pages; assignments maps pages to the org. - [ ] Owner/admin gated, consistent with the rest of the Organizations view. - [ ] OAuth opens the OS browser — no WebView2. openparityP3area:orgsarea:linkedinEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/104Sep 15, 2026, 5:28 PMSep 16, 2026, 8:41 PM
103Organizations: use the org-scoped user search when adding members## Gap Member-add currently searches via the global `GET /api/users/search`. The API provides a purpose-built `GET /api/organizations/{id}/users` — *"Search for users to add as members"* — which is scoped to the organization and therefore gives the right candidate set. ## Acceptance criteria - [ ] `SearchOrgCandidateUsersAsync(orgId, query)` implemented. - [ ] The add-member picker uses it instead of the global search. - [ ] Existing members are excluded or marked, so they cannot be added twice. - [ ] Response shape verified live (the previously-documented 401 wall on `.../members` is already known to be stale; confirm this sibling route the same way). closedparityP2area:orgsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/103Sep 15, 2026, 5:28 PMSep 16, 2026, 7:57 PM
102Epic: Organizations — member search and LinkedIn org integration## Summary `Views/OrganizationsView.xaml` covers browse, create, get, update, delete and full member management (list/add/change-role/remove). Five operations remain. ## Unimplemented endpoints | Method | Path | Feature | |---|---|---| | `GET` | `/api/organizations/{id}/users` | *"Search for users to add as members"* | | `GET` | `/api/organizations/{id}/linkedin/status` | org LinkedIn link state | | `POST` | `/api/organizations/{id}/linkedin/sync-pages` | sync the org's LinkedIn pages | | `PUT` | `/api/organizations/{id}/linkedin/assignments` | assign pages | | `DELETE` | `/api/organizations/{id}/linkedin/credential` | unlink the org credential | Creating an organization is a **Subscriber** feature; **joining one is free** (`/help/account`). ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. openepicparityP2area:orgsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/102Sep 15, 2026, 5:28 PMSep 15, 2026, 5:28 PM
101Notifications: engagement summary (no longer cookie-blocked)## Status correction `CLAUDE.md` states that `GET /api/user/engagement` *"returns 401 with a valid bearer token"* and is therefore out of scope. **Re-probed live 2026-09-15 — it returns 200:** ``` GET /api/user/engagement -> 200 {"totalDigs":24,"totalPushes":6,"recent":[{"id":"...","title":"I Dig! on your message — Adron Hall (@adron)","body":"..."}, ...]} ``` It is *"aggregate dig/push engagement received on the current user's own messages"* — totals plus a recent feed. ## Acceptance criteria - [ ] `GetEngagementAsync` + model. - [ ] Totals and the recent list surfaced (the Alerts rail or a profile summary both fit). - [ ] `CLAUDE.md` corrected — this endpoint is no longer bearer-blocked. closedparityP3area:notificationsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/101Sep 15, 2026, 5:28 PMSep 16, 2026, 8:18 PM
100Notifications: honor the user's tray limit preference`GET /api/user` returns **`notificationTrayLimit`** — *"how many notifications the bell tray holds before older ones drop off. The default is 20 and you can set any value from 10 to 40."* The app passes its own hard-coded `limit`. Depends on the `CurrentUser` work in the Settings epic. ## Acceptance criteria - [ ] The tray fetch uses `notificationTrayLimit`. - [ ] The value is editable in Settings (10–40, validated). - [ ] A change made on the web is honored after a session refresh. openparityP3area:notificationsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/100Sep 15, 2026, 5:28 PMSep 15, 2026, 5:28 PM
99Notifications: click-through routing to the related content## Product behaviour *"Clicking a notification marks it read and takes you straight to the related content: a reply opens its thread, a follow opens that person's profile, and so on."* The app's notification items are inert — nothing navigates. ## Routing table to implement | Notification kind | Destination | |---|---| | reply | the message thread (needs the thread view — Messages epic) | | dig / push | the message | | follow / follow request | that person's profile | | mention | the message containing the mention | | list share / document share | that list or document | ## Acceptance criteria - [ ] `Models/NotificationItem.cs` models whatever target id/kind the payload carries — inspect a real populated tray first; the test account has live notifications (`GET /api/user/engagement` shows dig/push activity). - [ ] Clicking routes through `Services/Navigator.cs`, extending it with the destinations it lacks. - [ ] Clicking also marks the item read. - [ ] An unrecognized kind does nothing rather than throwing. closedparityP2area:notificationsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/99Sep 15, 2026, 5:28 PMSep 16, 2026, 8:24 PM
98Notifications: a full notifications view (not just the tray)## Gap `ViewModels/NotificationsViewModel.cs` only ever calls `GET /api/notifications?scope=tray&limit=`. There is no way to see anything older than the tray holds, and `the-gaps.md` has carried "a full standalone notifications view" as open since session 6. ## Acceptance criteria - [ ] `GetNotificationsAsync` supports the non-tray scope with paging. - [ ] A full notifications view (a center view, consistent with how `MainWindow.xaml.cs` caches views by nav tag). - [ ] A **See all** affordance in the Alerts rail opens it. - [ ] Mark-all-read, mark-one-read and delete-one all work there (the service methods already exist). - [ ] Read and unread are visually distinct. closedparityP2area:notificationsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/98Sep 15, 2026, 5:28 PMSep 16, 2026, 8:24 PM
97Epic: Notifications — full page, click-through routing, engagement## Summary The app has an Alerts rail (tray fetch, mark-all-read, mark-one-read, delete-one). The product has more: a **full notifications page**, **click-through routing** to the related content, a **user-configurable tray limit**, and an **engagement** summary. ## Product behaviour (`/help/settings`) Notifications appear in two places: - the **bell tray** — an unread badge plus the most recent notifications, *up to your Notification tray limit*; - the **Notifications page** — reached via *See all* in the tray, showing **every** notification, with mark-all-read. *"Clicking a notification marks it read and takes you straight to the related content: a reply opens its thread, a follow opens that person's profile, and so on."* ## Endpoint status `GET /api/notifications?scope=tray&limit=` is implemented. The non-tray scope (the full page) is not used. `GET /api/user/engagement` is **not implemented** — and is no longer blocked (below). ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. openepicparityP2area:notificationsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/97Sep 15, 2026, 5:28 PMSep 15, 2026, 5:28 PM
96People: private-account follow-request flow end to end## Gap The approve/reject endpoints are wired, but the surrounding flow is not coherent: the app cannot make the account private (Settings epic), does not show a pending-request count anywhere in the shell, and a Follow button on a private account does not communicate that it sends a **request** rather than following. ## Documented behaviour (`/help/account`) With Private Account on: new followers must request; you approve or reject; existing followers are unchanged. ## Acceptance criteria - [ ] On a private account, the Follow button reads as "Request to follow" and shows a pending state afterward. - [ ] `GET /api/follow/requests` drives a visible pending-request count in the shell or Alerts rail. - [ ] Approve/reject are reachable without hunting, and the list refreshes after each. - [ ] Follow state after each action comes from `GET /api/follow/{userId}/status` (read-after-write), not from local assumption. openparityP2area:peopleEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/96Sep 15, 2026, 5:28 PMSep 15, 2026, 5:28 PM
95People: paginate followers/following and surface remove-follower## Gap `Services/InterlinedApiClient.Follow.cs` implements `followers`, `following`, `mutual`, `counts`, `requests`, `approve`, `reject` and `remove`. Per `CLAUDE.md`'s own cross-cutting note, *"follow lists ... all paginate"* — but the app fetches one page. And `DELETE /api/follow/{userId}/remove` (remove a follower) exists in the service with no UI. ## Acceptance criteria - [ ] Follower and following lists page through the full set (same convention as the feed). - [ ] A **Remove follower** action on the followers list, with in-place confirmation. - [ ] Counts from `GET /api/follow/{userId}/counts` reconcile with the paged lists. - [ ] Each entry opens that profile via `Services/Navigator.cs`. closedparityP2area:peopleEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/95Sep 15, 2026, 5:28 PMSep 16, 2026, 8:04 PM
94People: handle lookup (`GET /api/users/lookup`)`GET /api/users/lookup` resolves a handle to a user; **bearer auth required**. ## Live verification (2026-09-15) ``` GET /api/users/lookup?username=adron -> 400 {"error":"missing_handle","code":"bad_request"} ``` So the parameter is **`handle`**. Confirm the exact accepted forms (bare username vs `@user` vs a cross-platform handle) before building. ## Why it is useful here It gives the app a way to turn a typed or pasted handle — or an `@mention` in message text — into a real profile, rather than relying on `GET /api/users/search`. ## Acceptance criteria - [ ] `LookupUserAsync(handle)` with the parameter name confirmed live. - [ ] The People view accepts a pasted handle and resolves it. - [ ] `@mentions` in feed and DM text become clickable, opening the profile through `Services/Navigator.cs`. - [ ] An unresolvable handle reports plainly instead of opening an empty profile. closedparityP3area:peopleEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/94Sep 15, 2026, 5:28 PMSep 16, 2026, 8:07 PM
93People: read block and mute state on a profile## Gap The app can **block, unblock, mute, unmute and report** (`Services/InterlinedApiClient.Moderation.cs`) and lists blocks/mutes in Settings — but it never reads the state for the profile currently on screen, so the button can contradict reality. ## Endpoints - `GET /api/users/{username}/block` — *"whether the current user is blocking the target"* - `GET /api/users/{username}/mute` — *"whether the current user is muting the target"* ## Acceptance criteria - [ ] Both reads implemented and called when a profile opens. - [ ] The button reflects actual state (Block vs Unblock, Mute vs Unmute) instead of assuming. - [ ] State stays correct after acting, via read-after-write. - [ ] Pairs with `viewerHasMuted` on feed messages (Messages epic) so feed and profile agree. closedparityP2area:peopleEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/93Sep 15, 2026, 5:27 PMSep 16, 2026, 7:59 PM
92Epic: People, profiles and the social graph — remaining gaps## Summary `Views/PeopleView.xaml` covers profile lookup, follow/unfollow, request approve/reject, a user's messages, counts, mutual connections, and block/mute/report. What remains are the profile's content tabs, the block/mute **read** endpoints, handle lookup, and full follower-list paging. ## Unimplemented endpoints | Method | Path | Feature | |---|---|---| | `GET` | `/api/users/lookup` | resolve a handle (bearer required) | | `GET` | `/api/users/{username}/block` | am I blocking this user | | `GET` | `/api/users/{username}/mute` | am I muting this user | | `GET` | `/api/users/{username}/lists` \| `/documents` | their public content (see the Lists/Documents epics) | ## Live verification (2026-09-15) ``` GET /api/users/lookup?username=adron -> 400 {"error":"missing_handle","code":"bad_request"} ``` The endpoint is reachable with the bearer token; the parameter is **`handle`**, not `username`. ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. openepicparityP2area:peopleEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/92Sep 15, 2026, 5:27 PMSep 15, 2026, 5:27 PM
91DM: load older messages on scroll-up, and use the documented updates endpoint## Two gaps **1. History paging.** *"Older messages load as you scroll up."* The app loads a thread page and stops, so long conversations are truncated with no way back. **2. Polling endpoint.** `the-gaps.md` records 5-second polling of `.../updates`. The documented endpoint is `GET /api/dm/thread/{username}/updates` — *"lightweight incremental fetch for near-real-time polling of an open thread"* — live-verified `200 {"items":[]}`. Confirm the app is on that exact route with its documented parameters, and that dedupe-by-id still holds. ## Acceptance criteria - [ ] Scroll-up loads older messages, preserving scroll position. - [ ] Polling uses `GET /api/dm/thread/{username}/updates` with its documented incremental parameter. - [ ] Results dedupe by id; polling stops when the view is inactive. - [ ] Cadence sanity-checked against the rate-limit posture; nothing blocks the WPF dispatcher. opendocumentationparityP2area:dmEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/91Sep 15, 2026, 5:27 PMSep 16, 2026, 8:00 PM
90DM: markdown rendering, the 10,000-character limit, and 8-photo cap## Documented limits and behaviour - DM bodies support **Markdown** formatting, up to **10,000 characters**. - **Up to 8 photos** per message; photos are resized automatically. - A **verified email** is required to send images. - Direct messages, **including photos, are free for everyone** — no subscriber gate. The app's DM composer is plain text with no counter, no attachment cap, and no verified-email check. ## Acceptance criteria - [ ] Markdown rendered in the thread (the same renderer as documents, if there is one). - [ ] A 10,000-character counter with the limit enforced before sending. - [ ] Attachments capped at 8 per message, with the cap stated. - [ ] Unverified email disables image attachment with the reason given, rather than failing on send. - [ ] DMs are **not** gated on `customerStatus` — but note that account status `new` does lock DMs (see the Settings epic). openparityP2area:dmEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/90Sep 15, 2026, 5:27 PMSep 15, 2026, 5:27 PM
89DM: mutual-follow gating and a Message button on profiles## Product rule (`/help/direct-messages`) *"You can only message people you follow each other"* — you follow them **and** they follow you back, both requests approved. You also cannot message someone if either party has blocked the other. If the recipient list is empty, nobody who follows you follows you back yet. ## Missing affordance On a profile, *"if the two of you follow each other, a **Message** button appears next to Follow"*, opening the conversation directly. `Views/PeopleView.xaml` has no Message button. ## Acceptance criteria - [ ] A Message button on the profile, shown only for mutual followers. - [ ] It opens (or starts) that conversation in the Messages view via `Services/Navigator.cs`. - [ ] An empty recipient list explains the mutual-follow requirement instead of showing a bare empty state. - [ ] The block/mute state is respected — no Message button where either side has blocked. openparityP1area:dmarea:peopleEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/89Sep 15, 2026, 5:27 PMSep 15, 2026, 5:27 PM
88DM: unread dots and a navigation badge## Product behaviour *"Unread messages in your Inbox show a small blue dot. A badge in the navigation shows how many unread messages you have."* Opening a conversation marks its messages read. `GET /api/dm/unread-count` is already implemented but nothing renders it, and the left nav has no badge. `the-gaps.md` has carried "DM-unread badge on the nav" as an open item since session 1. ## Acceptance criteria - [ ] Unread dot per unread Inbox message and per conversation row. - [ ] A count badge on the **Messages** nav item in `MainWindow.xaml`, refreshed on a timer and after opening a thread. - [ ] Opening a conversation marks read (`POST /api/dm/{id}/read`, already implemented) and updates the badge. - [ ] The badge disappears at zero rather than showing "0". openparityP1area:dmEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/88Sep 15, 2026, 5:27 PMSep 15, 2026, 5:27 PM
87DM: Inbox / Sent / Deleted tabs and the conversations list## Product behaviour The Messages page has three tabs: - **Inbox** — messages other people sent you; - **Sent** — messages you sent; - **Deleted** — messages you removed from Inbox or Sent. The app instead lists *recipients*, so there is no way to see what was sent, and trashed messages are unreachable even though trash/restore is implemented. ## Endpoint `GET /api/dm/conversations` — *"One row per conversation (grouped by pairKey), newest activity first"*, cursor-paginated. Live-verified `200 {"items":[],"nextCursor":null}`. ## Acceptance criteria - [ ] Three tabs backed by the real endpoints (`/api/dm` for folders, `/api/dm/conversations` for the conversation list). - [ ] The conversation list replaces the recipient list as the default view, newest activity first. - [ ] The Deleted tab lists trashed messages with the existing restore action. - [ ] `nextCursor` paging, passed back verbatim. - [ ] `GET /api/dm/{id}` used where a single message must be re-fetched. closedparityP1area:dmEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/87Sep 15, 2026, 5:27 PMSep 16, 2026, 8:15 PM
86Epic: Direct Messages parity (folders, conversations, gating, limits)## Summary `Views/DirectMessagesView.xaml` has a recipient list, a thread, a composer, image attachments, trash/restore and 5-second polling. The documented product has more structure around it. ## Unimplemented endpoints | Method | Path | Feature | |---|---|---| | `GET` | `/api/dm/conversations` | one row per conversation, newest activity first | | `GET` | `/api/dm/{id}` | fetch a single message the caller participates in | | `GET` | `/api/dm/thread/{username}/updates` | the documented incremental-poll endpoint | ## Live verification (2026-09-15) ``` GET /api/dm/conversations -> 200 {"items":[],"nextCursor":null} GET /api/dm/thread/adron/updates -> 200 {"items":[]} ``` ## Missing product behaviour (`/help/direct-messages`) Three tabs **Inbox / Sent / Deleted** · unread blue dots + a nav badge · **mutual-follow gating** · a **Message** button on a mutual's profile · **Markdown** formatting up to **10,000 characters** · **up to 8 photos** per message · verified email required to send images · load-older-on-scroll-up. ### Repo conventions to follow - Service: one `partial class InterlinedApiClient` file per domain in `InterlinedList/Services/`. - Wire types in `InterlinedList/Models/` matching real API JSON. - ViewModel per view in `InterlinedList/ViewModels/` (CommunityToolkit.Mvvm `ObservableObject` + `[RelayCommand]`). - View is a self-contained `UserControl` in `InterlinedList/Views/` that news up its own VM from `AppServices.Session`. - **Strata tokens only** — no colors/fonts/radii outside `Resources/Palette.xaml` + `Theme.*.xaml`. - Read-after-write for any mutating endpoint whose response envelope has not been live-verified. openepicparityP1area:dmEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/86Sep 15, 2026, 5:27 PMSep 15, 2026, 5:27 PM
85Documents: live presence / collaborative cursors## Endpoints - `POST /api/documents/{id}/presence` — *"combined heartbeat + poll for the self-hosted live-cursor feature"*. - `DELETE /api/documents/{id}/presence` — explicit "leave" so the caller's caret disappears for peers immediately. ## Acceptance criteria - [ ] Shapes verified live (a second account, or two sessions of the test account, is needed to see a peer). - [ ] Heartbeat while a document is open; `DELETE` on close, view switch and app exit. - [ ] Peer carets or at minimum a "N others editing" indicator. - [ ] Polling stops when the view is inactive; nothing blocks the WPF dispatcher. - [ ] Degrades silently — a presence failure must never affect editing or saving. openparityP3area:documentsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/85Sep 15, 2026, 5:27 PMSep 15, 2026, 5:27 PM
84Documents: seed default templates`POST /api/documents/templates/seed-defaults` creates the default document templates for an account. The app reads templates (`GET /api/documents/templates`) but cannot seed them, so an account with none has an empty template picker and no way forward from inside the app. The live tree already shows a `_templates` folder on the test account, so seeded templates land in a real place. ## Acceptance criteria - [ ] Service method; response shape verified live before being typed strictly. - [ ] When the template list is empty, offer "Add default templates" instead of an empty state. - [ ] After seeding, the template picker refreshes from a fresh `GET`. - [ ] Idempotency checked — calling it twice must not duplicate templates. openparityP3area:documentsEmptyhttps://github.com/CompositeCode/interlinedlist-windows-app/issues/84Sep 15, 2026, 5:27 PMSep 15, 2026, 5:27 PM