Browse Source

Merge branch '0.2.0b' into main

MartinNYHC 3 months ago
parent
commit
f021de64dd

+ 28 - 4
backend/app/api/routes/maintenance.py

@@ -177,7 +177,11 @@ async def get_maintenance_types(
 ):
 ):
     """Get all maintenance types."""
     """Get all maintenance types."""
     await ensure_default_types(db)
     await ensure_default_types(db)
-    result = await db.execute(select(MaintenanceType).order_by(MaintenanceType.is_system.desc(), MaintenanceType.name))
+    result = await db.execute(
+        select(MaintenanceType)
+        .where(MaintenanceType.is_deleted.is_(False))
+        .order_by(MaintenanceType.is_system.desc(), MaintenanceType.name)
+    )
     return result.scalars().all()
     return result.scalars().all()
 
 
 
 
@@ -230,20 +234,40 @@ async def delete_maintenance_type(
     db: AsyncSession = Depends(get_db),
     db: AsyncSession = Depends(get_db),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.MAINTENANCE_DELETE),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.MAINTENANCE_DELETE),
 ):
 ):
-    """Delete a custom maintenance type."""
+    """Delete a maintenance type."""
     result = await db.execute(select(MaintenanceType).where(MaintenanceType.id == type_id))
     result = await db.execute(select(MaintenanceType).where(MaintenanceType.id == type_id))
     maint_type = result.scalar_one_or_none()
     maint_type = result.scalar_one_or_none()
     if not maint_type:
     if not maint_type:
         raise HTTPException(status_code=404, detail="Maintenance type not found")
         raise HTTPException(status_code=404, detail="Maintenance type not found")
 
 
     if maint_type.is_system:
     if maint_type.is_system:
-        raise HTTPException(status_code=400, detail="Cannot delete system maintenance type")
+        maint_type.is_deleted = True
+        await db.commit()
+        return {"status": "deleted"}
 
 
     await db.delete(maint_type)
     await db.delete(maint_type)
     await db.commit()
     await db.commit()
     return {"status": "deleted"}
     return {"status": "deleted"}
 
 
 
 
+@router.post("/types/restore-defaults")
+async def restore_default_maintenance_types(
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.MAINTENANCE_DELETE),
+):
+    """Restore deleted default maintenance types."""
+    await ensure_default_types(db)
+    result = await db.execute(
+        select(MaintenanceType).where(MaintenanceType.is_system.is_(True)).where(MaintenanceType.is_deleted.is_(True))
+    )
+    deleted_types = result.scalars().all()
+    for maint_type in deleted_types:
+        maint_type.is_deleted = False
+
+    await db.commit()
+    return {"restored": len(deleted_types)}
+
+
 # ============== Printer Maintenance ==============
 # ============== Printer Maintenance ==============
 
 
 
 
@@ -264,7 +288,7 @@ async def _get_printer_maintenance_internal(
     total_hours = await get_printer_total_hours(db, printer_id)
     total_hours = await get_printer_total_hours(db, printer_id)
 
 
     # Get all maintenance types
     # Get all maintenance types
-    result = await db.execute(select(MaintenanceType))
+    result = await db.execute(select(MaintenanceType).where(MaintenanceType.is_deleted.is_(False)))
     all_types = result.scalars().all()
     all_types = result.scalars().all()
 
 
     # Get printer's maintenance items
     # Get printer's maintenance items

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

@@ -188,6 +188,13 @@ async def run_migrations(conn):
         # Column already exists
         # Column already exists
         pass
         pass
 
 
+    # Migration: Add is_deleted column to maintenance_types for soft-deletes
+    try:
+        await conn.execute(text("ALTER TABLE maintenance_types ADD COLUMN is_deleted BOOLEAN DEFAULT 0"))
+    except OperationalError:
+        # Column already exists
+        pass
+
     # Migration: Add custom_interval_type column to printer_maintenance
     # Migration: Add custom_interval_type column to printer_maintenance
     try:
     try:
         await conn.execute(text("ALTER TABLE printer_maintenance ADD COLUMN custom_interval_type VARCHAR(20)"))
         await conn.execute(text("ALTER TABLE printer_maintenance ADD COLUMN custom_interval_type VARCHAR(20)"))

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

@@ -22,6 +22,7 @@ class MaintenanceType(Base):
     icon: Mapped[str | None] = mapped_column(String(50))  # Icon name for UI
     icon: Mapped[str | None] = mapped_column(String(50))  # Icon name for UI
     wiki_url: Mapped[str | None] = mapped_column(String(500))  # Documentation link
     wiki_url: Mapped[str | None] = mapped_column(String(500))  # Documentation link
     is_system: Mapped[bool] = mapped_column(Boolean, default=False)  # Pre-defined vs custom
     is_system: Mapped[bool] = mapped_column(Boolean, default=False)  # Pre-defined vs custom
+    is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)  # Hidden/removed type
     created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
     created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
 
 
     # Relationships
     # Relationships

+ 1 - 1
backend/tests/unit/test_code_quality.py

@@ -137,7 +137,7 @@ def find_import_shadowing(file_path: Path) -> list[tuple[str, int, str]]:
     Returns list of (name, line_number, function_name) tuples.
     Returns list of (name, line_number, function_name) tuples.
     """
     """
     try:
     try:
-        with open(file_path) as f:
+        with open(file_path, encoding="utf-8") as f:
             source = f.read()
             source = f.read()
         tree = ast.parse(source)
         tree = ast.parse(source)
         visitor = DangerousImportVisitor()
         visitor = DangerousImportVisitor()

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

@@ -3462,6 +3462,8 @@ export const api = {
     }),
     }),
   deleteMaintenanceType: (id: number) =>
   deleteMaintenanceType: (id: number) =>
     request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }),
     request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }),
+  restoreDefaultMaintenanceTypes: () =>
+    request<{ restored: number }>(`/maintenance/types/restore-defaults`, { method: 'POST' }),
   getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
   getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
   getPrinterMaintenance: (printerId: number) =>
   getPrinterMaintenance: (printerId: number) =>
     request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),
     request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),

+ 14 - 2
frontend/src/components/ConfirmModal.tsx

@@ -9,6 +9,8 @@ interface ConfirmModalProps {
   message: string;
   message: string;
   confirmText?: string;
   confirmText?: string;
   cancelText?: string;
   cancelText?: string;
+  cancelVariant?: 'primary' | 'secondary' | 'danger' | 'ghost';
+  cardClassName?: string;
   variant?: 'danger' | 'warning' | 'default';
   variant?: 'danger' | 'warning' | 'default';
   isLoading?: boolean;
   isLoading?: boolean;
   loadingText?: string;
   loadingText?: string;
@@ -21,6 +23,8 @@ export function ConfirmModal({
   message,
   message,
   confirmText,
   confirmText,
   cancelText,
   cancelText,
+  cancelVariant,
+  cardClassName,
   variant = 'default',
   variant = 'default',
   isLoading = false,
   isLoading = false,
   loadingText,
   loadingText,
@@ -62,7 +66,10 @@ export function ConfirmModal({
       className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
       className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
       onClick={isLoading ? undefined : onCancel}
       onClick={isLoading ? undefined : onCancel}
     >
     >
-      <Card className="w-full max-w-md" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
+      <Card
+        className={`w-full max-w-md ${cardClassName ?? ''}`}
+        onClick={(e: React.MouseEvent) => e.stopPropagation()}
+      >
         <CardContent className="p-6">
         <CardContent className="p-6">
           <div className="flex items-start gap-4">
           <div className="flex items-start gap-4">
             <div className={`p-2 rounded-full bg-bambu-dark ${styles.icon}`}>
             <div className={`p-2 rounded-full bg-bambu-dark ${styles.icon}`}>
@@ -74,7 +81,12 @@ export function ConfirmModal({
             </div>
             </div>
           </div>
           </div>
           <div className="flex gap-3 mt-6">
           <div className="flex gap-3 mt-6">
-            <Button variant="secondary" onClick={onCancel} className="flex-1" disabled={isLoading}>
+            <Button
+              variant={cancelVariant ?? 'secondary'}
+              onClick={onCancel}
+              className="flex-1"
+              disabled={isLoading}
+            >
               {resolvedCancelText}
               {resolvedCancelText}
             </Button>
             </Button>
             <Button
             <Button

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

@@ -958,6 +958,7 @@ export default {
     maintenanceTypes: 'Wartungstypen',
     maintenanceTypes: 'Wartungstypen',
     maintenanceTypesDescription: 'Systemtypen und Ihre benutzerdefinierten Wartungsaufgaben',
     maintenanceTypesDescription: 'Systemtypen und Ihre benutzerdefinierten Wartungsaufgaben',
     addCustomType: 'Benutzerdefinierten Typ hinzufügen',
     addCustomType: 'Benutzerdefinierten Typ hinzufügen',
+    restoreDefaults: 'Standardaufgaben wiederherstellen',
     intervalType: 'Intervalltyp',
     intervalType: 'Intervalltyp',
     intervalValue: 'Intervall ({{type}})',
     intervalValue: 'Intervall ({{type}})',
     icon: 'Symbol',
     icon: 'Symbol',
@@ -1001,11 +1002,14 @@ export default {
     maintenanceComplete: 'Wartung als abgeschlossen markiert',
     maintenanceComplete: 'Wartung als abgeschlossen markiert',
     typeUpdated: 'Wartungstyp aktualisiert',
     typeUpdated: 'Wartungstyp aktualisiert',
     typeDeleted: 'Wartungstyp gelöscht',
     typeDeleted: 'Wartungstyp gelöscht',
+    defaultsRestored: '{{count}} Standardaufgabe(n) wiederhergestellt',
     printHoursUpdated: 'Druckstunden aktualisiert',
     printHoursUpdated: 'Druckstunden aktualisiert',
     printerAssigned: 'Drucker zugewiesen',
     printerAssigned: 'Drucker zugewiesen',
     printerRemoved: 'Drucker entfernt',
     printerRemoved: 'Drucker entfernt',
     // Confirmation
     // Confirmation
     deleteTypeConfirm: '"{{name}}" löschen?',
     deleteTypeConfirm: '"{{name}}" löschen?',
+    deleteSystemTypeTitle: 'Standard-Wartungsaufgabe löschen?',
+    deleteSystemTypeMessage: 'Möchten Sie die Standard-Wartungsaufgabe "{{name}}" wirklich löschen?',
     // Permissions
     // Permissions
     noPermissionUpdate: 'Sie haben keine Berechtigung, Wartungselemente zu aktualisieren',
     noPermissionUpdate: 'Sie haben keine Berechtigung, Wartungselemente zu aktualisieren',
     noPermissionPerform: 'Sie haben keine Berechtigung, Wartungen durchzuführen',
     noPermissionPerform: 'Sie haben keine Berechtigung, Wartungen durchzuführen',

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

@@ -958,6 +958,7 @@ export default {
     maintenanceTypes: 'Maintenance Types',
     maintenanceTypes: 'Maintenance Types',
     maintenanceTypesDescription: 'System types and your custom maintenance tasks',
     maintenanceTypesDescription: 'System types and your custom maintenance tasks',
     addCustomType: 'Add Custom Type',
     addCustomType: 'Add Custom Type',
+    restoreDefaults: 'Restore Default Tasks',
     intervalType: 'Interval Type',
     intervalType: 'Interval Type',
     intervalValue: 'Interval ({{type}})',
     intervalValue: 'Interval ({{type}})',
     icon: 'Icon',
     icon: 'Icon',
@@ -1001,11 +1002,14 @@ export default {
     maintenanceComplete: 'Maintenance marked as complete',
     maintenanceComplete: 'Maintenance marked as complete',
     typeUpdated: 'Maintenance type updated',
     typeUpdated: 'Maintenance type updated',
     typeDeleted: 'Maintenance type deleted',
     typeDeleted: 'Maintenance type deleted',
+    defaultsRestored: 'Restored {{count}} default task(s)',
     printHoursUpdated: 'Print hours updated',
     printHoursUpdated: 'Print hours updated',
     printerAssigned: 'Printer assigned',
     printerAssigned: 'Printer assigned',
     printerRemoved: 'Printer removed',
     printerRemoved: 'Printer removed',
     // Confirmation
     // Confirmation
     deleteTypeConfirm: 'Delete "{{name}}"?',
     deleteTypeConfirm: 'Delete "{{name}}"?',
+    deleteSystemTypeTitle: 'Delete default maintenance task?',
+    deleteSystemTypeMessage: 'Are you sure you want to delete the default maintenance task "{{name}}"?',
     // Permissions
     // Permissions
     noPermissionUpdate: 'You do not have permission to update maintenance items',
     noPermissionUpdate: 'You do not have permission to update maintenance items',
     noPermissionPerform: 'You do not have permission to perform maintenance',
     noPermissionPerform: 'You do not have permission to perform maintenance',

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

@@ -945,6 +945,7 @@ export default {
     maintenanceTypes: 'Tipi di manutenzione',
     maintenanceTypes: 'Tipi di manutenzione',
     maintenanceTypesDescription: 'Tipi di sistema e tue attivita personalizzate',
     maintenanceTypesDescription: 'Tipi di sistema e tue attivita personalizzate',
     addCustomType: 'Aggiungi tipo personalizzato',
     addCustomType: 'Aggiungi tipo personalizzato',
+    restoreDefaults: 'Ripristina attivita predefinite',
     intervalType: 'Tipo intervallo',
     intervalType: 'Tipo intervallo',
     intervalValue: 'Intervallo ({{type}})',
     intervalValue: 'Intervallo ({{type}})',
     icon: 'Icona',
     icon: 'Icona',
@@ -988,11 +989,14 @@ export default {
     maintenanceComplete: 'Manutenzione segnata come completata',
     maintenanceComplete: 'Manutenzione segnata come completata',
     typeUpdated: 'Tipo manutenzione aggiornato',
     typeUpdated: 'Tipo manutenzione aggiornato',
     typeDeleted: 'Tipo manutenzione eliminato',
     typeDeleted: 'Tipo manutenzione eliminato',
+    defaultsRestored: 'Ripristinate {{count}} attivita predefinite',
     printHoursUpdated: 'Ore di stampa aggiornate',
     printHoursUpdated: 'Ore di stampa aggiornate',
     printerAssigned: 'Stampante assegnata',
     printerAssigned: 'Stampante assegnata',
     printerRemoved: 'Stampante rimossa',
     printerRemoved: 'Stampante rimossa',
     // Confirmation
     // Confirmation
     deleteTypeConfirm: 'Eliminare "{{name}}"?',
     deleteTypeConfirm: 'Eliminare "{{name}}"?',
+    deleteSystemTypeTitle: 'Eliminare attività di manutenzione predefinita?',
+    deleteSystemTypeMessage: 'Sei sicuro di voler eliminare l\'attività di manutenzione predefinita "{{name}}"?',
     // Permissions
     // Permissions
     noPermissionUpdate: 'Non hai il permesso di aggiornare elementi manutenzione',
     noPermissionUpdate: 'Non hai il permesso di aggiornare elementi manutenzione',
     noPermissionPerform: 'Non hai il permesso di eseguire manutenzione',
     noPermissionPerform: 'Non hai il permesso di eseguire manutenzione',

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

@@ -1001,6 +1001,7 @@ export default {
     months: '{{count}}ヶ月',
     months: '{{count}}ヶ月',
     maintenanceTypes: 'メンテナンスタイプ',
     maintenanceTypes: 'メンテナンスタイプ',
     addCustomType: 'カスタムタイプを追加',
     addCustomType: 'カスタムタイプを追加',
+    restoreDefaults: 'デフォルトタスクを復元',
     intervalType: 'インターバルタイプ',
     intervalType: 'インターバルタイプ',
     icon: 'アイコン',
     icon: 'アイコン',
     documentationLink: 'ドキュメントリンク(任意)',
     documentationLink: 'ドキュメントリンク(任意)',
@@ -1034,9 +1035,12 @@ export default {
     },
     },
     typeUpdated: 'メンテナンスタイプを更新しました',
     typeUpdated: 'メンテナンスタイプを更新しました',
     typeDeleted: 'メンテナンスタイプを削除しました',
     typeDeleted: 'メンテナンスタイプを削除しました',
+    defaultsRestored: 'デフォルトタスクを{{count}}件復元しました',
     printerAssigned: 'プリンターを割り当てました',
     printerAssigned: 'プリンターを割り当てました',
     printerRemoved: 'プリンターを削除しました',
     printerRemoved: 'プリンターを削除しました',
     deleteTypeConfirm: '「{{name}}」を削除しますか?',
     deleteTypeConfirm: '「{{name}}」を削除しますか?',
+    deleteSystemTypeTitle: 'デフォルトのメンテナンスタスクを削除しますか?',
+    deleteSystemTypeMessage: 'デフォルトのメンテナンスタスク「{{name}}」を削除してもよろしいですか?',
     noPermissionUpdate: 'メンテナンス記録を更新する権限がありません',
     noPermissionUpdate: 'メンテナンス記録を更新する権限がありません',
     noPermissionPerform: 'メンテナンスを実行する権限がありません',
     noPermissionPerform: 'メンテナンスを実行する権限がありません',
     noPermissionEditTypes: 'メンテナンスタイプを編集する権限がありません',
     noPermissionEditTypes: 'メンテナンスタイプを編集する権限がありません',

+ 66 - 8
frontend/src/pages/MaintenancePage.tsx

@@ -41,6 +41,7 @@ import type { MaintenanceStatus, PrinterMaintenanceOverview, MaintenanceType, Pe
 import { Card, CardContent } from '../components/Card';
 import { Card, CardContent } from '../components/Card';
 import { Button } from '../components/Button';
 import { Button } from '../components/Button';
 import { Toggle } from '../components/Toggle';
 import { Toggle } from '../components/Toggle';
+import { ConfirmModal } from '../components/ConfirmModal';
 import { useToast } from '../contexts/ToastContext';
 import { useToast } from '../contexts/ToastContext';
 import { useAuth } from '../contexts/AuthContext';
 import { useAuth } from '../contexts/AuthContext';
 
 
@@ -552,6 +553,8 @@ function SettingsSection({
   onAddType,
   onAddType,
   onUpdateType,
   onUpdateType,
   onDeleteType,
   onDeleteType,
+  onRestoreDefaults,
+  isRestoringDefaults,
   onAssignType,
   onAssignType,
   onRemoveItem,
   onRemoveItem,
   hasPermission,
   hasPermission,
@@ -563,6 +566,8 @@ function SettingsSection({
   onAddType: (data: { name: string; description?: string; default_interval_hours: number; interval_type: 'hours' | 'days'; icon?: string; wiki_url?: string | null }, printerIds: number[]) => void;
   onAddType: (data: { name: string; description?: string; default_interval_hours: number; interval_type: 'hours' | 'days'; icon?: string; wiki_url?: string | null }, printerIds: number[]) => void;
   onUpdateType: (id: number, data: { name?: string; default_interval_hours?: number; interval_type?: 'hours' | 'days'; icon?: string; wiki_url?: string | null }) => void;
   onUpdateType: (id: number, data: { name?: string; default_interval_hours?: number; interval_type?: 'hours' | 'days'; icon?: string; wiki_url?: string | null }) => void;
   onDeleteType: (id: number) => void;
   onDeleteType: (id: number) => void;
+  onRestoreDefaults: () => void;
+  isRestoringDefaults: boolean;
   onAssignType: (printerId: number, typeId: number) => void;
   onAssignType: (printerId: number, typeId: number) => void;
   onRemoveItem: (itemId: number) => void;
   onRemoveItem: (itemId: number) => void;
   hasPermission: (permission: Permission) => boolean;
   hasPermission: (permission: Permission) => boolean;
@@ -579,6 +584,7 @@ function SettingsSection({
   const [newTypeWikiUrl, setNewTypeWikiUrl] = useState('');
   const [newTypeWikiUrl, setNewTypeWikiUrl] = useState('');
   const [selectedPrinters, setSelectedPrinters] = useState<Set<number>>(new Set());
   const [selectedPrinters, setSelectedPrinters] = useState<Set<number>>(new Set());
   const [expandedType, setExpandedType] = useState<number | null>(null);
   const [expandedType, setExpandedType] = useState<number | null>(null);
+  const [pendingSystemDelete, setPendingSystemDelete] = useState<MaintenanceType | null>(null);
 
 
   // Get unique printers from overview
   // Get unique printers from overview
   const printers = useMemo(() => {
   const printers = useMemo(() => {
@@ -697,14 +703,24 @@ function SettingsSection({
             <h2 className="text-lg font-semibold text-white">{t('maintenance.maintenanceTypes')}</h2>
             <h2 className="text-lg font-semibold text-white">{t('maintenance.maintenanceTypes')}</h2>
             <p className="text-sm text-bambu-gray mt-1">{t('maintenance.maintenanceTypesDescription')}</p>
             <p className="text-sm text-bambu-gray mt-1">{t('maintenance.maintenanceTypesDescription')}</p>
           </div>
           </div>
-          <Button
-            onClick={() => setShowAddType(!showAddType)}
-            disabled={!hasPermission('maintenance:create')}
-            title={!hasPermission('maintenance:create') ? t('maintenance.noPermissionEditTypes') : undefined}
-          >
-            <Plus className="w-4 h-4" />
-            {t('maintenance.addCustomType')}
-          </Button>
+          <div className="flex items-center gap-2">
+            <Button
+              variant="secondary"
+              onClick={onRestoreDefaults}
+              disabled={!hasPermission('maintenance:delete') || isRestoringDefaults}
+              title={!hasPermission('maintenance:delete') ? t('maintenance.noPermissionDeleteTypes') : undefined}
+            >
+              {t('maintenance.restoreDefaults')}
+            </Button>
+            <Button
+              onClick={() => setShowAddType(!showAddType)}
+              disabled={!hasPermission('maintenance:create')}
+              title={!hasPermission('maintenance:create') ? t('maintenance.noPermissionEditTypes') : undefined}
+            >
+              <Plus className="w-4 h-4" />
+              {t('maintenance.addCustomType')}
+            </Button>
+          </div>
         </div>
         </div>
 
 
         {/* Add custom type form */}
         {/* Add custom type form */}
@@ -846,6 +862,17 @@ function SettingsSection({
                       {formatIntervalLabel(type.default_interval_hours, intervalType, t)}
                       {formatIntervalLabel(type.default_interval_hours, intervalType, t)}
                     </div>
                     </div>
                   </div>
                   </div>
+                  <button
+                    onClick={() => {
+                      if (!hasPermission('maintenance:delete')) return;
+                      setPendingSystemDelete(type);
+                    }}
+                    disabled={!hasPermission('maintenance:delete')}
+                    title={!hasPermission('maintenance:delete') ? t('maintenance.noPermissionDeleteTypes') : undefined}
+                    className={`p-2 rounded-lg hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors ${!hasPermission('maintenance:delete') ? 'opacity-50 cursor-not-allowed' : ''}`}
+                  >
+                    <Trash2 className="w-4 h-4" />
+                  </button>
                 </div>
                 </div>
               </div>
               </div>
             );
             );
@@ -1124,6 +1151,23 @@ function SettingsSection({
           </CardContent>
           </CardContent>
         </Card>
         </Card>
       )}
       )}
+
+      {pendingSystemDelete && (
+        <ConfirmModal
+          title={t('maintenance.deleteSystemTypeTitle')}
+          message={t('maintenance.deleteSystemTypeMessage', { name: pendingSystemDelete.name })}
+          confirmText={t('common.delete')}
+          cancelText={t('common.cancel')}
+          variant="danger"
+          cancelVariant="primary"
+          cardClassName="bg-red-950/70 border border-red-800/70"
+          onConfirm={() => {
+            onDeleteType(pendingSystemDelete.id);
+            setPendingSystemDelete(null);
+          }}
+          onCancel={() => setPendingSystemDelete(null)}
+        />
+      )}
     </div>
     </div>
   );
   );
 }
 }
@@ -1199,6 +1243,18 @@ export function MaintenancePage() {
     },
     },
   });
   });
 
 
+  const restoreDefaultsMutation = useMutation({
+    mutationFn: api.restoreDefaultMaintenanceTypes,
+    onSuccess: (data: { restored: number }) => {
+      queryClient.invalidateQueries({ queryKey: ['maintenanceTypes'] });
+      queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] });
+      showToast(t('maintenance.defaultsRestored', { count: data.restored }));
+    },
+    onError: (error: Error) => {
+      showToast(error.message, 'error');
+    },
+  });
+
   const setHoursMutation = useMutation({
   const setHoursMutation = useMutation({
     mutationFn: ({ printerId, hours }: { printerId: number; hours: number }) =>
     mutationFn: ({ printerId, hours }: { printerId: number; hours: number }) =>
       api.setPrinterHours(printerId, hours),
       api.setPrinterHours(printerId, hours),
@@ -1352,6 +1408,8 @@ export function MaintenancePage() {
           }}
           }}
           onUpdateType={(id, data) => updateTypeMutation.mutate({ id, data })}
           onUpdateType={(id, data) => updateTypeMutation.mutate({ id, data })}
           onDeleteType={(id) => deleteTypeMutation.mutate(id)}
           onDeleteType={(id) => deleteTypeMutation.mutate(id)}
+          onRestoreDefaults={() => restoreDefaultsMutation.mutate()}
+          isRestoringDefaults={restoreDefaultsMutation.isPending}
           onAssignType={(printerId, typeId) => assignTypeMutation.mutate({ printerId, typeId })}
           onAssignType={(printerId, typeId) => assignTypeMutation.mutate({ printerId, typeId })}
           onRemoveItem={(itemId) => removeItemMutation.mutate(itemId)}
           onRemoveItem={(itemId) => removeItemMutation.mutate(itemId)}
           hasPermission={hasPermission}
           hasPermission={hasPermission}

+ 23 - 2
frontend/src/pages/PrintersPage.tsx

@@ -636,10 +636,11 @@ function NozzleSlotHoverCard({ slot, index, activeStatus, filamentName, children
 }
 }
 
 
 // Dual-nozzle hover card showing L and R nozzle details side by side
 // Dual-nozzle hover card showing L and R nozzle details side by side
-function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, children }: {
+function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, filamentInfo, children }: {
   leftSlot?: import('../api/client').NozzleRackSlot;
   leftSlot?: import('../api/client').NozzleRackSlot;
   rightSlot?: import('../api/client').NozzleRackSlot;
   rightSlot?: import('../api/client').NozzleRackSlot;
   activeNozzle: 'L' | 'R';
   activeNozzle: 'L' | 'R';
+  filamentInfo?: Record<string, { name: string; k: number | null }>;
   children: React.ReactNode;
   children: React.ReactNode;
 }) {
 }) {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -684,6 +685,8 @@ function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, children }: {
     const isActive = activeNozzle === side;
     const isActive = activeNozzle === side;
     const typeFull = nozzleTypeName(slot.nozzle_type, t);
     const typeFull = nozzleTypeName(slot.nozzle_type, t);
     const flowFull = nozzleFlowName(slot.nozzle_type, t);
     const flowFull = nozzleFlowName(slot.nozzle_type, t);
+    const filamentCss = parseFilamentColor(slot.filament_color);
+    const filamentName = slot.filament_id ? filamentInfo?.[slot.filament_id]?.name : undefined;
     return (
     return (
       <div className="flex-1 space-y-1.5">
       <div className="flex-1 space-y-1.5">
         <div className={`text-[10px] font-bold pb-1 border-b border-bambu-dark-tertiary/50 ${isActive ? 'text-amber-400' : 'text-bambu-gray'}`}>
         <div className={`text-[10px] font-bold pb-1 border-b border-bambu-dark-tertiary/50 ${isActive ? 'text-amber-400' : 'text-bambu-gray'}`}>
@@ -736,6 +739,19 @@ function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, children }: {
             <span className="text-[10px] text-white font-mono">{slot.serial_number}</span>
             <span className="text-[10px] text-white font-mono">{slot.serial_number}</span>
           </div>
           </div>
         )}
         )}
+        {(filamentCss || slot.filament_type || slot.filament_id) && (
+          <div className="flex items-center justify-between">
+            <span className="text-[10px] text-bambu-gray">{t('printers.nozzleFilament')}</span>
+            <div className="flex items-center gap-1">
+              {filamentCss && (
+                <div className="w-3 h-3 rounded-sm border border-white/20" style={{ backgroundColor: filamentCss }} />
+              )}
+              <span className="text-[10px] text-white font-semibold truncate max-w-[100px]">
+                {filamentName || slot.filament_type || slot.filament_id || ''}
+              </span>
+            </div>
+          </div>
+        )}
       </div>
       </div>
     );
     );
   };
   };
@@ -2508,7 +2524,12 @@ function PrinterCard({
                   )}
                   )}
                   {/* Active nozzle indicator for dual-nozzle printers */}
                   {/* Active nozzle indicator for dual-nozzle printers */}
                   {isDualNozzle && (
                   {isDualNozzle && (
-                    <DualNozzleHoverCard leftSlot={leftNozzleSlot} rightSlot={rightNozzleSlot} activeNozzle={activeNozzle}>
+                    <DualNozzleHoverCard
+                      leftSlot={leftNozzleSlot}
+                      rightSlot={rightNozzleSlot}
+                      activeNozzle={activeNozzle}
+                      filamentInfo={filamentInfo}
+                    >
                       <div className="text-center px-3 py-1.5 bg-bambu-dark rounded-lg h-full flex flex-col justify-center items-center cursor-default" title={t('printers.activeNozzle', { nozzle: activeNozzle === 'L' ? t('common.left') : t('common.right') })}>
                       <div className="text-center px-3 py-1.5 bg-bambu-dark rounded-lg h-full flex flex-col justify-center items-center cursor-default" title={t('printers.activeNozzle', { nozzle: activeNozzle === 'L' ? t('common.left') : t('common.right') })}>
                         <div className="flex items-center gap-2 mb-1">
                         <div className="flex items-center gap-2 mb-1">
                           <span className={`text-[11px] font-bold ${activeNozzle === 'L' ? 'text-amber-400' : 'text-gray-500'}`}>
                           <span className={`text-[11px] font-bold ${activeNozzle === 'L' ? 'text-amber-400' : 'text-gray-500'}`}>

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


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


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


+ 2 - 2
static/index.html

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

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