From d626cb58b1dd65dfac87beefa3e73f3ef53e657b Mon Sep 17 00:00:00 2001 From: xarmian Date: Mon, 6 Jul 2026 14:17:20 +0000 Subject: [PATCH] feat(web): dedicated /console/deleted-workspaces page Add a full-page table of the caller's soft-deleted workspaces (name, deleted date, days-left, Restore per row) backed by api.workspaces.listDeleted / restore. Restore removes the row and toasts success; loading/error/empty states included. Wire a "Deleted workspaces" link into the console layout nav. Closes TASK-1975 Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST --- web/src/routes/console/+layout.svelte | 9 + .../console/deleted-workspaces/+page.svelte | 335 ++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 web/src/routes/console/deleted-workspaces/+page.svelte diff --git a/web/src/routes/console/+layout.svelte b/web/src/routes/console/+layout.svelte index b44312a0..aa84b867 100644 --- a/web/src/routes/console/+layout.svelte +++ b/web/src/routes/console/+layout.svelte @@ -135,6 +135,15 @@ > Settings + + Deleted workspaces + {#if authStore.cloudMode} + import { onMount } from 'svelte'; + import { api } from '$lib/api/client'; + import { toastStore } from '$lib/stores/toast.svelte'; + import type { DeletedWorkspace } from '$lib/types'; + + // Deleted workspaces page (TASK-1975). Lists the caller's soft-deleted + // workspaces still inside the purge window and lets them restore any of + // them before permanent deletion. Restore un-deletes the workspace and + // drops the row from the table. + + let workspaces = $state([]); + let loading = $state(true); + let error = $state(''); + + // Per-slug pending flag so a Restore button disables while its request + // is in flight without blocking the other rows. + let restoring = $state>({}); + + async function load() { + loading = true; + error = ''; + try { + const result = await api.workspaces.listDeleted(); + workspaces = Array.isArray(result) ? result : []; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to load deleted workspaces'; + } finally { + loading = false; + } + } + + async function restore(ws: DeletedWorkspace) { + if (restoring[ws.slug]) return; + restoring[ws.slug] = true; + try { + await api.workspaces.restore(ws.slug); + workspaces = workspaces.filter((w) => w.slug !== ws.slug); + toastStore.show(`Restored "${ws.name}"`, 'success'); + } catch (e) { + const msg = e instanceof Error ? e.message : 'Failed to restore workspace'; + toastStore.show(msg, 'error'); + } finally { + restoring[ws.slug] = false; + } + } + + function formatDate(dateStr: string | undefined | null): string { + if (!dateStr) return '—'; + const d = new Date(dateStr); + if (Number.isNaN(d.getTime())) return '—'; + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + } + + // "N days left" until permanent deletion. When the window is nearly up + // (0 days) we swap in a plain-language warning instead of "0 days left". + function daysLeftLabel(days: number): string { + if (days <= 0) return 'Permanently deleted soon'; + if (days === 1) return '1 day left'; + return `${days} days left`; + } + + // Low-window rows get a warning accent so the deadline stands out. + function isLowWindow(days: number): boolean { + return days <= 3; + } + + onMount(load); + + + + Deleted workspaces - Pad + + +
+ + + {#if loading} +
Loading…
+ {:else if error} +
+

{error}

+ +
+ {:else if workspaces.length === 0} +
+

No recently deleted workspaces

+

+ Workspaces you delete will appear here until they're permanently removed. +

+
+ {:else} +
+ + + + + + + + + + + {#each workspaces as ws (ws.slug)} + + + + + + + {/each} + +
NameDeletedTime leftActions
+ {ws.name} + {ws.slug} + + {formatDate(ws.deleted_at)} + + + {daysLeftLabel(ws.days_left)} + + + +
+
+ {/if} +
+ +