From ba99c93eb67399a9c9809758db73addfa6686560 Mon Sep 17 00:00:00 2001 From: Adron Hall Date: Fri, 31 Jul 2026 17:48:08 -0700 Subject: [PATCH] Add account deletion; effective feature parity reached MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch 7 (branch feature/parity-v5). Builds clean Debug + Release. - Account deletion — a guarded "Danger zone" in Settings (type your exact username to enable) -> POST /api/user/delete {username,email}, then clears the token and returns the shell to the login screen via a new Navigator.OnLoggedOut hook. This closes the last non-blocked user feature. Remaining gaps are genuinely blocked, not unbuilt: Materialize (opaque {source} request), per-list schema DSL (not reverse-engineered), GitHub (account not linked + undocumented shapes), DM inbox-folder (needs real DMs to learn shape), billing (cookie-only -> web handoff). See the-gaps.md "Effective feature parity reached". Co-Authored-By: Claude Opus 4.8 (1M context) --- InterlinedList/MainWindow.xaml.cs | 2 + InterlinedList/Services/Navigator.cs | 5 +++ .../ViewModels/SettingsViewModel.cs | 33 +++++++++++++++++ InterlinedList/Views/SettingsView.xaml | 27 ++++++++++++++ the-gaps.md | 37 +++++++++++++++++++ 5 files changed, 104 insertions(+) diff --git a/InterlinedList/MainWindow.xaml.cs b/InterlinedList/MainWindow.xaml.cs index ad0a571..e26f596 100644 --- a/InterlinedList/MainWindow.xaml.cs +++ b/InterlinedList/MainWindow.xaml.cs @@ -26,6 +26,8 @@ public MainWindow() // Feed/search cards raise this to open a user's profile in the People tab. Navigator.OnOpenProfile = OpenProfile; + // Account deletion (Settings) routes back to the login screen through here. + Navigator.OnLoggedOut = () => LoggedOut?.Invoke(this, EventArgs.Empty); StartClock(); diff --git a/InterlinedList/Services/Navigator.cs b/InterlinedList/Services/Navigator.cs index 5e05305..7b083e6 100644 --- a/InterlinedList/Services/Navigator.cs +++ b/InterlinedList/Services/Navigator.cs @@ -10,9 +10,14 @@ public static class Navigator { public static Action? OnOpenProfile { get; set; } + /// Set by the shell; lets a center view (e.g. account deletion) return the app to the login screen. + public static Action? OnLoggedOut { get; set; } + public static void OpenProfile(string username) { if (!string.IsNullOrWhiteSpace(username)) OnOpenProfile?.Invoke(username); } + + public static void RequestLogout() => OnLoggedOut?.Invoke(); } diff --git a/InterlinedList/ViewModels/SettingsViewModel.cs b/InterlinedList/ViewModels/SettingsViewModel.cs index dafc684..db8b710 100644 --- a/InterlinedList/ViewModels/SettingsViewModel.cs +++ b/InterlinedList/ViewModels/SettingsViewModel.cs @@ -44,6 +44,10 @@ public partial class SettingsViewModel : ObservableObject [ObservableProperty] private string newEmail = ""; + // Account deletion requires typing your exact username as a guard. + [ObservableProperty] + private string deleteConfirmUsername = ""; + public SettingsViewModel(SessionService session) { _session = session; @@ -234,6 +238,35 @@ private async Task UnmuteAsync(ModeratedUser u) [RelayCommand] private Task ExportFollowsAsync() => ExportCsvAsync(_session.Api.ExportFollowsCsvAsync, "follows.csv"); + // Destructive. Enabled only when the typed username matches exactly. On + // success the token is cleared and the shell returns to the login screen. + private bool CanDeleteAccount() => + _session.CurrentUser is { } u && + string.Equals(DeleteConfirmUsername.Trim(), u.Username, StringComparison.Ordinal); + + [RelayCommand(CanExecute = nameof(CanDeleteAccount))] + private async Task DeleteAccountAsync() + { + if (_session.CurrentUser is not { } user) return; + IsBusy = true; + try + { + await _session.Api.DeleteAccountAsync(user.Username, user.Email); + _session.Logout(); + Navigator.RequestLogout(); + } + catch (InterlinedApiException ex) + { + ErrorMessage = ex.Message; + } + finally + { + IsBusy = false; + } + } + + partial void OnDeleteConfirmUsernameChanged(string value) => DeleteAccountCommand.NotifyCanExecuteChanged(); + private async Task ExportCsvAsync(Func> fetch, string defaultFileName) { try diff --git a/InterlinedList/Views/SettingsView.xaml b/InterlinedList/Views/SettingsView.xaml index c869736..48996d0 100644 --- a/InterlinedList/Views/SettingsView.xaml +++ b/InterlinedList/Views/SettingsView.xaml @@ -563,6 +563,33 @@ + + + + + + + + + + diff --git a/the-gaps.md b/the-gaps.md index ba8900b..081c89f 100644 --- a/the-gaps.md +++ b/the-gaps.md @@ -187,6 +187,43 @@ per-list schema/columns, a full standalone notifications view. --- +## Progress — Session 7 (2026-08-01) — account deletion + parity assessment + +- ✅ **Account deletion** — a guarded "Danger zone" in Settings (type your exact + username to enable), calling `POST /api/user/delete {username,email}`, then + clearing the token and returning the shell to the login screen (via a new + `Navigator.OnLoggedOut` hook). Builds clean Debug + Release. + +### ✅ Effective feature parity reached +Every core, verifiable, user-facing product surface is now built (feed + +media + replies + scheduling, DMs with media/polling, People + full follow +graph + moderation, Settings incl. sessions/exports/account, Lists + rows + +folders + sharing + watchers, Documents + folders + sharing + collaborators, +Organizations + members, Search, Connected Accounts, auth self-service). + +**The only remaining gaps are genuinely blocked, not merely unbuilt:** +- **Materialize** ("Create from…") — the API request is a single opaque + `source` string with no documented structure; can't be built reliably without + more API detail. *API-blocked.* +- **Per-list schema/columns DSL** (`PUT /api/lists/{id}/schema`) — only partially + reverse-engineered; schema-less rows are the confirmed-working path (see + CLAUDE.md). *API-blocked.* +- **GitHub issue sync** — the test account has no GitHub linked (every + `/api/github/*` call returns "GitHub account not linked") and the wire shapes + aren't documented, so it can't be verified. *Verification-blocked* — build it + once an account with GitHub linked is available. +- **DM inbox-folder view** — `GET /api/dm` item shape can't be learned without + sending real DMs to a real person; DMs already work via the recipients list. + *Low value / verification-blocked.* +- **Billing UI** — Stripe endpoints are cookie-session-only; handled by the + "Manage account on the web" handoff. *Auth-model-blocked (by design.)* + +To close the verification-blocked items, provision **(a)** a second test account +(two-sided DM/follow/moderation/sharing checks) and **(b)** GitHub linked on a +test account. Everything else is either shipped or API-limited. + +--- + ## 1. Parity snapshot by domain | Domain (product's name) | Web/API has | App has today | Status |