220 lines
8.2 KiB
HTML
220 lines
8.2 KiB
HTML
<!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>
|