chore: sync all changes from feat/ui-archive-upload session

This commit is contained in:
“Naeel”
2026-05-02 16:50:44 +04:00
parent aea4e93ca1
commit d7352532dc
15 changed files with 2933 additions and 19 deletions
+173 -16
View File
@@ -337,8 +337,37 @@
</head>
<body>
<!-- login-overlay будет динамически подгружен из templates/modals/login.html -->
<div id="login-overlay"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.85); z-index:999; align-items:center; justify-content:center; padding:16px;">
<div class="panel" style="width:min(440px,100%);">
<div class="brand" style="margin-bottom:24px;">
<div class="brand-mark">N</div>
<div class="brand-text">
<div class="nubes">NUBES</div>
<div class="product">FISSION CONSOLE</div>
</div>
</div>
<div class="row" style="flex-direction:column; gap:6px; margin-bottom:12px;">
<label style="font-size:12px; color:#999;">Стенд</label>
<select id="l-env"
style="background:var(--bg-base); border:1px solid var(--border); color:var(--fg); padding:8px 10px; border-radius:6px;">
<option value="dev">Dev</option>
<option value="test" selected>Test</option>
<option value="prod">Prod</option>
</select>
</div>
<div class="row" style="flex-direction:column; gap:6px; margin-bottom:12px;">
<label style="font-size:12px; color:#999;">Токен</label>
<textarea id="l-token" rows="5"
style="background:var(--bg-base); border:1px solid var(--border); color:var(--fg); padding:8px 10px; border-radius:6px; font-family:monospace; font-size:12px; resize:vertical; width:100%; box-sizing:border-box;"
placeholder="Введите токен..."></textarea>
<div style="font-size:11px; color:var(--text-secondary); margin-top:4px;">Токен: <strong
style="color:#8bc7ff;">Личный кабинет → Профиль пользователя → Токены</strong></div>
</div>
<div id="l-error" style="display:none; color:#ff6b6b; font-size:13px; margin-bottom:10px;"></div>
<button id="l-btn" class="btn" style="width:100%;" onclick="doLogin()">Войти</button>
</div>
</div>
<!-- Overlay: инициализация нового окружения -->
<div id="init-overlay"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.88); z-index:1000; align-items:center; justify-content:center; padding:16px;">
@@ -900,11 +929,70 @@
}
};
function onLangChange() {
var lang = document.getElementById('c-lang').value;
var t = LANG_TEMPLATES[lang];
if (t) {
document.getElementById('c-entry').value = t.entrypoint;
document.getElementById('c-code').value = t.code;
}
}
// Подключаем CRUD-функции для создания
const scriptFunctions = document.createElement('script');
scriptFunctions.src = 'js/functions.js';
document.head.appendChild(scriptFunctions);
function openCreate() {
document.getElementById('c-lang').value = 'python';
onLangChange();
document.getElementById('c-timeout').value = '60';
document.getElementById('c-schedule-enabled').checked = false;
document.getElementById('c-cron').value = '';
toggleScheduleFields('c');
document.getElementById('create-modal').classList.add('open');
document.getElementById('c-name').focus();
}
function closeCreate() {
document.getElementById('create-modal').classList.remove('open');
}
async function submitCreate() {
const btn = document.getElementById('c-submit');
btn.disabled = true;
const progress = startTimedStatus('Создаём функцию...', 'Создание функции...', explainDelay);
try {
const name = document.getElementById('c-name').value.trim();
if (!name) throw new Error('name is required');
const lang = document.getElementById('c-lang').value.trim();
if (!lang) throw new Error('language is required');
await requestJSON(API_BASE + '/functions', 'POST', {
name: name,
language: lang,
entrypoint: document.getElementById('c-entry').value.trim(),
route: document.getElementById('c-route').value.trim(),
methods: parseMethods(document.getElementById('c-methods').value),
timeout: parseTimeout(document.getElementById('c-timeout').value),
code: document.getElementById('c-code').value
});
try {
await syncScheduleForFunction(name, 'c');
} catch (scheduleErr) {
try {
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE');
} catch (rollbackErr) {
scheduleErr.message += '; rollback failed: ' + rollbackErr.message;
}
throw scheduleErr;
}
closeCreate();
progress.stop('\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0430: ' + name, 'ok');
await reloadAll();
} catch (e) {
progress.stop('\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f: ' + e.message, 'err');
} finally {
btn.disabled = false;
}
}
async function openEdit(name) {
try {
@@ -1127,18 +1215,87 @@
}
}
function showLoginOverlay() {
document.getElementById('login-overlay').style.display = 'flex';
}
// Динамически подгружаем login-overlay
fetch('templates/modals/login.html')
.then(r => r.text())
.then(html => {
document.body.insertAdjacentHTML('afterbegin', html);
});
function hideLoginOverlay() {
document.getElementById('login-overlay').style.display = 'none';
}
// Подключаем логику авторизации
const script = document.createElement('script');
script.src = 'js/auth.js';
document.head.appendChild(script);
async function doLogin() {
var btn = document.getElementById('l-btn');
var errEl = document.getElementById('l-error');
var token = (document.getElementById('l-token').value || '').trim();
var env = document.getElementById('l-env').value;
if (!token) { errEl.textContent = 'Введите токен'; errEl.style.display = 'block'; return; }
btn.disabled = true;
errEl.style.display = 'none';
try {
const res = await fetch(API_BASE + '/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token, env: env })
});
if (!res.ok) {
const d = await res.json().catch(function () { return {}; });
throw new Error(d.error || 'Ошибка входа');
}
localStorage.setItem('auth_token', token);
localStorage.setItem('auth_env', env);
setUserAvatar(token);
hideLoginOverlay();
// Проверяем статус NS — если не ready, показываем init overlay
try {
var sr = await fetch(API_BASE + '/ns/status', { headers: { 'X-Auth-Token': token, 'X-Auth-Env': env } });
var sd = await sr.json();
if (!sd.ready) {
pollNSStatus();
return;
}
} catch (_) { }
reloadAll();
} catch (e) {
errEl.textContent = e.message;
errEl.style.display = 'block';
} finally {
btn.disabled = false;
}
}
function jwtEmail(token) {
try {
var payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
return payload.email || payload.sub || null;
} catch (_) { return null; }
}
function setUserAvatar(token) {
var el = document.getElementById('user-avatar');
if (!el) return;
var email = token ? jwtEmail(token) : null;
if (email) {
el.textContent = email;
el.title = email;
el.style.fontSize = '0.7rem';
el.style.minWidth = '60px';
el.style.padding = '0 8px';
} else {
el.textContent = 'N';
el.title = '';
el.style.fontSize = '';
el.style.minWidth = '';
el.style.padding = '';
}
}
function doLogout() {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_env');
try { document.getElementById('l-token').value = ''; } catch (_) { }
setUserAvatar(null);
showLoginOverlay();
}
async function aiCheck(codeId, langId, resultId) {
var code = document.getElementById(codeId).value.trim();