Improve operational UX, recovery flows, and action safety

Implement the approved UX review across Fleet, Betas, Library, deployments, and Settings. Add reviewed promotion and reconciliation plans, atomic queued-removal cancellation, recoverable beta drafts and launch inputs, accessible controls, and mobile improvements.

Validated with 473 tests, a clean typecheck, production build, and isolated desktop/mobile browser walkthroughs. Include the review and implementation coverage notes.
This commit is contained in:
2026-09-19 21:34:34 -04:00
parent 940f81e6ca
commit 8ac420fe61
83 changed files with 2574 additions and 1190 deletions
@@ -1,13 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { goto } from '$app/navigation';
import type { GithubPluginRelease, PluginUpdate } from '$lib/plugin-github.js';
import Card from './primitives/Card.svelte';
import Button from './primitives/Button.svelte';
import Badge from './primitives/Badge.svelte';
import { toast } from '$ui/toast.js';
let { pluginId, targetPath, disabled = false, onimport }: {
pluginId?: number; targetPath?: string; disabled?: boolean; onimport?: () => void | Promise<void>;
let { pluginId, targetPath, disabled = false, busy = $bindable(false), onimport }: {
pluginId?: number; targetPath?: string; disabled?: boolean; busy?: boolean; onimport?: (result: { pluginId: number; betaUrl: string; alreadyImported: boolean }) => void | Promise<void>;
} = $props();
let url = $state('');
let name = $state('');
@@ -15,10 +16,11 @@
let release = $state<GithubPluginRelease | null>(null);
let assetId = $state('');
let update = $state<PluginUpdate | null>(null);
let busy = $state(false);
let checking = $state(false);
let error = $state('');
let success = $state<{ pluginId: number; betaUrl: string; sha256: string; alreadyImported: boolean } | null>(null);
let active = true;
onDestroy(() => { active = false; });
function showRelease(value: GithubPluginRelease) {
release = value;
@@ -58,6 +60,7 @@
async function importRelease(startBeta: boolean) {
if (!release || !assetId || busy || disabled) return;
const startPath = window.location.pathname;
busy = true; error = ''; success = null;
try {
const response = await fetch('/api/plugins/github/import', {
@@ -67,11 +70,12 @@
});
const body = await response.json();
if (!response.ok) throw new Error(body.error ?? 'Import failed.');
if (!active) { toast('Plugin imported. Review its version when ready.', { variant: 'success', href: `/plugins/${body.pluginId}` }); return; }
success = body;
await onimport?.();
if (startBeta) await goto(body.betaUrl);
await onimport?.(body);
if (startBeta && window.location.pathname === startPath) await goto(body.betaUrl);
else if (pluginId) await checkUpdates();
} catch (e) { error = (e as Error).message; }
} catch (e) { error = (e as Error).message; if (!active) toast(`Plugin import failed: ${error}`, { variant: 'danger', href: '/plugins' }); }
finally { busy = false; }
}
</script>
@@ -172,4 +176,5 @@
.success { display: block; padding: var(--space-3); background: var(--surface-sunken); border-radius: var(--radius-md); font-size: var(--text-sm); }
.success strong, .checksum { display: block; margin-bottom: var(--space-2); }
.checksum { overflow-wrap: anywhere; font: var(--text-xs) var(--font-mono); margin-top: var(--space-2); }
input:not([type='checkbox']):not([type='file']), select { border-color: var(--border-control); }
</style>