summaryrefslogtreecommitdiffhomepage
path: root/src/features/concurrency/ui/AutoReduceBanner.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-28 13:18:25 +0900
committerAdam Malczewski <[email protected]>2026-06-28 14:41:47 +0900
commit0f1b04bd7976ea416f11d83e4b7b78cf537bfdf3 (patch)
treeb198da2719987ffd58e9c2633fc36684cf2ad316 /src/features/concurrency/ui/AutoReduceBanner.svelte
parenta59200e786f7d97d7ba5b9cd2bee9ffef531dac2 (diff)
downloaddispatch-web-0f1b04bd7976ea416f11d83e4b7b78cf537bfdf3.tar.gz
dispatch-web-0f1b04bd7976ea416f11d83e4b7b78cf537bfdf3.zip
feat(concurrency): configurable cooldown + adaptive-headroom auto-reduce banner
Consumes the backend's concurrency-fixes (commit 2d27666) — additive to [email protected], NO version bump. ConcurrencyStatusEntry gains cooldownMs / autoReduced / autoReducedFrom? / notice?; new ConcurrencyCooldownResponse / SetConcurrencyCooldownRequest + GET/PUT /concurrency/cooldown/:providerId. FE: - Pure core (logic/view-model.ts): DEFAULT_COOLDOWN_MS; parseCooldownInput (non-negative int — 0 is valid, unlike the limit); normalizeCooldown; cooldownLabel ("350ms"/"1.2s"/"0ms (off)"); viewConcurrencyStatus extended (cooldown + autoReduce fields; auto-reduce → warning badge, not busy); viewAutoReduce/autoReduceNotices (banner view — prefers backend notice, synthesizes a fallback); summarizeStatus "N auto-reduced" fragment; normalizeConcurrencyStatus coerces new fields (immutable readonly build; autoReducedFrom/notice only when autoReduced===true); normalizeConcurrencyCooldown. - Types (logic/types.ts): re-export the 2 new contract types + ConcurrencyCooldownResult + Get/SaveConcurrencyCooldown ports. - UI: ConcurrencyCooldownRow.svelte (inline-edit cooldown + Save → PUT, seeded via the ChatLimitField pattern); AutoReduceBanner.svelte (dismissible banner — backend notice + "Was N, now M." + "Restore to N"); ConcurrencyView renders the cooldown per status card + the banner section. The banner persists while autoReduced===true, is dismissible, and clears automatically once a poll shows autoReduced===false after a restore PUT. - Store (store.svelte.ts): getConcurrencyCooldown + setConcurrencyCooldown (surface 400/404/503 as ok:false; normalize at the seam) + interface decls. - App.svelte: saveConcurrencyCooldown adapter → ConcurrencyView. - Tests: +47 (view-model cooldown/auto-reduce/normalizers; component banner render, cooldown PUT, restore clears banner, dismiss; store cooldown GET/PUT). Re-synced the file: dep (bun install) + re-mirrored .dispatch/transport-contract. reference.md. typecheck 0/0, 1048 tests green (run twice), biome clean, build OK. Worktree env: an untracked dispatch-backend → backend symlink was created in the worktree parent so the canonical file:../dispatch-backend/... paths resolve (NOT committed — per the §2d/§2j worktree convention).
Diffstat (limited to 'src/features/concurrency/ui/AutoReduceBanner.svelte')
-rw-r--r--src/features/concurrency/ui/AutoReduceBanner.svelte81
1 files changed, 81 insertions, 0 deletions
diff --git a/src/features/concurrency/ui/AutoReduceBanner.svelte b/src/features/concurrency/ui/AutoReduceBanner.svelte
new file mode 100644
index 0000000..132ebc7
--- /dev/null
+++ b/src/features/concurrency/ui/AutoReduceBanner.svelte
@@ -0,0 +1,81 @@
+<script lang="ts">
+ import type { AutoReduceNotice } from "../logic/view-model";
+ import type { RestoreOutcome } from "../logic/types";
+
+ let {
+ notice,
+ onRestore,
+ onDismiss,
+ }: {
+ /** The auto-reduce banner view (providerId + message + from/current limit). */
+ notice: AutoReduceNotice;
+ /**
+ * "Restore to N" — PUT the limit back to `fromLimit`. Returns the outcome so
+ * a FAILED restore surfaces an inline error here (the banner owns its error
+ * display; the parent only refreshes on success).
+ */
+ onRestore: (providerId: string, limit: number) => Promise<RestoreOutcome>;
+ /** Hide this banner locally (persists hidden while autoReduced stays true). */
+ onDismiss: (providerId: string) => void;
+ } = $props();
+
+ let restoring = $state(false);
+ /** Inline restore error (e.g. "Concurrency service not available"); cleared on retry. */
+ let error = $state<string | null>(null);
+
+ async function handleRestore(): Promise<void> {
+ restoring = true;
+ error = null;
+ // The parent PUTs the limit + refreshes status on success; the banner clears
+ // once the next poll shows autoReduced===false. On failure the outcome is
+ // bubbled back here so the error shows inline next to the button.
+ const result = await onRestore(notice.providerId, notice.fromLimit);
+ restoring = false;
+ if (!result.ok) {
+ error = result.error;
+ }
+ }
+</script>
+
+<div
+ class="alert alert-warning flex flex-col gap-2 py-2 text-xs"
+ role="status"
+ data-testid={`auto-reduce-banner-${notice.providerId}`}
+>
+ <div class="flex items-start gap-2">
+ <span class="shrink-0">⚠</span>
+ <div class="flex-1">
+ <p>{notice.message}</p>
+ <p class="opacity-70">
+ Was {notice.fromLimit}, now {notice.currentLimit}.
+ </p>
+ </div>
+ <div class="flex shrink-0 items-center gap-1">
+ <!-- The "Restore to N" text stays visible while loading (only the spinner is
+ prepended) so the button keeps its accessible name during the PUT — a
+ spinner-only button loses its name for screen-reader users. -->
+ <button
+ type="button"
+ class="btn btn-warning btn-xs gap-1"
+ disabled={restoring}
+ onclick={handleRestore}
+ >
+ {#if restoring}
+ <span class="loading loading-spinner loading-xs"></span>
+ {/if}
+ Restore to {notice.fromLimit}
+ </button>
+ <button
+ type="button"
+ class="btn btn-ghost btn-xs"
+ aria-label={`Dismiss auto-reduce notice for ${notice.providerId}`}
+ onclick={() => onDismiss(notice.providerId)}
+ >
+ ✕
+ </button>
+ </div>
+ </div>
+ {#if error}
+ <p class="font-mono text-error" data-testid={`restore-error-${notice.providerId}`}>{error}</p>
+ {/if}
+</div>