Browse Source

Add auto-dispatch toggle for virtual printer queue mode (#587)

  Virtual printers in Queue mode now have an "Auto-dispatch" setting.
  When enabled (default), prints start automatically — preserving current
  behavior. When disabled, prints are added with manual_start so they
  wait for manual dispatch from the queue UI.
maziggy 2 months ago
parent
commit
b3c37072b2

+ 3 - 0
CHANGELOG.md

@@ -4,6 +4,9 @@ All notable changes to Bambuddy will be documented in this file.
 
 ## [0.2.3b1] - Unreleased
 
+### New Features
+- **Virtual Printer Queue Auto-Dispatch Toggle** ([#587](https://github.com/maziggy/bambuddy/issues/587)) — Added an "Auto-dispatch" toggle to virtual printers in Queue mode. When enabled (default), prints sent from the slicer are added to the queue and start automatically on the assigned printer — matching the current behavior. When disabled, prints are added to the queue with `manual_start` set, so they wait for manual dispatch. This allows users who want to review and manually assign prints before they start. Requested by @Percy2Live.
+
 ## [0.2.2b3] - Unreleased
 
 ### New Features

+ 6 - 0
backend/app/api/routes/virtual_printers.py

@@ -23,6 +23,7 @@ class VirtualPrinterCreate(BaseModel):
     model: str | None = None
     access_code: str | None = None
     target_printer_id: int | None = None
+    auto_dispatch: bool = True
     bind_ip: str | None = None
     remote_interface_ip: str | None = None
 
@@ -34,6 +35,7 @@ class VirtualPrinterUpdate(BaseModel):
     model: str | None = None
     access_code: str | None = None
     target_printer_id: int | None = None
+    auto_dispatch: bool | None = None
     bind_ip: str | None = None
     remote_interface_ip: str | None = None
 
@@ -56,6 +58,7 @@ def _vp_to_dict(vp, status: dict | None = None) -> dict:
         "access_code_set": bool(vp.access_code),
         "serial": serial,
         "target_printer_id": vp.target_printer_id,
+        "auto_dispatch": vp.auto_dispatch,
         "bind_ip": vp.bind_ip,
         "remote_interface_ip": vp.remote_interface_ip,
         "position": vp.position,
@@ -169,6 +172,7 @@ async def create_virtual_printer(
         model=body.model or DEFAULT_VIRTUAL_PRINTER_MODEL,
         access_code=body.access_code,
         target_printer_id=body.target_printer_id,
+        auto_dispatch=body.auto_dispatch,
         bind_ip=body.bind_ip,
         remote_interface_ip=body.remote_interface_ip,
         serial_suffix=new_suffix,
@@ -265,6 +269,8 @@ async def update_virtual_printer(
                 status_code=400, content={"detail": f"Printer with ID {body.target_printer_id} not found"}
             )
         vp.target_printer_id = body.target_printer_id
+    if body.auto_dispatch is not None:
+        vp.auto_dispatch = body.auto_dispatch
     if body.bind_ip is not None:
         vp.bind_ip = body.bind_ip
     if body.remote_interface_ip is not None:

+ 6 - 0
backend/app/core/database.py

@@ -1401,6 +1401,12 @@ async def run_migrations(conn):
     except OperationalError:
         pass  # Already migrated or table does not exist yet
 
+    # Migration: Add auto_dispatch column to virtual_printers
+    try:
+        await conn.execute(text("ALTER TABLE virtual_printers ADD COLUMN auto_dispatch BOOLEAN DEFAULT 1"))
+    except OperationalError:
+        pass  # Already applied
+
     # Cleanup: Remove obsolete settings keys that are no longer used
     obsolete_keys = ["slicer_binary_path"]
     for key in obsolete_keys:

+ 1 - 0
backend/app/models/virtual_printer.py

@@ -15,6 +15,7 @@ class VirtualPrinter(Base):
     name: Mapped[str] = mapped_column(String(100), default="Bambuddy")
     enabled: Mapped[bool] = mapped_column(Boolean, default=False)
     mode: Mapped[str] = mapped_column(String(20), default="immediate")  # immediate|review|print_queue|proxy
+    auto_dispatch: Mapped[bool] = mapped_column(Boolean, default=True)  # print_queue mode: auto-start or manual
     model: Mapped[str | None] = mapped_column(String(50), nullable=True)  # SSDP model code (server mode)
     access_code: Mapped[str | None] = mapped_column(String(8), nullable=True)  # 8 chars (server mode)
     target_printer_id: Mapped[int | None] = mapped_column(

+ 6 - 0
backend/app/services/virtual_printer/manager.py

@@ -102,6 +102,7 @@ class VirtualPrinterInstance:
         target_printer_ip: str = "",
         target_printer_serial: str = "",
         target_printer_id: int | None = None,
+        auto_dispatch: bool = True,
         bind_ip: str = "",
         remote_interface_ip: str = "",
         base_dir: Path,
@@ -116,6 +117,7 @@ class VirtualPrinterInstance:
         self.target_printer_ip = target_printer_ip
         self.target_printer_serial = target_printer_serial
         self.target_printer_id = target_printer_id
+        self.auto_dispatch = auto_dispatch
         self.bind_ip = bind_ip
         self.remote_interface_ip = remote_interface_ip
         self._session_factory = session_factory
@@ -320,6 +322,7 @@ class VirtualPrinterInstance:
                         plate_id=plate_id,
                         position=1,
                         status="pending",
+                        manual_start=not self.auto_dispatch,
                     )
                     db.add(queue_item)
                     await db.commit()
@@ -641,6 +644,7 @@ class VirtualPrinterManager:
                 or instance.bind_ip != (vp.bind_ip or "")
                 or instance.remote_interface_ip != (vp.remote_interface_ip or "")
                 or instance.target_printer_id != vp.target_printer_id
+                or instance.auto_dispatch != vp.auto_dispatch
             )
 
             if changed:
@@ -672,6 +676,7 @@ class VirtualPrinterManager:
                     serial_suffix=vp.serial_suffix,
                     target_printer_ip=target_ip,
                     target_printer_serial=target_serial,
+                    auto_dispatch=vp.auto_dispatch,
                     bind_ip=vp.bind_ip or "",
                     remote_interface_ip=vp.remote_interface_ip or "",
                     base_dir=self._base_dir,
@@ -689,6 +694,7 @@ class VirtualPrinterManager:
                     access_code=vp.access_code or "",
                     serial_suffix=vp.serial_suffix,
                     target_printer_id=vp.target_printer_id,
+                    auto_dispatch=vp.auto_dispatch,
                     bind_ip=vp.bind_ip or "",
                     remote_interface_ip=vp.remote_interface_ip or "",
                     base_dir=self._base_dir,

+ 1 - 0
backend/tests/conftest.py

@@ -88,6 +88,7 @@ async def test_engine():
         spool_catalog,
         spool_usage_history,
         user,
+        virtual_printer,
     )
 
     async with engine.begin() as conn:

+ 68 - 0
backend/tests/integration/test_virtual_printer_api.py

@@ -270,3 +270,71 @@ class TestPendingUploadsAPI:
         assert response.status_code == 200
         result = response.json()
         assert "discarded" in result
+
+
+class TestVirtualPrinterAutoDispatchAPI:
+    """Integration tests for auto_dispatch on /api/v1/virtual-printers endpoints."""
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_create_virtual_printer_auto_dispatch_default(self, async_client: AsyncClient):
+        """Verify creating a VP without auto_dispatch defaults to true."""
+        response = await async_client.post(
+            "/api/v1/virtual-printers",
+            json={
+                "name": "TestDefaultDispatch",
+                "mode": "print_queue",
+                "access_code": "12345678",
+            },
+        )
+
+        assert response.status_code == 200
+        result = response.json()
+        assert result["auto_dispatch"] is True
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_create_virtual_printer_auto_dispatch_false(self, async_client: AsyncClient):
+        """Verify creating a VP with auto_dispatch=false persists correctly."""
+        response = await async_client.post(
+            "/api/v1/virtual-printers",
+            json={
+                "name": "TestManualDispatch",
+                "mode": "print_queue",
+                "access_code": "12345678",
+                "auto_dispatch": False,
+            },
+        )
+
+        assert response.status_code == 200
+        result = response.json()
+        assert result["auto_dispatch"] is False
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_update_virtual_printer_auto_dispatch(self, async_client: AsyncClient):
+        """Verify auto_dispatch can be toggled via PUT and persists."""
+        # Create with auto_dispatch=True (default)
+        create_resp = await async_client.post(
+            "/api/v1/virtual-printers",
+            json={
+                "name": "TestToggleDispatch",
+                "mode": "print_queue",
+                "access_code": "12345678",
+            },
+        )
+        assert create_resp.status_code == 200
+        vp_id = create_resp.json()["id"]
+
+        # Update to auto_dispatch=False
+        update_resp = await async_client.put(
+            f"/api/v1/virtual-printers/{vp_id}",
+            json={"auto_dispatch": False},
+        )
+        assert update_resp.status_code == 200
+        assert update_resp.json()["auto_dispatch"] is False
+
+        # Verify it persists by fetching
+        get_resp = await async_client.get(f"/api/v1/virtual-printers/{vp_id}")
+        assert get_resp.status_code == 200
+        assert get_resp.json()["auto_dispatch"] is False

+ 122 - 0
backend/tests/unit/services/test_virtual_printer.py

@@ -156,6 +156,127 @@ class TestVirtualPrinterInstance:
 
             assert "verify_job" not in instance._pending_files
 
+    # ========================================================================
+    # Tests for auto_dispatch
+    # ========================================================================
+
+    def test_auto_dispatch_defaults_to_true(self, tmp_path):
+        """Verify auto_dispatch defaults to True when not specified."""
+        from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
+
+        inst = VirtualPrinterInstance(
+            vp_id=10,
+            name="DefaultDispatch",
+            mode="print_queue",
+            model="C11",
+            access_code="12345678",
+            serial_suffix="391800010",
+            base_dir=tmp_path,
+        )
+        assert inst.auto_dispatch is True
+
+    @pytest.mark.asyncio
+    async def test_add_to_print_queue_with_auto_dispatch_on(self, tmp_path):
+        """Verify queue items have manual_start=False when auto_dispatch=True."""
+        from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
+
+        mock_db = AsyncMock()
+        added_items = []
+
+        def capture_add(item):
+            added_items.append(item)
+
+        mock_db.add = MagicMock(side_effect=capture_add)
+        mock_db.commit = AsyncMock()
+
+        mock_session_factory = MagicMock()
+        mock_session_ctx = AsyncMock()
+        mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_db)
+        mock_session_ctx.__aexit__ = AsyncMock(return_value=False)
+        mock_session_factory.return_value = mock_session_ctx
+
+        inst = VirtualPrinterInstance(
+            vp_id=11,
+            name="AutoDispatchOn",
+            mode="print_queue",
+            model="C11",
+            access_code="12345678",
+            serial_suffix="391800011",
+            auto_dispatch=True,
+            base_dir=tmp_path,
+            session_factory=mock_session_factory,
+        )
+
+        # Create a temp 3mf file
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(b"fake3mf")
+
+        mock_archive = MagicMock()
+        mock_archive.id = 1
+        mock_archive.print_name = "test"
+
+        with patch(
+            "backend.app.services.archive.ArchiveService.archive_print",
+            new_callable=AsyncMock,
+            return_value=mock_archive,
+        ):
+            await inst._add_to_print_queue(file_path, "192.168.1.100")
+
+        assert len(added_items) == 1
+        queue_item = added_items[0]
+        assert queue_item.manual_start is False
+
+    @pytest.mark.asyncio
+    async def test_add_to_print_queue_with_auto_dispatch_off(self, tmp_path):
+        """Verify queue items have manual_start=True when auto_dispatch=False."""
+        from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
+
+        mock_db = AsyncMock()
+        added_items = []
+
+        def capture_add(item):
+            added_items.append(item)
+
+        mock_db.add = MagicMock(side_effect=capture_add)
+        mock_db.commit = AsyncMock()
+
+        mock_session_factory = MagicMock()
+        mock_session_ctx = AsyncMock()
+        mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_db)
+        mock_session_ctx.__aexit__ = AsyncMock(return_value=False)
+        mock_session_factory.return_value = mock_session_ctx
+
+        inst = VirtualPrinterInstance(
+            vp_id=12,
+            name="AutoDispatchOff",
+            mode="print_queue",
+            model="C11",
+            access_code="12345678",
+            serial_suffix="391800012",
+            auto_dispatch=False,
+            base_dir=tmp_path,
+            session_factory=mock_session_factory,
+        )
+
+        # Create a temp 3mf file
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(b"fake3mf")
+
+        mock_archive = MagicMock()
+        mock_archive.id = 1
+        mock_archive.print_name = "test"
+
+        with patch(
+            "backend.app.services.archive.ArchiveService.archive_print",
+            new_callable=AsyncMock,
+            return_value=mock_archive,
+        ):
+            await inst._add_to_print_queue(file_path, "192.168.1.100")
+
+        assert len(added_items) == 1
+        queue_item = added_items[0]
+        assert queue_item.manual_start is True
+
 
 class TestVirtualPrinterManager:
     """Tests for VirtualPrinterManager orchestrator."""
@@ -327,6 +448,7 @@ class TestVirtualPrinterManager:
             "bind_ip": "",
             "remote_interface_ip": "",
             "target_printer_id": None,
+            "auto_dispatch": True,
             "position": 0,
         }
         defaults.update(overrides)

+ 136 - 0
frontend/src/__tests__/components/VirtualPrinterCard.test.tsx

@@ -0,0 +1,136 @@
+/**
+ * Tests for the VirtualPrinterCard component.
+ *
+ * Tests the auto-dispatch toggle behavior:
+ * - Visibility based on mode (print_queue only)
+ * - Default state (on)
+ * - API mutation on toggle click
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from '../utils';
+import { VirtualPrinterCard } from '../../components/VirtualPrinterCard';
+import type { VirtualPrinterConfig } from '../../api/client';
+
+// Mock the API client
+vi.mock('../../api/client', () => ({
+  multiVirtualPrinterApi: {
+    update: vi.fn().mockResolvedValue({}),
+    remove: vi.fn().mockResolvedValue({}),
+  },
+  api: {
+    getSettings: vi.fn().mockResolvedValue({}),
+    getPrinters: vi.fn().mockResolvedValue([]),
+    getNetworkInterfaces: vi.fn().mockResolvedValue({ interfaces: [] }),
+  },
+}));
+
+import { multiVirtualPrinterApi } from '../../api/client';
+
+const models: Record<string, string> = {
+  '3DPrinter-X1-Carbon': 'X1C',
+  'C12': 'P1S',
+};
+
+const createMockPrinter = (overrides: Partial<VirtualPrinterConfig> = {}): VirtualPrinterConfig => ({
+  id: 1,
+  name: 'Test VP',
+  enabled: false,
+  mode: 'immediate',
+  model: '3DPrinter-X1-Carbon',
+  model_name: 'X1C',
+  access_code_set: false,
+  serial: '00M00A391800001',
+  target_printer_id: null,
+  auto_dispatch: true,
+  bind_ip: null,
+  remote_interface_ip: null,
+  position: 0,
+  status: { running: false, pending_files: 0 },
+  ...overrides,
+});
+
+describe('VirtualPrinterCard - auto-dispatch toggle', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.mocked(multiVirtualPrinterApi.update).mockResolvedValue(createMockPrinter());
+  });
+
+  it('renders auto-dispatch toggle when mode is print_queue', async () => {
+    const printer = createMockPrinter({ mode: 'print_queue' });
+    render(<VirtualPrinterCard printer={printer} models={models} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('Auto-dispatch')).toBeInTheDocument();
+    });
+  });
+
+  it('does not render auto-dispatch toggle when mode is immediate', async () => {
+    const printer = createMockPrinter({ mode: 'immediate' });
+    render(<VirtualPrinterCard printer={printer} models={models} />);
+
+    // Wait for the card to render fully (check for something that should be there)
+    await waitFor(() => {
+      expect(screen.getByText('Test VP')).toBeInTheDocument();
+    });
+
+    expect(screen.queryByText('Auto-dispatch')).not.toBeInTheDocument();
+  });
+
+  it('does not render auto-dispatch toggle when mode is proxy', async () => {
+    const printer = createMockPrinter({ mode: 'proxy' });
+    render(<VirtualPrinterCard printer={printer} models={models} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('Test VP')).toBeInTheDocument();
+    });
+
+    expect(screen.queryByText('Auto-dispatch')).not.toBeInTheDocument();
+  });
+
+  it('auto-dispatch toggle defaults to on', async () => {
+    const printer = createMockPrinter({ mode: 'print_queue', auto_dispatch: true });
+    render(<VirtualPrinterCard printer={printer} models={models} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('Auto-dispatch')).toBeInTheDocument();
+    });
+
+    // The auto-dispatch section container has the toggle button as a sibling of the text div
+    const title = screen.getByText('Auto-dispatch');
+    const section = title.closest('.flex.items-center.justify-between');
+    expect(section).toBeTruthy();
+    const toggleButton = section!.querySelector('button');
+    expect(toggleButton).toBeTruthy();
+    expect(toggleButton!.className).toContain('bg-bambu-green');
+  });
+
+  it('clicking auto-dispatch toggle calls update API', async () => {
+    const user = userEvent.setup();
+    const printer = createMockPrinter({ mode: 'print_queue', auto_dispatch: true });
+    vi.mocked(multiVirtualPrinterApi.update).mockResolvedValue(
+      createMockPrinter({ mode: 'print_queue', auto_dispatch: false })
+    );
+
+    render(<VirtualPrinterCard printer={printer} models={models} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('Auto-dispatch')).toBeInTheDocument();
+    });
+
+    // Find the auto-dispatch toggle via the section container
+    const title = screen.getByText('Auto-dispatch');
+    const section = title.closest('.flex.items-center.justify-between');
+    expect(section).toBeTruthy();
+    const toggleButton = section!.querySelector('button');
+    expect(toggleButton).toBeTruthy();
+
+    await user.click(toggleButton!);
+
+    await waitFor(() => {
+      expect(multiVirtualPrinterApi.update).toHaveBeenCalledWith(1, { auto_dispatch: false });
+    });
+  });
+});

+ 3 - 0
frontend/src/api/client.ts

@@ -4681,6 +4681,7 @@ export interface VirtualPrinterConfig {
   access_code_set: boolean;
   serial: string;
   target_printer_id: number | null;
+  auto_dispatch: boolean;
   bind_ip: string | null;
   remote_interface_ip: string | null;
   position: number;
@@ -4704,6 +4705,7 @@ export const multiVirtualPrinterApi = {
     model?: string;
     access_code?: string;
     target_printer_id?: number;
+    auto_dispatch?: boolean;
     bind_ip?: string;
     remote_interface_ip?: string;
   }) =>
@@ -4719,6 +4721,7 @@ export const multiVirtualPrinterApi = {
     model?: string;
     access_code?: string;
     target_printer_id?: number;
+    auto_dispatch?: boolean;
     bind_ip?: string;
     remote_interface_ip?: string;
   }) =>

+ 32 - 0
frontend/src/components/VirtualPrinterCard.tsx

@@ -42,6 +42,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
   const [localBindIp, setLocalBindIp] = useState(printer.bind_ip || '');
   const [localRemoteInterfaceIp, setLocalRemoteInterfaceIp] = useState(printer.remote_interface_ip || '');
   const [localModel, setLocalModel] = useState(printer.model || '');
+  const [localAutoDispatch, setLocalAutoDispatch] = useState(printer.auto_dispatch ?? true);
   const [showAccessCode, setShowAccessCode] = useState(false);
   const [pendingAction, setPendingAction] = useState<string | null>(null);
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -56,6 +57,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
       setLocalBindIp(printer.bind_ip || '');
       setLocalRemoteInterfaceIp(printer.remote_interface_ip || '');
       setLocalModel(printer.model || '');
+      setLocalAutoDispatch(printer.auto_dispatch ?? true);
     }
   }, [printer, pendingAction]);
 
@@ -278,6 +280,36 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
               </div>
             </div>
 
+            {/* Auto-dispatch toggle - only for print_queue mode */}
+            {localMode === 'print_queue' && (
+              <div className="pt-2 border-t border-bambu-dark-tertiary">
+                <div className="flex items-center justify-between">
+                  <div>
+                    <div className="text-white text-sm font-medium">{t('virtualPrinter.autoDispatch.title')}</div>
+                    <div className="text-[10px] text-bambu-gray">{t('virtualPrinter.autoDispatch.description')}</div>
+                  </div>
+                  <button
+                    onClick={() => {
+                      const newVal = !localAutoDispatch;
+                      setLocalAutoDispatch(newVal);
+                      setPendingAction('autoDispatch');
+                      updateMutation.mutate({ auto_dispatch: newVal });
+                    }}
+                    disabled={pendingAction === 'autoDispatch'}
+                    className={`relative w-10 h-5 rounded-full transition-colors flex-shrink-0 ${
+                      localAutoDispatch ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
+                    } ${pendingAction === 'autoDispatch' ? 'opacity-50' : ''}`}
+                  >
+                    <span
+                      className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full transition-transform ${
+                        localAutoDispatch ? 'translate-x-5' : ''
+                      }`}
+                    />
+                  </button>
+                </div>
+              </div>
+            )}
+
             {/* Printer Model - for non-proxy modes */}
             {localMode !== 'proxy' && (
               <div className="pt-2 border-t border-bambu-dark-tertiary">

+ 4 - 0
frontend/src/i18n/locales/de.ts

@@ -3416,6 +3416,10 @@ export default {
       proxy: 'Proxy',
       proxyDesc: 'An echten Drucker weiterleiten',
     },
+    autoDispatch: {
+      title: 'Automatisch starten',
+      description: 'Drucke automatisch starten, wenn sie zur Warteschlange hinzugefügt werden. Wenn deaktiviert, warten Drucke auf manuellen Start.',
+    },
     setupRequired: {
       title: 'Einrichtung erforderlich',
       description: 'Die virtuelle Druckerfunktion erfordert zusätzliche Systemkonfiguration, bevor sie funktioniert. Dies beinhaltet Portweiterleitung, Firewall-Regeln und plattformspezifische Einstellungen.',

+ 4 - 0
frontend/src/i18n/locales/en.ts

@@ -3421,6 +3421,10 @@ export default {
       proxy: 'Proxy',
       proxyDesc: 'Relay to real printer',
     },
+    autoDispatch: {
+      title: 'Auto-dispatch',
+      description: 'Automatically start prints when added to queue. When off, prints wait for manual dispatch.',
+    },
     setupRequired: {
       title: 'Setup Required',
       description: 'The virtual printer feature requires additional system configuration before it will work. This includes port forwarding, firewall rules, and platform-specific settings.',

+ 4 - 0
frontend/src/i18n/locales/fr.ts

@@ -3408,6 +3408,10 @@ export default {
       proxy: 'Proxy',
       proxyDesc: 'Relais vers imprimante réelle',
     },
+    autoDispatch: {
+      title: 'Lancement automatique',
+      description: 'Lancer automatiquement les impressions ajoutées à la file. Désactivé, les impressions attendent un lancement manuel.',
+    },
     setupRequired: {
       title: 'Configuration requise',
       description: 'Nécessite des réglages système (ports, pare-feu).',

+ 4 - 0
frontend/src/i18n/locales/it.ts

@@ -3407,6 +3407,10 @@ export default {
       proxy: 'Proxy',
       proxyDesc: 'Inoltra a stampante reale',
     },
+    autoDispatch: {
+      title: 'Avvio automatico',
+      description: 'Avvia automaticamente le stampe aggiunte alla coda. Se disattivato, le stampe attendono l\'avvio manuale.',
+    },
     setupRequired: {
       title: 'Configurazione necessaria',
       description: 'La stampante virtuale richiede configurazioni di sistema aggiuntive prima di funzionare. Include port forwarding, regole firewall e impostazioni specifiche della piattaforma.',

+ 4 - 0
frontend/src/i18n/locales/ja.ts

@@ -3421,6 +3421,10 @@ export default {
       proxy: 'プロキシ',
       proxyDesc: '実際のプリンターに転送',
     },
+    autoDispatch: {
+      title: '自動ディスパッチ',
+      description: 'キューに追加されたときに自動的に印刷を開始します。オフの場合、手動ディスパッチを待ちます。',
+    },
     setupRequired: {
       title: 'セットアップが必要です',
       description: '仮想プリンター機能を使用するには追加のシステム設定が必要です。ポートフォワーディング、ファイアウォールルール、プラットフォーム固有の設定が含まれます。',

+ 4 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -3407,6 +3407,10 @@ export default {
       proxy: 'Proxy',
       proxyDesc: 'Retransmitir para impressora real',
     },
+    autoDispatch: {
+      title: 'Envio automático',
+      description: 'Iniciar impressões automaticamente quando adicionadas à fila. Quando desativado, as impressões aguardam envio manual.',
+    },
     setupRequired: {
       title: 'Configuração Necessária',
       description: 'O recurso de impressora virtual requer configuração adicional do sistema antes de funcionar. Isso inclui encaminhamento de portas, regras de firewall e configurações específicas da plataforma.',

+ 4 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -3407,6 +3407,10 @@ export default {
       proxy: '代理',
       proxyDesc: '中继到真实打印机',
     },
+    autoDispatch: {
+      title: '自动派发',
+      description: '添加到队列时自动开始打印。关闭后,打印任务等待手动派发。',
+    },
     setupRequired: {
       title: '需要设置',
       description: '虚拟打印机功能需要额外的系统配置才能工作。包括端口转发、防火墙规则和平台特定设置。',

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DNkwGxkk.js


+ 1 - 1
static/index.html

@@ -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-CDBlYFWf.js"></script>
+    <script type="module" crossorigin src="/assets/index-DNkwGxkk.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DfcIVNpM.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff