Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Build

on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
workflow_dispatch:

jobs:
build-app:
name: Build app (sanity check)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Build WPF app
run: dotnet build InterlinedList/InterlinedList.csproj -c Release -r win-x64

build-msi:
name: Build MSI installer
runs-on: windows-latest
needs: build-app
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Publish app (installer payload)
run: dotnet publish InterlinedList/InterlinedList.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=false

- name: Build MSI
run: dotnet build installer/InterlinedList.Installer.wixproj -c Release

- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: InterlinedList-Setup-msi
path: installer/bin/**/*.msi
if-no-files-found: error

build-msix:
name: Build MSIX (Store package)
runs-on: windows-latest
needs: build-app
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- uses: microsoft/setup-msbuild@v2

- name: Build MSIX package
run: >-
msbuild InterlinedList.Package/InterlinedList.Package.wapproj
/restore
/p:Configuration=Release
/p:Platform=x64
/p:AppxBundlePlatforms=x64
/p:AppxBundle=Always
/p:UapAppxPackageBuildMode=StoreUpload

- name: Upload MSIX artifact
uses: actions/upload-artifact@v4
with:
name: InterlinedList-Store-package
path: InterlinedList.Package/AppPackages/**
if-no-files-found: error
12 changes: 12 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"microsoft-learn": {
"type": "http",
"url": "https://learn.microsoft.com/api/mcp"
},
"nuget": {
"command": "dotnet",
"args": ["dnx", "NuGet.Mcp.Server", "--source", "https://api.nuget.org/v3/index.json", "--yes"]
}
}
}
210 changes: 204 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,222 @@ InterlinedList.slnx
InterlinedList/
InterlinedList.csproj (net10.0-windows, UseWPF=true)
app.manifest (Windows 10+ DPI awareness + UAC)
App.xaml / App.xaml.cs (App entry point, theme switching)
MainWindow.xaml (Shell: custom title bar, nav, feed, right rail)
MainWindow.xaml.cs (Code-behind: clock, nav, window chrome)
App.xaml / App.xaml.cs (Entry point: theme switching + login/session orchestration)
LoginWindow.xaml / .xaml.cs (Email/password gate, shown when no session restores)
MainWindow.xaml (Shell: custom title bar, left nav, center ContentControl, right rail)
MainWindow.xaml.cs (Code-behind: clock, nav → view switching, window chrome)
Resources/
Palette.xaml (Invariant brand colors)
Theme.Light.xaml (Light semantic brushes)
Theme.Dark.xaml (Dark semantic brushes)
Models/
Post.cs (Post data model)
Models/ (Wire types matching the real API JSON — see API integration below)
Services/
ApiConfig.cs (Base URL: https://interlinedlist.com/)
InterlinedApiClient.cs (Core: HTTP/JSON plumbing + auth/user/messages/dig/notifications)
InterlinedApiClient.Lists.cs (partial class: Lists domain)
InterlinedApiClient.Documents.cs (partial class: Documents domain)
InterlinedApiClient.Organizations.cs (partial class: Organizations domain)
InterlinedApiClient.Search.cs (partial class: cross-resource search)
InterlinedApiClient.CrossPost.cs (partial class: linked-identity/OAuth helpers)
InterlinedApiException.cs
CredentialStore.cs (DPAPI-encrypted sync-token persistence)
SessionService.cs (login/logout/restore, exposes CurrentUser)
AppServices.cs (process-lifetime singletons: Api, Session)
ViewModels/ (CommunityToolkit.Mvvm ObservableObject + [RelayCommand])
LoginViewModel.cs, FeedViewModel.cs, MessageItemViewModel.cs,
NotificationsViewModel.cs, NotificationItemViewModel.cs, ProfileSummaryViewModel.cs,
ListsViewModel.cs, DocumentsViewModel.cs, OrganizationsViewModel.cs,
SearchViewModel.cs, ConnectedAccountsViewModel.cs
Views/ (Self-contained UserControls; each owns its ViewModel —
`DataContext = new XyzViewModel(AppServices.Session)` in its
own constructor, not injected by MainWindow)
FeedView, ListsView, DocumentsView, OrganizationsView, SearchView, ConnectedAccountsView
installer/
InterlinedList.Installer.wixproj (WiX v5 SDK project → classic .msi)
Package.wxs (product/feature/shortcut definition)
License.rtf (placeholder EULA for WixUI_Minimal)
InterlinedList.Package/
InterlinedList.Package.wapproj (Windows Application Packaging Project → MSIX)
Package.appxmanifest (Store identity, visual elements, capabilities)
Images/ (tile/splash assets generated from brand-kit logo)
```

## API integration

The app talks to the real InterlinedList backend at `https://interlinedlist.com`
(154-endpoint REST API, OpenAPI spec at `/api/openapi.json`) — there is no mock
data layer. Auth is a long-lived bearer token from `POST /api/auth/sync-token`
(the same mechanism the `il-sync` CLI and other native clients use — no cookie
jar), persisted DPAPI-encrypted via `CredentialStore`. The token is
long-lived, so treat `%LocalAppData%\InterlinedList\session.dat` as a standing
credential — but note the server **does** expose session management:
`GET /api/user/sessions` lists a user's active sync tokens and
`DELETE /api/user/sessions/{id}` revokes one (both accept the bearer token —
verified live 2026-07-31). An earlier revision of this file claimed no revoke
endpoint existed; that is no longer true.

Covered now (greatly expanded in the 2026-07-31 parity build-out):
login/session restore, paginated **feed** with compose (text + **image
attachments**, cross-post toggles), Dig/Undig, **replies/threads**, **edit/
delete** own posts, **report** posts, and **click-through to author profiles**;
notifications tray with **mark-one-read / delete-one / mark-all**; **Direct
Messages** (recipient list + thread + send); **People** (profile lookup,
follow/unfollow, follow-request approve/reject, a user's messages,
**block/mute/report**); **Lists** (browse/create/delete, freeform JSON data
rows with **row edit + delete** — no schema/column editor, see below);
**Documents** (root docs, templates, create/edit/delete + **folder CRUD /
new-doc-in-folder**); **Organizations** (browse + create + **full member
management**: add via search, change role, remove, edit/delete org);
**Settings** (profile edit, avatar-from-URL, email change, notification
preferences, blocked/muted management, **API-session list + revoke**, **CSV
data export**); unified **Search**; and **Connected Accounts** (Bluesky/
Mastodon/LinkedIn/Twitter linking + cross-post toggles). Still not built:
Stripe billing UI, register/forgot-password, GitHub issue sync (endpoints work
but the test account has no GitHub linked), per-list schema/column definitions,
LinkedIn per-page posting targets, scheduled-post UI (the service supports
`scheduledAt`), media *video* upload, list watchers/sharing, document sharing/
collaborators, Materialize ("Create from…"), and account deletion UI (the
service method exists, intentionally unsurfaced).

**Load-bearing constraints discovered by live-probing the API — don't
"fix" these without re-verifying, they're not bugs in this app:**

1. **A few endpoints only accept cookie-session auth, not the bearer
sync-token.** Re-probed live 2026-07-31 with the test account:
`GET /api/user/engagement` and `GET/PUT /api/user/dashboard-layout` return
`401` with a valid bearer token (Stripe billing + some `/api/auth/*` session
flows are the same shape). A native bearer-token client structurally can't
get a cookie session, so those are either browser-handoff (like OAuth) or
out of scope. **Correction to an earlier claim:** `GET
/api/organizations/{id}/members`, `GET /api/linkedin/targets`, and
`GET /api/linkedin/posting-targets` were *previously* documented here as
`401`-walled, but as of 2026-07-31 they return `200` with the bearer token —
member-management and LinkedIn per-page targeting **are** buildable now.
(Member *mutations* — POST/PUT/DELETE — still need live write-verification.)
2. **The per-provider `GET /api/auth/{provider}/status` endpoints are a red
herring** — they report whether the *server* has that OAuth integration
configured, not whether *this user* has linked it. The real per-user link
state is `GET /api/user/identities` (works fine with the bearer token),
which is what `ConnectedAccountsViewModel` actually uses.

**Write-path caution, same pattern throughout:** `PostMessageAsync`/
`DigAsync`/`UndigAsync`/`AddListRowAsync`/`CreateOrganizationAsync`/
`RemoveIdentityAsync` don't parse their response bodies — some were
deliberately never exercised live (creating an org, disconnecting a linked
identity) to avoid mutating shared test infrastructure, so callers re-fetch
from a `GET` afterward rather than trusting a typed write response. Lists'
schema/column DSL (`PUT /api/lists/{id}/schema`) was only partially reverse
engineered and is **not implemented** — data rows work fine schema-less
(confirmed live), so that's the supported path. If you pick up any of this,
verify the actual response shape against a real (test) account before typing
it strictly, and prefer read-after-write over trusting an unverified envelope.

Left nav maps to real views now: **Feed**, **Messages** (Direct Messages),
**Lists**, **Documents**, **Organizations**, **People** (profiles + follow),
**Search** (`MainWindow.xaml.cs` `NavItem_Click` swaps a `ContentControl` via a
small per-tag cache in `_views`), **Accounts** (Connected Accounts),
**Settings**, and **Alerts** (right-rail toggle, not a center view). Feed/search
cards open a profile in the People tab via the `Navigator` hub
(`Services/Navigator.cs`) → `MainWindow.OpenProfile`.

## Packaging & distribution

Two independent, parallel packaging tracks — both wrap the same
`InterlinedList/InterlinedList.csproj` build, neither depends on the other:

**MSI (`installer/`)** — WiX Toolset v5 (SDK-style, NuGet-restored via
`WixToolset.Sdk`). It's a **two-step build, not one**: publish the app first,
then build the installer —

```sh
dotnet publish InterlinedList/InterlinedList.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=false
dotnet build installer/InterlinedList.Installer.wixproj -c Release
```

WiX then harvests everything under
`InterlinedList/bin/Release/net10.0-windows/win-x64/publish/**` into the MSI via
a `<Files Include>` glob (no manual harvesting/heat step). **A single-command
`BeforeTargets="Build"` auto-publish target was tried and removed** — WiX's
file harvesting runs before that hook ever fires, confirmed empirically in CI
(the publish directory was still missing when harvesting ran), so don't
reintroduce that pattern without verifying it actually executes. Produces
`InterlinedList-Setup.msi` for direct download/side-loading, Start Menu
shortcut, per-machine install under Program Files. **Before shipping:**
replace `installer/License.rtf` with the real EULA.

**MSIX (`InterlinedList.Package/`)** — classic Desktop Bridge "Windows
Application Packaging Project" (`.wapproj`), the standard route for putting
an existing Win32/.NET desktop app into the Microsoft Store or sideloaded
MSIX. This project type is **not** `dotnet build`-able — its targets come
from `Microsoft.DesktopBridge.props/.targets`, installed with Visual Studio's
"Universal Windows Platform development" workload, not a NuGet package. It
builds fine headlessly with classic MSBuild once that workload is present
(confirmed in CI — see below); you don't need the VS IDE itself, just its
installed build tools. Command (matches what CI runs):

```powershell
msbuild InterlinedList.Package/InterlinedList.Package.wapproj /restore `
/p:Configuration=Release /p:Platform=x64 /p:AppxBundlePlatforms=x64 `
/p:AppxBundle=Always /p:UapAppxPackageBuildMode=StoreUpload
```

`UapAppxPackageBuildMode=StoreUpload` produces an unsigned `.msixupload`
bundle meant to be uploaded directly to Partner Center — Partner Center signs
it during ingestion, so **no code-signing certificate is needed for Store
submission** (you would need one for direct sideloading instead, a different
`UapAppxPackageBuildMode`). **Before Store submission:** replace the
placeholder `Publisher` value in `Package.appxmanifest` with the identity
reserved in Partner Center (VS's "Associate App with the Store" wizard will
rewrite `Identity`/`Properties` for you if you do it from the IDE instead).

**`TargetPlatformVersion` must match a UAP SDK actually installed on the
build machine** — this isn't a fixed "latest is fine" choice. Different
machines (and different GitHub Actions runner image versions over time) have
different SDKs installed; check what's present rather than assuming
(`Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\Platforms\UAP"`) if a
future SDK-not-found error (`APPX3217`) shows up after a runner image update.

Image assets in `InterlinedList.Package/Images/` were generated from
`brand-kit/logo/logo-icon-master.png` with transparent padding (tiles) and a
teal-deep `#0C2C3A` background (splash) — regenerate them the same way if the
mark changes, don't hand-edit the PNGs.

## Continuous integration

`.github/workflows/build.yml` runs on every push/PR to `main`/`dev` (and via
manual `workflow_dispatch`), on `windows-latest`, with three jobs:

- **`build-app`** — fast sanity-check `dotnet build` of the WPF app alone; the
other two jobs `needs:` this one so a trivial compile break fails fast
instead of waiting on a much slower packaging build.
- **`build-msi`** — publishes the app, then builds the WiX MSI, uploads
`InterlinedList-Setup-msi` as a workflow artifact.
- **`build-msix`** — adds `microsoft/setup-msbuild` (locates VS's MSBuild)
then builds the `.wapproj` directly (see above), uploads
`InterlinedList-Store-package` (the AppxBundle + `.msixupload`) as an
artifact.

Both packaging jobs were debugged against real CI runs, not assumptions —
three real, non-obvious issues surfaced and are fixed in the current state
(don't reintroduce them):
1. `Package.wxs` declared `ARPNOMODIFY` itself, which collides with the same
property already set by the `WixUI_Minimal` wixlib (`WIX0091` duplicate
symbol) — removed.
2. The app project needs `<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>`
declared (not just passed via `-r win-x64` on the CLI) — the MSIX
packaging project triggers a *nested* publish of it as a `ProjectReference`
that needs the RID available at restore time (`NETSDK1047` otherwise).
3. `TargetPlatformVersion` in the `.wapproj` must match an SDK actually
installed on the runner (see above) — it drifts as GitHub updates runner
images, so a future image update could reintroduce this failure.

## Windows-specific rules

- App icon: `brand-kit/icons/windows/InterlinedList.ico` (set via ApplicationIcon in .csproj)
- Title bar: `WindowChrome` (custom chrome, native resize); deep-teal `#0C2C3A`
- Window controls: right-aligned min / max / close; close highlights red on hover
- Card corners: 4px (`CornerRadius="4"`)
- Post card left edge: 4px wide, colored by stream type (teal / green / amber)
- Post card left edge: 4px wide; teal by default, amber once you've Dug that message (there's no server-side "stream type" to color by — see API integration)
- Dark mode: read from HKCU registry at launch + listen via `SystemEvents.UserPreferenceChanged`

## Developing on macOS
Expand Down
Binary file added InterlinedList.Package/Images/SplashScreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added InterlinedList.Package/Images/Square44x44Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added InterlinedList.Package/Images/StoreLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions InterlinedList.Package/InterlinedList.Package.wapproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>08FF27C8-1F81-4A8F-B4B4-F7471E74710A</ProjectGuid>
<!-- Must match a UAP platform SDK actually installed on the build machine
(GitHub's windows-latest runner has 10.0.26100.0 and 10.0.10240.0, not
every version — confirmed in CI, don't assume one is present). -->
<TargetPlatformVersion>10.0.26100.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>15</MinimumVisualStudioVersion>
<DefaultLanguage>en-US</DefaultLanguage>
<AppxPackageDir>AppPackages\</AppxPackageDir>
<AppxBundle>Always</AppxBundle>
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
<AppInstallerUpdateFrequency>1</AppInstallerUpdateFrequency>
<AppInstallerCheckForUpdateFrequency>OnApplicationRun</AppInstallerCheckForUpdateFrequency>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<AppxSymbolStorePath></AppxSymbolStorePath>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x64</Platform>
<ProjectTypeGuids>{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<EntryPointProjectUniqueName>..\InterlinedList\InterlinedList.csproj</EntryPointProjectUniqueName>
</PropertyGroup>

<Import Project="$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\Microsoft.DesktopBridge.props" />

<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>

<ItemGroup>
<Content Include="Images\Square44x44Logo.png" />
<Content Include="Images\Square150x150Logo.png" />
<Content Include="Images\StoreLogo.png" />
<Content Include="Images\SplashScreen.png" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\InterlinedList\InterlinedList.csproj">
<Name>InterlinedList</Name>
</ProjectReference>
</ItemGroup>

<Import Project="$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\Microsoft.DesktopBridge.targets" />
</Project>
Loading
Loading