diff --git a/internal/api/ui/iot-console.html b/internal/api/ui/iot-console.html index db91c07..e1a1f5d 100644 --- a/internal/api/ui/iot-console.html +++ b/internal/api/ui/iot-console.html @@ -333,6 +333,21 @@ async function namespaceFromToken(token) { // API // ============================================================= +// sanitizeApiError — убирает служебные детали из сообщений об ошибках API. +// Пользователь не должен видеть namespace, k8s-пути, internal имена объектов. +function sanitizeApiError(msg) { + if (!msg) return 'Внутренняя ошибка. Попробуйте ещё раз.'; + // Скрываем k8s-сообщения с namespace/not found + if (/namespaces?.*not found/i.test(msg)) return 'Ошибка инициализации. Выйдите и войдите снова.'; + if (/not found/i.test(msg) && /sless-/i.test(msg)) return 'Ресурс не найден.'; + if (/already exists/i.test(msg)) return 'Устройство с таким именем уже существует.'; + if (/device.*id.*already/i.test(msg) || /device_id.*exists/i.test(msg)) return 'Device ID уже занят.'; + if (/cannot decode jwt/i.test(msg) || /not a jwt/i.test(msg)) return 'Неверный токен.'; + // HTTP 5xx — общая фраза + if (/HTTP 5/i.test(msg)) return 'Ошибка сервера. Попробуйте ещё раз.'; + return msg; +} + // apiCall — универсальная обёртка для REST запросов к оператору async function apiCall(method, path, body) { const opts = { @@ -738,12 +753,17 @@ async function doCreate() { btn.disabled = true; btn.textContent = 'Создание...'; try { + // EnsureNamespace — идемпотентный вызов. + // Страховка на случай если сессия была сохранена до автосоздания namespace при логине. + await apiCall('POST', `/v1/namespaces/${S.ns}/ensure`, {}); await apiCreateDevice(name, did); closeModal(); await renderDevices(); } catch(e) { btn.disabled = false; btn.textContent = 'Создать'; - document.getElementById('m-err').innerHTML = `