я косяк, поправил веб. Капитан, работайте!
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" translate="no" class="notranslate" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="google" content="notranslate">
|
||||
<meta name="googlebot" content="notranslate">
|
||||
<meta http-equiv="Content-Language" content="ru">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="/static/favicon-32.png" type="image/png" sizes="32x32">
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
|
||||
<title>WESP — загрузка</title>
|
||||
<link rel="stylesheet" href="/static/css/wesp-logo-loader.css">
|
||||
<link rel="stylesheet" href="/static/css/wesp-setup.css">
|
||||
<link rel="stylesheet" href="/static/css/wesp-startup.css">
|
||||
<link rel="modulepreload" href="/static/js/wesp-logo-loader.js?v=startup11s">
|
||||
</head>
|
||||
<body class="wesp-startup-screen">
|
||||
<div class="wesp-startup__stage">
|
||||
<div class="wesp-setup__intro" id="startupIntro" aria-hidden="false"></div>
|
||||
<p class="wesp-startup__quip" id="startupQuip" aria-live="polite"></p>
|
||||
</div>
|
||||
<noscript>
|
||||
<p class="wesp-startup__quip wesp-startup__quip--error">
|
||||
Ошибка: нужен JavaScript для экрана загрузки.
|
||||
</p>
|
||||
</noscript>
|
||||
<script type="module">
|
||||
import { mountWespLogoLoader } from "/static/js/wesp-logo-loader.js?v=startup11s";
|
||||
|
||||
const quipEl = document.getElementById("startupQuip");
|
||||
const introEl = document.getElementById("startupIntro");
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let defaultReturn = "/login";
|
||||
const rawNext = params.get("next");
|
||||
let returnTo = defaultReturn;
|
||||
if (rawNext && String(rawNext).trim().startsWith("/")) {
|
||||
returnTo = String(rawNext).trim();
|
||||
} else {
|
||||
const stored = sessionStorage.getItem("wespStartupReturn");
|
||||
if (stored && String(stored).trim().startsWith("/") && stored !== "/") {
|
||||
returnTo = String(stored).trim();
|
||||
}
|
||||
}
|
||||
sessionStorage.setItem("wespStartupReturn", returnTo);
|
||||
const theme = (params.get("theme") || "dark").trim().toLowerCase();
|
||||
if (theme === "light" || theme === "dark" || theme === "organic") {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
|
||||
const QUIPS = [
|
||||
"Сортируем гранулы по размеру мысли…",
|
||||
"Калибруем весы — не дышите на тару",
|
||||
"Синхронизируем рецепты с ближайшей галактикой",
|
||||
"Ищем соседей по сети — здравствуйте, VLAN",
|
||||
"Считаем компоненты рациона (почти честно)",
|
||||
"Проверяем: server и client — не один Pi",
|
||||
"Чайник почти вскипел, осталось чуть-чуть",
|
||||
"Подтягиваем склад: где мука, там и отчёты",
|
||||
"Собираем пиксели логотипа в боевой порядок",
|
||||
"Готовим весы к первому «ну почти ноль»",
|
||||
"Дозатор №3 просит ещё пять грамм терпения",
|
||||
"Зоотехник ещё не проснулся — мы уже работаем",
|
||||
"Перемалываем корм в биты и обратно",
|
||||
"Сверяем тару с совестью",
|
||||
"Логотип синеет — это не баг, это бренд",
|
||||
"Кормление через пять… четыре… ну почти",
|
||||
"mDNS: «есть кто живой?» — ждём ответа",
|
||||
"Весы молчат — значит, всё стабильно",
|
||||
"Рецепт «на глаз» конвертируем в граммы",
|
||||
"Проверяем, что ноль — действительно ноль",
|
||||
"Комптон заряжает пиксели…",
|
||||
];
|
||||
|
||||
const READY_QUIP = "Готово — открываем двери";
|
||||
const QUIP_ROTATE_MS = 1500;
|
||||
const STARTUP_MIN_MS = 10000;
|
||||
/** Скорость волн/shimmer — как раньше (не растягивать на 10 с). */
|
||||
const STARTUP_LOGO_ANIM_MS = 6000;
|
||||
const READY_HOLD_MS = 700;
|
||||
|
||||
function shuffleQuips(list) {
|
||||
const deck = list.slice();
|
||||
for (let i = deck.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[deck[i], deck[j]] = [deck[j], deck[i]];
|
||||
}
|
||||
return deck;
|
||||
}
|
||||
|
||||
let quipDeck = shuffleQuips(QUIPS);
|
||||
let deckPos = 0;
|
||||
let quipFadeSeq = 0;
|
||||
let quipRotationEnabled = true;
|
||||
let pollTimer = null;
|
||||
let quipTimer = null;
|
||||
let serverReady = false;
|
||||
let finishStarted = false;
|
||||
const minLeaveAt = performance.now() + STARTUP_MIN_MS;
|
||||
|
||||
function pickQuip() {
|
||||
if (deckPos >= quipDeck.length) {
|
||||
quipDeck = shuffleQuips(QUIPS);
|
||||
deckPos = 0;
|
||||
}
|
||||
return quipDeck[deckPos++];
|
||||
}
|
||||
|
||||
function setQuip(text, isError = false) {
|
||||
if (!quipEl) return;
|
||||
const next = String(text || "").trim();
|
||||
if (!next) return;
|
||||
|
||||
const seq = ++quipFadeSeq;
|
||||
quipEl.classList.add("wesp-startup__quip--fade");
|
||||
quipEl.classList.toggle("wesp-startup__quip--error", Boolean(isError));
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (seq !== quipFadeSeq) return;
|
||||
quipEl.textContent = next;
|
||||
quipEl.classList.remove("wesp-startup__quip--fade");
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function stopQuipRotation() {
|
||||
quipRotationEnabled = false;
|
||||
if (quipTimer) {
|
||||
clearInterval(quipTimer);
|
||||
quipTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function rotateQuip() {
|
||||
if (!quipRotationEnabled || document.hidden || finishStarted) return;
|
||||
setQuip(pickQuip());
|
||||
}
|
||||
|
||||
function tryFinish() {
|
||||
if (finishStarted) return;
|
||||
if (!serverReady) return;
|
||||
if (performance.now() < minLeaveAt) return;
|
||||
|
||||
finishStarted = true;
|
||||
stopQuipRotation();
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
|
||||
setQuip(READY_QUIP);
|
||||
const target = returnTo.startsWith("/") ? returnTo : defaultReturn;
|
||||
window.setTimeout(() => window.location.replace(target), READY_HOLD_MS);
|
||||
}
|
||||
|
||||
async function pollReady() {
|
||||
if (finishStarted) return;
|
||||
try {
|
||||
const r = await fetch("/api/startup/status", { cache: "no-store" });
|
||||
const d = await r.json().catch(() => ({}));
|
||||
|
||||
if (d.default_next && String(d.default_next).trim().startsWith("/")) {
|
||||
defaultReturn = String(d.default_next).trim();
|
||||
if (!rawNext) {
|
||||
returnTo = defaultReturn;
|
||||
sessionStorage.setItem("wespStartupReturn", returnTo);
|
||||
}
|
||||
}
|
||||
|
||||
if (d.ready) {
|
||||
serverReady = true;
|
||||
tryFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.phase === "error" || d.error) {
|
||||
finishStarted = true;
|
||||
stopQuipRotation();
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
setQuip(d.error || d.message || "Ошибка инициализации", true);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (quipRotationEnabled && !quipEl.textContent) {
|
||||
setQuip("Ожидание сервера…");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await mountWespLogoLoader(introEl, {
|
||||
loop: false,
|
||||
durationMs: STARTUP_LOGO_ANIM_MS,
|
||||
embedded: true,
|
||||
transparentBackground: true,
|
||||
knockoutBackground: true,
|
||||
theme: document.documentElement.getAttribute("data-theme") || theme,
|
||||
hideChrome: false,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("startup logo loader failed", err);
|
||||
finishStarted = true;
|
||||
stopQuipRotation();
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
setQuip(
|
||||
"Не удалось загрузить анимацию. Проверьте /static/js/wesp-logo-loader.js и логи сервера.",
|
||||
true,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (quipEl) quipEl.classList.remove("wesp-startup__quip--error");
|
||||
setQuip(pickQuip());
|
||||
quipTimer = window.setInterval(rotateQuip, QUIP_ROTATE_MS);
|
||||
pollReady();
|
||||
pollTimer = window.setInterval(pollReady, 800);
|
||||
window.setTimeout(tryFinish, STARTUP_MIN_MS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user