Keep the printer's name on statistics after it is deleted (issue #2873)

Every per-printer breakdown resolved the name against the printers that
exist now, so deleting a printer and choosing to keep its prints turned
"Ultron" into "Printer 1" in Prints by Printer, the success-rate and
time-accuracy lists, and Failures by Printer. Archives lose their printer
on that delete as well, so nothing was left to read a name from.

The runs themselves recorded the name they printed on. /archives/stats now
reports the last name each id was known by - taken from the newest run that
has one, so a later name-less row cannot blank it - and failure analysis
falls back to the same thing for ids with no printer left. The client keeps
preferring a live printer's own record, so a rename still shows up straight
away rather than after the next print.
This commit is contained in:
maziggy
2026-08-18 14:04:18 +02:00
parent cfecfa360e
commit 28781ea558
10 changed files with 221 additions and 4 deletions
@@ -2,7 +2,7 @@
* Tests for the StatsPage component.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import { render } from '../utils';
import { StatsPage } from '../../pages/StatsPage';
@@ -621,4 +621,46 @@ describe('StatsPage', () => {
expect(printTime?.querySelector('svg[aria-label]')).toBeNull();
});
});
describe('deleted printers (#2873)', () => {
// The per-printer breakdown only renders at half width, and the dashboard
// reads its sizes back from localStorage.
const halfWidthSuccessRate = JSON.stringify({
order: ['success-rate'],
hidden: [],
sizes: { 'success-rate': 2 },
});
afterEach(() => {
vi.mocked(localStorage.getItem).mockReset();
});
it('labels history from a deleted printer with the name it ran under', async () => {
vi.mocked(localStorage.getItem).mockImplementation((key: string) =>
key === 'bambusy-dashboard-layout-v2' ? halfWidthSuccessRate : null
);
server.use(
http.get('/api/v1/archives/stats', () =>
HttpResponse.json({
...mockStats,
// Printer 7 was deleted with its prints kept, so it is missing from
// /printers and used to show as "Printer 7".
prints_by_printer: { '1': 100, '7': 12 },
printer_names: { '1': 'Name At The Time', '7': 'Ultron' },
})
)
);
render(<StatsPage />);
await waitFor(() => {
expect(screen.getByText('Ultron')).toBeInTheDocument();
});
expect(screen.queryByText('Printer 7')).not.toBeInTheDocument();
// A printer that still exists is named from the live record instead, so a
// rename shows up without waiting for the next print.
expect(screen.getAllByText('X1 Carbon').length).toBeGreaterThan(0);
expect(screen.queryByText('Name At The Time')).not.toBeInTheDocument();
});
});
});
+3
View File
@@ -813,6 +813,9 @@ export interface ArchiveStats {
total_cost: number;
prints_by_filament_type: Record<string, number>;
prints_by_printer: Record<string, number>;
// Name each printer id was last recorded under in the print log, so history
// belonging to a deleted printer keeps its label.
printer_names?: Record<string, string>;
average_time_accuracy: number | null;
time_accuracy_by_printer: Record<string, number> | null;
total_energy_kwh: number;
+12 -1
View File
@@ -1103,7 +1103,18 @@ export function StatsPage() {
const isRefetching = (isStatsFetching || isArchivesFetching) && !isLoading;
const currency = getCurrencySymbol(settings?.currency || 'USD');
const printerMap = new Map(printers?.map((p) => [String(p.id), p.name]) || []);
// History outlives the printer that made it: a deleted printer is gone from
// `printers`, so its rows fell back to "Printer 1" (#2873). The stats response
// carries the name each id was last recorded under; a printer that still
// exists overrides it, so a rename shows up straight away.
const printerMap = useMemo(
() =>
new Map<string, string>([
...Object.entries(stats?.printer_names || {}),
...(printers?.map((p) => [String(p.id), p.name] as [string, string]) || []),
]),
[stats?.printer_names, printers],
);
const printDates = useMemo(() => archives?.map((a) => a.created_at) || [], [archives]);
if (isLoading) {