Extract state management from inline health page into HealthCenterModel:
- HealthCenterModel.svelte.ts (new): hosts all state atoms (),
derived values (), and core actions (load, filter, delete)
- health page reduced from ~120 to ~27 lines of script — thin shell
delegating to model; only DOM/template concerns remain
- Integration test updated for model-based architecture
- HealthCenterModel.test.ts (new): model invariant tests
Extract all state management from the inline page into a dedicated
DatasetReviewModel class following the Screen Model pattern:
- DatasetReviewModel.svelte.ts (new): hosts all state atoms (),
derived values (), and core actions (load, submit, export)
- review-workspace-helpers.ts moved from routes/ to /helpers/
- useReviewSession.ts moved from routes/ to /api/dataset-review/
- DatasetReviewModel.test.ts (new): model invariant tests
- [id]/+page.svelte: reduced from ~380 to ~190 lines — thin shell
delegating to model; only navigation/DOM concerns remain inline
- Old files deleted: routes/datasets/review/{review-workspace-helpers,useReviewSession}.ts
- Updated ux test for new model-based architecture
- Add FetchOptions.signal for request cancellation (timeout or unmount)
- Propagate signal to native fetch() in fetchApi, fetchApiBlob, postApi,
requestApi, patchApi, putApi, and deleteApi
- Export API_REQUEST_TIMEOUT constant (30s default)
- Add @INVARIANT for signal propagation contract
- Add @RATIONALE documenting the anti-loop protocol motivation
Add support for regex-based dictionary entries across the full stack:
Backend:
- DictionaryEntry model: add is_regex column (Boolean, default False)
- DictionaryEntryCRUD: validate regex on add_entry(), compile on creation
- _enforce_dictionary: match by regex pattern when is_regex=True
- dictionary_filter: support is_regex in filter/query
- metrics: include is_regex entries in metrics calculations
- Alembic migration: 20260604_add_is_regex_to_dictionary_entries
- Merge migration: 351afb8f961a (merge is_regex + composite index heads)
Frontend:
- DictionaryDetailModel: add is_regex field to DictionaryEntry interface,
EditForm, and addForm; sync edit/add form state with backend schema
- Add composite index ix_translation_records_run_source_hash
for NOT EXISTS dedup subquery performance + Alembic migration
- Remove duplicate #endregion in orchestrator.py (INV_3)
- Replace hardcoded RU fallback 'Статус' with 'Status'
- Add early return guard to loadMoreRecords()
- Show records summary always when recordsTotal > 0
Backend:
- deduplicate param in GET /runs/{id}/records — NOT EXISTS subquery
with (created_at, id) tiebreaker for one row per source_hash
- source_data and source_hash added to records JSON response
- fix missing status import in _run_history_routes.py (NameError)
- fix tab/space mixup in orchestrator_aggregator.py
- remove duplicated @RELATION edges in metrics.py
- fix #region/#endregion style and @PURPOSE→@BRIEF in metrics.py
Frontend:
- RunTabContent: summary metrics bar (fetchJobMetrics) with HelpTooltips
- RunTabContent: trigger_type badge, duration, cache rate in run rows
- TranslationRunResult: deduplicate=true by default, paginated Load more
- TranslationRunResult: per-language token_count and estimated_cost
- TranslationRunResult: collapsible batch breakdown with timing
- TranslationRunResult: Bulk Replace button in header and records table
- TranslationRunResult: source_data key values shown under source text
i18n: all new keys in EN + RU (load_more_records, showing_records,
sum_*, help_sum_*, trigger_*, batch_*, duration_label, cost_label,
cache_rate, load_more_records)
- Add typescript-eslint parser for .svelte.ts and <script lang='ts'>
- Disable no-console (CoT logging is intentional)
- Downgrade require-each-key and no-navigation-without-resolve to warn
- Add Svelte 5 runes (, , etc.) as globals for .svelte.ts
- 19 tests covering:
- disableReasoning load from API (true, false, omitted, null)
- disableReasoning save to API (POST new + PUT update)
- databaseDialect save to API (populated, empty, PUT)
- datasourceSearch name lookup on job page load
(found by ID, found by string ID, not found, no ID, API error)
- uxState transitions: loading→configured, idle on job fetch
failure, idle on Promise.all entry failure
Also changed datasourceSearch lookup from fire-and-forget .then()
to awaited try/catch — guarantees name is populated before
uxState transitions to 'configured'.
When opening a saved translation job, the datasource search input was empty
because datasourceSearch was never populated from the loaded job data.
The raw datasourceId was loaded correctly, but the display name
("table_name (database · dialect)") was missing.
Added fetchDatasources lookup in loadInitialData after environmentId is set:
finds the matching datasource by ID in the list and sets datasourceSearch
to the same format used in selectDatasource().
- PolicyForm.svelte: replaced raw inputs/selects/buttons with /ui atoms
(Button, Input, Select), added i18n for all labels and messages
- Automation page: replaced manual layout with PageHeader/Card/EmptyState
atoms, added i18n everywhere, stripped debug logging
- Added i18n keys for en/ru (settings: 45 new keys, validation: 1 new key)
- Fixed validation new page description to use dedicated i18n key
Button.onclick, Input.id, Select.id were declared without defaults
(implicitly undefined) which could cause Svelte 5 warnings or incorrect
() destructuring behavior. Set explicit = undefined.
- HIGH: disableReasoning was declared as and bound in UI but never
loaded from API (loadInitialData) nor sent in save payload. Checkbox was
decorative — value always defaulted to false and never persisted.
Backend fully supports disable_reasoning column + schema + runtime logic.
- MEDIUM: databaseDialect was loaded from API and displayed in UI but never
returned in save payload, causing unnecessary back-end re-detection on
every save.
- Documents includeSourceReference as known limitation (no backend column).
Three fields were never loaded from backend job data:
- targetLanguageColumn (job.target_language_column)
- targetSourceColumn (job.target_source_column)
- targetSourceLanguageColumn (job.target_source_language_column)
Caused empty inputs in Target Config tab after page load.
Also added missing target_source_language_column to save payload.
@RATIONALE The model's loadJob() method was incomplete — it only
loaded targetColumn but omitted the other three target mapping fields.
Save payload also omitted target_source_language_column.
Verification: browser reconfirmed — all 4 target columns pre-filled
with saved values after reload.
Root cause: onchange on native <select> passes a DOM Event.
The parent handler used e.detail, which is always 0 for native events.
Sequence:
1. bind:value sets environmentId correctly
2. onchange fires -> handleEnvChange(0) overwrites to falsy
3. <select> re-renders showing 'Select environment...'
Fix:
- ConfigTabForm: onchange now passes e.target.value (the actual envId)
- +page.svelte: callback receives the envId string directly, not e.detail
@RATIONALE bind:value on <select> fires on the same change event.
The handler must not re-set the same bindable to a stale value.
Verification: browser reconfirmed — ss-dev stays selected after click.
Following the documented pattern from 27435e4 (bug report:
docs/bug-reports/2026-06-02-svelte5-proxy-store-t-undefined.md).
$t.x references in .svelte.ts files are fragile — Svelte 5 compiler
can fail to detect the Proxy-based t store as subscribable, causing
ReferenceError: $t is not defined at runtime.
Changed 3 model files:
- DashboardDetailModel.svelte.ts — 8 $t.dashboard?.x → getT()?.dashboard?.x
- TranslateHistoryModel.svelte.ts — 4 $t.translate?.run?.x → getT()?.translate?.run?.x
- ValidationTasksListModel.svelte.ts — 2 $t.validation?.x → getT()?.validation?.x
All imports changed from { t } to { getT } (or { _, getT } for
TranslateHistoryModel which already used _() for keyed lookups).
@RATIONALE getT() returns the plain translation object directly,
bypassing the Proxy store entirely — this avoids Svelte 5's fragile
static store-detection on Proxy objects.
Verification: npm run build clean, browser renders for all 4 affected
routes with zero console errors.
The export from i18n is a Proxy that supports property access (t.dashboard)
and .subscribe(), but is NOT callable as t(key). Passing it as a function
argument to getPathLabel() and getTriggerLabel() caused:
TypeError: tFn is not a function at getPathLabel
Fix: import the callable translation function (key: string) => string
and pass that instead. The import is kept for template usage (t.nav?.home).
@RATIONALE Svelte 5 i18n Proxy () supports property access but not
function calls. The function is the correct callable formatter.
Svelte 5 compiler fails to detect Proxy-based i18n store `t` as a store
in deeply nested/complex templates and in `.svelte.ts` model files,
generating `ReferenceError: $t is not defined` at runtime.
Fix: replace `$t.` references with:
- Template: `_t.` where `const _t = $derived(getT())`
- Script: `getT()?.` direct function call
Applied to:
- translate/[id]/+page.svelte (the failing page)
- TranslationJobModel.svelte.ts ($derived.by() in `tabs`)
- 12 translate components (ConfigTabForm, ScheduleConfig,
TranslationPreview, TargetTabForm, RunTabContent, BulkReplaceModal,
TranslationRunProgress, TranslationRunResult, TermCorrectionPopup,
BulkCorrectionSidebar, TranslationMetricsDashboard, CorrectionCell,
TranslationRunGlobalIndicator)
Verified all 5 tabs render correctly in browser:
Config, Preview, Target, Run, Schedule.
Build clean, 698/698 tests pass, 0 color violations.
Problem: closing and reopening the translate job page (or tab close+reopen)
lost the progress bar because the in-memory store starts fresh. The backend
keeps translating, but the UI shows no progress.
Fix:
- Store: save {runId, jobId, isFullRun} to sessionStorage on startTranslationRun()
via _saveToSessionStorage()
- Store: add reconnectToRun() — re-establishes WS without resetting store state,
preserving any progress data already received
- Store: add getStoredActiveRun() — public reader for sessionStorage
- Store: clear sessionStorage via _clearSessionStorage() on terminal states
(added to _cleanup() called by WS terminal, max-reconnects, stop, reset)
- Page: add that checks getStoredActiveRun() after job loads
and calls reconnectToRun() to pick up the live progress
Edge cases handled:
- Store already connected to same run → skip (no duplicate WS)
- sessionStorage unavailable → silent catch
- SPA navigation → store already has the run, WS still connected → skip
- Page reload → sessionStorage has the run, reconnects
- Tab close + reopen → same as reload
- Run completes while away → sessionStorage cleared by _cleanup()
- Run already finished → getStoredActiveRun() returns null → skip
Build ✅ 698 tests ✅
QA found critical regression: after removing HTTP polling, a WS
disconnect left the UI permanently stuck (uxState='running' forever).
Fixes:
- WS reconnect: up to 3 attempts with 10s backoff on close (non-1000/1005)
- App timeout: 600s (10 min) max — transitions to failed if stuck
- Stale log message: removed 'falling back to polling' (no longer exists)
- Contract header: updated @BRIEF, restored @UX_STATE/@UX_FEEDBACK/@UX_RECOVERY
- cleanup(): unified WS close + timer clear (used by stop, timeout, terminal)
- onclose handler: triggers reconnect via _handleWsFailure()
- onerror handler: defers to onclose (fires after error)
- onopen handler: resets _reconnectCount on successful connect
- Terminal states: call _cleanup() to stop reconnect timer + timeout
Build ✅ 698 tests ✅
Previously startTranslationRun() used both WebSocket + 2s HTTP polling.
After WS debug logging was added, the HTTP fallback is no longer needed:
- WS streams status every 1s from backend
- WS handler already detects terminal states + fires onComplete
- WS errors are now logged (onerror → console.warn)
Removed: pollStatus(), _pollingInterval, _pollCount, MAX_POLLS,
fetchRunStatus import, setInterval in startTranslationRun,
clearInterval in stopTranslationRun.
Build ✅ 698 tests ✅
Backend:
- TranslationRun model: add cache_hits Integer column (default 0)
- TranslationRunResponse schema: add cache_hits field
- _helpers.py/_run_list_routes.py/orchestrator_aggregator.py: include
cache_hits in all run API responses
- _batch_proc.py: count pre_rows served from translation cache per
batch, return cache_hits in batch result
- executor.py: accumulate cache_hits across batches, persist to run
- Alembic migration: dabc9709 — add cache_hits column to translation_runs
Frontend:
- translationRun.svelte.ts: add cacheHits to store state, WS handler,
polling handler
- TranslationRunProgress.svelte: 5-col stats grid with purple Cache card
- TranslationRunGlobalIndicator.svelte: 5-col stats with Cache
- TranslationRunResult.svelte: 5-col detail stats with Cache card
- History page: cache_hits shown in run list row + detail panel
Visual: cache hits shown in purple alongside green/yellow/red metrics
(total/success/failed/skipped). Visible during run + in history.
Tests: backend 69/69 translate tests ✅, frontend 698/698 tests ✅,
frontend build ✅
- DashboardHealthItem schema: add run_id, policy_id, execution_path,
issues_count, timings, token_usage, screenshot_paths, chunk_count,
dashboard_name fields for v2 LLM validation reporting.
- HealthService: populate v2 fields from validation run records.
- ValidationService: update to support v2 validation run fields.
- validation_tasks route: minor fix for v2 field handling.
- tailwind.config.js: refactor design tokens — add success/warning/info
palettes, surface/border/text hierarchy, semantic action colors.
- Agent configs: update svelte-coder and semantics-svelte for
.svelte.ts runes and Screen Model patterns.
downloadSkippedCsv and downloadFailedCsv call api.fetchApiBlob()
but it was not exposed on the api object — causing
"o.fetchApiBlob is not a function" at runtime.
Deleted stale .js files (taskService, toolsService, report_page.contract.test)
7 .svelte.js test files intentionally preserved (Svelte 5 convention)
Final metrics:
- Production JS files: 0
- Production TS/TSX/Svelte files: all
- npm run build ✅
- npm run test ✅ (645 passed, 6 pre-existing)