Files
MeshChatX/electron/loading.html

308 lines
13 KiB
HTML

<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://127.0.0.1:9337 https://127.0.0.1:9337 http://localhost:9337 https://localhost:9337;"
/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>MeshChatX</title>
<script src="./assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js"></script>
<style>
@keyframes meshchatx-indeterminate {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(350%);
}
}
.meshchatx-bar {
animation: meshchatx-indeterminate 1.4s ease-in-out infinite;
}
</style>
</head>
<body
class="min-h-screen antialiased bg-gradient-to-b from-slate-100 to-slate-200 text-slate-800 dark:from-zinc-950 dark:to-zinc-900 dark:text-zinc-100"
>
<main class="flex min-h-screen items-center justify-center px-4 py-10 sm:px-6">
<div class="w-full max-w-sm">
<div
class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white/90 shadow-lg shadow-slate-200/50 backdrop-blur-sm dark:border-zinc-700/80 dark:bg-zinc-900/90 dark:shadow-black/40"
>
<div class="px-6 pt-8 pb-2 text-center">
<div
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white shadow-inner ring-1 ring-slate-200/80 dark:bg-zinc-950 dark:ring-zinc-700"
>
<img class="h-10 w-10 object-contain" src="./assets/images/logo.png" alt="" />
</div>
<h1 class="text-xl font-semibold tracking-tight text-slate-900 dark:text-white">MeshChatX</h1>
<p id="status-line" class="mt-2 text-sm leading-relaxed text-slate-600 dark:text-zinc-400">
Starting…
</p>
</div>
<div class="px-6 pb-6">
<div class="h-1 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-zinc-800">
<div
class="meshchatx-bar h-full w-1/3 rounded-full bg-blue-600 dark:bg-blue-500"
aria-hidden="true"
></div>
</div>
<p
id="attempt-hint"
class="mt-3 min-h-[1.25rem] text-center text-xs text-slate-500 dark:text-zinc-500"
></p>
<p
id="connection-notice"
class="mt-2 min-h-[2rem] text-center text-xs leading-relaxed text-amber-700 dark:text-amber-300"
></p>
<p class="mt-4 text-center text-[11px] text-slate-400 dark:text-zinc-600" id="app-version">
v0.0.0
</p>
</div>
</div>
</div>
</main>
<script src="./loadingStatusNotice.js"></script>
<script>
const statusLine = document.getElementById("status-line");
const attemptHint = document.getElementById("attempt-hint");
const connectionNotice = document.getElementById("connection-notice");
const API_HOST = "127.0.0.1";
const API_PORT = "9337";
const base = (protocol) => `${protocol}://${API_HOST}:${API_PORT}`;
const startupParams = new URLSearchParams(window.location.search);
const NOTICE_AFTER_ATTEMPTS = 10;
const NETWORK_WARNING_AFTER_ATTEMPTS = 24;
const RUNTIME_RECHECK_INTERVAL = 4;
const MAX_FAILURE_HISTORY = 8;
let protocolOrder = ["https", "http"];
applyTheme(detectPreferredTheme());
showAppVersion();
listenForSystemThemeChanges();
(async function bootstrap() {
applyStartupErrorHint();
try {
if (window.electron && typeof window.electron.backendHttpOnly === "function") {
const httpOnly = await window.electron.backendHttpOnly();
if (httpOnly) {
protocolOrder = ["http", "https"];
}
}
} catch (e) {}
check();
})();
function applyStartupErrorHint() {
const startupError = startupParams.get("startup_error");
if (startupError !== "backend_unreachable") {
return;
}
statusLine.textContent = "Lost connection to local backend.";
connectionNotice.textContent =
"Still retrying. If this continues, firewall or localhost filtering may be blocking access.";
}
async function showAppVersion() {
try {
const appVersion = await window.electron.appVersion();
document.getElementById("app-version").innerText = "v" + appVersion;
} catch (e) {}
}
function detectPreferredTheme() {
try {
const storedTheme =
localStorage.getItem("meshchat.theme") || localStorage.getItem("meshchatx.theme");
if (storedTheme === "dark" || storedTheme === "light") {
return storedTheme;
}
} catch (e) {}
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function applyTheme(theme) {
const isDark = theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
document.body.dataset.theme = isDark ? "dark" : "light";
}
function listenForSystemThemeChanges() {
if (!window.matchMedia) {
return;
}
const media = window.matchMedia("(prefers-color-scheme: dark)");
media.addEventListener("change", (event) => {
applyTheme(event.matches ? "dark" : "light");
});
}
let detectedProtocol = "http";
let attemptCount = 0;
let runtimeProbeAttempt = 0;
let cachedRuntimeState = null;
const recentFailures = [];
function parseStatusJson(text) {
try {
return JSON.parse(text);
} catch (e) {
return null;
}
}
function rememberFailure(failure) {
if (!failure || typeof failure !== "object") {
return;
}
recentFailures.push(failure);
if (recentFailures.length > MAX_FAILURE_HISTORY) {
recentFailures.shift();
}
}
async function refreshBackendRuntimeStateIfNeeded() {
if (!window.electron || typeof window.electron.backendRuntimeState !== "function") {
return;
}
if (attemptCount - runtimeProbeAttempt < RUNTIME_RECHECK_INTERVAL && cachedRuntimeState) {
return;
}
runtimeProbeAttempt = attemptCount;
try {
cachedRuntimeState = await window.electron.backendRuntimeState();
} catch (e) {}
}
function resolveFetchFailureKind(error) {
const helper = window.MeshchatLoadingStatusNotice;
if (helper && typeof helper.classifyFetchError === "function") {
return helper.classifyFetchError(error);
}
return "network-error";
}
async function tryOnce(protocol) {
const url = `${base(protocol)}/api/v1/status`;
try {
const result = await fetch(url, { cache: "no-store" });
const text = await result.text();
if (result.status !== 200) {
return {
ok: false,
failure: { kind: "http-error", status: result.status, protocol: protocol },
};
}
const data = parseStatusJson(text);
if (data && data.status === "ok") {
return { ok: true, protocol: protocol };
}
return {
ok: false,
failure: { kind: "invalid-payload", protocol: protocol },
};
} catch (error) {
return {
ok: false,
failure: {
kind: resolveFetchFailureKind(error),
protocol: protocol,
message: String((error && error.message) || ""),
},
};
}
}
function classifyConnectionIssue() {
const helper = window.MeshchatLoadingStatusNotice;
if (helper && typeof helper.classifyConnectionIssue === "function") {
return helper.classifyConnectionIssue(recentFailures, cachedRuntimeState, {
attemptCount: attemptCount,
networkWarnAfterAttempts: NETWORK_WARNING_AFTER_ATTEMPTS,
});
}
return {
reason: "starting",
headline: "Waiting for backend startup.",
detail: "MeshChatX is still initializing services.",
};
}
async function updateStartupNotice() {
if (attemptCount < NOTICE_AFTER_ATTEMPTS) {
connectionNotice.textContent = "";
return;
}
await refreshBackendRuntimeStateIfNeeded();
const issue = classifyConnectionIssue();
statusLine.textContent = issue.headline;
connectionNotice.textContent = issue.detail;
}
async function check() {
attemptCount += 1;
if (attemptCount === 1) {
attemptHint.textContent = "";
connectionNotice.textContent = "";
} else {
attemptHint.textContent = "Still starting…";
}
// Prefer HTTPS unless the backend was started with --no-https (then HTTP first).
for (const protocol of protocolOrder) {
const result = await tryOnce(protocol);
if (result.ok) {
detectedProtocol = result.protocol;
statusLine.textContent = "Opening the app…";
attemptHint.textContent = "";
connectionNotice.textContent = "";
syncThemeFromConfig();
setTimeout(onReady, 200);
return;
}
if (result.failure) {
rememberFailure(result.failure);
}
}
await updateStartupNotice();
setTimeout(check, 350);
}
function onReady() {
const timestamp = new Date().getTime();
window.location.href = `${detectedProtocol}://${API_HOST}:${API_PORT}/?nocache=${timestamp}`;
}
async function syncThemeFromConfig() {
try {
const response = await fetch(`${detectedProtocol}://${API_HOST}:${API_PORT}/api/v1/config`, {
cache: "no-store",
});
if (!response.ok) {
return;
}
const body = await response.json();
const cfg = body && body.config ? body.config : body;
const theme = cfg && cfg.theme;
if (theme === "dark" || theme === "light") {
applyTheme(theme);
try {
localStorage.setItem("meshchat.theme", theme);
} catch (e) {}
}
} catch (e) {}
}
</script>
</body>
</html>