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);
+}