mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Let a print with no 3MF be given its filament weight (issue #1820)
When the sliced file stays somewhere Bambuddy cannot read, the archive is built from the printer's report alone and carries no weight. Nothing could supply one afterwards: rescan reads the figure out of the 3MF, and that archive has no file to read. The reporter's H2S print left 46.16 g on the spool with nothing recording it, and he corrected Spoolman by hand. Edit Archive now has a Filament used (g) field. It is written to the archive's most recent run as well, because the Projects roll-up and the Prometheus counter sum PrintLogEntry rather than the cards - correcting only the archive would fix the display and leave every aggregate reading the old figure, or none at all. But not over a figure the run measured for itself. A run's grams come from the tracked spool delta when there is one and only fall back to copying the archive's estimate when there is not, so mirroring unconditionally would overwrite a measurement with a typed estimate. The mirror now takes a run that has no figure, or one holding exactly what this archive held - which also makes the undo complete, since clearing the archive clears the copy it made and leaves a measured run alone. The field is text rather than a number input. A number input reports an empty string for anything the browser judges malformed, a decimal comma in a locale that does not expect one included, and that reads here as "the user cleared it" - it would have wiped a good figure while the field still showed what was typed. Filtering on the way in keeps what is displayed and what would be sent the same string, and clamps it to the range the API accepts: this modal has no error surface, so a refused save looks like nothing happened at all. Saving also invalidates the archive's runs query. The Print Log this modal renders at its top reads them separately and kept serving the pre-edit row, so a correction looked like it had not taken - true for the status and failure-reason mirrors since #1444 as well. Second half of the same report: the internal-storage probe (#2856) logged which file it found but not where. On a printer that keeps uploads for weeks - the reporter has months of them in /cache - a reprint of a name that was re-sliced but never re-sent can match an older copy, and without the directory that mismatch is invisible rather than merely rare. The download helper returns the path that served the file instead of a bare flag; every caller only ever tested it for truth.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, Save, Tag, Camera, Trash2, Loader2, Plus, FolderKanban, Hash, Link } from 'lucide-react';
|
||||
import { X, Save, Tag, Camera, Trash2, Loader2, Plus, FolderKanban, Hash, Link, Weight } from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import type { Archive } from '../api/client';
|
||||
import { Button } from './Button';
|
||||
@@ -29,6 +29,12 @@ export const FAILURE_REASON_KEYS = [
|
||||
// Keys for archive statuses - translated at render time
|
||||
const ARCHIVE_STATUS_KEYS = ['completed', 'failed', 'aborted', 'printing'] as const;
|
||||
|
||||
// Mirrors the API's own bound on filament_used_grams. Clamped here as well so
|
||||
// the field cannot produce a request the backend would reject with a 422 —
|
||||
// this modal has no error surface, so a refused save looks like nothing
|
||||
// happened at all (#1820).
|
||||
const MAX_FILAMENT_GRAMS = 100000;
|
||||
|
||||
interface EditArchiveModalProps {
|
||||
archive: Archive;
|
||||
onClose: () => void;
|
||||
@@ -67,6 +73,12 @@ export function EditArchiveModal({ archive, onClose, existingTags = [] }: EditAr
|
||||
});
|
||||
const [status, setStatus] = useState(archive.status);
|
||||
const [quantity, setQuantity] = useState(archive.quantity ?? 1);
|
||||
// Kept as a string so the field can be genuinely empty: a print archived
|
||||
// without its 3MF has no figure at all, and "" has to stay distinguishable
|
||||
// from 0 both on the way in and on the way out (#1820).
|
||||
const [filamentGrams, setFilamentGrams] = useState(
|
||||
archive.filament_used_grams != null ? String(archive.filament_used_grams) : ''
|
||||
);
|
||||
const [photos, setPhotos] = useState<string[]>(archive.photos || []);
|
||||
const [externalUrl, setExternalUrl] = useState(archive.external_url || '');
|
||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||
@@ -151,6 +163,12 @@ export function EditArchiveModal({ archive, onClose, existingTags = [] }: EditAr
|
||||
// This form can change the archive's project, so the project detail
|
||||
// views need refreshing too — not just the overview cards (#2731).
|
||||
invalidateArchiveAndProjectViews(queryClient);
|
||||
// Some of what this form writes is mirrored onto the archive's most
|
||||
// recent run — status and failure reason since #1444, filament grams
|
||||
// since #1820 — and the Print Log this modal renders at its top reads
|
||||
// the runs through their own query. Without this it serves the cached
|
||||
// pre-edit row, so the correction looks like it did not take.
|
||||
queryClient.invalidateQueries({ queryKey: ['archive-runs', archive.id] });
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
@@ -202,6 +220,22 @@ export function EditArchiveModal({ archive, onClose, existingTags = [] }: EditAr
|
||||
updateData.status = status;
|
||||
}
|
||||
|
||||
// Sent only when the user actually touched it, so an ordinary save of an
|
||||
// archive that has its 3MF cannot overwrite the sliced figure with a
|
||||
// rounded one from the input.
|
||||
const trimmedGrams = filamentGrams.trim();
|
||||
const typedGrams = trimmedGrams === '' ? null : Number(trimmedGrams);
|
||||
// An empty field means "no figure" and clears the stored one; a field that
|
||||
// holds something unparseable (a lone decimal point, mid-typing) means the
|
||||
// user is not finished, and must not read as a clear. Clamped here as well
|
||||
// as on blur because Enter submits without the field losing focus.
|
||||
const gramsUnparseable = typedGrams !== null && !Number.isFinite(typedGrams);
|
||||
const parsedGrams = typedGrams === null ? null : Math.min(Math.max(typedGrams, 0), MAX_FILAMENT_GRAMS);
|
||||
const originalGrams = archive.filament_used_grams ?? null;
|
||||
if (!gramsUnparseable && parsedGrams !== originalGrams) {
|
||||
updateData.filament_used_grams = parsedGrams;
|
||||
}
|
||||
|
||||
// Handle failure_reason based on status
|
||||
if (status === 'failed' || status === 'aborted') {
|
||||
updateData.failure_reason = failureReason || undefined;
|
||||
@@ -310,6 +344,43 @@ export function EditArchiveModal({ archive, onClose, existingTags = [] }: EditAr
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filament used - the only way to supply a figure for a print that
|
||||
archived without its 3MF, which no rescan can repair (#1820). */}
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1" htmlFor="archive-filament-grams">
|
||||
<Weight className="w-4 h-4 inline mr-1" />
|
||||
{t('editArchive.filamentUsed')}
|
||||
</label>
|
||||
<input
|
||||
id="archive-filament-grams"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={filamentGrams}
|
||||
// Text rather than number, and filtered on the way in. A number
|
||||
// input reports an empty string for anything the browser judges
|
||||
// malformed — including a decimal comma in a locale it doesn't
|
||||
// expect — which would read here as "the user cleared it" and
|
||||
// wipe a good figure. Filtering keeps what is displayed and what
|
||||
// would be sent the same thing, and keeps the value inside the
|
||||
// range the API accepts: this modal shows nothing at all when a
|
||||
// save is refused, so it must not be able to send a refusable one.
|
||||
onChange={(e) => {
|
||||
const next = e.target.value.replace(',', '.');
|
||||
if (next === '' || /^\d*\.?\d*$/.test(next)) setFilamentGrams(next);
|
||||
}}
|
||||
onBlur={() => setFilamentGrams((current) => {
|
||||
const parsed = Number(current);
|
||||
if (current === '' || !Number.isFinite(parsed)) return '';
|
||||
return String(Math.min(parsed, MAX_FILAMENT_GRAMS));
|
||||
})}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
placeholder={t('editArchive.filamentUsedPlaceholder')}
|
||||
/>
|
||||
<p className="text-xs text-bambu-gray mt-1">
|
||||
{t('editArchive.filamentUsedHelp')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('editArchive.notes')}</label>
|
||||
|
||||
Reference in New Issue
Block a user