Translate components:
- BulkReplaceModal: add dictionary selection dropdown to save replacements
directly to a dictionary after applying bulk find-replace
- CorrectionCell: improve inline edit UX with better state handling
- TermCorrectionPopup: enhanced popup for term corrections
- ConfigTabForm: update form field bindings
- RunTabContent: minor layout adjustments
- ScheduleConfig: improved schedule configuration UI
- TranslationPreview, TranslationRunGlobalIndicator, TranslationRunProgress:
UX polish and state management improvements
BackupManager:
- Add AbortSignal.timeout(30s) to prevent infinite loading state
- Add onDestroy AbortController cleanup to prevent stale state
- Add error toasts for failure states (was missing — state hung forever)
- Import API_REQUEST_TIMEOUT from api.ts
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
- Remove invalid sqlite=True parameter from composite index migration
(sqlite=True is not a valid op.create_index parameter)
- Fix test_assistant_api assertions for updated response format
- Fix test_git_status_route edge case assertions
- Fix test_audit_service expected value after metric changes
- Fix test_session_repository assertion after store refactor
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
Extract inline edit, bulk find-replace, and override language endpoints from
_run_routes.py into dedicated _run_edit_routes.py and _run_history_routes.py
modules to reduce module complexity below INV_7 limits.
Changes:
- _run_routes.py now only handles execution, retry, and cancel endpoints
- _run_edit_routes.py (new): inline edit, bulk find-replace, override language
- __init__.py registers the new route modules
- schemas/translate.py: add imports for extracted endpoints
BaseHTTPMiddleware (Starlette 0.50.0) uses anyio.create_task_group() internally,
creating separate asyncio tasks for dispatch vs call_next. ContextVars set in
dispatch() were not visible to outer middleware like log_requests.
Converting to raw ASGI middleware ensures trace_id is seeded in the root task
context, visible to ALL middleware layers.
Key changes:
- Replace BaseHTTPMiddleware with raw ASGI __call__(self, scope, receive, send)
- UUID v4 validation: check parsed.version == 4 explicitly instead of relying
on uuid.UUID(hex=..., version=4) which silently mutates non-v4 UUIDs
- Add @RATIONALE and @REJECTED tags per semantics-core protocol
- Update app.py comment to document the architectural decision
- 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.
- Removes source_dialect and target_dialect from TerminologyDictionary model,
schemas, routes, helper serialization, and all tests
- Adds allowed_languages config field (BCP-47 language codes) to GlobalSettings
with validation, consolidated settings response, and API endpoint
- Adds alembic migration f1a2b3c4d5e6 to drop the two columns
- 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.
Documents the 12-iteration binary search, root cause analysis,
fix pattern, and lessons learned for the Svelte 5 store-detection
failure on Proxy objects with subscribe getter.
Includes recommended follow-ups: ESLint rule, document the _t
pattern in semantics-svelte skill, audit other complex-template
pages for the same latent bug.
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 ✅
1. dict_snapshot_hash now includes entry count + max(updated_at)
per dictionary. Previously only hashed dictionary IDs, meaning
edits to dictionary entries did NOT invalidate the translation
cache. Stale cached translations could be served after editing
dictionary entries. (HIGH severity)
2. _batch_insert.py now uses SQLGenerator.generate_batch() with
500-row chunking instead of a single massive INSERT statement.
Prevents potential SQL size limit issues with large batches or
many target languages. (LOW severity)
3. Fixed same bug in preview_response_parser.py —
compute_dict_snapshot_hash had identical ID-only hash flaw.
Tests: 69/69 translate tests pass.
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 ✅