diff --git a/backend/app/api/routes/smart_plugs.py b/backend/app/api/routes/smart_plugs.py index 51e3fcdc3..40cb69312 100644 --- a/backend/app/api/routes/smart_plugs.py +++ b/backend/app/api/routes/smart_plugs.py @@ -201,23 +201,6 @@ async def get_script_plugs_by_printer( return ha_entities -@router.get("/by-printer/{printer_id}/all", response_model=list[SmartPlugResponse]) -async def get_all_smart_plugs_by_printer( - printer_id: int, - db: AsyncSession = Depends(get_db), - _: User | None = RequirePermissionIfAuthEnabled(Permission.SMART_PLUGS_READ), -): - """Get all smart plugs assigned to a printer that should appear on the printer card. - - Returns power plugs (tasmota, mqtt) plus HA entities with show_on_printer_card enabled. - """ - result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id == printer_id)) - plugs = result.scalars().all() - return [ - plug for plug in plugs if plug.plug_type != "homeassistant" or (plug.ha_entity_id and plug.show_on_printer_card) - ] - - # Tasmota Discovery Endpoints # NOTE: These must be defined BEFORE /{plug_id} routes to avoid path conflicts diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 5ce2e8b2c..01f6864ac 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -3277,7 +3277,6 @@ export const api = { getSmartPlug: (id: number) => request(`/smart-plugs/${id}`), getSmartPlugByPrinter: (printerId: number) => request(`/smart-plugs/by-printer/${printerId}`), getScriptPlugsByPrinter: (printerId: number) => request(`/smart-plugs/by-printer/${printerId}/scripts`), - getAllSmartPlugsByPrinter: (printerId: number) => request(`/smart-plugs/by-printer/${printerId}/all`), createSmartPlug: (data: SmartPlugCreate) => request('/smart-plugs/', { method: 'POST', diff --git a/frontend/src/components/AddSmartPlugModal.tsx b/frontend/src/components/AddSmartPlugModal.tsx index 183bc2d1c..e6fdddeae 100644 --- a/frontend/src/components/AddSmartPlugModal.tsx +++ b/frontend/src/components/AddSmartPlugModal.tsx @@ -255,7 +255,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); // Also invalidate printer card HA entity queries - queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter'] }); + queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] }); onClose(); }, onError: (err: Error) => { @@ -269,7 +269,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); // Also invalidate printer card HA entity queries - queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter'] }); + queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] }); onClose(); }, onError: (err: Error) => { diff --git a/frontend/src/components/SmartPlugCard.tsx b/frontend/src/components/SmartPlugCard.tsx index 3e34103e6..b78856743 100644 --- a/frontend/src/components/SmartPlugCard.tsx +++ b/frontend/src/components/SmartPlugCard.tsx @@ -80,7 +80,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) { queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); // Also invalidate printer-specific smart plug queries to keep PrintersPage in sync if (plug.printer_id) { - queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', plug.printer_id] }); + queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', plug.printer_id] }); + queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] }); } }, }); @@ -92,7 +93,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) { queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); // Also invalidate printer card HA entity queries if (plug.printer_id) { - queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', plug.printer_id] }); + queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] }); } }, }); diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 7cc37c030..2b19d7978 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -374,12 +374,12 @@ export default { resumeTitle: 'Druck fortsetzen', resumeMessage: 'Möchten Sie den Druck auf "{{name}}" fortsetzen?', resumeButton: 'Druck fortsetzen', - powerOnTitle: 'Schalter einschalten', - powerOnMessage: 'Möchten Sie "{{name}}" wirklich EINSCHALTEN?', + powerOnTitle: 'Drucker einschalten', + powerOnMessage: 'Möchten Sie die Stromversorgung für "{{name}}" wirklich EINSCHALTEN?', powerOnButton: 'Einschalten', - powerOffTitle: 'Schalter ausschalten', - powerOffMessage: 'Möchten Sie "{{name}}" wirklich AUSSCHALTEN?', - powerOffWarning: 'WARNUNG: "{{name}}" druckt gerade! Möchten Sie wirklich AUSSCHALTEN? Dies unterbricht den Druck und kann den Drucker beschädigen.', + powerOffTitle: 'Drucker ausschalten', + powerOffMessage: 'Möchten Sie die Stromversorgung für "{{name}}" wirklich AUSSCHALTEN?', + powerOffWarning: 'WARNUNG: "{{name}}" druckt gerade! Möchten Sie die Stromversorgung wirklich AUSSCHALTEN? Dies unterbricht den Druck und kann den Drucker beschädigen.', powerOffButton: 'Ausschalten', }, // Discovery diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 0142ff953..d7d091a5c 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -374,12 +374,12 @@ export default { resumeTitle: 'Resume Print', resumeMessage: 'Are you sure you want to resume the print on "{{name}}"?', resumeButton: 'Resume Print', - powerOnTitle: 'Power On Switch', - powerOnMessage: 'Are you sure you want to turn ON "{{name}}"?', + powerOnTitle: 'Power On Printer', + powerOnMessage: 'Are you sure you want to turn ON the power for "{{name}}"?', powerOnButton: 'Power On', - powerOffTitle: 'Power Off Switch', - powerOffMessage: 'Are you sure you want to turn OFF "{{name}}"?', - powerOffWarning: 'WARNING: "{{name}}" is currently printing! Are you sure you want to turn it OFF? This will interrupt the print and may damage the printer.', + powerOffTitle: 'Power Off Printer', + powerOffMessage: 'Are you sure you want to turn OFF the power for "{{name}}"?', + powerOffWarning: 'WARNING: "{{name}}" is currently printing! Are you sure you want to turn OFF the power? This will interrupt the print and may damage the printer.', powerOffButton: 'Power Off', }, // Discovery diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 3646d8ada..67b027d3f 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -374,12 +374,12 @@ export default { resumeTitle: 'Reprendre l\'impression', resumeMessage: 'Reprendre l\'impression sur "{{name}}" ?', resumeButton: 'Reprendre', - powerOnTitle: 'Allumer Interrupteur', + powerOnTitle: 'Allumer l\'imprimante', powerOnMessage: 'Allumer "{{name}}" ?', powerOnButton: 'Allumer', - powerOffTitle: 'Éteindre Interrupteur', + powerOffTitle: 'Éteindre l\'imprimante', powerOffMessage: 'Éteindre "{{name}}" ?', - powerOffWarning: 'ATTENTION : "{{name}}" imprime actuellement ! Êtes-vous sûr de vouloir l\'éteindre ? Cela interrompra l\'impression et pourrait endommager l\'imprimante.', + powerOffWarning: 'ATTENTION : "{{name}}" imprime ! L\'éteindre maintenant peut endommager l\'imprimante.', powerOffButton: 'Éteindre', }, // Discovery diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 6ff1c9e3c..ad295f2a1 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -374,12 +374,12 @@ export default { resumeTitle: 'Riprendi Stampa', resumeMessage: 'Sei sicuro di riprendere la stampa su "{{name}}"?', resumeButton: 'Riprendi Stampa', - powerOnTitle: 'Accendi Interruttore', + powerOnTitle: 'Accendi Stampante', powerOnMessage: 'Sei sicuro di accendere "{{name}}"?', powerOnButton: 'Accendi', - powerOffTitle: 'Spegni Interruttore', + powerOffTitle: 'Spegni Stampante', powerOffMessage: 'Sei sicuro di spegnere "{{name}}"?', - powerOffWarning: 'AVVISO: "{{name}}" sta stampando! Sei sicuro di spegnerlo? Questo interromperà la stampa e potrebbe danneggiare la stampante.', + powerOffWarning: 'AVVISO: "{{name}}" sta stampando! Sei sicuro di spegnere? Questo interromperà la stampa e potrebbe danneggiare la stampante.', powerOffButton: 'Spegni', }, // Discovery diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index c7df021e0..c6283197a 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -373,12 +373,12 @@ export default { resumeTitle: '印刷を再開', resumeMessage: '「{{name}}」の印刷を再開しますか?', resumeButton: '印刷を再開', - powerOnTitle: 'スイッチをオン', - powerOnMessage: '「{{name}}」をオンにしますか?', + powerOnTitle: 'プリンターの電源をオン', + powerOnMessage: '「{{name}}」の電源をオンにしますか?', powerOnButton: '電源オン', - powerOffTitle: 'スイッチをオフ', - powerOffMessage: '「{{name}}」をオフにしますか?', - powerOffWarning: '警告: 「{{name}}」は現在印刷中です!オフにしますか?印刷が中断され、プリンターが損傷する可能性があります。', + powerOffTitle: 'プリンターの電源をオフ', + powerOffMessage: '「{{name}}」の電源をオフにしますか?', + powerOffWarning: '警告: 「{{name}}」は現在印刷中です!電源をオフにしますか?印刷が中断され、プリンターが損傷する可能性があります。', powerOffButton: '電源オフ', }, // Discovery diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index 169cb2659..c9ab8a433 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -374,12 +374,12 @@ export default { resumeTitle: 'Retomar Impressão', resumeMessage: 'Tem certeza de que deseja retomar a impressão em "{{name}}"?', resumeButton: 'Retomar Impressão', - powerOnTitle: 'Ligar Interruptor', - powerOnMessage: 'Tem certeza de que deseja ligar "{{name}}"?', + powerOnTitle: 'Ligar Impressora', + powerOnMessage: 'Tem certeza de que deseja ligar a impressora "{{name}}"?', powerOnButton: 'Ligar', - powerOffTitle: 'Desligar Interruptor', - powerOffMessage: 'Tem certeza de que deseja desligar "{{name}}"?', - powerOffWarning: 'AVISO: "{{name}}" está imprimindo! Tem certeza de que deseja desligá-lo? Isso interromperá a impressão e pode danificar a impressora.', + powerOffTitle: 'Desligar Impressora', + powerOffMessage: 'Tem certeza de que deseja desligar a impressora "{{name}}"?', + powerOffWarning: 'AVISO: "{{name}}" está imprimindo no momento! Tem certeza de que deseja desligar a impressora? Isso interromperá a impressão e pode danificar a impressora.', powerOffButton: 'Desligar', }, // Discovery diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index d5460d1de..59440f916 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -374,12 +374,12 @@ export default { resumeTitle: '继续打印', resumeMessage: '确定要继续"{{name}}"上的打印吗?', resumeButton: '继续打印', - powerOnTitle: '开启开关', - powerOnMessage: '确定要开启"{{name}}"吗?', - powerOnButton: '开启', - powerOffTitle: '关闭开关', - powerOffMessage: '确定要关闭"{{name}}"吗?', - powerOffWarning: '警告:"{{name}}"正在打印中!确定要关闭吗?这将中断打印并可能损坏打印机。', + powerOnTitle: '开启打印机', + powerOnMessage: '确定要打开"{{name}}"的电源吗?', + powerOnButton: '开机', + powerOffTitle: '关闭打印机', + powerOffMessage: '确定要关闭"{{name}}"的电源吗?', + powerOffWarning: '警告:"{{name}}"正在打印中!确定要关闭电源吗?这将中断打印并可能损坏打印机。', powerOffButton: '关机', }, // Discovery diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index 656b5437e..530fac87d 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/frontend/src/pages/PrintersPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; -import { useQuery, useQueries, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useTheme } from '../contexts/ThemeContext'; import { useAuth } from '../contexts/AuthContext'; @@ -41,6 +41,7 @@ import { CheckCircle, XCircle, User, + Home, Printer as PrinterIcon, Info, Cable, @@ -1553,8 +1554,8 @@ function PrinterCard({ const [showEditModal, setShowEditModal] = useState(false); const [showFileManager, setShowFileManager] = useState(false); const [showMQTTDebug, setShowMQTTDebug] = useState(false); - const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(null); - const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(null); + const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false); + const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false); const [showHMSModal, setShowHMSModal] = useState(false); const [showStopConfirm, setShowStopConfirm] = useState(false); const [showPauseConfirm, setShowPauseConfirm] = useState(false); @@ -1779,19 +1780,24 @@ function PrinterCard({ ? currentTrayNow : cachedTrayNow.current; - // Fetch smart plugs for this printer - const { data: smartPlugs } = useQuery({ - queryKey: ['smartPlugsByPrinter', printer.id], - queryFn: () => api.getAllSmartPlugsByPrinter(printer.id), + // Fetch smart plug for this printer + const { data: smartPlug } = useQuery({ + queryKey: ['smartPlugByPrinter', printer.id], + queryFn: () => api.getSmartPlugByPrinter(printer.id), }); - // Fetch smart plug status for all plugs (faster refresh for energy monitoring) - const plugStatusResults = useQueries({ - queries: (smartPlugs || []).map(plug => ({ - queryKey: ['smartPlugStatus', plug.id], - queryFn: () => api.getSmartPlugStatus(plug.id), - refetchInterval: 10000, // 10 seconds for real-time power display - })), + // Fetch script plugs for this printer (for multi-device control) + const { data: scriptPlugs } = useQuery({ + queryKey: ['scriptPlugsByPrinter', printer.id], + queryFn: () => api.getScriptPlugsByPrinter(printer.id), + }); + + // Fetch smart plug status if plug exists (faster refresh for energy monitoring) + const { data: plugStatus } = useQuery({ + queryKey: ['smartPlugStatus', smartPlug?.id], + queryFn: () => smartPlug ? api.getSmartPlugStatus(smartPlug.id) : null, + enabled: !!smartPlug, + refetchInterval: 10000, // 10 seconds for real-time power display }); // Fetch queue count for this printer @@ -1886,26 +1892,26 @@ function PrinterCard({ // Smart plug control mutations const powerControlMutation = useMutation({ - mutationFn: ({ plugId, action }: { plugId: number; action: 'on' | 'off' }) => - api.controlSmartPlug(plugId, action), - onSuccess: (_data, variables) => { - queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', printer.id] }); - queryClient.invalidateQueries({ queryKey: ['smartPlugStatus', variables.plugId] }); + mutationFn: (action: 'on' | 'off') => + smartPlug ? api.controlSmartPlug(smartPlug.id, action) : Promise.reject('No plug'), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['smartPlugStatus', smartPlug?.id] }); }, }); const toggleAutoOffMutation = useMutation({ - mutationFn: ({ plugId, enabled }: { plugId: number; enabled: boolean }) => - api.updateSmartPlug(plugId, { auto_off: enabled }), + mutationFn: (enabled: boolean) => + smartPlug ? api.updateSmartPlug(smartPlug.id, { auto_off: enabled }) : Promise.reject('No plug'), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', printer.id] }); + queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', printer.id] }); + // Also invalidate the smart-plugs list to keep Settings page in sync queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); }, }); - // Run HA script mutation + // Run HA entity mutation — scripts use 'on' (trigger), switches use 'toggle' const runScriptMutation = useMutation({ - mutationFn: (id: number) => api.controlSmartPlug(id, 'on'), + mutationFn: ({ id, action }: { id: number; action: 'on' | 'toggle' }) => api.controlSmartPlug(id, action), onSuccess: () => { showToast(t('printers.toast.scriptTriggered')); }, @@ -3963,104 +3969,118 @@ function PrinterCard({ )} {/* Smart Plug Controls - hidden in compact mode */} - {smartPlugs && smartPlugs.length > 0 && viewMode === 'expanded' && ( -
- {smartPlugs.map((plug, index) => { - const plugStatus = plugStatusResults[index]?.data; - const isScript = plug.plug_type === 'homeassistant' && plug.ha_entity_id?.startsWith('script.'); - return ( -
-
- - {plug.name} - {plugStatus && ( - - {plugStatus.state || '?'} - {plugStatus.state === 'ON' && plugStatus.energy?.power != null && ( - · {plugStatus.energy.power}W - )} - + {smartPlug && viewMode === 'expanded' && ( +
+
+ {/* Plug name and status */} +
+ + {smartPlug.name} + {plugStatus && ( + + {plugStatus.state || '?'} + {plugStatus.state === 'ON' && plugStatus.energy?.power != null && ( + · {plugStatus.energy.power}W )} -
-
- {isScript ? ( - - ) : ( -
- - -
- )} - {!isScript && ( -
- - -
- )} + + )} +
+ + {/* Spacer */} +
+ + {/* Power buttons */} +
+ + +
+ + {/* Auto-off toggle */} +
+ + +
+
+ + {/* HA entity buttons row */} + {scriptPlugs && scriptPlugs.length > 0 && ( +
+ + HA: +
+ {scriptPlugs.map(script => { + const isScript = script.ha_entity_id?.startsWith('script.'); + return ( + + ); + })}
- ); - })} +
+ )}
)} @@ -4494,36 +4514,36 @@ function PrinterCard({ )} {/* Power On Confirmation */} - {showPowerOnConfirm !== null && ( + {showPowerOnConfirm && smartPlug && ( p.id === showPowerOnConfirm)?.name || '' })} + message={t('printers.confirm.powerOnMessage', { name: printer.name })} confirmText={t('printers.confirm.powerOnButton')} variant="default" onConfirm={() => { - powerControlMutation.mutate({ plugId: showPowerOnConfirm, action: 'on' }); - setShowPowerOnConfirm(null); + powerControlMutation.mutate('on'); + setShowPowerOnConfirm(false); }} - onCancel={() => setShowPowerOnConfirm(null)} + onCancel={() => setShowPowerOnConfirm(false)} /> )} {/* Power Off Confirmation */} - {showPowerOffConfirm !== null && ( + {showPowerOffConfirm && smartPlug && ( p.id === showPowerOffConfirm)?.name || '' }) - : t('printers.confirm.powerOffMessage', { name: smartPlugs?.find(p => p.id === showPowerOffConfirm)?.name || '' }) + ? t('printers.confirm.powerOffWarning', { name: printer.name }) + : t('printers.confirm.powerOffMessage', { name: printer.name }) } confirmText={t('printers.confirm.powerOffButton')} variant="danger" onConfirm={() => { - powerControlMutation.mutate({ plugId: showPowerOffConfirm, action: 'off' }); - setShowPowerOffConfirm(null); + powerControlMutation.mutate('off'); + setShowPowerOffConfirm(false); }} - onCancel={() => setShowPowerOffConfirm(null)} + onCancel={() => setShowPowerOffConfirm(false)} /> )}