Browse Source

Add configurable default print options (#858)

maziggy 1 month ago
parent
commit
9aa9fdc586

+ 1 - 0
CHANGELOG.md

@@ -13,6 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Bulk Printer Actions** ([#825](https://github.com/maziggy/bambuddy/issues/825)) — Select multiple printer cards and apply bulk actions from a floating toolbar. Toggle selection mode from the header, then click cards to select. Use "Select All", "Select by State" (printing, paused, finished, idle, error, offline), or "Select by Location" to quickly pick printers. Available actions: Stop, Pause, Resume, Clear Notifications, and Clear Bed — each button is smart-enabled based on the selected printers' current states. Confirmation modals for destructive actions (Stop, Pause, Clear Bed). The status summary bar now shows all printer states (printing, paused, finished, idle, error, offline). Requested by @therevoman.
 - **Prefer Lowest Remaining Filament** ([#805](https://github.com/maziggy/bambuddy/issues/805)) — New optional setting in Settings → Filament that prefers AMS spools with the lowest remaining filament during auto-matching. When multiple spools match the same type and color, the one with the least filament remaining is selected first. Helps consume partial spools before starting new ones. Applies to queue scheduling, print modal, and multi-printer mapping. Unknown remain values (e.g. external spools without sensors) are treated as full. Disabled by default. Requested by @Mofoss.
 - **REST/Webhook Smart Plug Type** ([#472](https://github.com/maziggy/bambuddy/issues/472)) — New "REST" smart plug type for controlling power via generic HTTP APIs. Works with any home automation platform that has an HTTP endpoint (openHAB, ioBroker, FHEM, Node-RED, etc.). Configure separate ON/OFF URLs with custom HTTP methods (GET/POST/PUT/PATCH), request bodies, and headers. Optional status polling via a GET endpoint with JSON path extraction for state, power, and energy monitoring. Fully controllable — supports auto on/off with prints, daily scheduling, sidebar quick-toggle, and power alerts. Requested by @Percy2Live.
+- **Configurable Default Print Options** ([#858](https://github.com/maziggy/bambuddy/issues/858)) — Print options (bed levelling, flow calibration, vibration calibration, first layer inspection, timelapse) now have configurable defaults in Settings → Workflow. Set your preferred defaults once and every new print dialog starts with those values. Still overridable per print. Requested by @NoahTingey.
 
 ### Improved
 - **Queue Page Visual Refresh** — Compact stats bar replaces the five summary cards (saves vertical space), color-coded left borders on all queue items for instant status scanning, collapsible history section (collapsed by default), and condensed single-line rows for history items showing more prints at a glance.

+ 1 - 0
README.md

@@ -109,6 +109,7 @@ Perfect for remote print farms, traveling makers, or accessing your home printer
 - Print queue with drag-and-drop and timeline schedule view
 - Multi-printer selection (send to multiple printers at once)
 - Staggered batch start (start printers in groups with configurable interval to avoid power spikes — works in both Print and Queue dialogs)
+- Configurable default print options (bed levelling, flow/vibration calibration, first layer inspection, timelapse) in Settings → Workflow
 - Model-based queue assignment (send to "any X1C" for load balancing) with location filtering
 - Filament override for model-based queue (swap filament colors/types before scheduling)
 - Filament validation (only assign to printers with required filaments)

+ 5 - 0
backend/app/api/routes/settings.py

@@ -104,6 +104,11 @@ async def get_settings(
                 "queue_drying_block",
                 "ambient_drying_enabled",
                 "require_plate_clear",
+                "default_bed_levelling",
+                "default_flow_cali",
+                "default_vibration_cali",
+                "default_layer_inspect",
+                "default_timelapse",
             ]:
                 settings_dict[setting.key] = setting.value.lower() == "true"
             elif setting.key in [

+ 16 - 0
backend/app/schemas/settings.py

@@ -190,6 +190,17 @@ class AppSettings(BaseModel):
         description="Enable user email notifications for print job events (requires Advanced Authentication)",
     )
 
+    # Default print options
+    default_bed_levelling: bool = Field(default=True, description="Default bed levelling option for new prints")
+    default_flow_cali: bool = Field(default=False, description="Default flow calibration option for new prints")
+    default_vibration_cali: bool = Field(
+        default=True, description="Default vibration calibration option for new prints"
+    )
+    default_layer_inspect: bool = Field(
+        default=False, description="Default first layer inspection option for new prints"
+    )
+    default_timelapse: bool = Field(default=False, description="Default timelapse option for new prints")
+
     # Staggered batch start for multi-printer jobs
     stagger_group_size: int = Field(
         default=2, ge=1, le=50, description="Number of printers to start simultaneously in staggered mode"
@@ -279,6 +290,11 @@ class AppSettingsUpdate(BaseModel):
     prometheus_token: str | None = None
     low_stock_threshold: float | None = Field(default=None, ge=0.1, le=99.9)
     user_notifications_enabled: bool | None = None
+    default_bed_levelling: bool | None = None
+    default_flow_cali: bool | None = None
+    default_vibration_cali: bool | None = None
+    default_layer_inspect: bool | None = None
+    default_timelapse: bool | None = None
     stagger_group_size: int | None = Field(default=None, ge=1, le=50)
     stagger_interval_minutes: int | None = Field(default=None, ge=1, le=60)
     require_plate_clear: bool | None = None

+ 81 - 0
backend/tests/integration/test_settings_api.py

@@ -474,6 +474,87 @@ class TestSettingsAPI:
         response = await async_client.put("/api/v1/settings/", json={"stagger_interval_minutes": 61})
         assert response.status_code == 422
 
+    # ========================================================================
+    # Default print options tests
+    # ========================================================================
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_default_print_options_defaults(self, async_client: AsyncClient):
+        """Verify default print options have correct defaults."""
+        response = await async_client.get("/api/v1/settings/")
+        result = response.json()
+
+        assert result["default_bed_levelling"] is True
+        assert result["default_flow_cali"] is False
+        assert result["default_vibration_cali"] is True
+        assert result["default_layer_inspect"] is False
+        assert result["default_timelapse"] is False
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_update_default_print_options(self, async_client: AsyncClient):
+        """Verify default print options can be updated."""
+        response = await async_client.put(
+            "/api/v1/settings/",
+            json={
+                "default_bed_levelling": False,
+                "default_flow_cali": True,
+                "default_vibration_cali": False,
+                "default_layer_inspect": True,
+                "default_timelapse": True,
+            },
+        )
+
+        assert response.status_code == 200
+        result = response.json()
+        assert result["default_bed_levelling"] is False
+        assert result["default_flow_cali"] is True
+        assert result["default_vibration_cali"] is False
+        assert result["default_layer_inspect"] is True
+        assert result["default_timelapse"] is True
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_default_print_options_persist(self, async_client: AsyncClient):
+        """CRITICAL: Verify default print options persist after update."""
+        await async_client.put(
+            "/api/v1/settings/",
+            json={
+                "default_bed_levelling": False,
+                "default_timelapse": True,
+            },
+        )
+
+        response = await async_client.get("/api/v1/settings/")
+        result = response.json()
+        assert result["default_bed_levelling"] is False
+        assert result["default_timelapse"] is True
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_default_print_options_partial_update(self, async_client: AsyncClient):
+        """Verify partial updates don't affect other default print options."""
+        # Set all to non-default
+        await async_client.put(
+            "/api/v1/settings/",
+            json={
+                "default_bed_levelling": False,
+                "default_flow_cali": True,
+            },
+        )
+
+        # Update only one
+        response = await async_client.put(
+            "/api/v1/settings/",
+            json={"default_bed_levelling": True},
+        )
+
+        assert response.status_code == 200
+        result = response.json()
+        assert result["default_bed_levelling"] is True
+        assert result["default_flow_cali"] is True  # Should remain from previous update
+
     # ========================================================================
     # Home Assistant environment variable tests
     # ========================================================================

+ 35 - 0
frontend/src/__tests__/pages/SettingsPage.test.tsx

@@ -269,6 +269,41 @@ describe('SettingsPage', () => {
         expect(screen.getByText('Queue Auto-Drying')).toBeInTheDocument();
       });
     });
+
+    it('shows default print options on Workflow tab', async () => {
+      const user = userEvent.setup();
+      render(<SettingsPage />);
+
+      await waitFor(() => {
+        expect(screen.getByText('Workflow')).toBeInTheDocument();
+      });
+
+      await user.click(screen.getByText('Workflow'));
+
+      await waitFor(() => {
+        expect(screen.getByText('Default Print Options')).toBeInTheDocument();
+        expect(screen.getByText('Bed Levelling')).toBeInTheDocument();
+        expect(screen.getByText('Flow Calibration')).toBeInTheDocument();
+        expect(screen.getByText('Vibration Calibration')).toBeInTheDocument();
+        expect(screen.getByText('First Layer Inspection')).toBeInTheDocument();
+        expect(screen.getByText('Timelapse')).toBeInTheDocument();
+      });
+    });
+
+    it('shows default print options description', async () => {
+      const user = userEvent.setup();
+      render(<SettingsPage />);
+
+      await waitFor(() => {
+        expect(screen.getByText('Workflow')).toBeInTheDocument();
+      });
+
+      await user.click(screen.getByText('Workflow'));
+
+      await waitFor(() => {
+        expect(screen.getByText(/overridden per print in the print dialog/)).toBeInTheDocument();
+      });
+    });
   });
 
   describe('API Keys tab', () => {

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

@@ -887,6 +887,12 @@ export interface AppSettings {
   low_stock_threshold: number;
   // User email notifications toggle
   user_notifications_enabled: boolean;
+  // Default print options
+  default_bed_levelling: boolean;
+  default_flow_cali: boolean;
+  default_vibration_cali: boolean;
+  default_layer_inspect: boolean;
+  default_timelapse: boolean;
   // Staggered batch start defaults
   stagger_group_size: number;
   stagger_interval_minutes: number;

+ 14 - 0
frontend/src/components/PrintModal/index.tsx

@@ -220,6 +220,20 @@ export function PrintModal({
     queryFn: api.getSettings,
   });
 
+  // Sync print option defaults from settings once available
+  const printDefaultsApplied = useRef(false);
+  useEffect(() => {
+    if (!settings || printDefaultsApplied.current || mode === 'edit-queue-item') return;
+    printDefaultsApplied.current = true;
+    setPrintOptions({
+      bed_levelling: settings.default_bed_levelling ?? DEFAULT_PRINT_OPTIONS.bed_levelling,
+      flow_cali: settings.default_flow_cali ?? DEFAULT_PRINT_OPTIONS.flow_cali,
+      vibration_cali: settings.default_vibration_cali ?? DEFAULT_PRINT_OPTIONS.vibration_cali,
+      layer_inspect: settings.default_layer_inspect ?? DEFAULT_PRINT_OPTIONS.layer_inspect,
+      timelapse: settings.default_timelapse ?? DEFAULT_PRINT_OPTIONS.timelapse,
+    });
+  }, [settings, mode]);
+
   // Sync stagger defaults from settings once available
   const staggerDefaultsApplied = useRef(false);
   useEffect(() => {

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

@@ -1580,6 +1580,18 @@ export default {
     historyRetention: 'Verlaufsaufbewahrung',
     keepSensorHistory: 'Sensorverlauf behalten für',
     historyRetentionDescription: 'Ältere Feuchtigkeits- und Temperaturdaten werden automatisch gelöscht',
+    defaultPrintOptions: 'Standard-Druckoptionen',
+    defaultPrintOptionsDescription: 'Standardwerte für Druckoptionen bei neuen Drucken festlegen. Diese können im Druckdialog pro Druck überschrieben werden.',
+    defaultBedLevelling: 'Bett-Nivellierung',
+    defaultBedLevellingDesc: 'Bett vor dem Druck automatisch nivellieren',
+    defaultFlowCali: 'Fluss-Kalibrierung',
+    defaultFlowCaliDesc: 'Extrusionsfluss kalibrieren',
+    defaultVibrationCali: 'Vibrationskalibrierung',
+    defaultVibrationCaliDesc: 'Ringing-Artefakte reduzieren',
+    defaultLayerInspect: 'Erste-Schicht-Inspektion',
+    defaultLayerInspectDesc: 'KI-Inspektion der ersten Schicht',
+    defaultTimelapse: 'Zeitraffer',
+    defaultTimelapseDesc: 'Zeitraffervideo aufnehmen',
     staggeredStart: 'Staggered Start',
     staggeredStartDescription: 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.',
     plateClear: 'Druckplatte-Bestätigung',

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

@@ -1581,6 +1581,18 @@ export default {
     historyRetention: 'History Retention',
     keepSensorHistory: 'Keep sensor history for',
     historyRetentionDescription: 'Older humidity and temperature data will be automatically deleted',
+    defaultPrintOptions: 'Default Print Options',
+    defaultPrintOptionsDescription: 'Set default values for print options when starting new prints. These can be overridden per print in the print dialog.',
+    defaultBedLevelling: 'Bed Levelling',
+    defaultBedLevellingDesc: 'Auto-level bed before print',
+    defaultFlowCali: 'Flow Calibration',
+    defaultFlowCaliDesc: 'Calibrate extrusion flow',
+    defaultVibrationCali: 'Vibration Calibration',
+    defaultVibrationCaliDesc: 'Reduce ringing artifacts',
+    defaultLayerInspect: 'First Layer Inspection',
+    defaultLayerInspectDesc: 'AI inspection of first layer',
+    defaultTimelapse: 'Timelapse',
+    defaultTimelapseDesc: 'Record timelapse video',
     staggeredStart: 'Staggered Start',
     staggeredStartDescription: 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.',
     plateClear: 'Plate-Clear Confirmation',

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

@@ -1580,6 +1580,18 @@ export default {
     historyRetention: 'Rétention d\'historique',
     keepSensorHistory: 'Garder l\'historique pendant',
     historyRetentionDescription: 'Les anciennes données seront supprimées.',
+    defaultPrintOptions: 'Options d\'impression par défaut',
+    defaultPrintOptionsDescription: 'Définir les valeurs par défaut des options d\'impression. Modifiables dans la boîte de dialogue d\'impression.',
+    defaultBedLevelling: 'Nivellement du lit',
+    defaultBedLevellingDesc: 'Niveler automatiquement le lit avant l\'impression',
+    defaultFlowCali: 'Calibration du flux',
+    defaultFlowCaliDesc: 'Calibrer le flux d\'extrusion',
+    defaultVibrationCali: 'Calibration des vibrations',
+    defaultVibrationCaliDesc: 'Réduire les artefacts de ringing',
+    defaultLayerInspect: 'Inspection première couche',
+    defaultLayerInspectDesc: 'Inspection IA de la première couche',
+    defaultTimelapse: 'Timelapse',
+    defaultTimelapseDesc: 'Enregistrer une vidéo timelapse',
     staggeredStart: 'Staggered Start',
     staggeredStartDescription: 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.',
     plateClear: 'Confirmation de plateau libre',

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

@@ -1580,6 +1580,18 @@ export default {
     historyRetention: 'Conservazione cronologia',
     keepSensorHistory: 'Mantieni cronologia sensori per',
     historyRetentionDescription: 'I dati più vecchi saranno eliminati automaticamente',
+    defaultPrintOptions: 'Opzioni di stampa predefinite',
+    defaultPrintOptionsDescription: 'Imposta i valori predefiniti per le opzioni di stampa. Possono essere modificati nella finestra di stampa.',
+    defaultBedLevelling: 'Livellamento piatto',
+    defaultBedLevellingDesc: 'Livellamento automatico del piatto prima della stampa',
+    defaultFlowCali: 'Calibrazione flusso',
+    defaultFlowCaliDesc: 'Calibra il flusso di estrusione',
+    defaultVibrationCali: 'Calibrazione vibrazioni',
+    defaultVibrationCaliDesc: 'Riduce gli artefatti di ringing',
+    defaultLayerInspect: 'Ispezione primo strato',
+    defaultLayerInspectDesc: 'Ispezione IA del primo strato',
+    defaultTimelapse: 'Timelapse',
+    defaultTimelapseDesc: 'Registra un video timelapse',
     staggeredStart: 'Staggered Start',
     staggeredStartDescription: 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.',
     plateClear: 'Conferma piatto libero',

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

@@ -1579,6 +1579,18 @@ export default {
     historyRetention: '履歴の保持',
     keepSensorHistory: 'センサー履歴の保持期間',
     historyRetentionDescription: '古い湿度と温度データは自動的に削除されます',
+    defaultPrintOptions: 'デフォルト印刷オプション',
+    defaultPrintOptionsDescription: '新しい印刷のデフォルト値を設定します。印刷ダイアログで個別に変更できます。',
+    defaultBedLevelling: 'ベッドレベリング',
+    defaultBedLevellingDesc: '印刷前にベッドを自動レベリング',
+    defaultFlowCali: 'フローキャリブレーション',
+    defaultFlowCaliDesc: '押出フローのキャリブレーション',
+    defaultVibrationCali: '振動キャリブレーション',
+    defaultVibrationCaliDesc: 'リンギングアーティファクトを低減',
+    defaultLayerInspect: '第1層検査',
+    defaultLayerInspectDesc: 'AIによる第1層の検査',
+    defaultTimelapse: 'タイムラプス',
+    defaultTimelapseDesc: 'タイムラプス動画を記録',
     staggeredStart: 'Staggered Start',
     staggeredStartDescription: 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.',
     plateClear: 'プレートクリア確認',

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

@@ -1580,6 +1580,18 @@ export default {
     historyRetention: 'Retenção de Histórico',
     keepSensorHistory: 'Manter histórico do sensor por',
     historyRetentionDescription: 'Dados antigos de umidade e temperatura serão automaticamente excluídos',
+    defaultPrintOptions: 'Opções de impressão padrão',
+    defaultPrintOptionsDescription: 'Defina valores padrão para opções de impressão. Podem ser alterados no diálogo de impressão.',
+    defaultBedLevelling: 'Nivelamento da mesa',
+    defaultBedLevellingDesc: 'Nivelar automaticamente a mesa antes da impressão',
+    defaultFlowCali: 'Calibração de fluxo',
+    defaultFlowCaliDesc: 'Calibrar fluxo de extrusão',
+    defaultVibrationCali: 'Calibração de vibração',
+    defaultVibrationCaliDesc: 'Reduzir artefatos de ringing',
+    defaultLayerInspect: 'Inspeção da primeira camada',
+    defaultLayerInspectDesc: 'Inspeção IA da primeira camada',
+    defaultTimelapse: 'Timelapse',
+    defaultTimelapseDesc: 'Gravar vídeo timelapse',
     staggeredStart: 'Staggered Start',
     staggeredStartDescription: 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.',
     plateClear: 'Confirmação de placa livre',

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

@@ -1580,6 +1580,18 @@ export default {
     historyRetention: '历史保留',
     keepSensorHistory: '保留传感器历史',
     historyRetentionDescription: '较旧的湿度和温度数据将被自动删除',
+    defaultPrintOptions: '默认打印选项',
+    defaultPrintOptionsDescription: '设置新打印的默认选项值。可在打印对话框中逐次覆盖。',
+    defaultBedLevelling: '热床调平',
+    defaultBedLevellingDesc: '打印前自动调平热床',
+    defaultFlowCali: '流量校准',
+    defaultFlowCaliDesc: '校准挤出流量',
+    defaultVibrationCali: '振动校准',
+    defaultVibrationCaliDesc: '减少振纹伪影',
+    defaultLayerInspect: '首层检测',
+    defaultLayerInspectDesc: 'AI首层检测',
+    defaultTimelapse: '延时摄影',
+    defaultTimelapseDesc: '录制延时摄影视频',
     staggeredStart: 'Staggered Start',
     staggeredStartDescription: 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.',
     plateClear: '热床清空确认',

+ 55 - 1
frontend/src/pages/SettingsPage.tsx

@@ -772,6 +772,11 @@ export function SettingsPage() {
       settings.prometheus_enabled !== localSettings.prometheus_enabled ||
       settings.prometheus_token !== localSettings.prometheus_token ||
       (settings.user_notifications_enabled ?? true) !== (localSettings.user_notifications_enabled ?? true) ||
+      (settings.default_bed_levelling ?? true) !== (localSettings.default_bed_levelling ?? true) ||
+      (settings.default_flow_cali ?? false) !== (localSettings.default_flow_cali ?? false) ||
+      (settings.default_vibration_cali ?? true) !== (localSettings.default_vibration_cali ?? true) ||
+      (settings.default_layer_inspect ?? false) !== (localSettings.default_layer_inspect ?? false) ||
+      (settings.default_timelapse ?? false) !== (localSettings.default_timelapse ?? false) ||
       (settings.stagger_group_size ?? 2) !== (localSettings.stagger_group_size ?? 2) ||
       (settings.stagger_interval_minutes ?? 5) !== (localSettings.stagger_interval_minutes ?? 5) ||
       (settings.require_plate_clear ?? true) !== (localSettings.require_plate_clear ?? true);
@@ -848,6 +853,11 @@ export function SettingsPage() {
         prometheus_enabled: localSettings.prometheus_enabled,
         prometheus_token: localSettings.prometheus_token,
         user_notifications_enabled: localSettings.user_notifications_enabled,
+        default_bed_levelling: localSettings.default_bed_levelling,
+        default_flow_cali: localSettings.default_flow_cali,
+        default_vibration_cali: localSettings.default_vibration_cali,
+        default_layer_inspect: localSettings.default_layer_inspect,
+        default_timelapse: localSettings.default_timelapse,
         stagger_group_size: localSettings.stagger_group_size,
         stagger_interval_minutes: localSettings.stagger_interval_minutes,
         require_plate_clear: localSettings.require_plate_clear,
@@ -3340,7 +3350,47 @@ export function SettingsPage() {
       {/* Filament Tab */}
       {/* Queue Tab */}
       {activeTab === 'queue' && localSettings && (
-        <div className="max-w-2xl space-y-6">
+        <div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
+          {/* Left Column */}
+          <div className="lg:w-1/2 space-y-6">
+          {/* Default Print Options */}
+          <Card>
+            <CardHeader>
+              <h3 className="text-base font-semibold text-white flex items-center gap-2">
+                <ListOrdered className="w-4 h-4 text-bambu-green" />
+                {t('settings.defaultPrintOptions', 'Default Print Options')}
+              </h3>
+            </CardHeader>
+            <CardContent className="space-y-4">
+              <p className="text-xs text-bambu-gray">
+                {t('settings.defaultPrintOptionsDescription', 'Set default values for print options when starting new prints. These can be overridden per print in the print dialog.')}
+              </p>
+              {[
+                { key: 'default_bed_levelling' as const, label: t('settings.defaultBedLevelling', 'Bed Levelling'), desc: t('settings.defaultBedLevellingDesc', 'Auto-level bed before print'), fallback: true },
+                { key: 'default_flow_cali' as const, label: t('settings.defaultFlowCali', 'Flow Calibration'), desc: t('settings.defaultFlowCaliDesc', 'Calibrate extrusion flow'), fallback: false },
+                { key: 'default_vibration_cali' as const, label: t('settings.defaultVibrationCali', 'Vibration Calibration'), desc: t('settings.defaultVibrationCaliDesc', 'Reduce ringing artifacts'), fallback: true },
+                { key: 'default_layer_inspect' as const, label: t('settings.defaultLayerInspect', 'First Layer Inspection'), desc: t('settings.defaultLayerInspectDesc', 'AI inspection of first layer'), fallback: false },
+                { key: 'default_timelapse' as const, label: t('settings.defaultTimelapse', 'Timelapse'), desc: t('settings.defaultTimelapseDesc', 'Record timelapse video'), fallback: false },
+              ].map(({ key, label, desc, fallback }) => (
+                <div key={key} className="flex items-center justify-between">
+                  <div className="flex-1 mr-4">
+                    <p className="text-sm text-white">{label}</p>
+                    <p className="text-xs text-bambu-gray mt-0.5">{desc}</p>
+                  </div>
+                  <label className="relative inline-flex items-center cursor-pointer">
+                    <input
+                      type="checkbox"
+                      checked={localSettings[key] ?? fallback}
+                      onChange={(e) => updateSetting(key, e.target.checked)}
+                      className="sr-only peer"
+                    />
+                    <div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
+                  </label>
+                </div>
+              ))}
+            </CardContent>
+          </Card>
+
           {/* Staggered Batch Start */}
           <Card>
             <CardHeader>
@@ -3421,6 +3471,9 @@ export function SettingsPage() {
             </CardContent>
           </Card>
 
+          </div>
+          {/* Right Column */}
+          <div className="lg:w-1/2 space-y-6">
           {/* Auto-Drying */}
           <Card>
             <CardHeader>
@@ -3580,6 +3633,7 @@ export function SettingsPage() {
               </div>
             </CardContent>
           </Card>
+          </div>
         </div>
       )}
 

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


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


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CHZDJElS.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-Bz3PqAxB.css">
+    <script type="module" crossorigin src="/assets/index-BhHLaTQH.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BGA3I7Jb.css">
   </head>
   <body>
     <div id="root"></div>

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