Files
bambuddy/frontend/src/components/PrintLogTable.tsx
T
maziggy 60b253f98a fix(failure-reason): consistent camelCase keys across UI surfaces
The Stats page's Failure Analysis widget and the per-archive run
  sub-table rendered the raw PrintLogEntry.failure_reason value
  without translating, so the camelCase keys saved by the new
  Print Log row editor (#1687 part 4) surfaced as literal
  "filamentRunout" / "cloggedNozzle" text. The Print Log table did
  translate the value, so the inconsistency was visible from one
  surface to the next.

  EditArchiveModal was also still saving the localised label as the
  column value while the new editor saved the key - same column,
  two formats, two failure modes (group fragmentation on language
  switch, new PATCH validation rejection on round-trip).

  Three surfaces fixed in one drop:

  1. StatsPage.tsx and PrintLogTable.tsx wrap the value in
     t('editArchive.failureReasons.${reason}', { defaultValue: reason }) -
     the defaultValue path keeps legacy translated-text rows rendering
     unchanged.

  2. EditArchiveModal stores the camelCase key on save and reverse-
     looks up any legacy translated-text value against the current
     locale on open. Every save thereafter converts that row forward
     to the key format, so the column self-heals over time.

  3. Added htmlFor/id to the failure-reason label/select pair (a11y
     plus testability).
2026-06-10 08:02:36 +02:00

104 lines
3.7 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Loader2 } from 'lucide-react';
import { api } from '../api/client';
import { parseUTCDate } from '../utils/date';
interface PrintLogTableProps {
archiveId: number;
}
function formatDuration(seconds: number | null): string {
if (!seconds || seconds <= 0) return '—';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
function formatDate(isoString: string | null): string {
if (!isoString) return '—';
const d = parseUTCDate(isoString);
return d ? d.toLocaleString() : '—';
}
export function PrintLogTable({ archiveId }: PrintLogTableProps) {
const { t } = useTranslation();
const { data, isLoading } = useQuery({
queryKey: ['archive-runs', archiveId],
queryFn: () => api.getArchiveRuns(archiveId),
});
if (isLoading) {
return (
<div className="flex justify-center py-4">
<Loader2 className="w-5 h-5 text-bambu-gray animate-spin" />
</div>
);
}
const runs = data?.items || [];
if (runs.length === 0) {
return (
<p className="text-sm text-bambu-gray italic py-2">
{t('archives.runLog.empty')}
</p>
);
}
return (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-bambu-gray border-b border-bambu-dark-tertiary">
<th className="text-left py-1.5 pr-2 font-medium">{t('archives.runLog.col.date')}</th>
<th className="text-left py-1.5 pr-2 font-medium">{t('archives.runLog.col.status')}</th>
<th className="text-right py-1.5 pr-2 font-medium">{t('archives.runLog.col.duration')}</th>
<th className="text-right py-1.5 pr-2 font-medium">{t('archives.runLog.col.filament')}</th>
<th className="text-right py-1.5 font-medium">{t('archives.runLog.col.cost')}</th>
</tr>
</thead>
<tbody>
{runs.map((run) => {
const statusClass =
run.status === 'completed'
? 'text-bambu-green'
: run.status === 'failed'
? 'text-red-400'
: 'text-bambu-gray';
return (
<tr
key={run.id}
className="border-b border-bambu-dark-tertiary/40 last:border-0"
>
<td className="py-1.5 pr-2 text-bambu-gray-light">
{formatDate(run.started_at || run.created_at)}
</td>
<td className={`py-1.5 pr-2 font-medium ${statusClass}`}>
{t(`archives.runLog.status.${run.status}`, { defaultValue: run.status })}
{run.failure_reason && (
<span className="block text-[10px] text-bambu-gray font-normal">
{t(`editArchive.failureReasons.${run.failure_reason}`, { defaultValue: run.failure_reason })}
</span>
)}
</td>
<td className="py-1.5 pr-2 text-right text-bambu-gray-light">
{formatDuration(run.duration_seconds)}
</td>
<td className="py-1.5 pr-2 text-right text-bambu-gray-light">
{run.filament_used_grams != null
? `${run.filament_used_grams.toFixed(1)} g`
: '—'}
</td>
<td className="py-1.5 text-right text-bambu-gray-light">
{run.cost != null ? run.cost.toFixed(2) : '—'}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}