interlinedlist-windows-app
126 rows · Updated
| Issue # | Title | Body | State | Labels | Assignees | Link | Created | Updated |
|---|---|---|---|---|---|---|---|---|
| 83 | Documents: browse another user's public documents | `GET /api/users/{username}/documents` — live-verified `200` against `adron`, returning `{id, title, folderId, relativePath, createdAt, updatedAt}` per document. ## Acceptance criteria - [ ] A **Documents** tab on the profile in `Views/PeopleView.xaml`. - [ ] Opening one renders it read-only. - [ ] No owner-only affordances leak. - [ ] Pairs with the public-lists tab in the Lists epic. | open | parityP2area:documentsarea:people | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/83 | Sep 15, 2026, 5:27 PM | Sep 15, 2026, 5:27 PM |
| 82 | Documents: move / reparent documents and folders | ## Gap The app can create, rename and delete folders but cannot **move** anything. Per `/help/documents` an **Admin** collaborator can "move" a document, so moving is a first-class operation. ## Endpoints - `PATCH /api/documents/{id}` with `folderId` — moves a document. - Folder reparenting: `PUT /api/documents/folders/{id}` (the list-folder equivalent, `PUT /api/folders/{id}`, is documented as *"rename or move"*, so the same shape is likely). ## ⚠️ Load-bearing risk already recorded `CLAUDE.md` flags exactly this: *"moving a doc between folders pushes `folderId` on `PATCH /api/documents/{id}`, which is **not yet live-verified**"* — and `InterlinedList.Sync.Core/SyncEngine.cs` depends on it for folder-tree mirroring. **Verifying this write closes a known risk in the sync engine as well as unblocking this feature.** ## Acceptance criteria - [ ] `PATCH /api/documents/{id}` with `folderId` **verified live** on a throwaway document, and the result recorded in `CLAUDE.md` and `synch-plan.md`. - [ ] Folder reparenting verified and implemented. - [ ] Move via the tree (drag or a Move action) for both documents and folders. - [ ] A folder cannot be moved into its own descendant (cycle guard). | open | parityP2area:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/82 | Sep 15, 2026, 5:27 PM | Sep 16, 2026, 8:08 PM |
| 81 | Documents: inline image upload (paste and drag-and-drop) | ## Product behaviour - **Paste:** copy an image and paste (Ctrl+V) into the editor. - **Drag and drop:** drag an image file onto the editor. - Images upload to cloud storage and embed as a URL in the markdown. ## Endpoint `POST /api/documents/{id}/images/upload` — multipart. Per `the-gaps.md`, the verified multipart field name for the other upload endpoints is **`file`**, returning `{url}`; verify this one matches before typing it strictly. A multipart helper already exists on `InterlinedApiClient` (added for message/DM/avatar uploads). ## Acceptance criteria - [ ] Ctrl+V of a bitmap from the Windows clipboard uploads and inserts `` at the caret. - [ ] Drag-and-drop of image files does the same. - [ ] Format and size validated client-side against `GET /api/limits`. - [ ] Upload failure leaves the document text untouched and reports inline. - [ ] `402`/`403` renders the upgrade-needed state. | open | parityP2area:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/81 | Sep 15, 2026, 5:27 PM | Sep 15, 2026, 5:27 PM |
| 80 | Documents: autosave with retry (no Save button) | ## Product behaviour (`/help/documents`) *"Changes save automatically shortly after you pause typing (about a second), and about every eight seconds while you keep typing without a pause."* The editor also saves **when you switch tabs or leave the page**. There is **no separate Save button**; if a save fails, a **Retry** appears next to the error message. The app's document editor saves explicitly, so a user who closes the view loses work — the opposite of the product's contract. ## Acceptance criteria - [ ] Debounced save ~1s after typing stops. - [ ] Periodic save ~every 8s during continuous typing. - [ ] Save on view switch and on window close. - [ ] A visible save state (saving / saved / failed) with an inline **Retry** on failure. - [ ] A failed save never silently discards the buffer. - [ ] Coordinate with `InterlinedList.Sync` — a document edited in the app and by the tray sync must not fight; server-wins conflict handling already lives in `InterlinedList.Sync.Core/ConflictResolver.cs`. | open | parityP1area:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/80 | Sep 15, 2026, 5:27 PM | Sep 15, 2026, 5:27 PM |
| 79 | Documents: folder tree UX parity — nesting, hover actions, breadcrumbs | ## Product behaviour (`/help/documents`) - Browse with a **folder tree on the left**; click a folder to see its documents, click a document to open it. - Two ways to create a subfolder: **New Subfolder** near the top of a folder's page, or **hover a folder name in the tree** and click the folder-plus icon that appears — alongside **rename** and **delete** icons. - Inside a subfolder, a **breadcrumb bar** shows the full path back to root, each step a clickable link. - **New Document** both at root and within a folder. The app has folder CRUD but presents it as a flat list with buttons, not a nested tree with per-node actions. ## Acceptance criteria - [ ] Nested tree with expand/collapse, built from the tree payload. - [ ] Hover actions per folder node: add subfolder, rename, delete. - [ ] Breadcrumb bar with clickable segments. - [ ] New Document at root and in the selected folder. - [ ] Strata tokens only; 4pt grid. | open | parityP1area:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/79 | Sep 15, 2026, 5:27 PM | Sep 16, 2026, 8:08 PM |
| 78 | Documents: use `GET /api/documents/tree` for the sidebar | ## Gap `ViewModels/DocumentsViewModel.cs` builds its picture from separate `GET /api/documents` and `GET /api/documents/folders` calls. The API offers the whole sidebar in **one** call. ## Live shape (2026-09-15) ```json { "folders": [ { "id":"...", "name":"One-Folder", "parentId":null, "documents":[{"id":"...","title":"...","relativePath":"...","isPublic":false}] } ], "rootDocuments": [ ... ] } ``` Note `relativePath` and `isPublic` per document, and `parentId` for nesting. ## Acceptance criteria - [ ] `GetDocumentTreeAsync` + a `DocumentTree` model. - [ ] The Documents sidebar is built from one request, nesting folders via `parentId`. - [ ] `relativePath` is modelled (the sync engine already depends on this concept — see `InterlinedList.Sync.Core/FileMapper.cs`). - [ ] Round-trip count for opening the Documents view drops measurably. | closed | parityP1area:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/78 | Sep 15, 2026, 5:27 PM | Sep 16, 2026, 8:05 PM |
| 77 | Epic: Documents depth — tree, autosave, inline images, presence, move | ## Summary Documents are markdown files in a folder hierarchy. The app has root docs, folder CRUD, templates, create/edit/delete, search, share-links and collaborators. Fifteen `/api/documents*` operations remain unimplemented, and several documented editor behaviours are missing. ## Unimplemented endpoints | Method | Path | Feature | |---|---|---| | `GET` | `/api/documents/tree` | combined sidebar tree in one call | | `POST` | `/api/documents/{id}/images/upload` | inline images | | `POST`/`DELETE` | `/api/documents/{id}/presence` | live cursors | | `POST` | `/api/documents/templates/seed-defaults` | seed default templates | | `GET` | `/api/users/{username}/documents` | a user's public documents | | `GET`/`POST` | `/api/documents/sync` | delta sync (**used by `InterlinedList.Sync.Core`, not the app**) | | — | `/api/documents/{id}/invites*`, `/api/documents/shared/{token}`, `/api/documents/invite/{token}` | covered by the Sharing epic | ## Live verification (2026-09-15) ``` GET /api/documents/tree -> 200 {"folders":[{"id":"...","name":"One-Folder","parentId":null, "documents":[{"id":"...","title":"a-single-doc","relativePath":"a-single-doc.md","isPublic":false}]}, ...]} GET /api/users/adron/documents -> 200 ``` ## Missing editor behaviour (`/help/documents`) Autosave (no Save button) · paste/drag image upload · a real folder tree with hover add/rename/delete · breadcrumbs · move/reparent. ### 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. | open | epicparityP1area:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/77 | Sep 15, 2026, 5:27 PM | Sep 15, 2026, 5:27 PM |
| 76 | GitHub: link and "Reconnect for GitHub Issues" scope handoff | ## Product behaviour GitHub can be linked for **sign-in only** or with the **Issues scope**. GitHub-backed lists need Issues. The web surfaces a **"Reconnect for GitHub Issues"** action in Connected Accounts to grant the extra scope. ## Endpoints `GET /api/auth/github/authorize` · `GET /api/auth/github/callback` · `GET /api/auth/github/status` (reports whether the **server** has GitHub OAuth configured and its public client id) · `POST /api/auth/{provider}/link`. **Per `CLAUDE.md`, `/api/auth/{provider}/status` is a red herring for per-user link state** — the real state is `GET /api/user/identities`, which `ConnectedAccountsViewModel` already uses. Keep that distinction. ## OAuth stays external No WebView2 in this app: linking opens the OS default browser and the user returns and refreshes — the pattern already used in `Views/ConnectedAccountsView.xaml`. ## Acceptance criteria - [ ] A "Reconnect for GitHub Issues" action in Connected Accounts, opening the authorize URL in the OS browser. - [ ] Link state is read from `GET /api/user/identities`, not from `/api/auth/github/status`. - [ ] After returning, a refresh reflects the new scope and unlocks the GitHub-backed list flow. - [ ] If the scope cannot be detected from `/api/user/identities`, say so plainly rather than guessing. | closed | parityP2area:githubarea:auth | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/76 | Sep 15, 2026, 5:26 PM | Sep 16, 2026, 10:15 PM |
| 75 | GitHub: issue create / update / comment from list rows | ## Scope Make the row↔issue mapping real in the app: title, body, labels, assignees and state. ## Endpoints - `POST /api/github/issues` — create - `PATCH /api/github/issues/{owner}/{repo}/{number}` — update (including state) - `POST /api/github/issues/{owner}/{repo}/{number}/comments` — comment - `GET /api/github/repos/{owner}/{repo}/labels` and `/assignees` — pickers - `GET /api/github/repos/{owner}/{repo}/next-issue-number` ## Acceptance criteria - [ ] On a GitHub-backed list, the row editor offers label and assignee pickers populated from the repo. - [ ] Row add/edit/delete maps to issue create/update/close, per the documented semantics. - [ ] A comment action on a row posts an issue comment. - [ ] `next-issue-number` shows the number a new issue will receive. - [ ] Every write is verified live against a repo on a test account before its response is typed strictly. | open | parityP2area:listsarea:github | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/75 | Sep 15, 2026, 5:26 PM | Sep 16, 2026, 8:10 PM |
| 74 | GitHub: repository link, private-repo tag, and Refresh from GitHub | ## Product behaviour (`/help/lists`) - The source repository is linked **directly under the list name** (e.g. `owner/repo issues`), opening that repo's issues page in a browser. - A **private repo** carries a `Private repo` tag. The tag is about **the repository on GitHub**, not about who can see the InterlinedList list — the two are set separately. Someone invited to the list may not have repo access, so the tag warns that GitHub will ask them to sign in or show "not found". - Visibility is **read from GitHub on each sync** (create, **Refresh from GitHub**, or the scheduled sync), so a repo flipping public/private picks it up next sync. Lists created before the tag existed show **no tag** until their first sync — an unrecorded value is never presented as public. ## Endpoint `POST /api/lists/{id}/refresh` — manual refresh for GitHub-backed lists. ## Acceptance criteria - [ ] `owner/repo` link under the list title, opening in the OS browser. - [ ] `Private repo` tag driven by `githubRepoPrivate`, with copy that distinguishes repo access from list visibility. - [ ] A **null** `githubRepoPrivate` renders **no tag** — never "public". - [ ] A "Refresh from GitHub" action calling `POST /api/lists/{id}/refresh`, then re-reading the list. | closed | parityP2area:listsarea:github | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/74 | Sep 15, 2026, 5:26 PM | Sep 16, 2026, 10:12 PM |
| 73 | GitHub: create a GitHub-backed list from a repository | ## Product behaviour (`/help/lists`) The new-list flow has two tabs: **Local List** and **GitHub-backed List**. On the GitHub tab: - pick a repository you have access to; - enter a list title (**defaults to the repo name**); - optionally set a parent list; - toggle Public list. GitHub-backed lists show a GitHub icon and sync issues from the repo. Rows map to issues — **add a row creates an issue, edit updates it, delete closes it**. **Prerequisite:** GitHub connected **with the Issues scope**. An account linked for sign-in only needs "Reconnect for GitHub Issues". ## Acceptance criteria - [ ] A GitHub-backed tab in the new-list flow with a repo picker (grouped by org, paginated). - [ ] Title defaults to the repo name; parent list and public toggle available. - [ ] `ListSummary.source`/`githubRepo`/`githubRepoPrivate` are modelled and a GitHub icon marks such lists. - [ ] Missing Issues scope is detected up front and offers the reconnect handoff rather than failing at create. | closed | parityP1area:listsarea:github | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/73 | Sep 15, 2026, 5:26 PM | Sep 16, 2026, 10:08 PM |
| 72 | GitHub: `InterlinedApiClient.GitHub.cs` service + models | Implement all nine `/api/github/*` endpoints. ## Verified reachable (2026-09-15) `GET /api/github/repos` → `200 []`; `GET /api/github/orgs` → `200 []`. Both answer the bearer sync-token; the empty arrays reflect no linked GitHub identity on the test account, not an auth wall. ## Notes - `GET /api/github/repos` is documented as **fully paginated** — implement paging, do not assume one page. - `GET /api/github/orgs` exists so the repo picker can group by organization. - `next-issue-number` supports showing the number a new issue will get before it is created. - `githubDefaultRepo` on `GET /api/user` is a per-user default worth honoring in pickers. ## Acceptance criteria - [ ] Nine service methods + models (`GitHubRepo`, `GitHubOrg`, `GitHubIssue`, `GitHubLabel`, `GitHubAssignee`). - [ ] Repo listing pages through the full set. - [ ] A missing/insufficient GitHub link is a distinguishable state, not a generic error. - [ ] Write shapes verified live before being typed strictly (read-after-write meanwhile). | closed | parityP1area:github | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/72 | Sep 15, 2026, 5:26 PM | Sep 16, 2026, 8:09 PM |
| 71 | Epic: GitHub integration and GitHub-backed lists | ## Summary GitHub is a substantial product feature: **lists that sync with GitHub Issues**. Rows map to issues (title, body, labels, assignees, state) — adding a row creates an issue, editing updates it, deleting closes it. The app implements none of the nine `/api/github/*` endpoints and does not model the GitHub fields on a list. ## Status correction `CLAUDE.md` records that *"every `/api/github/*` call returns 'GitHub account not linked'"*. That is no longer what happens: ``` GET /api/github/repos -> 200 [] GET /api/github/orgs -> 200 [] ``` (live, 2026-09-15). The endpoints answer the bearer token cleanly and return **empty collections** because the test account has no GitHub identity linked. So the service layer and UI are buildable now against an empty state; only the populated-data paths need an account with GitHub linked. ## Endpoints | Method | Path | |---|---| | `GET` | `/api/github/repos` | | `GET` | `/api/github/orgs` | | `GET` | `/api/github/issues` | | `POST` | `/api/github/issues` | | `PATCH` | `/api/github/issues/{owner}/{repo}/{number}` | | `POST` | `/api/github/issues/{owner}/{repo}/{number}/comments` | | `GET` | `/api/github/repos/{owner}/{repo}/assignees` | | `GET` | `/api/github/repos/{owner}/{repo}/labels` | | `GET` | `/api/github/repos/{owner}/{repo}/next-issue-number` | Plus `POST /api/lists/{id}/refresh` (manual refresh for GitHub-backed lists) and `githubDefaultRepo` on `GET /api/user`. ## Verification note Populated behaviour needs a test account with GitHub linked **with the Issues scope**. Build against the empty state, then verify the write paths before typing their responses strictly. ### 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. | open | epicparityP1area:github | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/71 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 70 | Lists: server-side row filtering, sorting and pagination | ## Gap `GET /api/users/{username}/lists/{id}/data` is documented as *"list data rows for a public list with pagination, filtering, sorting"*. `GET /api/lists/{id}/data` takes the same shape. `ViewModels/ListsViewModel.cs` fetches rows without using any of it, so large lists are fetched wholesale and cannot be filtered or sorted server-side. ## Acceptance criteria - [ ] Confirm the supported query parameters against the live OpenAPI spec and a real request. - [ ] `GetListDataAsync` accepts sort, filter and paging arguments. - [ ] Grid column headers sort server-side; a filter row filters server-side. - [ ] Paging uses the cursor if the endpoint offers one (consistent with the feed work). - [ ] Filters/sorts are what a saved view's `config` persists — coordinate with the saved-views issue. | open | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/70 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 69 | Lists: browse another user's public lists | ## Endpoints | Method | Path | |---|---| | `GET` | `/api/users/{username}/lists` | | `GET` | `/api/users/{username}/lists/{id}` (metadata + ancestor chain) | | `GET` | `/api/users/{username}/lists/{id}/data` (paged, filtered, sorted) | Live-verified against `adron`: all `200`. ## Acceptance criteria - [ ] A **Lists** tab on the profile in `Views/PeopleView.xaml`. - [ ] Opening one shows read-only metadata, breadcrumbs from the ancestor chain, and paged rows. - [ ] No owner-only affordances leak onto someone else's list. - [ ] Pairs with the same work for documents (Documents epic). | open | parityP2area:listsarea:people | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/69 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 68 | Lists: watch state and self-service watch/unwatch | ## Gap The app manages watchers **as an owner** (invite/list/remove) and shows "Shared with me" via `GET /api/lists/watching`, but never reads its own watch state for a given list. `GET /api/lists/{id}/watchers/me` → live-verified `200 {"watching":false,"role":null}`. ## Acceptance criteria - [ ] Service method + model. - [ ] A list detail shows whether the viewer is watching it and at what role. - [ ] The role drives which affordances appear (read-only vs edit vs admin). - [ ] Owner-managed watcher UI and this viewer-side state do not contradict each other. | open | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/68 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 67 | Lists: grid freshness poll and presence heartbeat | `POST /api/lists/{id}/data/versions` is a **combined freshness poll and presence heartbeat** for the list grid. ## Acceptance criteria - [ ] Request/response shape verified live before implementing. - [ ] While a list is open, poll on a timer to detect rows changed by collaborators and refresh without losing in-progress edits. - [ ] Presence (who else has the list open) shown if the response carries it. - [ ] Polling stops when the view is not active; cadence checked against `GET /api/limits` and the general rate-limit posture. - [ ] No blocking call on the WPF dispatcher (see the polling rule in `CLAUDE.md`). | open | parityP3area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/67 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 66 | Lists: connections | ## Endpoints - `GET /api/lists/connections` (live-verified `200 {"connections":[]}`) - `POST /api/lists/connections` - `DELETE /api/lists/connections/{id}` ## Work - [ ] Determine what a connection links (list↔list relationships) by creating one from the web UI and reading it back — the spec carries no description and the test account has none. - [ ] Service methods + model once the shape is known. - [ ] Surface connections on the list detail with create/remove. - [ ] If the concept turns out to be web-dashboard-specific, close with that finding recorded in `CLAUDE.md` rather than building speculatively. | open | parityP3area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/66 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 65 | Lists: contributors panel | `GET /api/lists/{id}/contributors` returns the **full ranked list of contributors** (no server paging). Live-verified: `200 {"contributors":[],"totalContributors":0}`. ## Acceptance criteria - [ ] Service method + model. - [ ] A contributors panel on the list detail, ranked, showing `totalContributors`. - [ ] Each contributor opens their profile through the existing `Services/Navigator.cs` hub. | open | parityP3area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/65 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 64 | Lists: parent/child hierarchy and breadcrumbs | ## Product behaviour (`/help/lists`) Lists form a tree via a parent-child relationship: create a parent list (e.g. "Projects"), then optionally set a parent when creating a new list (local or GitHub-backed). **Breadcrumbs navigate up and down the hierarchy.** For GitHub-backed lists the parent is changed via Edit after creation. `GET /api/users/{username}/lists/{id}` explicitly returns *"public list metadata and ancestor chain for breadcrumbs"*. ## Acceptance criteria - [ ] `parentId` settable on create and on edit. - [ ] Breadcrumb bar on the list detail view, each segment clickable. - [ ] Child lists are discoverable from the parent. - [ ] A list cannot be made its own ancestor (cycle guard). | open | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/64 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 63 | Lists: list folders (`/api/folders`) — a separate domain from document folders | ## Gap The app implements **document** folders (`/api/documents/folders`) but not **list** folders, which are a distinct resource under `/api/folders`. `Views/ListsView.xaml` has no folder tree. ## Endpoints | Method | Path | Notes | |---|---|---| | `GET` | `/api/folders` | all non-deleted list folders for the caller (live-verified `{"folders":[]}`) | | `POST` | `/api/folders` | create — **subscribers only** | | `PUT` | `/api/folders/{id}` | rename **or move** (reparent) | | `DELETE` | `/api/folders/{id}` | soft-delete a folder **and its subfolders** | Lists reference their folder through `ListSummary.folderId`. ## Acceptance criteria - [ ] `Services/InterlinedApiClient.ListFolders.cs` (or a section of `.Lists.cs`) with all four operations. - [ ] Folder tree in `Views/ListsView.xaml`, nesting via `parentId`. - [ ] Create / rename / move / delete; delete confirms in place and states that subfolders go too. - [ ] Lists can be assigned to a folder (`folderId`). - [ ] `402`/`403` on create renders the upgrade-needed state. | closed | parityP1area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/63 | Sep 15, 2026, 5:26 PM | Sep 16, 2026, 10:22 PM |
| 62 | Lists: saved views (create, fork, update, delete, default) | ## Endpoints | Method | Path | Purpose | |---|---|---| | `GET` | `/api/lists/{id}/views` | every **shared** view on the list, plus this user's own **personal** views | | `POST` | `/api/lists/{id}/views` | create a view — `{name, scope, config, isDefault}` | | `POST` | `/api/lists/{id}/views/{viewId}` | **fork** a shared view into a personal copy owned by the caller — the escape hatch when someone else's shared view does not suit you | | `PUT` | `/api/lists/{id}/views/{viewId}` | update name, config or default flag | | `DELETE` | `/api/lists/{id}/views/{viewId}` | delete | Live-verified: `GET` → `200 {"views":[]}`. ## Acceptance criteria - [ ] Service methods + models; the `config` payload shape confirmed live by creating a view from the web (or via a test write) before typing it strictly. - [ ] A view picker on the list detail, separating shared from personal views and marking the default. - [ ] Create / rename / set-default / delete, and **Fork** on a shared view. - [ ] Applying a view drives the grid's columns/filter/sort. - [ ] `scope` (shared vs personal) is explicit in the create UI. | open | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/62 | Sep 15, 2026, 5:26 PM | Sep 15, 2026, 5:26 PM |
| 61 | Lists: extend `ListSummary` to the full live wire type | ## Gap `Models/ListSummary.cs` models 6 of the 14 fields the API returns, dropping every field that drives hierarchy, folders, GitHub backing and provenance. ## Live payload (`GET /api/users/adron/lists`, 2026-09-15) ```json { "id":"...", "userId":"...", "messageId":null, "parentId":null, "folderId":null, "title":"dashingarrivals", "description":null, "isPublic":true, "metadata":null, "source":"github", "githubRepo":"Adron/dashingarrivals", "githubRepoPrivate":false, "createdAt":"...", "updatedAt":"..." } ``` ## What each unmodelled field unlocks - `parentId` → parent/child list hierarchy + breadcrumbs - `folderId` → list folders (`/api/folders`) - `source` / `githubRepo` / `githubRepoPrivate` → GitHub-backed lists - `messageId` → the list was created from a message ("Create from…") - `userId` → ownership checks for owner-only affordances - `metadata` → opaque; keep as raw JSON ## Acceptance criteria - [ ] All fields added; `metadata` kept as raw JSON so it round-trips. - [ ] Owner-only affordances key off `userId` rather than inference. - [ ] Existing `Views/ListsView.xaml` bindings keep working. | closed | parityP1area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/61 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 7:48 PM |
| 60 | Epic: Lists depth — views, folders, hierarchy, contributors, connections | ## Summary `/api/lists*` is the largest domain in the API (42 operations). After the schema and materialize epics, these are the remaining list features the app does not have. ## Unimplemented endpoints | Method | Path | Feature | |---|---|---| | `GET`/`POST` | `/api/lists/{id}/views` | saved views | | `POST`/`PUT`/`DELETE` | `/api/lists/{id}/views/{viewId}` | fork / update / delete a view | | `GET`/`POST`/`PUT`/`DELETE` | `/api/folders` | **list** folders (distinct from document folders) | | `GET` | `/api/lists/{id}/contributors` | ranked contributors | | `GET`/`POST`/`DELETE` | `/api/lists/connections` | list connections | | `POST` | `/api/lists/{id}/data/versions` | freshness poll + presence heartbeat | | `GET` | `/api/lists/{id}/watchers/me` | am I watching this list | | `POST` | `/api/lists/{id}/refresh` | manual refresh for GitHub-backed lists | | `GET` | `/api/users/{username}/lists` | a user's public lists | | `GET` | `/api/users/{username}/lists/{id}` | public list metadata + ancestor chain | | `GET` | `/api/users/{username}/lists/{id}/data` | public list rows, paged/filtered/sorted | ## Live verification (2026-09-15) — all bearer-reachable ``` GET /api/lists/{id}/views -> 200 {"views":[]} GET /api/lists/{id}/contributors -> 200 {"contributors":[],"totalContributors":0} GET /api/lists/{id}/watchers/me -> 200 {"watching":false,"role":null} GET /api/lists/connections -> 200 {"connections":[]} GET /api/folders -> 200 {"folders":[]} GET /api/users/adron/lists -> 200 (includes source/githubRepo/githubRepoPrivate/parentId/folderId) ``` ## Model gap `Models/ListSummary.cs` has only `Id, Title, Description, IsPublic, CreatedAt, UpdatedAt`. The live payload also carries **`userId`, `messageId`, `parentId`, `folderId`, `metadata`, `source`, `githubRepo`, `githubRepoPrivate`** — every one of which backs a feature 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. | open | epicparityP1area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/60 | Sep 15, 2026, 5:25 PM | Sep 15, 2026, 5:25 PM |
| 59 | Sharing: restructure the share UI to invite-first, with "Make public" secondary | ## Product change to match Per `/help/documents`: *"You no longer set Public with a separate toggle in the editor. Whether a document is public is now controlled entirely inside the Share window."* The window **leads with Invite people** and offers **Make public** below, with a copy-link row appearing once public. The app currently exposes `isPublic` on the document/list editor and treats share links as the primary sharing mechanism — the inverse of the product's current model. ## Acceptance criteria - [ ] One shared Share window for lists and documents: **Invite people** first, **Make public** second. - [ ] The public toggle is removed from the document/list editors and lives only in the Share window. - [ ] A copy-link row appears only when the item is public. - [ ] Copy explains that inviting keeps the item private. | open | parityP2area:sharing | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/59 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 9:43 PM |
| 58 | Sharing: role management and the notify-by-email toggle | ## Product behaviour (`/help/documents`) Three permission levels: | Role | Can | |---|---| | **Read-only** | view content only | | **Edit** | change content, but **not** rename, change public/private, or move | | **Admin** | also rename, switch public/private, move, delete | - Adding someone or changing their role sends an **in-app notification and, by default, an email**. An **"Email this person"** tick can be unticked to skip the email; delivery also depends on the recipient's own Sharing notification settings. - **Only the true owner** can add people, change roles or remove them — even an Admin collaborator cannot manage sharing. ## Acceptance criteria - [ ] Role selector on add and on change, using the documented three levels. - [ ] An "Email this person" toggle on the add path, defaulting to on. - [ ] Sharing management is visible only to the owner; Admin collaborators see the roster read-only. - [ ] Role semantics are enforced in the app's own affordances (an Edit collaborator does not see rename/move/visibility controls). | open | parityP2area:sharing | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/58 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 9:43 PM |
| 57 | Sharing: resolve and claim share links and email invites | ## Gap The app can **create and revoke** share links but cannot **consume** one. A user handed a share link or an email invite has no path through the desktop app. ## Endpoints | Method | Path | Purpose | |---|---|---| | `GET` | `/api/lists/shared/{token}` | resolve a share link for viewing | | `POST` | `/api/lists/shared/{token}` | claim an edit/admin link as the signed-in user | | `GET` | `/api/lists/shared/{token}/data` | token-authorized, read-only row data | | `GET`/`POST` | `/api/lists/invite/{token}` | resolve / claim an email invite | | `GET`/`POST` | `/api/documents/shared/{token}` | resolve / claim a document share link | | `GET`/`POST` | `/api/documents/invite/{token}` | resolve / claim a document email invite | ## Acceptance criteria - [ ] A "Open a share link" entry point accepting a pasted URL or token. - [ ] Resolve shows what is being shared and at what role before claiming. - [ ] Claiming an edit/admin link adds it to the user's own lists/documents. - [ ] Read-only links render the row data without claiming. - [ ] Claiming is free for the invitee regardless of tier (documented) — do not gate it on `customerStatus`. | open | parityP2area:sharing | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/57 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 9:43 PM |
| 56 | Sharing: invite people to a document by email | Same as the list invites, against `/api/documents/{id}/invites` (`GET`/`POST`) and `DELETE /api/documents/{id}/invites/{token}`. ## Acceptance criteria - [ ] Service methods on `Services/InterlinedApiClient.Documents.cs`. - [ ] Pending invites listed with address, role and Revoke in the document's share UI. - [ ] Subscriber-gated with the same locked-but-visible treatment. - [ ] Shares one invite component with the list flow rather than duplicating it. | closed | parityP1area:documentsarea:sharing | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/56 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 9:41 PM |
| 55 | Sharing: invite people to a list by email | ## Endpoints - `GET /api/lists/{id}/invites` — pending email invites (live-verified `{"invites":[]}`). - `POST /api/lists/{id}/invites` — invite by email address; the person **may not have an account yet**. - `DELETE /api/lists/{id}/invites/{token}` — revoke a pending invite. ## Acceptance criteria - [ ] Service methods on `Services/InterlinedApiClient.Lists.cs`, request shape verified live before typing the response strictly. - [ ] Pending invites listed in the list's share UI with the invited address, role and a Revoke action. - [ ] Invite takes an email plus a permission level. - [ ] Subscriber-gated with an upgrade prompt for free accounts — invite controls visible but locked, matching the web. | closed | parityP1area:listsarea:sharing | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/55 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 9:41 PM |
| 54 | Epic: Sharing, invites and collaboration depth | ## Summary The app ships share-links, list watchers and document collaborators. What it is missing is the **invite-by-email** path (which the web now leads with), the **claim/resolve** side of a share link, and the role/notification controls around an invite. ## Unimplemented endpoints | Method | Path | |---|---| | `GET`/`POST` | `/api/lists/{id}/invites` | | `DELETE` | `/api/lists/{id}/invites/{token}` | | `GET`/`POST` | `/api/lists/invite/{token}` | | `GET`/`POST` | `/api/lists/shared/{token}` | | `GET` | `/api/lists/shared/{token}/data` | | `GET`/`POST` | `/api/documents/{id}/invites` | | `DELETE` | `/api/documents/{id}/invites/{token}` | | `GET`/`POST` | `/api/documents/invite/{token}` | | `GET`/`POST` | `/api/documents/shared/{token}` | ## Live verification (2026-09-15) ``` GET /api/lists/{id}/invites -> 200 {"invites":[]} ``` ## Product behaviour (`/help/documents`) The Share window **leads with "Invite people"** — the way to give specific people access while the item stays private — and offers **Make public** as a secondary option below. The public toggle was removed from the editor and now lives only in the Share window. Inviting (and changing a role) is a subscriber feature, but **the people invited never need to pay** — granted access is free at whatever level was chosen. ### 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. | open | epicparityP1area:sharing | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/54 | Sep 15, 2026, 5:25 PM | Sep 15, 2026, 5:25 PM |
| 53 | Settings: subscription tier display and Stripe handoff | ## State `Services/InterlinedApiClient.Account.cs` reads `customerStatus`, and Settings already has a "Manage account on the web" handoff. What is missing is making the **tier itself legible**, which is what explains every gated feature in the app. ## Auth-model reality (confirmed, 2026-09-15) `POST /api/stripe/create-checkout-session` and `POST /api/stripe/create-portal-session` declare **`cookieAuth`** in the live spec. A bearer-token client structurally cannot obtain a cookie session, so **checkout and the billing portal stay a browser handoff** — same pattern as OAuth linking. This part is blocked by design, not unbuilt. ## What the tiers include (`/help/account`) - **Free:** read and post messages, following, replies, threads, Dig, tags, public profile, notifications, browse public lists/documents, join organizations, export your data, and **accept any list or document shared or invited to you** (that access stays free). - **Subscriber:** adds cross-posting (Mastodon/Bluesky/LinkedIn/X), image and video attachments, scheduled posts, a higher character limit, sharing lists/documents with specific people (including email invites), AI features, and creating lists, list folders, documents, document templates and organizations. - If a subscription lapses, existing lists/documents/organizations stay fully usable — **only creating new ones is gated**. ## Acceptance criteria - [ ] A Subscription block in Settings showing the current tier from `customerStatus`. - [ ] A plain-language list of what the current tier includes and what an upgrade unlocks. - [ ] Subscribe / Manage / Cancel open the web in the OS browser (documented as cookie-only in-app). - [ ] Every `402`/`403` upgrade prompt elsewhere in the app links here. | open | parityblockedP2area:settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/53 | Sep 15, 2026, 5:25 PM | Sep 15, 2026, 5:25 PM |
| 52 | Settings: profile location (lat/long) | ## Gap `GET /api/user` returns `latitude`/`longitude` — an optional profile location used by the Weather widget and other location-aware features. Not modelled, not editable. ## Live verification (2026-09-15) ``` GET /api/weather -> 400 {"error":"Latitude and longitude are required"} GET /api/location -> 400 {"error":"Latitude and longitude are required"} ``` Both are bearer-reachable and simply need coordinates — so a stored profile location makes them usable (see the Widgets epic). ## Acceptance criteria - [ ] `Latitude`/`Longitude` on `CurrentUser`, editable in Settings. - [ ] Coordinates are optional and clearable. - [ ] The app does **not** request OS geolocation — the web's browser-permission flow has no desktop equivalent here; manual entry is the parity-appropriate path. | open | parityP3area:settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/52 | Sep 15, 2026, 5:25 PM | Sep 15, 2026, 5:25 PM |
| 51 | Settings: permissions — private account and email visibility | ## Gap `/help/settings` → Permissions documents two controls: - **Email visibility** — who can see your email address; - **Account visibility** — make the account private; new followers must request to follow and you approve them. `isPrivateAccount` is already on `CurrentUser` but is not editable in the app. Email visibility is not modelled at all. ## Acceptance criteria - [ ] Private-account toggle in Settings via `PATCH /api/user/update`, with an explanation that existing followers are unaffected. - [ ] Email-visibility control — confirm the field name on `GET /api/user`/`PATCH /api/user/update` live before building. - [ ] Turning on private account makes the existing follow-request approve/reject UI in `Views/PeopleView.xaml` discoverable. | open | parityP2area:settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/51 | Sep 15, 2026, 5:25 PM | Sep 15, 2026, 5:25 PM |
| 50 | Settings: account-status banner and gated-feature messaging | ## Gap `GET /api/user` returns **`accountStatus`** (and `cleared`), which the app does not model or surface. Per `/help/account` the web shows a banner at the top of the home page explaining the current status. ## The five statuses | Status | Behaviour | |---|---| | **new** (on probation) | read/browse/follow/block/mute/report work. Plain posting limited to a few per hour. **Locked:** direct messages, image and video uploads, cross-posting, scheduled posts, creating lists/documents/organizations. Verifying email is the fastest way off it. | | **active** | normal; limited only by subscription tier | | **restricted** | temporarily read-only while reviewed; can sign in, read, browse; cannot post, reply, react, follow, send messages or create. Appealable. | | **suspended** | as restricted, applied by the team. Appealable. | | **banned** | closed; cannot sign in | ## Why it matters Without this, a probationary or restricted account gets bare `403`s from half the app with no explanation — the single worst first-run experience available. ## Acceptance criteria - [ ] `AccountStatus` and `Cleared` on `CurrentUser`. - [ ] A shell-level banner (`MainWindow`) for any status other than `active`, naming what is locked and linking to verify-email or support as appropriate. - [ ] Locked actions are disabled up front with the reason, instead of failing on click. - [ ] `banned`/`suspended` are handled gracefully at sign-in. | closed | parityP1area:settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/50 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 9:44 PM |
| 49 | Settings: email verification state, pending email, and resend | ## Gap Email verification is **load-bearing**: per `/help/account` you must verify before posting messages or attaching media. The app models `emailVerified` but never shows it, never shows `pendingEmail`, and cannot resend. ## Endpoints and their auth reality - `POST /api/auth/send-verification-email` — **cookie-session-only** per the live spec (`cookieAuth`). Rate limited to once per 10 minutes. - `POST /api/auth/verify-email`, `POST /api/auth/verify-email-change`, `POST /api/auth/undo-email-change` — token-driven flows, reachable with bearer per the spec. - `pendingEmail` on `GET /api/user` shows an email change awaiting confirmation. The app already implements `POST /api/user/change-email/request` but never shows the pending state. ## Acceptance criteria - [ ] Unverified accounts see a persistent, dismissible-per-session banner explaining what is locked. - [ ] `pendingEmail` is displayed with an option to undo the change (`undo-email-change`). - [ ] Resend verification: if bearer is rejected, hand off to the web rather than shipping a button that 401s — and note the 10-minute rate limit in the UI. - [ ] The auth reality of each endpoint is confirmed live and recorded in `CLAUDE.md`. | open | parityP1area:settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/49 | Sep 15, 2026, 5:25 PM | Sep 15, 2026, 5:25 PM |
| 48 | Settings: change password | ## Gap `/help/settings` → Security documents **Change password**: enter your current password and a new one. The app has no way to change a password; `Services/InterlinedApiClient.Account.cs` has no such method. ## Work - [ ] Locate the endpoint (check `/api/user/*` and `/api/auth/*` in the live OpenAPI spec) and **verify it accepts the bearer sync-token** before building — this is the pattern that has bitten this repo before. - [ ] If it is cookie-session-only, do not ship a failing button: hand off to the web with the existing "Manage account on the web" pattern and record the finding in `CLAUDE.md`. - [ ] If bearer works: a Security block in `Views/SettingsView.xaml` taking current + new password, with the server's validation message surfaced inline. - [ ] Consider whether a password change should offer to revoke other API sessions (`GET`/`DELETE /api/user/sessions/{id}` are already implemented). | open | parityP1area:settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/48 | Sep 15, 2026, 5:25 PM | Sep 15, 2026, 5:25 PM |
| 47 | Settings: notification preferences are missing the `email` channel | ## Bug `Models/NotificationPreference.cs` defines: ```csharp public sealed class NotificationChannels { public bool Push { get; init; } public bool InApp { get; init; } } ``` The live API returns a **third channel** — `email`: ``` GET /api/user/notification-preferences -> 200 {"events":[ {"key":"dig", "channels":{"push":true,"inApp":true}}, {"key":"push", "channels":{"push":true,"inApp":true}}, {"key":"follow", "channels":{"push":true,"email":true}}, {"key":"mention","channels":{"email":true,"inApp":true,"push":true}}, {"key":"reply", ...} ]} ``` So the app **silently drops the email toggle** — and for `follow` it drops one of only two channels the event has. A user cannot manage email notifications from the app, and a `PATCH` built from this model risks writing back an incomplete channel set. ## Documented email controls (`/help/settings`) Under Settings → Security: **Follows**, **Mentions**, **Replies**, plus a **Sharing** section with separate **Email** and **In-app notification** switches, and an **Integration reconnect reminders** setting. ## Acceptance criteria - [ ] `NotificationChannels` gains `Email`. - [ ] The prefs UI renders one toggle per channel **actually present** for each event — not a fixed two columns. - [ ] `PATCH` sends only the changed channel and never clears channels it did not render. - [ ] Verified live that a round-trip does not drop a channel. - [ ] The sharing email/in-app pair and reconnect reminders are covered if they appear in the `events` payload. | closed | bugparityP0area:settingsarea:notifications | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/47 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 7:46 PM |
| 46 | Settings: model and honor every server-side preference on `GET /api/user` | ## Scope Extend `Models/CurrentUser.cs` with the missing fields, expose them in `Views/SettingsView.xaml`, and make the rest of the app actually obey them. ## Preferences and where each must take effect | Field | Documented meaning | Must affect | |---|---|---| | `maxMessageLength` | per-message character limit (default 666) | composer counter | | `messagesPerPage` | 10–30 | feed page size (currently hard-coded `limit = 20`) | | `viewingPreference` | My / All / Followers Only / Following Only | feed query | | `showPreviews` | link preview cards on/off | feed card rendering | | `notificationTrayLimit` | 10–40, default 20 | Alerts rail fetch limit | | `defaultPubliclyVisible` | default visibility for new messages | composer default | | `showAdvancedPostSettings` | show the gear for media/cross-post | composer advanced panel | | `theme` | light / dark / system | theme switching in `App.xaml.cs` | | `latitude` / `longitude` | optional profile location | weather/location widgets | ## Theme note `App.xaml.cs` currently reads dark mode from HKCU and `SystemEvents.UserPreferenceChanged`. The server-side `theme` value must win when it is `light` or `dark`, with `system` falling back to the existing OS behaviour. ## Acceptance criteria - [ ] Every field above is on `CurrentUser` and editable in Settings via `PATCH /api/user/update`. - [ ] Each preference is honored at its consumption point, not merely stored. - [ ] `theme` precedence: explicit server value > OS preference. - [ ] A change made on the web is reflected in the app after a session refresh. | closed | parityP0area:settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/46 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 9:38 PM |
| 45 | Epic: Settings & Account parity (server-side preferences the app ignores) | ## Summary `Views/SettingsView.xaml` covers profile edit, avatar, email change, notification toggles, blocked/muted, API sessions, CSV export and account deletion. But `GET /api/user` returns a much larger preference surface, and `Models/CurrentUser.cs` drops most of it — so preferences the user sets on the web are silently ignored by the app. ## Live verification — `GET /api/user` field set (2026-09-15) ``` accountStatus, avatar, bio, cleared, createdAt, customerStatus, dashboardLayout, defaultPubliclyVisible, displayName, email, emailVerified, frontWallLayout, githubDefaultRepo, id, isAdministrator, isPrivateAccount, latitude, longitude, maxMessageLength, messagesPerPage, notificationPreferences, notificationTrayLimit, pendingEmail, showAdvancedPostSettings, showPreviews, stripeCustomerId, theme, username, viewingPreference ``` `Models/CurrentUser.cs` models only: `Id, Email, Username, DisplayName, Avatar, Bio, Theme, EmailVerified, MaxMessageLength, DefaultPubliclyVisible, IsPrivateAccount, CustomerStatus, IsAdministrator`. **Missing:** `accountStatus`, `cleared`, `messagesPerPage`, `viewingPreference`, `showPreviews`, `notificationTrayLimit`, `showAdvancedPostSettings`, `latitude`, `longitude`, `pendingEmail`, `dashboardLayout`, `frontWallLayout`, `githubDefaultRepo`, `stripeCustomerId`. ### 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. | open | epicparityP0area:settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/45 | Sep 15, 2026, 5:25 PM | Sep 15, 2026, 5:25 PM |
| 44 | App settings: move this app's own preferences into the synced store | ## Motivation The app and the sync tray utility keep local-only preferences (`InterlinedList.Sync/SyncPreferencesStore.cs`, theme handling in `App.xaml.cs`). With the app-settings store available, those can follow the account across machines — the feature's stated purpose. ## Split along the documented scopes - **Account scope (shared, follows the user):** UI preferences that should be the same everywhere — e.g. feed mode, page size, rail state. - **Device scope (per-machine):** anything machine-specific — above all the **sync folder path** in `InterlinedList.Sync`, which is meaningless on another machine. ## Acceptance criteria - [ ] A settings model with an explicit `schemaVersion`, written under the app's `appKey`. - [ ] Shared vs per-machine keys split per the rule above; the sync folder path is per-machine. - [ ] Reads seed from the synced document, falling back to local values when the API is unreachable — the app must keep working fully offline. - [ ] Writes are debounced so the ~60/min budget is never a factor. - [ ] The tray utility and the main app agree on the `appKey` and on which keys each owns. | open | parityP2area:app-settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/44 | Sep 15, 2026, 5:25 PM | Sep 16, 2026, 8:08 PM |
| 43 | App settings: Settings → Applications management UI | ## Scope Match the web's **Settings → Applications** section inside `Views/SettingsView.xaml`. ## Documented capabilities - List each app you have used, its registered machines, and a summary of stored settings (last updated, size). - **Set as main workstation** — promote a machine; only one at a time holds the role. - **Rename** a machine (`PATCH .../devices/{deviceId}`). - **Remove** a machine — deregisters it and deletes its per-machine settings. Removing the main workstation auto-promotes the most recently used machine. Shared account settings are unaffected. - **View** the stored settings in full (read-only; viewing never changes anything). - **Copy to shared settings** — replace the shared account document with this machine's per-machine settings, so future new machines seed from them. - **Delete shared settings** (`DELETE /api/user/app-settings/{appKey}`). ## Acceptance criteria - [ ] Applications section listing apps → devices, with the main workstation marked. - [ ] Rename, remove, set-as-main all wired, each with in-place confirmation for the destructive ones. - [ ] Read-only settings viewer (pretty-printed JSON is fine). - [ ] Copy-to-shared and delete-shared, both explicitly confirmed. - [ ] Available to every account — no subscription gate. | open | parityP2area:app-settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/43 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 8:08 PM |
| 42 | App settings: register this machine and bootstrap on first run | ## Scope On sign-in, register the machine and — on a brand-new machine — seed from the main workstation. ## Documented behaviour - `POST /api/user/app-settings/{appKey}/devices` registers or refreshes a device. **The first device registered automatically becomes the main workstation.** - `GET /api/user/app-settings/{appKey}/bootstrap?deviceId=` resolves what a fresh machine should seed from; it returns `404 not_found` when bootstrap resolves to **none** (nothing to seed). - After the first run, the machine keeps its own per-machine settings. ## Implementation notes - Pick a stable `appKey` for this client (e.g. `interlinedlist-windows`) and keep it fixed — it is the namespace for everything here. - `deviceId` must be stable across launches and match `^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$`. Derive it from a durable machine identifier and persist it next to the token under `%LocalAppData%\InterlinedList\`. - A friendly device name (the machine name) goes in the registration so **Settings → Applications** on the web shows something recognizable. ## Acceptance criteria - [ ] Device registered/refreshed on each successful sign-in. - [ ] `deviceId` persisted locally and stable across restarts and app updates. - [ ] On first run for this device, `bootstrap` is consulted and its settings seeded; `404` is treated as "nothing to seed", not an error. - [ ] Registration failures never block startup — log and continue (consistent with the `AppLog` crash-hardening already in `App.xaml.cs`). | open | parityP1area:app-settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/42 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 8:08 PM |
| 41 | App settings: service, models, and compare-and-swap write path | ## Scope `Services/InterlinedApiClient.AppSettings.cs` covering all ten endpoints, plus the concurrency protocol. ## Optimistic concurrency (mandatory) Writes use body-`baseVersion` compare-and-swap: - **first-ever write:** `baseVersion: 0` → server creates at `version: 1`; - **update:** `baseVersion` equal to the document's current `version` → server writes and increments; - **stale write:** `409 version_conflict`, and the body carries the full current document under **`current`** so the client can rebase and retry. A non-zero `baseVersion` when no document exists is also a conflict. ## Models `AppSettingsDocument` — `AppKey`, `Scope` (`account`|`device`), `DeviceId?`, `Version`, `UpdatedAt`, `SchemaVersion`, `Settings` (kept as raw JSON so it round-trips byte for byte — **do not** deserialize into a typed shape and re-serialize). `AppSettingsDevice` — id, name, default flag, timestamps. ## Error codes `400 bad_request` · `401 unauthorized` · `404 not_found` · `409 version_conflict` · `413 payload_too_large` · `429 rate_limited` · `500 internal_error`. ## Acceptance criteria - [ ] All ten endpoints implemented. - [ ] `409` is handled by rebasing onto `current` and retrying, not by surfacing an error. - [ ] `settings` is stored/sent as raw JSON; a read→write round-trip is byte-identical. - [ ] 64 KB cap enforced client-side before the request. - [ ] `429` honors `Retry-After`; writes to the account doc, device docs and the device registry share one budget. - [ ] `appKey` and `deviceId` validated against the documented regexes. | closed | parityP1area:app-settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/41 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 8:07 PM |
| 40 | Epic: Application Settings & Devices (built for native clients — entirely missing) | ## Summary The platform ships a **generic per-user, per-app settings store plus a device registry** — ten endpoints under `/api/user/app-settings/*`, documented at `/help/api/app-settings` and surfaced to users as **Settings → Applications** (`/help/applications`). This exists specifically so native companion apps can sync their own preferences across machines. **This app is exactly the intended consumer and uses none of it.** ## Live verification (2026-09-15) ``` GET /api/user/app-settings/test/devices -> 200 {"devices":[]} GET /api/user/app-settings/test -> 404 {"error":"Not found","code":"not_found"} (nothing synced yet) ``` Bearer sync-token accepted. **No subscription required.** ## The contract | Method | Path | Description | |---|---|---| | `GET`/`PUT`/`DELETE` | `/api/user/app-settings/{appKey}` | account-scoped (shared) settings document | | `GET`/`PUT` | `/api/user/app-settings/{appKey}/devices/{deviceId}/settings` | device-scoped (per-machine) document | | `GET`/`POST` | `/api/user/app-settings/{appKey}/devices` | list / register-or-refresh a device | | `PATCH`/`DELETE` | `/api/user/app-settings/{appKey}/devices/{deviceId}` | rename / promote to default; deregister | | `GET` | `/api/user/app-settings/{appKey}/bootstrap?deviceId=` | what a fresh machine should seed from | ## Document shape `{appKey, scope, deviceId, version, updatedAt, schemaVersion, settings}` — `settings` is **opaque and round-trips byte for byte**; keys are never dropped, renamed or reordered, and the server never logs or interprets the payload. ## Limits `appKey` `^[a-z0-9][a-z0-9-]{0,63}$` · `deviceId` `^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$` · `settings` ≤ **64 KB** UTF-8 (`413`) · writes ~**60/min** per user per app, shared across all write methods on the same `appKey` (`429` with `RateLimit-*` + `Retry-After`). Reads are not rate-limited. ### 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. | open | epicparityP1area:app-settings | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/40 | Sep 15, 2026, 5:24 PM | Sep 15, 2026, 5:24 PM |
| 39 | Messages: post as an organization (`organizationId`) | `POST /api/messages` accepts **`organizationId`**, letting a post be attributed to an organization the user belongs to. The app's composer has no such selector. ## Acceptance criteria - [ ] `PostMessageAsync` accepts an optional `organizationId`. - [ ] Composer gains a "post as" selector listing the user's organizations (`GET /api/user/organizations`, already implemented) plus themselves. - [ ] Feed cards attribute an organization post to the organization. - [ ] Verify the write live on the test account before typing the response strictly (read-after-write). | open | parityP2area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/39 | Sep 15, 2026, 5:24 PM | Sep 15, 2026, 5:24 PM |
| 38 | Messages: cross-post URL labels, `viewerHasMuted`, and scheduled cross-post config | Three live response fields the app drops on the floor. ## `crossPostUrls` The web feed shows per-platform labels under a post ("Mastodon (metalhead.club)", "Bluesky", "linkedin") linking to the syndicated copy — visible on the public landing page. The app shows nothing. - [ ] Model `CrossPostUrls` and render one label per platform, opening the target in the OS browser. ## `viewerHasMuted` Each message reports whether the viewer has muted its author. - [ ] Model `ViewerHasMuted` and collapse/soften such cards instead of rendering them identically to the rest. ## `scheduledCrossPostConfig` A scheduled post carries the cross-post selection it will use when it fires. - [ ] Model it and show, in the scheduled-posts view, which platforms a pending post will syndicate to. | open | parityP2area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/38 | Sep 15, 2026, 5:24 PM | Sep 15, 2026, 5:24 PM |
| 37 | Messages: honor `GET /api/limits` and handle subscriber/verification gates | ## Summary The server publishes its own media and content limits; the app hard-codes nothing and validates nothing, so an oversized upload fails server-side with a generic error. ## Live verification (2026-09-15) ``` GET /api/limits -> 200 {"media":{"image":{"maxBytes":1468006,"maxPixels":1200,"acceptedFormats":["jpeg","png","gif","webp"]}, "video":{"maxBytes":3145728,"acceptedFormats":["mp4","mov"]}}, "message":{"maxContentLength":5000}} ``` ## Gates to handle as first-class states, not generic errors From `/help/account`: - **Email must be verified** before posting messages or attaching media (`emailVerified` on `GET /api/user`). - **Account status** `new` (on probation) locks DMs, image/video upload, cross-posting, scheduled posts, and creating lists/documents/organizations, with a few posts per hour. - Media, cross-posting and scheduling are **Subscriber** features (`customerStatus`). - Effective message length is the caller's own `maxMessageLength` (`GET /api/user`), which may be below the server cap. ## Acceptance criteria - [ ] `GetLimitsAsync` added and cached for the session. - [ ] The file picker filters to `acceptedFormats`; oversized or over-dimension files are rejected client-side with the actual limit quoted. - [ ] Composer counter uses `min(user.maxMessageLength, limits.message.maxContentLength)`. - [ ] `402`/`403` on a gated action renders an upgrade-needed or verify-email state naming the reason. - [ ] Media/cross-post/schedule controls are disabled with an explanation when the account status or tier forbids them. | open | parityP1area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/37 | Sep 15, 2026, 5:24 PM | Sep 15, 2026, 5:24 PM |
| 36 | Messages: cursor pagination, `onlyMine`, and viewing-preference feed modes | ## Current state `GetMessagesAsync` uses `?limit=&offset=`. The live spec documents **`limit`, `onlyMine`, `tag`, `cursor`** for `GET /api/messages` — `offset` is no longer a documented parameter. ## Live verification (2026-09-15) `offset` **still works**, and the response now returns a cursor alongside it: ```json "pagination": { "total": 360, "offset": 0, "limit": 2, "hasMore": true, "nextCursor": "MjAyNi0wOS0xMlQyMDo0MDowNS43NzdafDVjODIzZDQ1..." } ``` So this is not a live break today — but the app is on an undocumented parameter while the documented keyset path is available, and the spec is explicit: *"Pass back the `nextCursor` from the previous response verbatim — never construct, parse, or modify one."* ## Viewing preferences (`/help/settings`) `GET /api/user` returns **`viewingPreference`** and **`messagesPerPage`** (10–30). Documented feed modes: **My Messages**, **All Messages**, **Followers Only**, **Following Only**. The app honors none of them. ## Acceptance criteria - [ ] `Models/Pagination.cs` gains `NextCursor`; `GetMessagesAsync` takes an opaque cursor and passes it back verbatim. - [ ] Infinite scroll in `FeedViewModel` drives off `nextCursor`, falling back to `offset` only if a cursor is absent. - [ ] `onlyMine` and `tag` are supported parameters. - [ ] Feed mode selector reflecting and honoring `viewingPreference`. - [ ] Page size honors `messagesPerPage` instead of the hard-coded `limit = 20`. | open | questionparityP1area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/36 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 9:48 PM |
| 35 | Messages: full thread view + batched reply counts | ## Product behaviour Replies nest under the parent. When a reply has sub-replies a **"+N replies"** link appears, opening the **full thread view**: the original post at the top and every reply chained below, each with its own Reply and Dig buttons. The app shows replies inline on a card but has no dedicated thread view and no nested-reply affordance. ## API - `GET /api/messages/{id}/replies` (already implemented). - `POST /api/messages/{id}/reply-counts` — **not implemented**; batch reply counts for a set of messages. - The live payload already carries `replyCounts`, `replyCount`, `_count` and `repliesCheckedAt` on each message — none are modelled. ## Acceptance criteria - [ ] `Models/Message.cs` gains `ReplyCounts`, `ReplyCount`, `RepliesCheckedAt`. - [ ] `PostReplyCountsAsync(ids)` added to `Services/InterlinedApiClient.Messages.cs`. - [ ] `+N replies` affordance on cards with sub-replies. - [ ] A thread view: original at top, replies chained below, each with Reply and Dig. - [ ] Reachable both from `+N replies` and from clicking a card's body. | open | parityP1area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/35 | Sep 15, 2026, 5:24 PM | Sep 15, 2026, 5:24 PM |
| 34 | Messages: permalink, copy link, and embed code | ## Product behaviour (`/help/messages`) Every message has a permanent page at `/user/<username>/status/<id>`. The action bar has a **Share** button with two options: - **Copy link** — the message's permanent address; - **Get embed code** — an HTML snippet to paste into a blog post; viewers do not need an account to see an embedded **public** message. Only public messages can be linked or embedded for other people; a private message's link and embed work only for the owner while signed in. ## Acceptance criteria - [ ] Share action on each feed card with Copy link and Get embed code. - [ ] Both write to the Windows clipboard and confirm in place (non-blocking). - [ ] The permalink resolves to `https://interlinedlist.com/user/<username>/status/<id>` and opens in the OS browser. - [ ] For a private message, both options are disabled with a note explaining why. | open | parityP2area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/34 | Sep 15, 2026, 5:24 PM | Sep 15, 2026, 5:24 PM |