diff options
| author | Adam Malczewski <[email protected]> | 2026-06-28 13:18:25 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-28 14:41:47 +0900 |
| commit | 0f1b04bd7976ea416f11d83e4b7b78cf537bfdf3 (patch) | |
| tree | b198da2719987ffd58e9c2633fc36684cf2ad316 /src/features/concurrency/ui/AutoReduceBanner.svelte | |
| parent | a59200e786f7d97d7ba5b9cd2bee9ffef531dac2 (diff) | |
| download | dispatch-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.svelte | 81 |
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> |
