Add manual ping/health check button to device cards
Adds a refresh icon button on each device card that triggers an immediate
health check via POST /devices/{id}/ping, showing online status with
latency or offline result as a toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -406,6 +406,26 @@ async def get_device_state(
|
|||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/devices/{device_id}/ping", response_model=DeviceStateResponse, tags=["Devices"])
|
||||||
|
async def ping_device(
|
||||||
|
device_id: str,
|
||||||
|
_auth: AuthRequired,
|
||||||
|
store: DeviceStore = Depends(get_device_store),
|
||||||
|
manager: ProcessorManager = Depends(get_processor_manager),
|
||||||
|
):
|
||||||
|
"""Force an immediate health check on a device."""
|
||||||
|
device = store.get_device(device_id)
|
||||||
|
if not device:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
state = await manager.force_device_health_check(device_id)
|
||||||
|
state["device_type"] = device.device_type
|
||||||
|
return DeviceStateResponse(**state)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
# ===== WLED BRIGHTNESS ENDPOINTS =====
|
# ===== WLED BRIGHTNESS ENDPOINTS =====
|
||||||
|
|
||||||
@router.get("/api/v1/devices/{device_id}/brightness", tags=["Settings"])
|
@router.get("/api/v1/devices/{device_id}/brightness", tags=["Settings"])
|
||||||
|
|||||||
@@ -1080,6 +1080,13 @@ class ProcessorManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to sync LED count for {device_id}: {e}")
|
logger.error(f"Failed to sync LED count for {device_id}: {e}")
|
||||||
|
|
||||||
|
async def force_device_health_check(self, device_id: str) -> dict:
|
||||||
|
"""Run an immediate health check for a device and return the result."""
|
||||||
|
if device_id not in self._devices:
|
||||||
|
raise ValueError(f"Device {device_id} not found")
|
||||||
|
await self._check_device_health(device_id)
|
||||||
|
return self.get_device_health_dict(device_id)
|
||||||
|
|
||||||
# ===== HELPERS =====
|
# ===== HELPERS =====
|
||||||
|
|
||||||
def has_device(self, device_id: str) -> bool:
|
def has_device(self, device_id: str) -> bool:
|
||||||
|
|||||||
@@ -147,6 +147,11 @@ h2 {
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Device card ping button spinning animation */
|
||||||
|
.card-ping-btn.spinning svg {
|
||||||
|
animation: conn-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
/* WLED device health indicator */
|
/* WLED device health indicator */
|
||||||
.health-dot {
|
.health-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal,
|
showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal,
|
||||||
saveDeviceSettings, updateBrightnessLabel, saveCardBrightness,
|
saveDeviceSettings, updateBrightnessLabel, saveCardBrightness,
|
||||||
turnOffDevice, removeDevice, loadDevices,
|
turnOffDevice, pingDevice, removeDevice, loadDevices,
|
||||||
updateSettingsBaudFpsHint, copyWsUrl,
|
updateSettingsBaudFpsHint, copyWsUrl,
|
||||||
} from './features/devices.js';
|
} from './features/devices.js';
|
||||||
import {
|
import {
|
||||||
@@ -211,6 +211,7 @@ Object.assign(window, {
|
|||||||
updateBrightnessLabel,
|
updateBrightnessLabel,
|
||||||
saveCardBrightness,
|
saveCardBrightness,
|
||||||
turnOffDevice,
|
turnOffDevice,
|
||||||
|
pingDevice,
|
||||||
removeDevice,
|
removeDevice,
|
||||||
loadDevices,
|
loadDevices,
|
||||||
updateSettingsBaudFpsHint,
|
updateSettingsBaudFpsHint,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { _fetchOpenrgbZones, _getCheckedZones, _splitOpenrgbZone, _getZoneMode }
|
|||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { showToast, showConfirm } from '../core/ui.js';
|
import { showToast, showConfirm } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB, ICON_PLUG } from '../core/icons.js';
|
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB, ICON_PLUG, ICON_REFRESH } from '../core/icons.js';
|
||||||
import { wrapCard } from '../core/card-colors.js';
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
||||||
|
|
||||||
@@ -133,6 +133,9 @@ export function createDeviceCard(device) {
|
|||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${renderTagChips(device.tags)}`,
|
${renderTagChips(device.tags)}`,
|
||||||
actions: `
|
actions: `
|
||||||
|
<button class="btn btn-icon btn-secondary card-ping-btn" onclick="event.stopPropagation(); pingDevice('${device.id}')" title="${t('device.button.ping')}">
|
||||||
|
${ICON_REFRESH}
|
||||||
|
</button>
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
||||||
${ICON_SETTINGS}
|
${ICON_SETTINGS}
|
||||||
</button>`,
|
</button>`,
|
||||||
@@ -157,6 +160,32 @@ export async function turnOffDevice(deviceId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function pingDevice(deviceId) {
|
||||||
|
const btn = document.querySelector(`[data-device-id="${deviceId}"] .card-ping-btn`);
|
||||||
|
if (btn) btn.classList.add('spinning');
|
||||||
|
try {
|
||||||
|
const resp = await fetchWithAuth(`/devices/${deviceId}/ping`, { method: 'POST' });
|
||||||
|
if (resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
const ms = data.device_latency_ms != null ? data.device_latency_ms.toFixed(0) : '?';
|
||||||
|
showToast(data.device_online
|
||||||
|
? t('device.ping.online', { ms })
|
||||||
|
: t('device.ping.offline'), data.device_online ? 'success' : 'error');
|
||||||
|
// Refresh device cards to update health dot
|
||||||
|
devicesCache.invalidate();
|
||||||
|
await window.loadDevices();
|
||||||
|
} else {
|
||||||
|
const err = await resp.json();
|
||||||
|
showToast(err.detail || 'Ping failed', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.isAuth) return;
|
||||||
|
showToast(t('device.ping.error'), 'error');
|
||||||
|
} finally {
|
||||||
|
if (btn) btn.classList.remove('spinning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function attachDeviceListeners(deviceId) {
|
export function attachDeviceListeners(deviceId) {
|
||||||
// Add any specific event listeners here if needed
|
// Add any specific event listeners here if needed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,10 @@
|
|||||||
"device.button.remove": "Remove",
|
"device.button.remove": "Remove",
|
||||||
"device.button.webui": "Open Device Web UI",
|
"device.button.webui": "Open Device Web UI",
|
||||||
"device.button.power_off": "Turn Off",
|
"device.button.power_off": "Turn Off",
|
||||||
|
"device.button.ping": "Ping Device",
|
||||||
|
"device.ping.online": "Online ({ms}ms)",
|
||||||
|
"device.ping.offline": "Device offline",
|
||||||
|
"device.ping.error": "Ping failed",
|
||||||
"device.power.off_success": "Device turned off",
|
"device.power.off_success": "Device turned off",
|
||||||
"device.status.connected": "Connected",
|
"device.status.connected": "Connected",
|
||||||
"device.status.disconnected": "Disconnected",
|
"device.status.disconnected": "Disconnected",
|
||||||
|
|||||||
@@ -186,6 +186,10 @@
|
|||||||
"device.button.remove": "Удалить",
|
"device.button.remove": "Удалить",
|
||||||
"device.button.webui": "Открыть веб-интерфейс устройства",
|
"device.button.webui": "Открыть веб-интерфейс устройства",
|
||||||
"device.button.power_off": "Выключить",
|
"device.button.power_off": "Выключить",
|
||||||
|
"device.button.ping": "Пинг устройства",
|
||||||
|
"device.ping.online": "Онлайн ({ms}мс)",
|
||||||
|
"device.ping.offline": "Устройство недоступно",
|
||||||
|
"device.ping.error": "Ошибка пинга",
|
||||||
"device.power.off_success": "Устройство выключено",
|
"device.power.off_success": "Устройство выключено",
|
||||||
"device.status.connected": "Подключено",
|
"device.status.connected": "Подключено",
|
||||||
"device.status.disconnected": "Отключено",
|
"device.status.disconnected": "Отключено",
|
||||||
|
|||||||
@@ -186,6 +186,10 @@
|
|||||||
"device.button.remove": "移除",
|
"device.button.remove": "移除",
|
||||||
"device.button.webui": "打开设备 Web UI",
|
"device.button.webui": "打开设备 Web UI",
|
||||||
"device.button.power_off": "关闭",
|
"device.button.power_off": "关闭",
|
||||||
|
"device.button.ping": "Ping 设备",
|
||||||
|
"device.ping.online": "在线 ({ms}ms)",
|
||||||
|
"device.ping.offline": "设备离线",
|
||||||
|
"device.ping.error": "Ping 失败",
|
||||||
"device.power.off_success": "设备已关闭",
|
"device.power.off_success": "设备已关闭",
|
||||||
"device.status.connected": "已连接",
|
"device.status.connected": "已连接",
|
||||||
"device.status.disconnected": "已断开",
|
"device.status.disconnected": "已断开",
|
||||||
|
|||||||
Reference in New Issue
Block a user