Fix inventory date format and AMS-HT slot labels (#463)

Two bugs in the inventory page:

1. Dates always displayed as DD/MM/YY regardless of the user's date
   format setting. The page had a local formatDate() hardcoding en-GB
   locale. Replaced with the shared formatDateOnly() from utils/date.ts.

2. AMS-HT slot locations showed accented characters (e.g., "Á1") instead
   of proper labels (e.g., "HT-A"). The code used String.fromCharCode(65
   + ams_id), which produces garbled chars for AMS-HT IDs (128+). Now
   uses the shared formatSlotLabel() from utils/amsHelpers.ts.
This commit is contained in:
maziggy
2026-02-20 15:22:44 +01:00
parent b22f091dd0
commit 40403ae584
4 changed files with 434 additions and 419 deletions
+2
View File
@@ -10,6 +10,8 @@ All notable changes to Bambuddy will be documented in this file.
- **Queue Stuck on "Busy" for "Any Model" Jobs** ([#435](https://github.com/maziggy/bambuddy/issues/435)) — When a print was queued with "Any [Model]" (e.g., "Any P1S"), it was created with `printer_id=NULL` and `target_model="P1S"`. After the assigned printer finished, the queue widget queried only for items matching `printer_id=X`, missing the next pending model-based item (`printer_id IS NULL`). With no next item found, the "Clear Plate & Start Next" button never appeared, leaving the scheduler stuck reporting "Busy". The queue API now accepts an optional `target_model` parameter; when combined with `printer_id`, it uses OR logic to also return unassigned items whose `target_model` matches the printer's model. The frontend passes the printer's model through to this query. Additionally, the backend now resolves the printer's model server-side from the database when the frontend doesn't provide `target_model` (e.g., when the printer was added without selecting a model), ensuring the OR logic works regardless of whether the client knows the printer's model.
- **Queue "Any Model" Jobs Stuck in "Waiting" After Plate Clear** ([#435](https://github.com/maziggy/bambuddy/issues/435)) — After the queue visibility fix above, "Any Model" jobs were correctly assigned to an idle printer but immediately crashed with `'>=' not supported between instances of 'str' and 'int'` when computing AMS filament mapping. MQTT raw data returns AMS unit and tray IDs as strings, but `_build_loaded_filaments()` compared them to integers without casting. The crash prevented the assignment from committing, so the scheduler retried every 30 seconds in an infinite loop. Cast `ams_id` and `tray_id` to `int()` to match the pattern already used for external spool IDs.
- **SD Card Cleanup After Print Never Runs** ([#374](https://github.com/maziggy/bambuddy/issues/374)) — The post-print SD card cleanup (which deletes uploaded gcode from the printer root to prevent phantom prints on power cycle) used `printer_manager.get_printer()`, which returns a `PrinterInfo` with only `name` and `serial_number`. Accessing `.ip_address`, `.access_code`, and `.model` raised `AttributeError`, silently caught by the outer exception handler. Replaced with a DB query for the `Printer` model, matching the pattern used everywhere else in `on_print_complete()`.
- **Inventory Date Format Ignores Settings** ([#463](https://github.com/maziggy/bambuddy/issues/463)) — The inventory page used a local `formatDate()` that hardcoded the `en-GB` locale, always displaying dates in a fixed format regardless of the date format setting. Now fetches the `date_format` setting and uses the shared `formatDateInput()` utility which formats as MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD, or browser locale based on the user's choice.
- **Inventory Location Shows Garbled Characters for AMS-HT Slots** ([#463](https://github.com/maziggy/bambuddy/issues/463)) — The inventory location column computed slot letters via `String.fromCharCode(65 + ams_id)`, which produced accented characters (e.g., `Á`) for AMS-HT units (ams_id ≥ 128). Now uses the shared `formatSlotLabel()` utility which correctly handles AMS-HT and external spool slots.
### New Features
- **Background Print Dispatch** ([#408](https://github.com/maziggy/bambuddy/pull/408), [#112](https://github.com/maziggy/bambuddy/issues/112)) — Printing from archives and the file manager now runs in the background via an async dispatch service. FTP uploads and print-start commands are decoupled from API request latency, so the UI responds immediately. Real-time progress is streamed to all clients via WebSocket, rendered as a persistent toast with per-job upload progress bars, status badges (dispatched/processing/completed/failed/cancelled), and a cancel button. The dispatcher supports concurrent uploads to different printers with per-printer queuing to prevent conflicts. Cancellation is cooperative — uploads abort at the next chunk boundary and clean up partial files on the printer. Batch progress tracking shows overall completion across multi-printer dispatches. Translations added for all 6 locales (en, de, fr, it, ja, pt-BR).
+28 -15
View File
@@ -15,6 +15,8 @@ import { ConfirmModal } from '../components/ConfirmModal';
import { ColumnConfigModal, type ColumnConfig } from '../components/ColumnConfigModal';
import { useToast } from '../contexts/ToastContext';
import { resolveSpoolColorName } from '../utils/colors';
import { formatDateInput, parseUTCDate, type DateFormat } from '../utils/date';
import { formatSlotLabel } from '../utils/amsHelpers';
type ArchiveFilter = 'active' | 'archived';
type UsageFilter = 'all' | 'used' | 'new';
@@ -98,10 +100,11 @@ const MATERIAL_COLORS: Record<string, string> = {
type TFn = (key: string) => string;
function formatDate(dateStr: string | null): string {
function formatInventoryDate(dateStr: string | null, dateFormat: DateFormat = 'system'): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: '2-digit' });
const date = parseUTCDate(dateStr);
if (!date) return '-';
return formatDateInput(date, dateFormat);
}
type CellCtx = {
@@ -109,6 +112,7 @@ type CellCtx = {
remaining: number;
pct: number;
assignmentMap: Record<number, SpoolAssignment>;
dateFormat: DateFormat;
};
// Column header labels (25 columns — matching SpoolBuddy exactly)
@@ -144,14 +148,14 @@ const columnCells: Record<string, (ctx: CellCtx) => ReactNode> = {
id: ({ spool }) => (
<span className="text-sm font-medium text-white">{spool.id}</span>
),
added_time: ({ spool }) => (
<span className="text-sm text-bambu-gray">{formatDate(spool.created_at)}</span>
added_time: ({ spool, dateFormat }) => (
<span className="text-sm text-bambu-gray">{formatInventoryDate(spool.created_at, dateFormat)}</span>
),
encode_time: ({ spool }) => (
<span className="text-sm text-bambu-gray">{formatDate(spool.encode_time)}</span>
encode_time: ({ spool, dateFormat }) => (
<span className="text-sm text-bambu-gray">{formatInventoryDate(spool.encode_time, dateFormat)}</span>
),
last_used_time: ({ spool }) => (
<span className="text-sm text-bambu-gray">{spool.last_used ? formatDate(spool.last_used) : 'Never'}</span>
last_used_time: ({ spool, dateFormat }) => (
<span className="text-sm text-bambu-gray">{spool.last_used ? formatInventoryDate(spool.last_used, dateFormat) : 'Never'}</span>
),
rgba: ({ spool }) => (
<div className="flex items-center justify-center">
@@ -183,12 +187,12 @@ const columnCells: Record<string, (ctx: CellCtx) => ReactNode> = {
const assignment = assignmentMap[spool.id];
if (!assignment) return <span className="text-sm text-bambu-gray">-</span>;
const printerLabel = assignment.printer_name || `Printer ${assignment.printer_id}`;
// Bambu slot notation: AMS 0=A, 1=B, 2=C, 3=D; tray 0-based → 1-based
const slotLetter = String.fromCharCode(65 + assignment.ams_id);
const slotNumber = assignment.tray_id + 1;
const isExternal = assignment.ams_id === 254 || assignment.ams_id === 255;
const isHt = !isExternal && assignment.ams_id >= 128;
const slotLabel = formatSlotLabel(assignment.ams_id, assignment.tray_id, isHt, isExternal);
return (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-500/20 text-purple-400">
{printerLabel} {slotLetter}{slotNumber}
{printerLabel} {slotLabel}
</span>
);
},
@@ -267,7 +271,9 @@ const columnSortValues: Record<string, (spool: InventorySpool, assignmentMap: Re
location: (s, am) => {
const a = am[s.id];
if (!a) return '';
return `${a.printer_name || ''} ${String.fromCharCode(65 + a.ams_id)}${a.tray_id + 1}`;
const isExt = a.ams_id === 254 || a.ams_id === 255;
const isHt = !isExt && a.ams_id >= 128;
return `${a.printer_name || ''} ${formatSlotLabel(a.ams_id, a.tray_id, isHt, isExt)}`;
},
label_weight: (s) => s.label_weight,
net: (s) => Math.max(0, s.label_weight - s.weight_used),
@@ -330,6 +336,13 @@ export default function InventoryPage() {
return 15;
});
const { data: settings } = useQuery({
queryKey: ['settings'],
queryFn: api.getSettings,
});
const dateFormat: DateFormat = settings?.date_format || 'system';
const { data: spools, isLoading } = useQuery({
queryKey: ['inventory-spools'],
queryFn: () => api.getSpools(true), // Always fetch all, filter client-side
@@ -927,7 +940,7 @@ export default function InventoryPage() {
>
{visibleColumns.map((colId) => (
<td key={colId} className="py-3 px-4">
{columnCells[colId]?.({ spool, remaining, pct, assignmentMap })}
{columnCells[colId]?.({ spool, remaining, pct, assignmentMap, dateFormat })}
</td>
))}
<td className="py-3 px-4">
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-BHkAk1mx.js"></script>
<script type="module" crossorigin src="/assets/index-uDg4paLx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DJax8qcY.css">
</head>
<body>