From f87092c4a714907144d5976eb6f83090dff2cc88 Mon Sep 17 00:00:00 2001 From: busya Date: Tue, 2 Jun 2026 11:59:57 +0300 Subject: [PATCH] =?UTF-8?q?feat(translate):=20add=20cache=5Fhits=20counter?= =?UTF-8?q?=20=E2=80=94=20backend=20+=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ✅ --- ...e_add_cache_hits_column_to_translation_.py | 40 +++++++++++++++++++ backend/src/api/routes/translate/_helpers.py | 1 + .../api/routes/translate/_run_list_routes.py | 1 + backend/src/models/translate.py | 1 + backend/src/plugins/translate/_batch_proc.py | 2 + backend/src/plugins/translate/executor.py | 4 +- .../translate/orchestrator_aggregator.py | 1 + backend/src/schemas/translate.py | 1 + .../TranslationRunGlobalIndicator.svelte | 7 +++- .../translate/TranslationRunProgress.svelte | 7 +++- .../translate/TranslationRunResult.svelte | 6 ++- .../src/lib/stores/translationRun.svelte.ts | 4 ++ .../src/routes/translate/history/+page.svelte | 7 +++- 13 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 backend/alembic/versions/dabc97097e0e_add_cache_hits_column_to_translation_.py diff --git a/backend/alembic/versions/dabc97097e0e_add_cache_hits_column_to_translation_.py b/backend/alembic/versions/dabc97097e0e_add_cache_hits_column_to_translation_.py new file mode 100644 index 00000000..a2fd5e74 --- /dev/null +++ b/backend/alembic/versions/dabc97097e0e_add_cache_hits_column_to_translation_.py @@ -0,0 +1,40 @@ +"""add cache_hits column to translation_runs + +Revision ID: dabc97097e0e +Revises: ed28d34edde7 +Create Date: 2026-06-02 11:54:03.550164 + +""" +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect + + +# revision identifiers, used by Alembic. +revision: str = 'dabc97097e0e' +down_revision: str | Sequence[str] | None = 'ed28d34edde7' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def _table_exists(table_name: str) -> bool: + conn = op.get_bind() + inspector = inspect(conn) + return table_name in inspector.get_table_names() + + +def upgrade() -> None: + """Add cache_hits column to translation_runs table.""" + if not _table_exists("translation_runs"): + return + op.add_column("translation_runs", sa.Column( + "cache_hits", sa.Integer(), nullable=False, server_default=sa.text("0"), + comment="Number of rows served from translation cache", + )) + + +def downgrade() -> None: + """Remove cache_hits column from translation_runs table.""" + op.drop_column("translation_runs", "cache_hits") diff --git a/backend/src/api/routes/translate/_helpers.py b/backend/src/api/routes/translate/_helpers.py index ab47b4a3..3c843e52 100644 --- a/backend/src/api/routes/translate/_helpers.py +++ b/backend/src/api/routes/translate/_helpers.py @@ -23,6 +23,7 @@ def _run_to_response(run: Any) -> dict: "successful_records": run.successful_records or 0, "failed_records": run.failed_records or 0, "skipped_records": run.skipped_records or 0, + "cache_hits": run.cache_hits or 0, "insert_status": run.insert_status, "superset_execution_id": run.superset_execution_id, "config_snapshot": run.config_snapshot, diff --git a/backend/src/api/routes/translate/_run_list_routes.py b/backend/src/api/routes/translate/_run_list_routes.py index f240ea9e..558a7770 100644 --- a/backend/src/api/routes/translate/_run_list_routes.py +++ b/backend/src/api/routes/translate/_run_list_routes.py @@ -104,6 +104,7 @@ async def list_runs( "successful_records": r.successful_records or 0, "failed_records": r.failed_records or 0, "skipped_records": r.skipped_records or 0, + "cache_hits": r.cache_hits or 0, "insert_status": r.insert_status, "superset_execution_id": r.superset_execution_id, "config_hash": r.config_hash, diff --git a/backend/src/models/translate.py b/backend/src/models/translate.py index 39c64ee9..74c91440 100644 --- a/backend/src/models/translate.py +++ b/backend/src/models/translate.py @@ -81,6 +81,7 @@ class TranslationRun(Base): successful_records = Column(Integer, default=0) failed_records = Column(Integer, default=0) skipped_records = Column(Integer, default=0) + cache_hits = Column(Integer, default=0, comment="Number of rows served from translation cache") insert_status = Column(String, nullable=True, comment="Status of the Superset insert/update operation") superset_execution_id = Column(String, nullable=True, comment="Superset execution/task ID") superset_execution_log = Column(JSON, nullable=True, comment="Superset execution log output") diff --git a/backend/src/plugins/translate/_batch_proc.py b/backend/src/plugins/translate/_batch_proc.py index e5b85435..7a24b05d 100644 --- a/backend/src/plugins/translate/_batch_proc.py +++ b/backend/src/plugins/translate/_batch_proc.py @@ -73,6 +73,8 @@ class BatchProcessingService: llm_rows, pre_rows = self._classify(batch_rows, preview_edits_cache, tls) result["successful"] += self._persist_pre(pre_rows, bid, run_id, tls) + # Count cache hits: pre_rows that have _cached_lang_values (served from cache, not same-lang/approved) + result["cache_hits"] = sum(1 for r in pre_rows if r.get("_cached_lang_values")) if llm_rows: llm_res = self._process_llm(job, run_id, llm_rows, dict_matches, bid, tls) for k in ("successful", "failed", "skipped", "retries"): diff --git a/backend/src/plugins/translate/executor.py b/backend/src/plugins/translate/executor.py index 1d51db0a..e845c048 100644 --- a/backend/src/plugins/translate/executor.py +++ b/backend/src/plugins/translate/executor.py @@ -129,7 +129,7 @@ class TranslationExecutor: target_languages: list[str], language_stats_map: dict[str, TranslationRunLanguageStats] | None = None) -> TranslationRun: """Process all batches: execute, insert to target, check cancellation.""" - successful_records = failed_records = skipped_records = 0 + successful_records = failed_records = skipped_records = cache_hits = 0 for batch_idx, batch_rows in enumerate(batches): batch_result = self._process_batch( job=job, run_id=run.id, batch_index=batch_idx, batch_rows=batch_rows, @@ -138,9 +138,11 @@ class TranslationExecutor: successful_records += batch_result.get("successful", 0) failed_records += batch_result.get("failed", 0) skipped_records += batch_result.get("skipped", 0) + cache_hits += batch_result.get("cache_hits", 0) run.successful_records = successful_records run.failed_records = failed_records run.skipped_records = skipped_records + run.cache_hits = cache_hits self.db.commit() batch_id = batch_result.get("batch_id") diff --git a/backend/src/plugins/translate/orchestrator_aggregator.py b/backend/src/plugins/translate/orchestrator_aggregator.py index 05602c52..1fc6d359 100644 --- a/backend/src/plugins/translate/orchestrator_aggregator.py +++ b/backend/src/plugins/translate/orchestrator_aggregator.py @@ -78,6 +78,7 @@ class TranslationResultAggregator: "successful_records": run.successful_records or 0, "failed_records": run.failed_records or 0, "skipped_records": run.skipped_records or 0, + "cache_hits": run.cache_hits or 0, "insert_status": run.insert_status, "superset_execution_id": run.superset_execution_id, "batch_count": batch_count, diff --git a/backend/src/schemas/translate.py b/backend/src/schemas/translate.py index e1820714..707f179a 100644 --- a/backend/src/schemas/translate.py +++ b/backend/src/schemas/translate.py @@ -406,6 +406,7 @@ class TranslationRunResponse(BaseModel): successful_records: int = 0 failed_records: int = 0 skipped_records: int = 0 + cache_hits: int = 0 insert_status: str | None = None superset_execution_id: str | None = None config_snapshot: dict[str, Any] | None = None diff --git a/frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte b/frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte index 82d5799f..5f0dfaf3 100644 --- a/frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte +++ b/frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte @@ -66,6 +66,7 @@ let successfulRecords = $derived(storeState?.successfulRecords || 0); let failedRecords = $derived(storeState?.failedRecords || 0); let skippedRecords = $derived(storeState?.skippedRecords || 0); + let cacheHits = $derived(storeState?.cacheHits || 0); let progressPct = $derived(storeState?.progressPct || 0); // Auto-dismiss terminal states after a few seconds @@ -154,7 +155,7 @@ -
+
{($t.translate?.run?.total || 'Total')} {totalRecords} @@ -171,6 +172,10 @@ {($t.translate?.run?.skipped || 'Skip')} {skippedRecords}
+
+ Cache + {cacheHits} +
diff --git a/frontend/src/lib/components/translate/TranslationRunProgress.svelte b/frontend/src/lib/components/translate/TranslationRunProgress.svelte index cab057b1..0ec6d35a 100644 --- a/frontend/src/lib/components/translate/TranslationRunProgress.svelte +++ b/frontend/src/lib/components/translate/TranslationRunProgress.svelte @@ -32,6 +32,7 @@ let successfulRecords = $derived(storeSnapshot?.successfulRecords || 0); let failedRecords = $derived(storeSnapshot?.failedRecords || 0); let skippedRecords = $derived(storeSnapshot?.skippedRecords || 0); + let cacheHits = $derived(storeSnapshot?.cacheHits || 0); let progressPct = $derived(storeSnapshot?.progressPct || 0); let batchCount = $derived(storeSnapshot?.batchCount || 0); let insertStatus = $derived(storeSnapshot?.insertStatus || null); @@ -96,7 +97,7 @@ -
+
{$t.translate?.run?.total}

{totalRecords}

@@ -113,6 +114,10 @@ {$t.translate?.run?.skipped}

{skippedRecords}

+
+ Cache +

{cacheHits}

+
diff --git a/frontend/src/lib/components/translate/TranslationRunResult.svelte b/frontend/src/lib/components/translate/TranslationRunResult.svelte index d4940f01..bdb8cb5a 100644 --- a/frontend/src/lib/components/translate/TranslationRunResult.svelte +++ b/frontend/src/lib/components/translate/TranslationRunResult.svelte @@ -214,7 +214,7 @@
-
+

{$t.translate?.run?.total_records}

{status.total_records || 0}

@@ -231,6 +231,10 @@

{$t.translate?.run?.skipped}

{status.skipped_records || 0}

+
+

Cache

+

{status.cache_hits || 0}

+
diff --git a/frontend/src/lib/stores/translationRun.svelte.ts b/frontend/src/lib/stores/translationRun.svelte.ts index 2f837bb8..6d403134 100644 --- a/frontend/src/lib/stores/translationRun.svelte.ts +++ b/frontend/src/lib/stores/translationRun.svelte.ts @@ -27,6 +27,7 @@ export interface TranslationRunState { successfulRecords: number; failedRecords: number; skippedRecords: number; + cacheHits: number; progressPct: number; insertStatus: string | null; batchCount: number; @@ -48,6 +49,7 @@ const initialState: TranslationRunState = { successfulRecords: 0, failedRecords: 0, skippedRecords: 0, + cacheHits: 0, progressPct: 0, insertStatus: null, batchCount: 0, @@ -167,6 +169,7 @@ function _connectWebSocket(runId: string): void { successfulRecords: (data.successful_records as number) || 0, failedRecords: (data.failed_records as number) || 0, skippedRecords: (data.skipped_records as number) || 0, + cacheHits: (data.cache_hits as number) || 0, progressPct: pct, insertStatus: insertS || null, batchCount: (data.batch_count as number) || 0, @@ -268,6 +271,7 @@ async function pollStatus(): Promise { successfulRecords: (data?.successful_records as number) || 0, failedRecords: (data?.failed_records as number) || 0, skippedRecords: (data?.skipped_records as number) || 0, + cacheHits: (data?.cache_hits as number) || 0, progressPct: pct, insertStatus: (data?.insert_status as string) || null, batchCount: (data?.batch_count as number) || 0, diff --git a/frontend/src/routes/translate/history/+page.svelte b/frontend/src/routes/translate/history/+page.svelte index 5d83a5a7..b081f880 100644 --- a/frontend/src/routes/translate/history/+page.svelte +++ b/frontend/src/routes/translate/history/+page.svelte @@ -287,6 +287,7 @@ {_('translate.history.records_label').replace('{count}', run.total_records || 0)} {_('translate.history.ok_label').replace('{count}', run.successful_records || 0)} {_('translate.history.fail_label').replace('{count}', run.failed_records || 0)} + Cache: {run.cache_hits || 0} {#if run.created_by} {_('translate.history.by_label').replace('{user}', run.created_by)} {/if} @@ -407,7 +408,7 @@
-
+

{selectedRunDetail.total_records || 0}

{$t.translate?.history?.total}

@@ -424,6 +425,10 @@

{selectedRunDetail.skipped_records || 0}

{$t.translate?.history?.skipped}

+
+

{selectedRunDetail.cache_hits || 0}

+

Cache

+