fix(translate): replace fromStore+ with +subscribe to prevent reactive loop, add runComplete flag, show error_message, collapsible run results

- TranslationRunGlobalIndicator: replace fromStore + 6x  with
  (() => store.subscribe()) to prevent accumulating render_effect
  instances via createSubscriber that caused infinite Svelte reactive flush
- translate/[id]/+page.svelte: add runComplete flag to hide progress bar
  after completion without touching the store; add collapsible run detail
  cards with status badge, counts, and error_message; filter invalid runs
- translationRun.js: remove stale guard (no longer needed)
- Sidebar.svelte: fix mobile overlay close on route change
- docs/adr/ADR-0007-rejected-fromStore-derived.md: document REJECTED pattern
This commit is contained in:
2026-05-18 11:43:27 +03:00
parent ee33f1d7fb
commit 59aedbe12c
5 changed files with 149 additions and 37 deletions

View File

@@ -187,12 +187,11 @@
closeMobile();
}
// Close mobile overlay on route change
// Close mobile overlay on route change — depends only on page.url.pathname, NOT isMobileOpen.
// Otherwise clicking the burger (which sets isMobileOpen=true) would immediately close it.
$effect(() => {
if (isMobileOpen) {
page.url.pathname;
closeMobile();
}
const _pathname = page.url.pathname;
closeMobile();
});
onMount(() => {

View File

@@ -9,7 +9,6 @@
<!-- @UX_FEEDBACK Rich stats during active run; compact auto-dismiss for terminal states -->
<script>
import { translationRunStore, stopTranslationRun } from '$lib/stores/translationRun.js';
import { fromStore } from 'svelte/store';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { t } from '$lib/i18n';
@@ -17,7 +16,17 @@
import { cancelRun } from '$lib/api/translate.js';
import { addToast } from '$lib/toasts.js';
const runState = fromStore(translationRunStore);
// Subscribe to store via $effect + subscribe (NOT fromStore + $derived).
// @REJECTED fromStore + multiple $derived(runState.current.xxx) creates render_effect
// instances via createSubscriber on every $derived re-evaluation. Over time these
// accumulate and trigger an infinite Svelte reactive flush loop after ~300 polls.
// @RATIONALE $effect(() => store.subscribe(s => state = s)) creates exactly ONE
// subscription per component lifecycle — no accumulating render_effects.
let storeState = $state({ uxState: 'idle', totalRecords: 0, successfulRecords: 0, failedRecords: 0, skippedRecords: 0, progressPct: 0, runId: null, jobId: null });
$effect(() => {
const unsub = translationRunStore.subscribe(s => { storeState = s; });
return () => unsub();
});
// Hide on the translate page itself to avoid duplication
let isOnTranslatePage = $derived(
@@ -29,7 +38,7 @@
let dismissed = $state(false);
let isCancelling = $state(false);
let uxState = $derived(runState.current?.uxState || 'idle');
let uxState = $derived(storeState?.uxState || 'idle');
let show = $derived.by(() => {
if (dismissed) return false;
@@ -52,11 +61,11 @@
return '';
});
let totalRecords = $derived(runState.current?.totalRecords || 0);
let successfulRecords = $derived(runState.current?.successfulRecords || 0);
let failedRecords = $derived(runState.current?.failedRecords || 0);
let skippedRecords = $derived(runState.current?.skippedRecords || 0);
let progressPct = $derived(runState.current?.progressPct || 0);
let totalRecords = $derived(storeState?.totalRecords || 0);
let successfulRecords = $derived(storeState?.successfulRecords || 0);
let failedRecords = $derived(storeState?.failedRecords || 0);
let skippedRecords = $derived(storeState?.skippedRecords || 0);
let progressPct = $derived(storeState?.progressPct || 0);
// Auto-dismiss terminal states after a few seconds
$effect(() => {
@@ -78,7 +87,7 @@
});
function handleClick() {
const jobId = runState.current?.jobId;
const jobId = storeState?.jobId;
if (jobId) {
goto(`/translate/${jobId}`);
}
@@ -86,7 +95,7 @@
async function handleCancel(e) {
e.stopPropagation();
const runId = runState.current?.runId;
const runId = storeState?.runId;
if (!runId || isCancelling) return;
isCancelling = true;
try {