Files
ss-tools/frontend/src/lib/components/translate/RunTabContent.svelte
busya 9ef9fae206 fix: critical QA findings — dead code and prop mismatch
1. Remove get_config_manager() call from translate_run_websocket
   (@app.websocket /ws/translate/run/{run_id}) — was not imported
   and unused. Also remove unused from sqlalchemy.orm import Session.
2. Fix prop name mismatch: showBulkReplace -> showPageBulkReplace
   in RunTabContent. Use () for reactive two-way binding.
3. Add ('DRAFT') for status prop in RunTabContent.
2026-05-29 16:47:50 +03:00

200 lines
9.4 KiB
Svelte

<!-- #region RunTabContent [C:3] [TYPE Component] [SEMANTICS translate, run, execution, progress] -->
<!-- @BRIEF Run tab — translation execution with incremental/full mode selection, progress
tracking via TranslationRunProgress, and run history with expandable details. -->
<!-- @LAYER UI -->
<!-- @RELATION DEPENDS_ON -> [TranslationRunProgress] -->
<!-- @RELATION DEPENDS_ON -> [TranslationRunResult] -->
<!-- @RELATION BINDS_TO -> [translationRunStore] -->
<!-- @UX_STATE Idle — no run active, run cards visible -->
<!-- @UX_STATE Running — TranslationRunProgress bar visible, cards show active state -->
<!-- @UX_STATE Completed — run history list with expandable details -->
<!-- @UX_STATE Error — error banner above run history -->
<!-- @UX_FEEDBACK Toast on status change / run error / run complete -->
<!-- @UX_FEEDBACK BulkReplace modal button in recent runs header -->
<!-- @UX_RECOVERY Retry button on TranslationRunProgress for failed runs -->
<!-- @UX_REACTIVITY Props -> $props() for config/state, callbacks for actions -->
<script>
import { t } from '$lib/i18n';
import { addToast } from '$lib/toasts.js';
import { updateJob } from '$lib/api/translate.js';
import TranslationRunProgress from './TranslationRunProgress.svelte';
import TranslationRunResult from './TranslationRunResult.svelte';
let {
status = $bindable('DRAFT'),
jobId = '',
isRunning = false,
isFullRun = false,
runError = null,
currentRunId = null,
runComplete = false,
completedRuns = [],
expandedRunIds = [],
showPageBulkReplace = $bindable(false),
onTriggerRun = (isFull) => {},
onRetryRun = () => {},
onRetryInsert = () => {},
onLoadRunHistory = () => {},
onToggleRunDetails = (id) => {},
getJobStatusLabel = (s) => s,
} = $props();
async function handleMarkReady() {
try {
await updateJob(jobId, { status: 'READY' });
status = 'READY';
addToast($t.translate?.config?.job_updated, 'success');
} catch (e) {
addToast(e?.message || 'Failed to update status', 'error');
}
}
</script>
<section class="bg-white border border-gray-200 rounded-lg p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{$t.translate?.config?.run_translation}</h2>
<!-- Status display + transition -->
<div class="flex items-center gap-3 mb-4 p-3 bg-gray-50 rounded-lg">
<span class="text-sm text-gray-600">{$t.translate?.config?.status}:</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{status === 'READY' ? 'bg-green-100 text-green-700' : ''}
{status === 'DRAFT' ? 'bg-yellow-100 text-yellow-700' : ''}
{status === 'ACTIVE' ? 'bg-emerald-100 text-emerald-700' : ''}
{status === 'RUNNING' ? 'bg-blue-100 text-blue-700' : ''}
{status === 'COMPLETED' ? 'bg-green-100 text-green-700' : ''}
{status === 'FAILED' ? 'bg-red-100 text-red-700' : ''}">
{getJobStatusLabel(status)}
</span>
{#if status === 'DRAFT'}
<button
onclick={handleMarkReady}
class="ml-auto px-3 py-1 text-xs bg-primary text-white rounded hover:bg-primary-hover transition-colors"
>
{$t.translate?.config?.mark_ready || 'Mark as READY'}
</button>
{/if}
</div>
<div class="space-y-4">
<!-- Run mode selection: two cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Incremental run card -->
<div class="border border-green-200 rounded-lg p-4 {isRunning && !isFullRun ? 'bg-green-50' : 'bg-white'}">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-sm font-semibold text-gray-900">{$t.translate?.config?.run_incremental}</h3>
<p class="text-xs text-gray-500 mt-1">{$t.translate?.config?.run_incremental_desc}</p>
<button
onclick={() => onTriggerRun(false)}
disabled={isRunning || status === 'DRAFT'}
class="mt-3 px-5 py-1.5 text-sm font-medium bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isRunning && !isFullRun ? $t.translate?.config?.running : $t.translate?.config?.run_translation}
</button>
</div>
</div>
</div>
<!-- Full run card -->
<div class="border border-primary-ring rounded-lg p-4 {isRunning && isFullRun ? 'bg-primary-light' : 'bg-white'}">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
<svg class="w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-sm font-semibold text-gray-900">{$t.translate?.config?.run_full}</h3>
<p class="text-xs text-gray-500 mt-1">{$t.translate?.config?.run_full_desc}</p>
<button
onclick={() => onTriggerRun(true)}
disabled={isRunning || status === 'DRAFT'}
class="mt-3 px-5 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isRunning && isFullRun ? $t.translate?.config?.running : $t.translate?.config?.full_translate}
</button>
</div>
</div>
</div>
</div>
{#if runError}
<div class="bg-destructive-light border border-destructive-light rounded-lg p-3">
<p class="text-sm text-red-600">{runError}</p>
</div>
{/if}
{#if currentRunId && !runComplete}
<TranslationRunProgress
runId={currentRunId}
onRetry={onRetryRun}
onRetryInsert={onRetryInsert}
/>
{/if}
{#if completedRuns.length > 0}
<div class="mt-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-medium text-gray-700">{$t.translate?.config?.recent_runs}</h3>
<button
onclick={() => showPageBulkReplace = true}
class="px-3 py-1.5 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors"
>
{$t.translate?.run?.bulk_replace || 'Bulk Replace'}
</button>
</div>
<div class="space-y-3">
{#each completedRuns as run}
<div class="border border-gray-200 rounded-lg bg-white">
<div class="flex items-center justify-between gap-3 p-4">
<div class="min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<span class="text-xs text-gray-500">{$t.translate?.run?.run_id}</span>
<code class="text-xs text-gray-700 break-all">{run.id}</code>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium
{run.status === 'COMPLETED' ? 'bg-green-100 text-green-700' : ''}
{run.status === 'FAILED' ? 'bg-red-100 text-red-700' : ''}
{run.status === 'CANCELLED' ? 'bg-gray-100 text-gray-700' : ''}
{run.status !== 'COMPLETED' && run.status !== 'FAILED' && run.status !== 'CANCELLED' ? 'bg-yellow-100 text-yellow-700' : ''}"
>
{run.status}
</span>
</div>
<div class="mt-2 flex flex-wrap gap-3 text-xs text-gray-500">
<span>{$t.translate?.run?.total}: {run.total_records || 0}</span>
<span>{$t.translate?.run?.success}: {run.successful_records || 0}</span>
<span>{$t.translate?.run?.failed}: {run.failed_records || 0}</span>
<span>{$t.translate?.run?.skipped}: {run.skipped_records || 0}</span>
</div>
{#if run.error_message}
<p class="mt-2 text-xs text-red-600 break-words">{run.error_message}</p>
{/if}
</div>
<button
onclick={() => onToggleRunDetails(run.id)}
class="shrink-0 px-3 py-1.5 text-xs border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors"
>
{expandedRunIds.includes(run.id)
? ($t.translate?.run?.hide_details || 'Hide details')
: ($t.translate?.run?.show_details || 'Show details')}
</button>
</div>
{#if expandedRunIds.includes(run.id)}
<div class="border-t border-gray-200 p-4">
<TranslationRunResult runId={run.id} onRefresh={onLoadRunHistory} />
</div>
{/if}
</div>
{/each}
</div>
</div>
{/if}
</div>
</section>
<!-- #endregion RunTabContent -->