From 0b85d42197c5978ffc67d6e2c4d7837f369f7adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 05:30:45 +0000 Subject: [PATCH] jobs keyed (73 -> 21) The three job detail panes and the jobs page. Status labels share one jobs.status.* group across all three panes rather than three copies of "Running". statusBadge in two of the panes is a module-level helper, not a component, so it takes `t` as a parameter -- the alternative is calling a hook outside a component body. ScriptJobDetail's version built "Failed (exit N)" by string concatenation; that is a key with a parameter now. --- .../Dashboard/Jobs/DownloadJobDetail.tsx | 48 +++++++++++++------ .../Screens/Dashboard/Jobs/JobDetail.tsx | 35 +++++++++----- .../Screens/Dashboard/Jobs/JobsPage.tsx | 36 ++++++++------ .../Dashboard/Jobs/ScriptJobDetail.tsx | 43 +++++++++++------ src/apps/officer-web/locales/en-GB.json | 36 ++++++++++++++ src/apps/officer-web/locales/pt-PT.json | 36 ++++++++++++++ 6 files changed, 177 insertions(+), 57 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Jobs/DownloadJobDetail.tsx b/src/apps/officer-web/Screens/Dashboard/Jobs/DownloadJobDetail.tsx index 7645e115..8f99ce44 100644 --- a/src/apps/officer-web/Screens/Dashboard/Jobs/DownloadJobDetail.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Jobs/DownloadJobDetail.tsx @@ -3,6 +3,9 @@ import { useParams, Link } from 'react-router'; import { Loader2, CheckCircle2, XCircle, Ban, Clock, ArrowLeft, Square, Film, Music } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { toast } from 'sonner'; +import { useTranslation } from '@/lib/i18n'; + +type TFn = ReturnType['t']; // Detail view for a `download` job — a compact two-phase progress readout (metadata → download), polled // from the job's persisted progress (the executor emits counter snapshots, not per-item events). @@ -29,26 +32,40 @@ type DownloadJob = { const POLL_MS = 1500; const isTerminal = (s: string) => s === 'completed' || s === 'failed' || s === 'stopped' || s === 'interrupted'; -const statusBadge = (status: string) => { +// Takes `t` because it is a module-level helper, not a component — the alternative is a hook +// call outside a component body. +const statusBadge = (status: string, t: TFn) => { switch (status) { case 'running': return { icon: , - label: 'Running', + label: t('jobs.status.running'), cls: 'text-amber-600 dark:text-amber-500', }; case 'pending': - return { icon: , label: 'Queued', cls: 'text-blue-600 dark:text-blue-400' }; + return { + icon: , + label: t('jobs.status.queued'), + cls: 'text-blue-600 dark:text-blue-400', + }; case 'completed': - return { icon: , label: 'Completed', cls: 'text-duck-teal' }; + return { icon: , label: t('jobs.status.completed'), cls: 'text-duck-teal' }; case 'failed': - return { icon: , label: 'Failed', cls: 'text-red-600 dark:text-red-400' }; + return { + icon: , + label: t('jobs.status.failed'), + cls: 'text-red-600 dark:text-red-400', + }; case 'stopped': - return { icon: , label: 'Stopped', cls: 'text-duck-dark/50 dark:text-foreground/50' }; + return { + icon: , + label: t('jobs.status.stopped'), + cls: 'text-duck-dark/50 dark:text-foreground/50', + }; default: return { icon: , - label: 'Interrupted', + label: t('jobs.status.interrupted'), cls: 'text-orange-600 dark:text-orange-400', }; } @@ -100,6 +117,7 @@ export const PhaseBar = ({ }; export const DownloadJobDetail = () => { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const client = useClient(); const [job, setJob] = useState(null); @@ -134,9 +152,9 @@ export const DownloadJobDetail = () => { if (!id) return; try { await client.post(`/jobs/${id}/stop`, {}); - toast.success('Stop requested'); + toast.success(t('jobs.stopRequested')); } catch { - toast.error('Failed to stop'); + toast.error(t('jobs.stopFailed')); } }; @@ -149,14 +167,14 @@ export const DownloadJobDetail = () => { if (!job) return (
- Job not found. + {t('jobs.notFound')} - Back to jobs + {t('jobs.backToJobs')}
); - const badge = statusBadge(job.status); + const badge = statusBadge(job.status, t); const running = !isTerminal(job.status); const p = job.progress; const isAudio = job.inputs?.format !== 'video'; @@ -197,7 +215,7 @@ export const DownloadJobDetail = () => { onClick={stop} className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20 cursor-pointer transition-colors" > - Stop + {t('jobs.stop')} )} @@ -206,14 +224,14 @@ export const DownloadJobDetail = () => {
{phaseLabel}
{ + const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); return ( @@ -183,14 +185,14 @@ const ToolCallEntry = ({ entry }: ToolCallEntryProps) => { {expanded && (
-
Input
+
{t('jobs.input')}
               {JSON.stringify(entry.toolInput, null, 2)}
             
{entry.output !== undefined && (
-
Output
+
{t('jobs.output')}
@@ -236,6 +238,7 @@ const DEFAULT_LAYOUT: LayoutNode = {
 };
 
 const StepsPanel = () => {
+  const { t } = useTranslation();
   const ctx = useJobPanel();
   const {
     displaySteps,
@@ -255,7 +258,7 @@ const StepsPanel = () => {
   return (
     
-

Steps

+

{t('jobs.steps')}

{displaySteps.map((step, i) => { @@ -336,6 +339,7 @@ const StepsPanel = () => { }; const OutputPanel = () => { + const { t } = useTranslation(); const ctx = useJobPanel(); const { selectedKey, outputMap, streamingMap, outputPanelRef } = ctx; @@ -345,7 +349,7 @@ const OutputPanel = () => { return (
-

Output

+

{t('jobs.output')}

{selectedKey && ( {selectedKey.includes(':') ? selectedKey.split(':')[1] : `step ${Number(selectedKey) + 1}`} @@ -355,12 +359,12 @@ const OutputPanel = () => {
{!selectedKey && (
- Select a step or sub-task to view output + {t('jobs.selectStep')}
)} {selectedKey && selectedOutputs.length === 0 && !selectedStreaming && (
- No output yet + {t('jobs.noOutput')}
)} {selectedOutputs.map((entry) => { @@ -385,6 +389,7 @@ const OutputPanel = () => { }; export const PipelineJobDetail = () => { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const client = useClient(); @@ -792,15 +797,15 @@ export const PipelineJobDetail = () => { ); if (isLoading) { - return
Loading...
; + return
{t('jobs.loading')}
; } if (!job) { return (
- Job not found + {t('jobs.notFound')} - Back to jobs + {t('jobs.backToJobs')}
); @@ -821,7 +826,11 @@ export const PipelineJobDetail = () => {
{/* The other two job detail panes already do this; this was the last imperative back in Jobs. */} - +
@@ -837,7 +846,7 @@ export const PipelineJobDetail = () => { {isRunning && ( )}
@@ -852,13 +861,13 @@ export const PipelineJobDetail = () => { {displayCost && ( <>
- Tokens: + {t('jobs.tokens')} {formatTokens(displayCost.inputTokens)} in / {formatTokens(displayCost.outputTokens)} out
- Cost: + {t('jobs.cost')} {formatCost(displayCost.totalUSD)}
diff --git a/src/apps/officer-web/Screens/Dashboard/Jobs/JobsPage.tsx b/src/apps/officer-web/Screens/Dashboard/Jobs/JobsPage.tsx index 837c9b41..69f6b22e 100644 --- a/src/apps/officer-web/Screens/Dashboard/Jobs/JobsPage.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Jobs/JobsPage.tsx @@ -22,6 +22,7 @@ import { Card } from '@/components/Card'; import { ScriptJobDetail } from './ScriptJobDetail'; import { DownloadJobDetail } from './DownloadJobDetail'; import { PipelineJobDetail } from './JobDetail'; +import { useTranslation } from '@/lib/i18n'; type JobSummary = { id: string; @@ -99,6 +100,7 @@ const useJobsData = () => { type JobRowProps = { job: JobSummary; onAction: (ev: MouseEvent, job: JobSummary) => void }; const JobRow = ({ job, onAction }: JobRowProps) => { + const { t } = useTranslation(); const { id: activeId } = useParams<{ id: string }>(); const isRunning = job.status === 'running'; const target = basename(job.target); @@ -126,7 +128,7 @@ const JobRow = ({ job, onAction }: JobRowProps) => { )}
{isLoading ? ( -
Loading…
+
{t('jobs.loading')}
) : history.length === 0 ? (
{search ? 'No matches' : 'No finished jobs'} @@ -263,6 +270,7 @@ const HistoryJobsPanel = () => { // ── Right panel: the selected job's detail, by mode (script terminal / pipeline steps) ── const JobDetailPanel = () => { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const client = useClient(); const [mode, setMode] = useState(null); @@ -284,7 +292,7 @@ const JobDetailPanel = () => {
- Select a job + {t('jobs.selectJob')}
); @@ -301,7 +309,7 @@ const JobDetailPanel = () => { if (mode === 'notfound') { return (
- Job not found. + {t('jobs.notFound')}
); } diff --git a/src/apps/officer-web/Screens/Dashboard/Jobs/ScriptJobDetail.tsx b/src/apps/officer-web/Screens/Dashboard/Jobs/ScriptJobDetail.tsx index 4176f9ad..64b6ea22 100644 --- a/src/apps/officer-web/Screens/Dashboard/Jobs/ScriptJobDetail.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Jobs/ScriptJobDetail.tsx @@ -4,6 +4,9 @@ import { Loader2, CheckCircle2, XCircle, Ban, Clock, ArrowLeft, Square } from 'l import { useClient } from 'hooks/useClient'; import { toast } from 'sonner'; import { PhaseBar, type DownloadProgress } from './DownloadJobDetail'; +import { useTranslation } from '@/lib/i18n'; + +type TFn = ReturnType['t']; type ScriptJob = { id: string; @@ -27,36 +30,46 @@ const isTerminal = (s: string) => s === 'completed' || s === 'failed' || s === ' const isDownloadProgress = (p: unknown): p is DownloadProgress => !!p && typeof p === 'object' && 'meta' in p && 'dl' in p; -const statusBadge = (status: string, exitCode: number | null) => { +// Takes `t` because it is a module-level helper, not a component. +const statusBadge = (status: string, exitCode: number | null, t: TFn) => { switch (status) { case 'running': return { icon: , - label: 'Running', + label: t('jobs.status.running'), cls: 'text-amber-600 dark:text-amber-500', }; case 'pending': - return { icon: , label: 'Queued', cls: 'text-blue-600 dark:text-blue-400' }; + return { + icon: , + label: t('jobs.status.queued'), + cls: 'text-blue-600 dark:text-blue-400', + }; case 'completed': - return { icon: , label: 'Completed', cls: 'text-duck-teal' }; + return { icon: , label: t('jobs.status.completed'), cls: 'text-duck-teal' }; case 'failed': return { icon: , - label: `Failed${exitCode != null ? ` (exit ${exitCode})` : ''}`, + label: exitCode != null ? t('jobs.failedWithCode', { code: exitCode }) : t('jobs.status.failed'), cls: 'text-red-600 dark:text-red-400', }; case 'stopped': - return { icon: , label: 'Stopped', cls: 'text-duck-dark/50 dark:text-foreground/50' }; + return { + icon: , + label: t('jobs.status.stopped'), + cls: 'text-duck-dark/50 dark:text-foreground/50', + }; default: return { icon: , - label: 'Interrupted', + label: t('jobs.status.interrupted'), cls: 'text-orange-600 dark:text-orange-400', }; } }; export const ScriptJobDetail = () => { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const client = useClient(); @@ -137,9 +150,9 @@ export const ScriptJobDetail = () => { if (!id) return; try { await client.post(`/jobs/${id}/stop`, {}); - toast.success('Stop requested'); + toast.success(t('jobs.stopRequested')); } catch { - toast.error('Failed to stop'); + toast.error(t('jobs.stopFailed')); } }; @@ -153,15 +166,15 @@ export const ScriptJobDetail = () => { if (!job) { return (
- Job not found. + {t('jobs.notFound')} - Back to jobs + {t('jobs.backToJobs')}
); } - const badge = statusBadge(job.status, job.exitCode); + const badge = statusBadge(job.status, job.exitCode, t); const running = !isTerminal(job.status); return ( @@ -185,7 +198,7 @@ export const ScriptJobDetail = () => { onClick={stop} className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20 cursor-pointer transition-colors" > - Stop + {t('jobs.stop')} )}
@@ -193,14 +206,14 @@ export const ScriptJobDetail = () => { {isDownloadProgress(job.progress) && (