mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-07 14:41:24 +02:00
#1429 (reported by @TrickShotMLG02, confirmed by @Mape6 on a flat single-LAN that rules out subnet / mDNS-reflector theories): with the physical printer off the slicer's "Send" landed in Bambuddy's archive; once the printer powered on every subsequent "Send" went straight to the printer's SD card and bypassed Bambuddy. Bundle analysis: mape6-before showed clean FTP receive + archive lines, mape6-after had zero FTP attempts to Bambuddy once the printer was online. Cause: mqtt_bridge.py::_resolve_client encoded _target_ip_uint32_le / _vp_ip_uint32_le ONLY on client-identity change and early-returned on every refresh tick when the same client object was still bound. If target_client.ip_address was empty at first bind (DB row stale, or client constructed before SSDP refresh filled it in), the encoding stayed None, the net.info[*].ip rewrite block was skipped, the cache filled with the real printer IP, sticky-key preservation kept the poisoned net value alive across every subsequent incremental push, and the slicer followed the leaked IP. Only Bambuddy-restart-with-printer-off cleared it — the workaround both reporters independently arrived at. Same shape on multi-NIC printers (X1C, H2D Pro): the rewrite only matched entries whose ip equalled _target_ip_uint32_le, so a secondary interface IP Bambuddy never saw would leak through unchanged. Bridge fix: - _resolve_client calls a new _refresh_ip_encoding() on every refresh tick, even when client identity is unchanged; self-heals once ip_address becomes valid. - _refresh_ip_encoding() sweeps the existing _latest_print_state when encoding becomes valid for the first time. Without the sweep, sticky-key preservation keeps the pre-arm poisoned cache alive forever — incremental pushes that don't include net carry the bad value forward. - _rewrite_net_info_ips() rewrites EVERY non-zero net.info[].ip entry that doesn't already equal the VP IP, not just entries matching _target_ip_uint32_le. Multi-NIC printers stop leaking secondary interfaces. Zero-IP placeholders are left alone so "active interface" detection still works. - INFO logging on encoding arm/update and on cache sweep so future bundles directly answer "did the rewrite fire?". Mode wire-value rename (#1429 follow-up, separate confusion source): - Both reporters' support bundles showed mode: immediate while the UI said "Archive"; @TrickShotMLG02 quoted: "I have no idea why it says immediate in the support-info.json file. In the webui the printer is set to archive". UI button "Archive" had always saved immediate, and "Queue" had always saved print_queue. Canonical wire values are now archive / review / queue / proxy, matching the button labels 1:1. - New normalize_vp_mode() + VP_MODE_* constants in models/virtual_printer.py; manager.py normalises on construction so a legacy row read pre-migration still dispatches correctly. - core/database.py::run_migrations rewrites existing virtual_printers and settings rows; idempotent (re-runs are no-ops); identical SQL under SQLite and Postgres. - API routes accept both legacy and canonical on input, normalise before storage. GET /settings/virtual-printer normalises on read so the frontend's mode-button highlight works for stale legacy values. - Three frontend VP components (VirtualPrinterSettings, VirtualPrinterCard, VirtualPrinterAddDialog) switched click handlers and type aliases to canonical; each got its own normalizeMode() helper so a stale-cached settings payload still highlights the right button. Two pre-existing `printer.mode === 'queue' ? 'review'` legacy mappings in VirtualPrinterCard were the source of a test failure caught mid-implementation where the new canonical 'queue' was being mis-aliased back to 'review' and hiding the auto-dispatch + force-color-match toggles. mode handler is NOT the dispatch bug: manager.py::_archive_file (the handler for archive mode) doesn't dispatch to the physical printer. The "files end up on the printer's SD card" symptom was the IP-leak from the bridge cache. The mode rename is purely clarity / support- bundle accuracy.
157 lines
5.8 KiB
TypeScript
157 lines
5.8 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { Loader2, ChevronDown, ArrowRightLeft } from 'lucide-react';
|
|
import { api, multiVirtualPrinterApi } from '../api/client';
|
|
import { Card, CardContent } from './Card';
|
|
import { Button } from './Button';
|
|
import { useToast } from '../contexts/ToastContext';
|
|
|
|
type Mode = 'archive' | 'review' | 'queue' | 'proxy';
|
|
|
|
const MODE_LABELS: Record<string, string> = {
|
|
archive: 'archive',
|
|
review: 'review',
|
|
queue: 'queue',
|
|
proxy: 'proxy',
|
|
};
|
|
|
|
interface VirtualPrinterAddDialogProps {
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function VirtualPrinterAddDialog({ onClose }: VirtualPrinterAddDialogProps) {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
const { showToast } = useToast();
|
|
|
|
const [name, setName] = useState('');
|
|
const [mode, setMode] = useState<Mode>('archive');
|
|
const [targetPrinterId, setTargetPrinterId] = useState<number | null>(null);
|
|
|
|
const { data: printers } = useQuery({
|
|
queryKey: ['printers'],
|
|
queryFn: api.getPrinters,
|
|
});
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: () =>
|
|
multiVirtualPrinterApi.create({
|
|
name: name.trim() || 'Bambuddy',
|
|
mode,
|
|
target_printer_id: mode === 'proxy' ? (targetPrinterId ?? undefined) : undefined,
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['virtual-printers'] });
|
|
showToast(t('virtualPrinter.toast.created'));
|
|
onClose();
|
|
},
|
|
onError: (error: Error) => {
|
|
showToast(error.message || t('virtualPrinter.toast.failedToCreate'), 'error');
|
|
},
|
|
});
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
|
onClick={onClose}
|
|
>
|
|
<Card
|
|
className="w-full max-w-md"
|
|
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
|
>
|
|
<CardContent className="p-6 space-y-4">
|
|
<h3 className="text-lg font-semibold text-white">{t('virtualPrinter.addDialog.title')}</h3>
|
|
|
|
{/* Name */}
|
|
<div>
|
|
<label className="text-sm text-white font-medium block mb-1">{t('virtualPrinter.addDialog.name')}</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Bambuddy"
|
|
className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white text-sm placeholder-bambu-gray"
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
{/* Mode */}
|
|
<div>
|
|
<label className="text-sm text-white font-medium block mb-1">{t('virtualPrinter.mode.title')}</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{(['archive', 'review', 'queue', 'proxy'] as const).map((m) => (
|
|
<button
|
|
key={m}
|
|
onClick={() => setMode(m)}
|
|
className={`p-2 rounded-lg border text-left transition-colors ${
|
|
mode === m
|
|
? m === 'proxy'
|
|
? 'border-blue-500 bg-blue-500/10'
|
|
: 'border-bambu-green bg-bambu-green/10'
|
|
: 'border-bambu-dark-tertiary hover:border-bambu-gray'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-1.5 text-white text-xs font-medium">
|
|
{m === 'proxy' && <ArrowRightLeft className="w-3 h-3" />}
|
|
{t(`virtualPrinter.mode.${MODE_LABELS[m]}`)}
|
|
</div>
|
|
<div className="text-[10px] text-bambu-gray">
|
|
{t(`virtualPrinter.mode.${MODE_LABELS[m]}Desc`)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Target Printer - only for proxy mode */}
|
|
{mode === 'proxy' && (
|
|
<div>
|
|
<label className="text-sm text-white font-medium block mb-1">{t('virtualPrinter.targetPrinter.title')}</label>
|
|
<div className="relative">
|
|
<select
|
|
value={targetPrinterId ?? ''}
|
|
onChange={(e) => {
|
|
const id = parseInt(e.target.value, 10);
|
|
setTargetPrinterId(isNaN(id) ? null : id);
|
|
}}
|
|
className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white text-sm appearance-none cursor-pointer pr-10"
|
|
>
|
|
<option value="">{t('virtualPrinter.targetPrinter.placeholder')}</option>
|
|
{printers?.map((p) => (
|
|
<option key={p.id} value={p.id}>{p.name} ({p.ip_address})</option>
|
|
))}
|
|
</select>
|
|
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<p className="text-xs text-bambu-gray">
|
|
{t('virtualPrinter.addDialog.hint')}
|
|
</p>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-3 pt-2">
|
|
<Button variant="secondary" onClick={onClose} className="flex-1" disabled={createMutation.isPending}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => createMutation.mutate()}
|
|
className="flex-1"
|
|
disabled={createMutation.isPending}
|
|
>
|
|
{createMutation.isPending ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
t('virtualPrinter.addDialog.create')
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|