mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 04:31:28 +02:00
Four frontend formatDate / formatDateTime helpers called new Date(iso) directly on backend timestamps that have no timezone indicator. Per ECMAScript, a bare "2026-06-02T07:50:00" is parsed as local time, so a UTC-stored value got displayed as if its numeric components were already local — visually identical to UTC. Same shape as the #504 fix from Feb 2026, which patched 13 sites but missed these four: PrintLogTable and SpoolUsageHistory hadn't been written yet; CameraTokensPage and SpoolBuddySettingsPage existed but were overlooked. Reporter #2 (@IndividualGhost1905) confirmed with a UTC+3 host: print log shows UTC clock value, system date shows local. PrintLogTable is the "logs/completion time" they called out; the other three are the same pattern in nearby surfaces. Replaced the bare new Date(iso) calls with parseUTCDate(iso) from utils/date.ts — the same helper every other date formatter in the codebase already uses. It appends "Z" to naive ISO strings and parses TZ-tagged strings as-is. CameraTokensPage.isExpired got the same fix because comparing a misparsed Date against Date.now() would produce false "not expired" / "expired" results around the TZ-offset boundary. Reporter #1's printer-card ETA complaint (10:50 + 57m showing 09:48) is NOT addressed by this fix. That ETA comes from formatETA(remainingMinutes) which is purely client-side (new Date() plus minutes from the WebSocket payload, then toLocaleTimeString) — for it to render UTC the browser timezone itself would need to be UTC, which is a browser / OS config issue. Audit confirmed no other regressions: grepped every new Date( call in frontend/src/. Remaining sites either pass an epoch-ms number, use the result only for .getTime() arithmetic where the offset cancels, already wrap in parseUTCDate fallback, or consume a backend timestamp that includes "+00:00" (FailureDetectionSettings reads obico_detection.py's tz-aware isoformat).
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Loader2, Trash2, Clock } from 'lucide-react';
|
|
import { api } from '../api/client';
|
|
import type { SpoolUsageRecord } from '../api/client';
|
|
import { Button } from './Button';
|
|
import { useToast } from '../contexts/ToastContext';
|
|
import { parseUTCDate } from '../utils/date';
|
|
function formatDate(dateStr: string): string {
|
|
const date = parseUTCDate(dateStr);
|
|
if (!date) return '';
|
|
return date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: '2-digit' }) +
|
|
' ' + date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
interface SpoolUsageHistoryProps {
|
|
spoolId: number;
|
|
}
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
completed: 'text-bambu-green',
|
|
failed: 'text-red-400',
|
|
aborted: 'text-yellow-400',
|
|
};
|
|
|
|
export function SpoolUsageHistory({ spoolId }: SpoolUsageHistoryProps) {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
const { showToast } = useToast();
|
|
|
|
const { data: history, isLoading } = useQuery({
|
|
queryKey: ['spool-usage', spoolId],
|
|
queryFn: () => api.getSpoolUsageHistory(spoolId),
|
|
});
|
|
|
|
const clearMutation = useMutation({
|
|
mutationFn: () => api.clearSpoolUsageHistory(spoolId),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['spool-usage', spoolId] });
|
|
showToast(t('inventory.historyCleared'), 'success');
|
|
},
|
|
});
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex justify-center py-4">
|
|
<Loader2 className="w-5 h-5 animate-spin text-bambu-green" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!history || history.length === 0) {
|
|
return (
|
|
<div className="text-center py-4 text-bambu-gray text-sm">
|
|
<Clock className="w-5 h-5 mx-auto mb-2 opacity-50" />
|
|
{t('inventory.noUsageHistory')}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="text-sm font-medium text-white">{t('inventory.usageHistory')}</h4>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => clearMutation.mutate()}
|
|
disabled={clearMutation.isPending}
|
|
className="text-xs text-bambu-gray hover:text-red-400"
|
|
>
|
|
<Trash2 className="w-3 h-3 mr-1" />
|
|
{t('inventory.clearHistory')}
|
|
</Button>
|
|
</div>
|
|
<div className="max-h-48 overflow-y-auto space-y-1">
|
|
{history.map((record: SpoolUsageRecord) => (
|
|
<div
|
|
key={record.id}
|
|
className="flex items-center justify-between p-2 rounded bg-bambu-dark/50 text-xs"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-bambu-gray">{formatDate(record.created_at)}</span>
|
|
{record.print_name && (
|
|
<span className="text-white ml-2 truncate" title={record.print_name}>
|
|
{record.print_name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
|
|
<span className="text-white font-medium">{record.weight_used.toFixed(1)}g</span>
|
|
<span className="text-bambu-gray">({record.percent_used}%)</span>
|
|
<span className={STATUS_COLORS[record.status] || 'text-bambu-gray'}>
|
|
{record.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|