interlinedlist-windows-app
126 rows · Updated
| Issue # | Title | Body | State | Labels | Assignees | Link | Created | Updated |
|---|---|---|---|---|---|---|---|---|
| 33 | Messages: link preview cards (`linkMetadata` / `/api/link-metadata`) | ## Summary The web feed renders rich link preview cards (title, description, thumbnail, host) — visible on the public landing page. The app renders the bare URL. ## Live verification (2026-09-15) ``` GET /api/link-metadata?url=https://jmap.io/ -> 200 {"link":{"url":"...","platform":"other","metadata":{"type":"link","title":"...","description":"...","thumbnail":"..."}}} GET /api/messages/{id}/metadata -> 200 {"links":[{"url":"...","metadata":{type,title,ogType,thumbnail,description}}]} ``` ## API - `linkMetadata` arrives **inline on each message** in `GET /api/messages` — often no extra call is needed. - `GET /api/messages/{id}/metadata` — stored metadata only (lightweight). - `POST /api/messages/{id}/metadata` — fetch **and persist** metadata for a message's links. - `GET /api/link-metadata?url=` — ad-hoc unfurl, e.g. a compose-time preview. ## Respect the user preference `GET /api/user` returns **`showPreviews`**. When it is off, do not render preview cards (see the Settings epic). ## Acceptance criteria - [ ] `Models/Message.cs` gains `LinkMetadata`. - [ ] Preview cards render on feed cards from inline `linkMetadata`, honoring `showPreviews`. - [ ] Thumbnails load lazily; a failed image degrades to a text-only card rather than a gap. - [ ] Compose-time preview via `GET /api/link-metadata` when the draft contains a URL. - [ ] Clicking a card opens the target in the OS browser. | closed | parityP1area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/33 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 8:47 PM |
| 32 | Messages: tags — compose, display, filter, trending and autocomplete | ## Summary Tags are a first-class product feature with their own endpoints. `Models/Message.cs` already carries `Tags`, but nothing composes, renders, or filters on them. ## Live verification (2026-09-15) ``` GET /api/tags/trending -> 200 {"tags":[{"tag":"Lego","count":2,"lastUsedAt":"..."}, ...]} GET /api/tags/autocomplete?q=le -> 200 {"tags":[{"tag":"Lego","count":2},{"tag":"learning","count":1}]} ``` Both answer the bearer sync-token. ## API - `POST /api/messages` accepts `tags: string[]`. - `GET /api/messages?tag=<tag>` filters the feed by tag. - `GET /api/tags/trending` — most-used tags on public messages in a trailing window. - `GET /api/tags/autocomplete?q=` — case-insensitive **literal prefix** match. ## Acceptance criteria - [ ] `Services/InterlinedApiClient.Tags.cs` with `GetTrendingTagsAsync` and `AutocompleteTagsAsync`. - [ ] Tag input in the composer with prefix autocomplete, sent as `tags[]`. - [ ] Tag chips render on feed cards and are clickable → filter the feed via `?tag=`. - [ ] A trending-tags panel (right rail fits the existing shell). - [ ] Pairs with the AI **Suggest tags** action — an accepted tag suggestion populates this input. | closed | parityP1area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/32 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 8:41 PM |
| 31 | Messages: Push (repost) and Quote | ## Product behaviour (`/help/messages`) Two ways to re-share someone else's message: - **Push (repost):** re-shares the original as-is, no added comment. - **Quote:** re-shares the original with your own note added; a banner confirms the post will be public before sending. **Both pushed and quoted posts are always public.** ## API `POST /api/messages` accepts `pushedMessageId`. The feed returns `pushCount`, `pushedMessageId` and a nested **`pushedMessage`** object for rendering the quoted original. `Models/Message.cs` already has `PushCount` but nothing reads it, and `pushedMessageId`/`pushedMessage` are not modelled at all. ## Acceptance criteria - [ ] `Models/Message.cs` gains `PushedMessageId` and `PushedMessage` (self-referencing). - [ ] `PostMessageAsync` accepts `pushedMessageId`. - [ ] Push and Quote actions on each feed card in `Views/FeedView.xaml`. - [ ] Quote opens the composer with the original attached; Push posts directly. - [ ] Both force public visibility, and Quote shows the "this will be public" confirmation before sending. - [ ] Cards render an embedded quoted original from `pushedMessage`. - [ ] `pushCount` is displayed alongside `digCount`. | closed | parityP0area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/31 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 8:35 PM |
| 30 | Epic: Messages / Feed parity gaps (push, quote, tags, link previews, permalinks) | ## Summary The feed is the app's most complete surface — compose, media, replies, edit/delete, report, scheduling and Dig all work. But `POST /api/messages` and the message wire type have grown, and several documented behaviours from `/help/messages` are missing. ## Fields `POST /api/messages` accepts that the app never sends From the live OpenAPI spec: `pushedMessageId` · `tags[]` · `organizationId` · `linkedInTargets[]` · `linkedInTarget` · `linkedInLinkAsFirstComment` `Services/InterlinedApiClient.cs` `PostMessageAsync` sends `content`, `publiclyVisible`, the four cross-post booleans, `mastodonProviderIds`, `parentId`, `scheduledAt`, `imageUrls`, `videoUrls` — and nothing else. ## Fields the live `GET /api/messages` returns that `Models/Message.cs` does not model Live probe, 2026-09-15: ``` id, content, publiclyVisible, userId, parentId, linkMetadata, imageUrls, videoUrls, crossPostUrls, scheduledAt, scheduledCrossPostConfig, replyCounts, repliesCheckedAt, tags, createdAt, updatedAt, digCount, pushCount, pushedMessageId, user, pushedMessage, _count, replyCount, dugByMe, viewerHasMuted ``` Missing from the model: `linkMetadata`, `crossPostUrls`, `scheduledCrossPostConfig`, `replyCounts`, `repliesCheckedAt`, `pushedMessageId`, `pushedMessage`, `viewerHasMuted`, `_count`. ## Missing user-facing behaviour (`/help/messages`) Push (repost) · Quote · tags on posts · link preview cards · permalink + embed code · full thread view · viewing-preference feed modes · `/api/limits`-driven media validation. ### 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:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/30 | Sep 15, 2026, 5:24 PM | Sep 15, 2026, 5:24 PM |
| 29 | Materialize: `target: "message"` → prefill the composer, with char limit and thread split | ## Behaviour `target: "message"` is the one destination that **creates nothing**. It returns a draft and the client publishes it through the normal composer — deliberately, so that every posting gate (account status, verified email, subscriber checks for media/cross-posting/scheduling, probation rate limits, `maxMessageLength`) stays in one place. ```json { "message": { "content": "...", "thread": ["..."], "isThread": false, "charLimit": 300 } } ``` ### `charLimit` The smaller of the caller's `maxMessageLength` (from `GET /api/user`) and **the tightest limit among `messageConfig.crossPostTargets`** — Bluesky's 300 is usually the binding one. Sizing to the account limit alone would show one post and then silently send a three-part thread to Bluesky. ### `allowThread` Defaults to `false`. If the body exceeds the limit the request is **rejected** unless `allowThread: true`, in which case `thread` carries the split parts in reply order. ### What each source becomes | Source | Draft | |---|---| | A list | title, description, up to five row headlines, and the true total (`"340 items in total"`) | | Rows | the selected rows' headline values, `"…and N more"` past five | | A document | its text, markdown → plain text, paragraphs preserved, **code blocks dropped** | | A highlighted passage | just that passage, same conversion | ## Acceptance criteria - [ ] The draft lands in the existing composer, editable, with cross-post/schedule controls live. - [ ] `crossPostTargets` reflects the composer's current selection, and the returned `charLimit` drives the counter. - [ ] Over-limit content offers "post as thread" → `allowThread: true`, and the returned `thread` parts are posted in reply order. - [ ] Publishing goes through `POST /api/messages`; the app does not re-implement any gate. | open | parityP2area:messages | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/29 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 8:01 PM |
| 28 | Materialize: the finalization preview dialog | ## Scope One shared dialog, used by every entry point, that shows exactly what will be created **before anything is saved** — and lets the destination be switched inside it (To List / To Doc / To List & Doc / To Message). ### For a list - list title + optional description, public toggle; - **edit the columns**: rename, change type, remove, **Add column** (a user-added column has `sourceKey: null`); - `includeData` toggle for a columns-only list; - a **live table preview** of the rows and columns on the right. ### For a document - document title and file name (`relativePath`), public toggle; - `listStyle` (numbered / bulleted) and `rowDataStyle` (inline / sub-items) when the source is a list or rows; - a **rendered preview** on the right. On success, offer links to open the new list and/or document. ## Acceptance criteria - [ ] Destination switchable in the dialog without losing entered config. - [ ] Column editor writes `propertyKey`/`propertyName`/`propertyType`/`sourceKey` correctly; `sourceKey: null` for added columns. - [ ] Live previews for both destinations. - [ ] Nothing is created until **Create** is pressed. - [ ] Strata tokens only. | open | parityP1area:listsarea:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/28 | Sep 15, 2026, 5:24 PM | Sep 16, 2026, 8:01 PM |
| 27 | Materialize: entry points on documents and highlighted passages | ## Scope `Views/DocumentsView.xaml`: - whole document → `{kind:"document", documentId}`; - a highlighted markdown selection in the editor → `{kind:"docElements", documentId, markdown}`, via a selection menu. ## Documented defaults A document → List: **each heading and bullet point becomes its own row**. ## Acceptance criteria - [ ] Create-from action on the document view. - [ ] A selection menu appears on highlighted markdown and passes the selected text as `markdown`. - [ ] Opens the shared finalization dialog. | open | parityP2area:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/27 | Sep 15, 2026, 5:24 PM | Sep 15, 2026, 5:24 PM |
| 26 | Materialize: entry points on lists and list rows | ## Scope `Views/ListsView.xaml` gains Create-from entry points: - on a single list (overview and detail) and on a multi-list selection → `{kind:"lists", listIds:[...]}`; - on a single row and on a multi-row selection → `{kind:"rows", listId, rowIds:[...]}`. ## Acceptance criteria - [ ] `+ Create ▾` on a list row and on a list; pills for multi-select. - [ ] Both source kinds sent id-only. - [ ] Opens the shared finalization dialog. | open | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/26 | Sep 15, 2026, 5:23 PM | Sep 15, 2026, 5:23 PM |
| 25 | Materialize: "+ Create ▾" action on messages (single + multi-select) | ## Scope - A `+ Create ▾` menu on each feed card in `Views/FeedView.xaml` offering **To List**, **To Doc**, **To List & Doc**, **To Message**. - A **Select mode** on the feed so several messages can be selected; the selection bar then shows To List / To Doc / To List & Doc pills. ## Documented defaults Messages → List: columns default to **Content, Author, Posted, Links, Tags**, one row per message. ## Notes - `message → message` is not offered — use Quote or Push instead (see the Messages epic). - Several messages can combine into **one** message draft. ## Acceptance criteria - [ ] Menu on a single card; multi-select mode with a selection bar. - [ ] `source` sent as `{kind:"messages", messageIds:[...]}` — ids only. - [ ] Opens the finalization dialog rather than creating immediately. | open | parityP1area:listsarea:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/25 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 8:01 PM |
| 24 | Materialize: `InterlinedApiClient.Materialize.cs` service + config models | ## Models - `MaterializeRequest` — `Target`, `Source`, `ListConfig?`, `DocConfig?`, `MessageConfig?`. - `MaterializeSource` — discriminated on `Kind`: `messages` / `lists` / `rows` / `document` / `docElements`. - `MaterializeListConfig` — `Title`, `Description`, `IsPublic`, `Fields[]`, `IncludeData` (default `true`). Each field: `propertyKey`, `propertyName`, `propertyType`, optional `isRequired`/`options`, and **`sourceKey`** mapping the column to a source attribute (`null` for a user-added empty column). The server re-derives values via `sourceKey` — it does **not** accept client cell values. - `MaterializeDocConfig` — `Title`, `RelativePath`, `IsPublic`, `ListStyle` (`numbered`|`bulleted`), `RowDataStyle` (`inline`|`sub-items`). - `MaterializeMessageConfig` — `Content?`, `CrossPostTargets[]`, `AllowThread`, `PubliclyVisible?`, `Tags?`, `ScheduledAt?`. - `MaterializeResult` — `List?`, `Document?`; `MessageDraft` — `Content`, `Thread[]`, `IsThread`, `CharLimit`. ## Acceptance criteria - [ ] `MaterializeAsync(MaterializeRequest)` in a new partial. - [ ] `403 subscription_required` surfaces as an upgrade-needed state, not a generic error. - [ ] `404` distinguishes "not found or not yours" from other failures. - [ ] Shape verified live with a `messages` source on the test account before typing the response strictly. | closed | parityP1area:listsarea:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/24 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 8:00 PM |
| 23 | Epic: "Create from…" (Materialize) — no longer API-blocked | ## Summary **Create from…** turns anything you already have — messages, lists, list rows, a document, or a highlighted passage — into a new **List**, a new **Document**, **both**, or a **Message draft**. The Windows app has none of it. `CLAUDE.md` and `the-gaps.md` both record this as *"API-blocked — the request is a single opaque `source` string"*. **That is stale.** `POST /api/materialize` is now fully documented at `/help/api/create-from`, with every source kind and config object spelled out. ## The contract `POST /api/materialize` — Session **or Bearer**. Subscriber-only. ```json { "target": "list | doc | both | message", "source": { "kind": "messages", "messageIds": ["..."] }, "listConfig": { }, "docConfig": { }, "messageConfig": { } } ``` **The request sends id-only references, never content.** The server re-fetches and authorizes every id under the calling user and derives the result from that authoritative data — client-supplied cell values and body text are not trusted. ### Source kinds | `source.kind` | Fields | |---|---| | `messages` | `messageIds: string[]` | | `lists` | `listIds: string[]` | | `rows` | `listId: string`, `rowIds: string[]` | | `document` | `documentId: string` | | `docElements` | `documentId: string`, `markdown: string` | Referencing something you do not own returns `404`. ### Responses `201` → `{ "list": {id,title}?, "document": {id,title}? }` `target: "message"` writes nothing and returns a **draft**: `{message:{content, thread[], isThread, charLimit}}`. ### Errors `401` unauthenticated · `403` not a subscriber · `400` missing/invalid `source` · `404` id not found or not owned · `500`. ### 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:listsarea:documents | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/23 | Sep 15, 2026, 5:23 PM | Sep 15, 2026, 5:23 PM |
| 22 | Schema: guard the destructive DSL rebuild vs the non-destructive `properties` edit | ## Problem `PUT /api/lists/{id}/schema` has two body shapes with **very different consequences**: - a `schema` DSL object → **destructive**: wipes and recreates all columns (and, per the docs, `name` becomes the list's new title); - a `properties` array → non-destructive: rename a label, add/remove one column, **row data preserved**. Shipping a single "Save schema" button that always sends the DSL form would silently destroy users' data. ## Acceptance criteria - [ ] Label edits, single add/remove, and reordering route through the `properties` shape. - [ ] The DSL rebuild path is a separate, explicitly-labelled action. - [ ] The rebuild requires in-place confirmation naming what is lost (no blocking native message box — see the dispatcher rule in `CLAUDE.md`). - [ ] The rebuild path notes that `schema.name` overwrites the list title. - [ ] Verified on a throwaway test list that the `properties` path preserves rows. | closed | parityP1area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/22 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 9:20 PM |
| 21 | Schema: replace the freeform JSON row editor with a typed, schema-driven one | ## Problem `Views/ListsView.xaml` edits rows as raw JSON text. That was the correct workaround while the DSL was unverified; with the schema readable it is now the main usability gap between the app and the web product. ## Scope Generate the row form from the list's schema: | Type | Control | |---|---| | `text` | single-line text box | | `textarea` | multi-line text box | | `number` | numeric input (honors `step`) | | `boolean` | True/False dropdown — plus **Neither** (`null`) when the field is optional | | `date` | date picker (`YYYY-MM-DD`) | | `datetime` | date + time picker (ISO) | | `email` | text box with email validation | | `url` | text box with URL validation | | `tel` | text box (+ `pattern` if set) | | `select` | single-select from `options` | | `multiselect` | multi-select from `options` | | `priority` | select over `low`/`medium`/`high`/`urgent` | Row data is keyed by each column's **`key`**. Respect `defaultValue`, `placeholder`, `helpText`, `visible`, `displayOrder` and the conditional-visibility rules. ## Acceptance criteria - [ ] Add and edit rows through typed controls driven by the live schema. - [ ] `defaultValue` pre-fills new rows. - [ ] The grid renders each column by its type (checkbox for `boolean`, chips for `multiselect`, etc.), not as JSON. - [ ] A raw-JSON escape hatch remains available for schema-less lists (confirmed still supported). - [ ] Rows created by the app are readable in the web UI and vice versa. | closed | parityP0area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/21 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 9:14 PM |
| 20 | Schema: conditional-visibility rule editor | ## Scope A column can be shown/hidden based on another column's value: ```json { "key": "studentId", "type": "text", "label": "Student ID", "visibility": { "condition": { "field": "ticketType", "operator": "equals", "value": "student" } } } ``` ## Operators `equals`, `notEquals`, `contains`, `notContains`, `greaterThan`, `lessThan`, `greaterThanOrEqual`, `lessThanOrEqual`, `isEmpty`, `isNotEmpty` (the last two ignore `value`). ## Documented semantics to honor - Visibility controls **form display**. A hidden column is skipped during row validation while empty — so a `required` column hidden by its condition is **not enforced** until the condition makes it visible. - Conditions may only reference columns appearing **earlier** in `fields`; visibility is evaluated in `displayOrder`. - **One condition per column** — there is no `and`/`or` grouping. ## Acceptance criteria - [ ] Rule builder: watched field (restricted to earlier columns), operator, value. - [ ] The row editor evaluates conditions live, showing/hiding fields as values change. - [ ] Hidden-and-empty required fields are not blocked client-side, matching server behaviour. | open | parityP2area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/20 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 8:15 PM |
| 19 | Schema: validation-rule editor + client-side row validation | ## Scope Expose each field's `validation` object and pre-validate rows before they hit the API. | Rule | Applies to | Server error message | |---|---|---| | `min` | `number`, `date`, `datetime` | `<label> must be at least <min>` / `... must be after <min>` | | `max` | `number`, `date`, `datetime` | `<label> must be at most <max>` / `... must be before <max>` | | `minLength` | `text`, `textarea` | `<label> must be at least <n> characters` | | `maxLength` | `text`, `textarea` | `<label> must be at most <n> characters` | | `pattern` | `text`, `textarea`, `email` | `<label> format is invalid` | | `step` | `number` | increment hint, UI only — not enforced server-side | Type-level checks always run regardless of `validation`: `email` must be a valid address, `url` a valid URL, `boolean` a real boolean, `select`/`multiselect` values must be within `options` (`<label> must be one of: ...`), and a required empty value yields `<label> is required`. Rules are applied by the server on `POST`/`PUT /api/lists/{id}/data` — **not** when the schema is created. ## Acceptance criteria - [ ] Editor exposes every applicable rule per field type (and hides the inapplicable ones). - [ ] Row entry validates client-side first, showing the message beside the offending field. - [ ] A server `400` still maps back to the right field rather than a generic toast. - [ ] `step` is applied to the number input only, and is not treated as a constraint. | open | parityP1area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/19 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 8:15 PM |
| 18 | Schema: column form builder UI (12 field types) | ## Scope A column editor in `Views/ListsView.xaml` — the app's equivalent of the web's schema form builder — for creating and editing a list's columns. ## Per column - `key` (machine key; unique, immutable once rows exist — warn on change), - `type` from the twelve supported types, - `label`, `required`, `defaultValue`, `placeholder`, `helpText`, - `options` (required for `select`/`multiselect`), - `visible`, `displayOrder` (reorderable). `priority` defaults to `low`/`medium`/`high`/`urgent` when no `options` are supplied. ## Acceptance criteria - [ ] Add / edit / remove / reorder columns; at least one column is always enforced client-side. - [ ] Duplicate `key`s are blocked before the request is sent. - [ ] `select`/`multiselect` require at least one option before save is enabled. - [ ] Used both at list-create time (`POST /api/lists` with `schema`) and for an existing list. - [ ] Strata tokens only; 4px corners; 4pt grid. | closed | parityP0area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/18 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 9:00 PM |
| 17 | Schema: models + `GET`/`PUT /api/lists/{id}/schema` service | ## Scope Model the DSL and add both schema calls to `Services/InterlinedApiClient.Lists.cs`. ## Models (`InterlinedList/Models/`) - `ListSchema` — `Name` (required), `Description`, `Fields` (>= 1). - `ListField` — `Key`, `Type`, `Label` (required) + `Required`, `DefaultValue`, `Options`, `Placeholder`, `HelpText`, `Validation`, `Visible`, `Visibility`, `DisplayOrder`. - `ListFieldValidation` — `Min`, `Max`, `MinLength`, `MaxLength`, `Pattern`, `Step`. - `ListFieldVisibility` — `Condition { Field, Operator, Value }`. - `ListFieldType` — the twelve types as a constrained set: `text`, `textarea`, `number`, `boolean`, `date`, `datetime`, `email`, `url`, `tel`, `select`, `multiselect`, `priority`. Note the wire mapping: each `fields[]` entry is stored as a `ListProperty` (`key`→`propertyKey`, `label`→`propertyName`, `type`→`propertyType`) — the `properties` body shape uses those names. ## Service - `GetListSchemaAsync(listId)` → `GET`, response is wrapped: `{"data": {...}}`. - `PutListSchemaDslAsync(listId, ListSchema)` → destructive rebuild. - `PutListPropertiesAsync(listId, properties)` → non-destructive edits. - `CreateListAsync` gains an optional `schema` argument. ## Error handling `400` responses name the offending column, e.g. `Invalid schema: Field 'year' has invalid type 'integer'. Valid types: text, number, ...` Surface that message against the field, not as a generic failure. Also handle: missing `name`, empty `fields`, a field missing `key`/`type`/`label`, `select`/`multiselect` without `options`, duplicate `key`s. ## Acceptance criteria - [ ] All four service methods land, shapes verified live against the test account. - [ ] `ListSummary` keeps compiling; schema is fetched separately, not folded into it. - [ ] A round-trip test (read schema → PUT it back unchanged) is verified on a throwaway test list. | closed | parityP0area:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/17 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 8:10 PM |
| 16 | Epic: List schema / columns (DSL) — no longer API-blocked | ## Summary Lists on `interlinedlist.com` are **structured data with typed columns**. The Windows app treats every row as a freeform JSON blob (`Views/ListsView.xaml` has an inline JSON editor), because `CLAUDE.md` records the schema DSL as *"only partially reverse engineered ... not implemented"*. **That blocker is stale.** The DSL is now fully published at `/help/api/lists-dsl`, and both endpoints answer the bearer sync-token. ## Live verification (2026-09-15, test account) ``` GET /api/lists/{id}/schema -> 200 {"data":{"name":"New list","fields":[]}} ``` ## The contract - `GET /api/lists/{id}/schema` — read the schema as DSL. - `PUT /api/lists/{id}/schema` — **two body shapes**, selected by payload: - a `schema` DSL object → **destructive rebuild** (wipes and recreates all columns); - a `properties` array → **non-destructive** small edits (rename a label, add/remove one column) preserving row data. - `POST /api/lists` also accepts `schema` at create time. ## The DSL ```json { "name": "Books to Read", "description": "My reading backlog.", "fields": [ { "key": "title", "type": "text", "label": "Title", "required": true }, { "key": "year", "type": "number", "label": "Year" }, { "key": "read", "type": "boolean", "label": "Read", "defaultValue": false } ] } ``` - `fields` must contain **at least one** column; `key`s must be unique. - Field props: `key`, `type`, `label` (required) + `required`, `defaultValue`, `options`, `placeholder`, `helpText`, `validation`, `visible`, `visibility`, `displayOrder`. - **Twelve types:** `text`, `textarea`, `number`, `boolean`, `date`, `datetime`, `email`, `url`, `tel`, `select`, `multiselect`, `priority`. - Row data (`POST /api/lists/{id}/data`) is keyed by each column's **`key`**, not its label. ## Why this matters Every other list feature — typed row entry, Powered Templates, Create from…, GitHub-backed lists, saved views — assumes real columns exist. This epic is the foundation for them. ### 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:lists | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/16 | Sep 15, 2026, 5:23 PM | Sep 15, 2026, 5:23 PM |
| 15 | AI: Powered Document (4 modes: article / from list / from article / research URL) | `feature: "powered_document"` drafts a full markdown document. Four modes, set in `context.mode`: | `context.mode` | Also requires | Source | |---|---|---| | `article` (default) | — | drafts from the topic in `input` alone | | `from_list` | `context.listId` | loads your list's schema + up to 50 rows server-side | | `from_article` | `context.documentId` | loads your document's title + body server-side | | `research_url` | `context.url` | fetches the page (http/https, SSRF-guarded), extracts title/description/text, cites the URL | You pass a **reference, not source text**. Derived modes are IDOR-guarded — a missing or non-owned reference returns `422 invalid_input`. Server-fetched context is truncated before it reaches the model. - `/suggest` → `kind: "document"`; `/generate` → `{documentId}`. ## Acceptance criteria - [ ] "Powered Document" action on `Views/DocumentsView.xaml`, gated on `IsAiAvailable`. - [ ] Mode picker; `from_list` / `from_article` pick their source from the user's own lists/documents; `research_url` takes a URL with http/https validation. - [ ] Rendered markdown preview, editable before confirming. - [ ] Confirm calls `/api/ai/generate` and opens the created document from a fresh `GET`. - [ ] `422 invalid_input` on a missing/non-owned reference is reported against the source field, not as a generic failure. | closed | parityP1area:ai | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/15 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 10:15 PM |
| 14 | AI: Powered Templates (generate a list from a description) | `feature: "powered_template"` drafts a complete list **schema plus starter rows** from a plain-language description (e.g. "a reading list with title, author, status, and rating"). On the web this is a **Powered Templates tab** in the new-list flow, shown only to subscribers. - `/suggest` → `kind: "list"` artifact (schema + starter rows). - `/generate` → `{listId}`. ## Depends on The schema/columns work in the **List Schema (DSL)** epic — the preview needs to render and edit real column definitions, not a JSON blob. ## Acceptance criteria - [ ] A "Powered Template" option in the new-list flow in `Views/ListsView.xaml`, gated on `IsAiAvailable`. - [ ] Preview renders the proposed columns (type, label, required) and the starter rows in a table. - [ ] Columns and rows are editable before confirming. - [ ] Confirm calls `/api/ai/generate`, then the list opens from a fresh `GET`. | closed | parityP1area:ai | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/14 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 10:05 PM |
| 13 | AI: Article Series planner (folder of documents) | `feature: "article_series"` plans and writes a coherent series of documents. - `/suggest` returns a `doc_series` artifact (the plan: one entry per part, in order). - `/generate` creates **a folder with one document per part** → `{folderId, documentIds[]}`. ## Acceptance criteria - [ ] Entry point in the composer (same gate as Message Series) and/or on the Documents view. - [ ] Preview shows the folder name and the ordered part titles/outlines, editable before confirming. - [ ] On success, the Documents view refreshes and navigates to the new folder. - [ ] Results are ordinary documents afterward — editable and deletable through the existing Documents UI. | open | parityP1area:ai | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/13 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 10:16 PM |
| 12 | AI: Message Series planner (scheduled-post series) | `feature: "message_series"` turns a brief into a sequence of short connected posts. ## Behaviour - Requires roughly a sentence or two of draft (there is a **10-word gate**; under it returns `422 invalid_input`). - `/suggest` returns a `message_series` artifact — the planned posts, for review. - `/generate` then either: - creates **a list** of the planned rows (default), or - with `scheduleImmediately: true`, creates **individual scheduled posts** spaced a few minutes apart → `{scheduledMessageIds, firstScheduledAt, lastScheduledAt}`. - `crossPost` may be passed on `/generate` (`crossPostToBluesky`, `selectedMastodonIds`, `crossPostToLinkedIn`, `selectedLinkedInTargets`, `crossPostToTwitter`, `linkedInLinkAsFirstComment`). It is sanitized server-side against the caller's connected accounts, and **the tightest selected service's character limit caps generation**. ## Acceptance criteria - [ ] Composer entry point appears once the draft clears the word gate. - [ ] Preview lists every planned post in order, editable before confirming. - [ ] Toggle for "schedule these posts" vs "save as a list" → maps to `scheduleImmediately`. - [ ] The composer's current cross-post selection is forwarded as `crossPost`. - [ ] After `scheduleImmediately`, the existing scheduled-posts view reflects the new posts. - [ ] The 10-word gate is enforced client-side so the call is not wasted against quota. | open | parityP1area:ai | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/12 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 10:16 PM |
| 11 | AI: Writing Assistant in the message composer | The web composer has an AI writing-assistant control offering six actions on the current draft. The app's composer (`Views/FeedView.xaml` + `ViewModels/FeedViewModel.cs`) has none. ## Actions (`feature: "writing_assist"`) | Action | Result | |---|---| | Rewrite | cleaner version of the same message | | Tighten | shorter, punchier | | Expand | longer, more detail | | Fix grammar | corrections only, voice intact | | Make a thread | split a long draft into a numbered sequence | | Suggest tags | recommended tags from the draft | ## Artifact handling `writing_assist` returns `kind: "message"`, `"thread"`, or `"tags"`. **None of these are persistable** — `/api/ai/generate` returns `422 invalid_input` for them. They are inserted into the composer client-side. - Text suggestions → offer **Replace draft** and **Use in draft**. - Tag suggestions → offer **Add tags** (pairs with the tags work in the Messages epic). - Nothing changes in the composer until the user picks one. ## Acceptance criteria - [ ] AI control in the composer toolbar, gated on `IsAiAvailable`. - [ ] All six actions call `POST /api/ai/suggest` with the current draft as `input`. - [ ] Suggestion panel with Replace draft / Use in draft / discard; tags get Add tags. - [ ] A `thread` artifact is shown as the numbered parts it will post as. - [ ] No call to `/api/ai/generate` from this surface. | open | parityP0area:ai | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/11 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 10:16 PM |
| 10 | AI: status, subscriber gating and daily-quota surfacing | AI controls must **hide themselves** rather than fail, matching the web app's documented behaviour. ## Rules from the product docs - `providers: []` → the site has no `ANTHROPIC_API_KEY`; hide all AI controls (calls would `409 no_provider_configured`). - `subscriber: false` → free account; **do not show AI controls at all**. - Quota is **50 actions/day** per account; a preview and its confirmed action each count. - `429 quota_exceeded` → tell the user to retry tomorrow; `429 rate_limited` → honor `Retry-After`. ## Acceptance criteria - [ ] `GET /api/ai/status` is fetched once per session (and on demand) and cached on a shared service. - [ ] A single `IsAiAvailable` gate (subscriber AND providers non-empty) drives visibility of every AI affordance. - [ ] Remaining quota is visible wherever an AI action is offered (e.g. `42 of 50 left today`). - [ ] `quota_exceeded` and `rate_limited` render as distinct, non-blocking in-place messages — not a generic error toast, and never a modal that blocks the dispatcher. - [ ] No AI control is ever shown to a free account. | closed | parityP0area:ai | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/10 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 9:56 PM |
| 9 | AI: add `InterlinedApiClient.Ai.cs` service + artifact models | Add the service layer for `/api/ai/*`. Everything after this is UI. ## Endpoints - `GET /api/ai/status` → `{subscriber, providers[], defaultModels{}, quota{usedToday,dailyLimit,remaining}}` - `POST /api/ai/suggest` → `{ok, feature, artifact, usage{inputTokens,outputTokens,model}, quota{}}` - `POST /api/ai/generate` → `{ok, feature, created{...}, quota{}}` ## Request shapes `/suggest`: `{feature, input, context?, model?, maxOutputTokens?}` `/generate`: `{feature, artifact, provider?, model?, scheduleImmediately?, crossPost?}` ## Artifact kinds by feature | feature | suggest artifact `kind` | generate `created` | |---|---|---| | `powered_template` | `list` | `{listId}` | | `powered_document` | `document` | `{documentId}` | | `message_series` | `message_series` | `{listId}` or `{scheduledMessageIds,firstScheduledAt,lastScheduledAt}` with `scheduleImmediately:true` | | `article_series` | `doc_series` | `{folderId, documentIds[]}` | | `writing_assist` | `message` \| `thread` \| `tags` | **not persistable** → `422 invalid_input` | ## Error codes to model as first-class states `401 unauthorized`, `403 subscription_required`, `409 no_provider_configured`, `422 invalid_input`, `422 invalid_ai_output`, `429 quota_exceeded`, `429 rate_limited` (honor `Retry-After`), `500/502 provider_error`. `InterlinedApiException` should carry the machine-readable `code` so the UI can branch on it instead of string-matching messages. ## Acceptance criteria - [ ] `Services/InterlinedApiClient.Ai.cs` with `GetAiStatusAsync`, `AiSuggestAsync`, `AiGenerateAsync`. - [ ] `Models/AiStatus.cs`, `Models/AiArtifact.cs` (+ per-kind payloads), `Models/AiQuota.cs`, `Models/AiUsage.cs`. - [ ] The `code` field from error bodies is surfaced on `InterlinedApiException`. - [ ] `GET /api/ai/status` verified live against the test account. | closed | parityP0area:ai | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/9 | Sep 15, 2026, 5:23 PM | Sep 16, 2026, 8:04 PM |
| 8 | Epic: AI Writing Assistance & Generation (entire pillar missing) | ## Summary `interlinedlist.com` ships a full **AI Writing Assistance** pillar (help: `/help/ai-writing-assistance`, API: `/help/api/ai-integration`). The Windows app implements **none of it** — there is no `InterlinedApiClient.Ai.cs`, no AI models, and no AI affordance in any view. This is the single largest feature-parity gap, and it is **fully unblocked**: three endpoints, complete public documentation, and bearer-token auth. ## Live verification (2026-09-15, test account) ``` GET /api/ai/status -> 200 {"subscriber":true,"providers":["anthropic"], "defaultModels":{"anthropic":"claude-sonnet-5", ...}, "quota":{"usedToday":0,"dailyLimit":50,"remaining":50}} ``` Bearer sync-token is accepted on all three AI routes. No per-user provider key exists any more (removed 2026-09-05) — access is gated on an active subscription alone. ## The API contract | Method | Path | Notes | |---|---|---| | `POST` | `/api/ai/suggest` | Runs a feature, returns a validated **preview artifact**. Writes nothing. Subscriber only. | | `POST` | `/api/ai/generate` | Persists a **confirmed** artifact from `/suggest`. Subscriber only. | | `GET` | `/api/ai/status` | Providers configured, caller's subscriber flag, daily quota. Any authenticated user. | Five `feature` values: `powered_template`, `powered_document`, `message_series`, `article_series`, `writing_assist`. **Two-step flow is mandatory:** `/suggest` → show preview → user confirms → `/generate`. Both steps count against the 50/day quota. ## Scope of this epic The sub-issues cover the service layer, quota/gating UX, and each of the five features as a distinct user-facing surface. ## Out of scope Nothing — every part of this pillar is documented and bearer-reachable. ### 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:ai | Empty | https://github.com/CompositeCode/interlinedlist-windows-app/issues/8 | Sep 15, 2026, 5:22 PM | Sep 15, 2026, 5:22 PM |