diff --git a/console/ui/index.html b/console/ui/index.html index 792201f..6ea1629 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -19,6 +19,7 @@ + @@ -417,185 +418,6 @@ currentInvoke: null }; - var _initPolling = false; - var _initTickBusy = false; - var _initStartedAt = 0; - - function initLog(msg) { - var el = document.getElementById('init-log'); - var ts = new Date().toLocaleTimeString('ru-RU'); - el.textContent += '[' + ts + '] ' + msg + '\n'; - el.scrollTop = el.scrollHeight; - } - - function formatSeconds(totalSeconds) { - totalSeconds = Math.max(0, Number(totalSeconds) || 0); - var minutes = Math.floor(totalSeconds / 60); - var seconds = totalSeconds % 60; - if (minutes <= 0) return seconds + 'с'; - return minutes + 'м ' + seconds + 'с'; - } - - function setInitElapsed(seconds) { - document.getElementById('init-elapsed').textContent = formatSeconds(seconds); - } - - function setInitCurrentStage(text) { - document.getElementById('init-current-stage').textContent = text || 'Ожидание старта'; - } - - function setBox(id, text) { - var el = document.getElementById(id); - if (!el) return; - if (!text) { - el.style.display = 'none'; - el.textContent = ''; - return; - } - el.style.display = 'block'; - el.textContent = text; - } - - function missingKeys(obj) { - return Object.keys(obj || {}).filter(function (key) { - return !obj[key]; - }); - } - - function firstPendingStage(stages) { - var list = stages || []; - for (var i = 0; i < list.length; i++) { - if (!list[i].done) return list[i].name; - } - return list.length ? list[list.length - 1].name : 'Ожидание старта'; - } - - function describeInitReason(debug) { - if (!debug) return 'Собираем диагностические данные по namespace и control-plane...'; - if (!debug.exists) return 'Ещё не создан Kubernetes namespace пользователя.'; - if (!debug.active) return 'Namespace создан, но Kubernetes ещё не перевёл его в состояние Active.'; - - var preparation = debug.preparation || {}; - if (!preparation.managedByConsole) return 'Namespace существует, но ещё не помечен как managed-by=fission-console.'; - if (!preparation.watcherLabel) return 'Namespace ещё не помечен label fission.io/managed=true для NSWatcher.'; - - var missingSAs = missingKeys(preparation.serviceAccounts); - if (missingSAs.length) return 'Не готовы bootstrap service accounts: ' + missingSAs.join(', ') + '.'; - - var missingRBs = missingKeys(preparation.roleBindings); - if (missingRBs.length) return 'Не готовы bootstrap role bindings: ' + missingRBs.join(', ') + '.'; - - var control = debug.controlPlane || {}; - if (!control.executor || !control.executor.ready) { - return 'Executor ещё не ready. Возможна задержка из-за cold start, image pull или specialization.'; - } - if (!control.router || !control.router.ready) { - return 'Router ещё не ready. HTTP маршрутизация пока не активна.'; - } - - return 'Ожидаем финальное подтверждение готовности control-plane.'; - } - - function summarizeInit(status, debug) { - var done = status && status.stages ? status.stages.filter(function (s) { return s.done; }).length : 0; - var namespace = debug && debug.namespace ? debug.namespace : 'не определён'; - return 'Namespace: ' + namespace + '. Завершено шагов: ' + done + '/3.'; - } - - function explainDelay(seconds) { - if (seconds < 5) return 'Обычная операция без заметной задержки.'; - if (seconds < 10) return 'Вероятен cold start: прогрев пула, image pull или ожидание specialization.'; - return 'Задержка выше нормы: возможны cold start, specialization или ожидание готовности control-plane.'; - } - - function startTimedStatus(startText, waitingPrefix, waitingHint) { - var startedAt = Date.now(); - showStatus(startText, ''); - var timer = setInterval(function () { - var elapsed = Math.max(1, Math.floor((Date.now() - startedAt) / 1000)); - var text = waitingPrefix + ' ' + formatSeconds(elapsed); - if (waitingHint) text += ' · ' + waitingHint(elapsed); - showStatus(text, ''); - }, 1000); - return { - stop: function (message, kind) { - clearInterval(timer); - var elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000)); - var text = message; - if (message) text += ' за ' + formatSeconds(elapsed); - showStatus(text, kind || ''); - } - }; - } - - function renderInitStages(stages) { - var el = document.getElementById('init-stages'); - el.innerHTML = stages.map(function (s) { - var icon = s.done ? '✅' : '⏳'; - var color = s.done ? '#4caf50' : '#8bc7ff'; - return '
' + icon + ' ' + s.name + '
'; - }).join(''); - } - - async function pollNSStatus() { - if (_initPolling) return; - _initPolling = true; - _initTickBusy = false; - _initStartedAt = Date.now(); - var overlay = document.getElementById('init-overlay'); - overlay.style.display = 'flex'; - document.getElementById('init-log').textContent = ''; - setInitElapsed(0); - setInitCurrentStage('Подготовка запуска'); - setBox('init-summary', 'Проверяем состояние namespace и control-plane...'); - setBox('init-reason', 'Если пользователь новый, возможны cold start и создание bootstrap ресурсов.'); - setBox('init-error', ''); - initLog('Запуск инициализации...'); - var attempt = 0; - var maxAttempts = 60; // 5 минут - var interval = setInterval(async function () { - if (_initTickBusy) return; - _initTickBusy = true; - attempt++; - try { - var elapsed = Math.floor((Date.now() - _initStartedAt) / 1000); - setInitElapsed(elapsed); - var statusResp = await fetch(API_BASE + '/ns/status', { headers: authHeaders() }); - var debugResp = await fetch(API_BASE + '/ns/debug', { headers: authHeaders() }); - var d = await statusResp.json(); - var dbg = await debugResp.json().catch(function () { return null; }); - if (d.stages) renderInitStages(d.stages); - setInitCurrentStage(firstPendingStage(d.stages)); - setBox('init-summary', summarizeInit(d, dbg)); - setBox('init-reason', describeInitReason(dbg)); - setBox('init-error', ''); - var done = d.stages ? d.stages.filter(function (s) { return s.done; }).length : 0; - initLog('Шагов завершено: ' + done + '/3'); - if (d.ready) { - initLog('✅ Готово! Загружаем консоль...'); - clearInterval(interval); - _initPolling = false; - _initTickBusy = false; - overlay.style.display = 'none'; - reloadAll(); - return; - } - } catch (e) { - setBox('init-error', 'Последняя ошибка: ' + e.message); - initLog('Ошибка опроса: ' + e.message); - } finally { - _initTickBusy = false; - } - if (attempt >= maxAttempts) { - clearInterval(interval); - _initPolling = false; - _initTickBusy = false; - setBox('init-error', 'Превышено время ожидания. Чаще всего это bootstrap RBAC, control-plane readiness или холодный старт.'); - initLog('⚠️ Превышено время ожидания. Попробуйте обновить страницу.'); - } - }, 5000); - } - checkAuth(); diff --git a/console/ui/js/init.js b/console/ui/js/init.js new file mode 100644 index 0000000..f191bbe --- /dev/null +++ b/console/ui/js/init.js @@ -0,0 +1,180 @@ +/* init.js — инициализация namespace, утилиты таймера и статуса */ + +var _initPolling = false; +var _initTickBusy = false; +var _initStartedAt = 0; + +function initLog(msg) { + var el = document.getElementById('init-log'); + var ts = new Date().toLocaleTimeString('ru-RU'); + el.textContent += '[' + ts + '] ' + msg + '\n'; + el.scrollTop = el.scrollHeight; +} + +function formatSeconds(totalSeconds) { + totalSeconds = Math.max(0, Number(totalSeconds) || 0); + var minutes = Math.floor(totalSeconds / 60); + var seconds = totalSeconds % 60; + if (minutes <= 0) return seconds + 'с'; + return minutes + 'м ' + seconds + 'с'; +} + +function setInitElapsed(seconds) { + document.getElementById('init-elapsed').textContent = formatSeconds(seconds); +} + +function setInitCurrentStage(text) { + document.getElementById('init-current-stage').textContent = text || 'Ожидание старта'; +} + +function setBox(id, text) { + var el = document.getElementById(id); + if (!el) return; + if (!text) { + el.style.display = 'none'; + el.textContent = ''; + return; + } + el.style.display = 'block'; + el.textContent = text; +} + +function missingKeys(obj) { + return Object.keys(obj || {}).filter(function (key) { + return !obj[key]; + }); +} + +function firstPendingStage(stages) { + var list = stages || []; + for (var i = 0; i < list.length; i++) { + if (!list[i].done) return list[i].name; + } + return list.length ? list[list.length - 1].name : 'Ожидание старта'; +} + +function describeInitReason(debug) { + if (!debug) return 'Собираем диагностические данные по namespace и control-plane...'; + if (!debug.exists) return 'Ещё не создан Kubernetes namespace пользователя.'; + if (!debug.active) return 'Namespace создан, но Kubernetes ещё не перевёл его в состояние Active.'; + + var preparation = debug.preparation || {}; + if (!preparation.managedByConsole) return 'Namespace существует, но ещё не помечен как managed-by=fission-console.'; + if (!preparation.watcherLabel) return 'Namespace ещё не помечен label fission.io/managed=true для NSWatcher.'; + + var missingSAs = missingKeys(preparation.serviceAccounts); + if (missingSAs.length) return 'Не готовы bootstrap service accounts: ' + missingSAs.join(', ') + '.'; + + var missingRBs = missingKeys(preparation.roleBindings); + if (missingRBs.length) return 'Не готовы bootstrap role bindings: ' + missingRBs.join(', ') + '.'; + + var control = debug.controlPlane || {}; + if (!control.executor || !control.executor.ready) { + return 'Executor ещё не ready. Возможна задержка из-за cold start, image pull или specialization.'; + } + if (!control.router || !control.router.ready) { + return 'Router ещё не ready. HTTP маршрутизация пока не активна.'; + } + + return 'Ожидаем финальное подтверждение готовности control-plane.'; +} + +function summarizeInit(status, debug) { + var done = status && status.stages ? status.stages.filter(function (s) { return s.done; }).length : 0; + var namespace = debug && debug.namespace ? debug.namespace : 'не определён'; + return 'Namespace: ' + namespace + '. Завершено шагов: ' + done + '/3.'; +} + +function explainDelay(seconds) { + if (seconds < 5) return 'Обычная операция без заметной задержки.'; + if (seconds < 10) return 'Вероятен cold start: прогрев пула, image pull или ожидание specialization.'; + return 'Задержка выше нормы: возможны cold start, specialization или ожидание готовности control-plane.'; +} + +function startTimedStatus(startText, waitingPrefix, waitingHint) { + var startedAt = Date.now(); + showStatus(startText, ''); + var timer = setInterval(function () { + var elapsed = Math.max(1, Math.floor((Date.now() - startedAt) / 1000)); + var text = waitingPrefix + ' ' + formatSeconds(elapsed); + if (waitingHint) text += ' · ' + waitingHint(elapsed); + showStatus(text, ''); + }, 1000); + return { + stop: function (message, kind) { + clearInterval(timer); + var elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000)); + var text = message; + if (message) text += ' за ' + formatSeconds(elapsed); + showStatus(text, kind || ''); + } + }; +} + +function renderInitStages(stages) { + var el = document.getElementById('init-stages'); + el.innerHTML = stages.map(function (s) { + var icon = s.done ? '✅' : '⏳'; + var color = s.done ? '#4caf50' : '#8bc7ff'; + return '
' + icon + ' ' + s.name + '
'; + }).join(''); +} + +async function pollNSStatus() { + if (_initPolling) return; + _initPolling = true; + _initTickBusy = false; + _initStartedAt = Date.now(); + var overlay = document.getElementById('init-overlay'); + overlay.style.display = 'flex'; + document.getElementById('init-log').textContent = ''; + setInitElapsed(0); + setInitCurrentStage('Подготовка запуска'); + setBox('init-summary', 'Проверяем состояние namespace и control-plane...'); + setBox('init-reason', 'Если пользователь новый, возможны cold start и создание bootstrap ресурсов.'); + setBox('init-error', ''); + initLog('Запуск инициализации...'); + var attempt = 0; + var maxAttempts = 60; // 5 минут + var interval = setInterval(async function () { + if (_initTickBusy) return; + _initTickBusy = true; + attempt++; + try { + var elapsed = Math.floor((Date.now() - _initStartedAt) / 1000); + setInitElapsed(elapsed); + var statusResp = await fetch(API_BASE + '/ns/status', { headers: authHeaders() }); + var debugResp = await fetch(API_BASE + '/ns/debug', { headers: authHeaders() }); + var d = await statusResp.json(); + var dbg = await debugResp.json().catch(function () { return null; }); + if (d.stages) renderInitStages(d.stages); + setInitCurrentStage(firstPendingStage(d.stages)); + setBox('init-summary', summarizeInit(d, dbg)); + setBox('init-reason', describeInitReason(dbg)); + setBox('init-error', ''); + var done = d.stages ? d.stages.filter(function (s) { return s.done; }).length : 0; + initLog('Шагов завершено: ' + done + '/3'); + if (d.ready) { + initLog('✅ Готово! Загружаем консоль...'); + clearInterval(interval); + _initPolling = false; + _initTickBusy = false; + overlay.style.display = 'none'; + reloadAll(); + return; + } + } catch (e) { + setBox('init-error', 'Последняя ошибка: ' + e.message); + initLog('Ошибка опроса: ' + e.message); + } finally { + _initTickBusy = false; + } + if (attempt >= maxAttempts) { + clearInterval(interval); + _initPolling = false; + _initTickBusy = false; + setBox('init-error', 'Превышено время ожидания. Чаще всего это bootstrap RBAC, control-plane readiness или холодный старт.'); + initLog('⚠️ Превышено время ожидания. Попробуйте обновить страницу.'); + } + }, 5000); +}