feat(translate): add cache_hits counter — backend + frontend

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 
This commit is contained in:
2026-06-02 11:59:57 +03:00
parent 7f48b19f28
commit f87092c4a7
13 changed files with 77 additions and 5 deletions

View File

@@ -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")

View File

@@ -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,

View File

@@ -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,

View File

@@ -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")

View File

@@ -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"):

View File

@@ -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")

View File

@@ -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,

View File

@@ -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