mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-25 08:52:15 +00:00
feat(electron): enhance backend connection handling and UI updates
This commit is contained in:
@@ -11,32 +11,23 @@
|
||||
<script src="./assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js"></script>
|
||||
</head>
|
||||
<body
|
||||
class="min-h-screen bg-slate-100 text-gray-900 antialiased dark:bg-zinc-950 dark:text-zinc-50 transition-colors"
|
||||
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 transition-colors"
|
||||
>
|
||||
<div class="absolute inset-0 -z-10 overflow-hidden">
|
||||
<div
|
||||
class="absolute -left-32 -top-40 h-80 w-80 rounded-full bg-gradient-to-br from-red-500/30 via-orange-500/20 to-rose-500/30 blur-3xl dark:from-red-600/25 dark:via-orange-600/25 dark:to-rose-600/25"
|
||||
></div>
|
||||
<div
|
||||
class="absolute -right-24 top-20 h-64 w-64 rounded-full bg-gradient-to-br from-orange-400/30 via-red-500/20 to-rose-500/30 blur-3xl dark:from-orange-500/25 dark:via-red-500/25 dark:to-rose-500/25"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<main class="relative flex min-h-screen items-center justify-center px-4 py-6 sm:px-6">
|
||||
<div class="w-full max-w-5xl">
|
||||
<main class="flex min-h-screen items-center justify-center px-4 py-8 sm:px-6">
|
||||
<div class="w-full max-w-4xl">
|
||||
<div
|
||||
class="rounded-2xl border border-slate-200/80 bg-white/80 shadow-2xl backdrop-blur-xl ring-1 ring-white/60 dark:border-zinc-800/70 dark:bg-zinc-900/70 dark:ring-zinc-800/70 transition-colors overflow-hidden"
|
||||
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="p-4 sm:p-6 space-y-4">
|
||||
<div class="p-5 sm:p-6 space-y-5">
|
||||
<div
|
||||
class="flex flex-col sm:flex-row items-center sm:items-start gap-3 text-center sm:text-left"
|
||||
>
|
||||
<div
|
||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-red-500 via-orange-500 to-rose-500 shadow-lg ring-4 ring-white/60 dark:ring-zinc-800/70"
|
||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-red-100 ring-1 ring-red-200 dark:bg-red-950/40 dark:ring-red-900/60"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-white"
|
||||
class="h-7 w-7 text-red-600 dark:text-red-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -50,18 +41,18 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
<div class="text-xl font-semibold tracking-tight text-gray-900 dark:text-white">
|
||||
<div class="text-xl font-semibold tracking-tight text-slate-900 dark:text-white">
|
||||
MeshChatX Crashed
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300">
|
||||
Critical error detected in backend service.
|
||||
<div class="text-xs text-slate-600 dark:text-zinc-400">
|
||||
The backend process exited unexpectedly.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||
<div
|
||||
class="rounded-xl border border-red-200/90 bg-red-50/70 p-3 dark:border-red-900/40 dark:bg-red-900/20 transition-colors"
|
||||
class="rounded-xl border border-red-200/80 bg-red-50/70 p-3 dark:border-red-900/50 dark:bg-red-950/30 transition-colors"
|
||||
>
|
||||
<div
|
||||
class="text-[10px] uppercase tracking-wide text-red-600 dark:text-red-400 font-semibold"
|
||||
@@ -76,50 +67,52 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-slate-200/90 bg-white/70 p-3 text-center sm:text-right dark:border-zinc-800/80 dark:bg-zinc-900/70 transition-colors"
|
||||
class="rounded-xl border border-slate-200/80 bg-slate-50/70 p-3 text-center sm:text-right dark:border-zinc-700/70 dark:bg-zinc-950/40 transition-colors"
|
||||
>
|
||||
<div class="text-[10px] uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<div class="text-[10px] uppercase tracking-wide text-slate-500 dark:text-zinc-400">
|
||||
Status
|
||||
</div>
|
||||
<div class="mt-0.5 text-base font-semibold text-red-600 dark:text-red-400">Offline</div>
|
||||
<div class="mt-0.5 text-base font-semibold text-red-600 dark:text-red-400">
|
||||
Backend unavailable
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 px-1">
|
||||
<h3
|
||||
class="text-[10px] font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
||||
class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 dark:text-zinc-400"
|
||||
>
|
||||
Diagnostic Logs
|
||||
</h3>
|
||||
<button
|
||||
onclick="copyLogs()"
|
||||
class="w-full sm:w-auto text-[10px] font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 bg-blue-50 dark:bg-blue-900/30 px-3 py-1 rounded-lg transition-colors"
|
||||
onclick="copyLogs(event)"
|
||||
class="w-full sm:w-auto text-[10px] font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 bg-blue-50 dark:bg-blue-950/40 px-3 py-1 rounded-lg transition-colors"
|
||||
>
|
||||
Copy all logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="text-[10px] font-medium text-gray-500 dark:text-gray-400 px-1">
|
||||
<div class="text-[10px] font-medium text-slate-500 dark:text-zinc-400 px-1">
|
||||
Standard Output (stdout)
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<pre
|
||||
id="stdout"
|
||||
class="h-52 overflow-auto rounded-xl border border-slate-200 bg-slate-50 p-3 font-mono text-[10px] text-slate-700 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 select-text scrollbar-thin scrollbar-thumb-slate-300 dark:scrollbar-thumb-zinc-800"
|
||||
class="h-52 overflow-auto rounded-xl border border-slate-200 bg-slate-50 p-3 font-mono text-[10px] text-slate-700 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-300 select-text"
|
||||
></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="text-[10px] font-medium text-gray-500 dark:text-gray-400 px-1">
|
||||
<div class="text-[10px] font-medium text-slate-500 dark:text-zinc-400 px-1">
|
||||
Standard Error (stderr)
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<pre
|
||||
id="stderr"
|
||||
class="h-64 overflow-auto rounded-xl border border-red-100 bg-red-50/50 p-3 font-mono text-[10px] text-red-700 dark:border-red-900/20 dark:bg-zinc-950 dark:text-red-400 select-text scrollbar-thin scrollbar-thumb-red-200 dark:scrollbar-thumb-zinc-800"
|
||||
class="h-64 overflow-auto rounded-xl border border-red-200/70 bg-red-50/60 p-3 font-mono text-[10px] text-red-700 dark:border-red-900/40 dark:bg-zinc-950 dark:text-red-400 select-text"
|
||||
></pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,19 +121,13 @@
|
||||
<div class="flex flex-wrap items-center justify-center sm:justify-start gap-2 pt-2">
|
||||
<button
|
||||
onclick="window.electron.relaunch()"
|
||||
class="w-full sm:w-40 rounded-xl bg-blue-600 px-4 py-2.5 text-xs font-semibold text-white shadow-lg shadow-blue-500/25 hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-all active:scale-[0.98]"
|
||||
class="w-full sm:w-40 rounded-xl bg-blue-600 px-4 py-2.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-colors"
|
||||
>
|
||||
Relaunch
|
||||
</button>
|
||||
<button
|
||||
onclick="window.electron.relaunchEmergency()"
|
||||
class="w-full sm:w-48 rounded-xl bg-orange-600 px-4 py-2.5 text-xs font-semibold text-white shadow-lg shadow-orange-500/25 hover:bg-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-all active:scale-[0.98]"
|
||||
>
|
||||
Engage Emergency Mode
|
||||
</button>
|
||||
<button
|
||||
onclick="window.electron.shutdown()"
|
||||
class="w-full sm:w-24 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-xs font-semibold text-gray-700 shadow-sm hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 transition-all active:scale-[0.98]"
|
||||
class="w-full sm:w-24 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-xs font-semibold text-slate-700 shadow-sm hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
Exit
|
||||
</button>
|
||||
@@ -169,7 +156,7 @@
|
||||
document.getElementById("stderr").innerText = "Error decoding logs.";
|
||||
}
|
||||
|
||||
function copyLogs() {
|
||||
function copyLogs(event) {
|
||||
const stdout = document.getElementById("stdout").innerText;
|
||||
const stderr = document.getElementById("stderr").innerText;
|
||||
const exitCode = document.getElementById("exit-code").innerText;
|
||||
@@ -177,7 +164,10 @@
|
||||
const fullReport = `MeshChatX Crash Report\nExit Code: ${exitCode}\n\n--- STDOUT ---\n${stdout}\n\n--- STDERR ---\n${stderr}`;
|
||||
|
||||
navigator.clipboard.writeText(fullReport).then(() => {
|
||||
const btn = event.target;
|
||||
const btn = event && event.currentTarget ? event.currentTarget : null;
|
||||
if (!btn) {
|
||||
return;
|
||||
}
|
||||
const originalText = btn.innerText;
|
||||
btn.innerText = "Copied!";
|
||||
btn.classList.replace("text-blue-600", "text-emerald-600");
|
||||
|
||||
@@ -55,6 +55,10 @@
|
||||
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>
|
||||
@@ -63,13 +67,20 @@
|
||||
</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"];
|
||||
|
||||
@@ -78,6 +89,7 @@
|
||||
listenForSystemThemeChanges();
|
||||
|
||||
(async function bootstrap() {
|
||||
applyStartupErrorHint();
|
||||
try {
|
||||
if (window.electron && typeof window.electron.backendHttpOnly === "function") {
|
||||
const httpOnly = await window.electron.backendHttpOnly();
|
||||
@@ -89,6 +101,16 @@
|
||||
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();
|
||||
@@ -127,6 +149,9 @@
|
||||
|
||||
let detectedProtocol = "http";
|
||||
let attemptCount = 0;
|
||||
let runtimeProbeAttempt = 0;
|
||||
let cachedRuntimeState = null;
|
||||
const recentFailures = [];
|
||||
|
||||
function parseStatusJson(text) {
|
||||
try {
|
||||
@@ -136,44 +161,120 @@
|
||||
}
|
||||
}
|
||||
|
||||
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`;
|
||||
const result = await fetch(url, { cache: "no-store" });
|
||||
const text = await result.text();
|
||||
if (result.status !== 200) {
|
||||
return null;
|
||||
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) || ""),
|
||||
},
|
||||
};
|
||||
}
|
||||
const data = parseStatusJson(text);
|
||||
if (data && data.status === "ok") {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
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 null;
|
||||
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) {
|
||||
try {
|
||||
const ok = await tryOnce(protocol);
|
||||
if (ok) {
|
||||
detectedProtocol = ok;
|
||||
statusLine.textContent = "Opening the app…";
|
||||
attemptHint.textContent = "";
|
||||
syncThemeFromConfig();
|
||||
setTimeout(onReady, 200);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
98
electron/loadingStatusNotice.js
Normal file
98
electron/loadingStatusNotice.js
Normal file
@@ -0,0 +1,98 @@
|
||||
(function (root, factory) {
|
||||
const exported = factory();
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = exported;
|
||||
}
|
||||
root.MeshchatLoadingStatusNotice = exported;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : window, function () {
|
||||
function toLowerText(value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return String(value).toLowerCase();
|
||||
}
|
||||
|
||||
function classifyConnectionIssue(failures, runtimeState, options) {
|
||||
const entries = Array.isArray(failures) ? failures : [];
|
||||
const state = runtimeState && typeof runtimeState === "object" ? runtimeState : null;
|
||||
const opts = options && typeof options === "object" ? options : {};
|
||||
const attemptCount = Number(opts.attemptCount) || 0;
|
||||
const networkWarnAfterAttempts = Number(opts.networkWarnAfterAttempts) || 24;
|
||||
|
||||
if (state && state.running === false && state.lastExitCode != null) {
|
||||
return {
|
||||
reason: "backend-exited",
|
||||
headline: "The backend process stopped unexpectedly.",
|
||||
detail: "Please restart MeshChatX. If this keeps happening, review crash logs.",
|
||||
};
|
||||
}
|
||||
|
||||
const hasAddressUnreachable = entries.some((entry) => entry && entry.kind === "address-unreachable");
|
||||
if (hasAddressUnreachable) {
|
||||
return {
|
||||
reason: "loopback-blocked",
|
||||
headline: "Cannot reach local backend on 127.0.0.1:9337.",
|
||||
detail: "A firewall, VPN, sandbox, or loopback policy may be blocking local connections.",
|
||||
};
|
||||
}
|
||||
|
||||
const hasNetworkError = entries.some((entry) => entry && entry.kind === "network-error");
|
||||
if (hasNetworkError) {
|
||||
if (attemptCount < networkWarnAfterAttempts) {
|
||||
return {
|
||||
reason: "starting",
|
||||
headline: "Waiting for backend startup.",
|
||||
detail: "MeshChatX is still initializing services.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
reason: "network-blocked",
|
||||
headline: "Still waiting for local backend connection.",
|
||||
detail: "If startup stays stuck, firewall or network filtering software may be blocking localhost traffic.",
|
||||
};
|
||||
}
|
||||
|
||||
const hasServerError = entries.some(
|
||||
(entry) => entry && entry.kind === "http-error" && Number(entry.status) >= 500
|
||||
);
|
||||
if (hasServerError) {
|
||||
return {
|
||||
reason: "backend-http-error",
|
||||
headline: "Backend is running but reported an internal error.",
|
||||
detail: "MeshChatX will keep retrying while the backend finishes startup.",
|
||||
};
|
||||
}
|
||||
|
||||
const hasInvalidPayload = entries.some((entry) => entry && entry.kind === "invalid-payload");
|
||||
if (hasInvalidPayload) {
|
||||
return {
|
||||
reason: "backend-invalid-response",
|
||||
headline: "Backend responded with invalid startup data.",
|
||||
detail: "MeshChatX will continue retrying while the backend stabilizes.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
reason: "starting",
|
||||
headline: "Waiting for backend startup.",
|
||||
detail: "MeshChatX is still initializing services.",
|
||||
};
|
||||
}
|
||||
|
||||
function classifyFetchError(error) {
|
||||
const message = `${toLowerText(error && error.name)} ${toLowerText(error && error.message)}`.trim();
|
||||
if (
|
||||
message.includes("err_address_unreachable") ||
|
||||
message.includes("address_unreachable") ||
|
||||
message.includes("ehostunreach")
|
||||
) {
|
||||
return "address-unreachable";
|
||||
}
|
||||
return "network-error";
|
||||
}
|
||||
|
||||
return {
|
||||
classifyConnectionIssue: classifyConnectionIssue,
|
||||
classifyFetchError: classifyFetchError,
|
||||
};
|
||||
});
|
||||
@@ -44,6 +44,14 @@ var isQuiting = false;
|
||||
|
||||
// remember child process for exe so we can kill it when app exits
|
||||
var exeChildProcess = null;
|
||||
var backendRuntimeState = {
|
||||
started: false,
|
||||
running: false,
|
||||
pid: null,
|
||||
lastExitCode: null,
|
||||
lastError: "",
|
||||
lastEventAt: null,
|
||||
};
|
||||
|
||||
// store integrity status
|
||||
var integrityStatus = {
|
||||
@@ -218,6 +226,18 @@ ipcMain.handle("backend-http-only", () => {
|
||||
return getUserProvidedArguments().includes("--no-https");
|
||||
});
|
||||
|
||||
ipcMain.handle("backend-runtime-state", () => {
|
||||
const isRunning =
|
||||
!!exeChildProcess &&
|
||||
exeChildProcess.exitCode === null &&
|
||||
exeChildProcess.signalCode === null &&
|
||||
backendRuntimeState.started;
|
||||
return {
|
||||
...backendRuntimeState,
|
||||
running: isRunning,
|
||||
};
|
||||
});
|
||||
|
||||
// add support for showing an alert window via ipc
|
||||
ipcMain.handle("alert", async (event, message) => {
|
||||
return await dialog.showMessageBox(mainWindow, {
|
||||
@@ -259,13 +279,25 @@ ipcMain.handle("prompt", async (event, message) => {
|
||||
|
||||
// allow relaunching app via ipc
|
||||
ipcMain.handle("relaunch", () => {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
const relaunchOptions = {};
|
||||
if (!process.defaultApp && process.platform === "linux" && process.env.APPIMAGE) {
|
||||
relaunchOptions.execPath = process.env.APPIMAGE;
|
||||
}
|
||||
app.relaunch(relaunchOptions);
|
||||
isQuiting = true;
|
||||
quit();
|
||||
});
|
||||
|
||||
ipcMain.handle("relaunch-emergency", () => {
|
||||
app.relaunch({ args: process.argv.slice(1).concat(["--emergency"]) });
|
||||
app.exit();
|
||||
const relaunchOptions = {
|
||||
args: process.argv.slice(1).concat(["--emergency"]),
|
||||
};
|
||||
if (!process.defaultApp && process.platform === "linux" && process.env.APPIMAGE) {
|
||||
relaunchOptions.execPath = process.env.APPIMAGE;
|
||||
}
|
||||
app.relaunch(relaunchOptions);
|
||||
isQuiting = true;
|
||||
quit();
|
||||
});
|
||||
|
||||
ipcMain.handle("shutdown", () => {
|
||||
@@ -451,6 +483,18 @@ function getAppIconPath() {
|
||||
return fs.existsSync(iconPath) ? iconPath : fallbackIconPath;
|
||||
}
|
||||
|
||||
function isLocalBackendUrl(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
url.startsWith("http://127.0.0.1:9337") ||
|
||||
url.startsWith("https://127.0.0.1:9337") ||
|
||||
url.startsWith("http://localhost:9337") ||
|
||||
url.startsWith("https://localhost:9337")
|
||||
);
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
tray = new Tray(getAppIconPath());
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
@@ -554,6 +598,29 @@ app.whenReady().then(async () => {
|
||||
mainWindow.webContents.on("unresponsive", () => {
|
||||
log("Renderer process became unresponsive.");
|
||||
});
|
||||
mainWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
async (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||
if (!isMainFrame || !isLocalBackendUrl(validatedURL)) {
|
||||
return;
|
||||
}
|
||||
log(`Failed to load backend URL (${errorCode}): ${errorDescription} - ${validatedURL}`);
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
const currentUrl = mainWindow.webContents.getURL();
|
||||
if (currentUrl.includes("loading.html")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await mainWindow.loadFile(path.join(__dirname, "loading.html"), {
|
||||
query: { startup_error: "backend_unreachable" },
|
||||
});
|
||||
} catch (error) {
|
||||
log(`Failed to restore loading screen after backend load failure: ${error.message}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// minimize to tray behavior
|
||||
mainWindow.on("close", (event) => {
|
||||
@@ -704,6 +771,14 @@ app.whenReady().then(async () => {
|
||||
if (!exeChildProcess || !exeChildProcess.pid) {
|
||||
throw new Error("Failed to start backend process (no PID).");
|
||||
}
|
||||
backendRuntimeState = {
|
||||
started: true,
|
||||
running: true,
|
||||
pid: exeChildProcess.pid,
|
||||
lastExitCode: null,
|
||||
lastError: "",
|
||||
lastEventAt: Date.now(),
|
||||
};
|
||||
|
||||
// log stdout
|
||||
var stdoutLines = [];
|
||||
@@ -736,10 +811,15 @@ app.whenReady().then(async () => {
|
||||
// log errors
|
||||
exeChildProcess.on("error", function (error) {
|
||||
log(error);
|
||||
backendRuntimeState.lastError = error && error.message ? error.message : String(error);
|
||||
backendRuntimeState.lastEventAt = Date.now();
|
||||
});
|
||||
|
||||
// quit electron app if exe dies
|
||||
exeChildProcess.on("exit", async function (code) {
|
||||
backendRuntimeState.running = false;
|
||||
backendRuntimeState.lastExitCode = code;
|
||||
backendRuntimeState.lastEventAt = Date.now();
|
||||
// if no exit code provided, we wanted exit to happen, so do nothing
|
||||
if (code == null) {
|
||||
return;
|
||||
@@ -790,6 +870,7 @@ function quit() {
|
||||
return;
|
||||
}
|
||||
if (exeChildProcess.exitCode !== null || exeChildProcess.signalCode !== null) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -96,4 +96,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
backendHttpOnly: async function () {
|
||||
return await ipcRenderer.invoke("backend-http-only");
|
||||
},
|
||||
backendRuntimeState: async function () {
|
||||
return await ipcRenderer.invoke("backend-runtime-state");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,27 +1,90 @@
|
||||
Reticulum MeshChatX - Third-party notices
|
||||
Generated at: 2026-04-16T23:23:28.244334Z
|
||||
Generated at: 2026-04-17T03:19:30.687503Z
|
||||
Frontend source: node_modules
|
||||
|
||||
Python dependencies
|
||||
-------------------
|
||||
aiohappyeyeballs 2.6.1
|
||||
License: PSF-2.0
|
||||
Author: J. Nick Koston
|
||||
aiohttp 3.13.5
|
||||
License: Apache-2.0 AND MIT
|
||||
Author: —
|
||||
aiohttp-session 2.12.1
|
||||
License: Apache 2
|
||||
Author: Andrew Svetlov
|
||||
aiosignal 1.4.0
|
||||
License: Apache 2.0
|
||||
Author: aiohttp team <team@aiohttp.org>
|
||||
attrs 26.1.0
|
||||
License: MIT
|
||||
Author: Hynek Schlawack <hs@ox.cx>
|
||||
audioop-lts 0.2.2
|
||||
License: PSF-2.0
|
||||
Author: —
|
||||
bcrypt 5.0.0
|
||||
License: Apache-2.0
|
||||
Author: The Python Cryptographic Authority developers <cryptography-dev@python.org>
|
||||
cffi 2.0.0
|
||||
License: MIT
|
||||
Author: Armin Rigo, Maciej Fijalkowski
|
||||
cryptography 46.0.7
|
||||
License: Apache-2.0 OR BSD-3-Clause
|
||||
Author: The Python Cryptographic Authority and individual contributors <cryptography-dev@python.org>
|
||||
jaraco.context 6.1.3.dev0+g098f39c91.d20260403
|
||||
frozenlist 1.8.0
|
||||
License: Apache-2.0
|
||||
Author: aiohttp team <team@aiohttp.org>
|
||||
idna 3.11
|
||||
License: BSD-3-Clause
|
||||
Author: Kim Davies <kim+pypi@gumleaf.org>
|
||||
jaraco.context 6.1.2
|
||||
License: MIT
|
||||
Author: "Jason R. Coombs" <jaraco@jaraco.com>
|
||||
lxmf 0.9.4
|
||||
License: Reticulum License
|
||||
Author: Mark Qvist
|
||||
lxmfy 1.6.2
|
||||
License: BSD-0-Clause
|
||||
Author: Quad4
|
||||
lxst 0.4.6
|
||||
License: Other/Proprietary License
|
||||
Author: Mark Qvist
|
||||
multidict 6.7.1
|
||||
License: Apache License 2.0
|
||||
Author: Andrew Svetlov
|
||||
numpy 2.4.4
|
||||
License: BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0
|
||||
Author: Travis E. Oliphant et al.
|
||||
ply 3.11
|
||||
License: BSD
|
||||
Author: David Beazley
|
||||
propcache 0.4.1
|
||||
License: Apache-2.0
|
||||
Author: Andrew Svetlov
|
||||
psutil 7.2.2
|
||||
License: BSD-3-Clause
|
||||
Author: Giampaolo Rodola
|
||||
pycodec2 4.1.1
|
||||
License: OSI Approved :: BSD License
|
||||
Author: Grzegorz Milka
|
||||
pycparser 3.0
|
||||
License: BSD-3-Clause
|
||||
Author: Eli Bendersky <eliben@gmail.com>
|
||||
pyserial 3.5
|
||||
License: BSD
|
||||
Author: Chris Liechti
|
||||
reticulum-meshchatx 4.5.0
|
||||
License: MIT
|
||||
Author: Sudo-Ivan
|
||||
rns 1.1.5
|
||||
License: Reticulum License
|
||||
Author: Mark Qvist
|
||||
websockets 16.0
|
||||
License: BSD-3-Clause
|
||||
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
|
||||
yarl 1.23.0
|
||||
License: Apache-2.0
|
||||
Author: Andrew Svetlov
|
||||
|
||||
Node dependencies
|
||||
-----------------
|
||||
@@ -868,10 +931,10 @@ ci-info 4.4.0
|
||||
clean-stack 2.2.0
|
||||
License: MIT
|
||||
Author: Sindre Sorhus <sindresorhus@gmail.com>
|
||||
cli-cursor 4.0.0
|
||||
cli-cursor 3.1.0
|
||||
License: MIT
|
||||
Author: Sindre Sorhus <sindresorhus@gmail.com>
|
||||
cli-cursor 3.1.0
|
||||
cli-cursor 4.0.0
|
||||
License: MIT
|
||||
Author: Sindre Sorhus <sindresorhus@gmail.com>
|
||||
cli-spinners 2.9.2
|
||||
@@ -2200,10 +2263,10 @@ resolve-protobuf-schema 2.1.0
|
||||
responselike 2.0.1
|
||||
License: MIT
|
||||
Author: lukechilds
|
||||
restore-cursor 3.1.0
|
||||
restore-cursor 4.0.0
|
||||
License: MIT
|
||||
Author: Sindre Sorhus <sindresorhus@gmail.com>
|
||||
restore-cursor 4.0.0
|
||||
restore-cursor 3.1.0
|
||||
License: MIT
|
||||
Author: Sindre Sorhus <sindresorhus@gmail.com>
|
||||
retry 0.12.0
|
||||
@@ -2485,10 +2548,10 @@ type-check 0.4.0
|
||||
type-fest 0.21.3
|
||||
License: (MIT OR CC0-1.0)
|
||||
Author: Sindre Sorhus <sindresorhus@gmail.com>
|
||||
type-fest 0.13.1
|
||||
type-fest 1.4.0
|
||||
License: (MIT OR CC0-1.0)
|
||||
Author: Sindre Sorhus <sindresorhus@gmail.com>
|
||||
type-fest 1.4.0
|
||||
type-fest 0.13.1
|
||||
License: (MIT OR CC0-1.0)
|
||||
Author: Sindre Sorhus <sindresorhus@gmail.com>
|
||||
typescript 5.4.5
|
||||
|
||||
@@ -1687,13 +1687,13 @@
|
||||
},
|
||||
{
|
||||
"name": "cli-cursor",
|
||||
"version": "4.0.0",
|
||||
"version": "3.1.0",
|
||||
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
|
||||
"license": "MIT"
|
||||
},
|
||||
{
|
||||
"name": "cli-cursor",
|
||||
"version": "3.1.0",
|
||||
"version": "4.0.0",
|
||||
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4351,13 +4351,13 @@
|
||||
},
|
||||
{
|
||||
"name": "restore-cursor",
|
||||
"version": "3.1.0",
|
||||
"version": "4.0.0",
|
||||
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
|
||||
"license": "MIT"
|
||||
},
|
||||
{
|
||||
"name": "restore-cursor",
|
||||
"version": "4.0.0",
|
||||
"version": "3.1.0",
|
||||
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4921,13 +4921,13 @@
|
||||
},
|
||||
{
|
||||
"name": "type-fest",
|
||||
"version": "0.13.1",
|
||||
"version": "1.4.0",
|
||||
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
|
||||
"license": "(MIT OR CC0-1.0)"
|
||||
},
|
||||
{
|
||||
"name": "type-fest",
|
||||
"version": "1.4.0",
|
||||
"version": "0.13.1",
|
||||
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
|
||||
"license": "(MIT OR CC0-1.0)"
|
||||
},
|
||||
|
||||
@@ -27,6 +27,8 @@ const globalState = reactive({
|
||||
ui_transparency: 0,
|
||||
ui_glass_enabled: true,
|
||||
message_list_virtualization: true,
|
||||
warn_on_stranger_links: true,
|
||||
messages_sidebar_position: "left",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:install": "playwright install chromium",
|
||||
"electron-postinstall": "electron-builder install-app-deps",
|
||||
"electron-postinstall": "electron-builder install-app-deps && node scripts/ensure-micron-parser-package.js",
|
||||
"electron": "pnpm run electron-postinstall && pnpm run build && electron .",
|
||||
"dist": "pnpm run electron-postinstall && pnpm run build && electron-builder --publish=never",
|
||||
"dist:linux": "pnpm run electron-postinstall && cross-env PLATFORM=linux pnpm run build && electron-builder --linux AppImage deb --publish=never",
|
||||
@@ -85,6 +85,7 @@
|
||||
"jsdom": "^29.0.2",
|
||||
"postcss": "^8.5.10",
|
||||
"prettier": "^3.8.3",
|
||||
"micron-parser": "github:RFnexus/micron-parser-js",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"terser": "^5.46.1",
|
||||
"vitest": "^4.1.4"
|
||||
@@ -278,7 +279,6 @@
|
||||
"emoji-picker-element": "^1.29.1",
|
||||
"emoji-picker-element-data": "^1.8.0",
|
||||
"marked": "^18.0.0",
|
||||
"micron-parser": "github:RFnexus/micron-parser-js",
|
||||
"mitt": "^3.0.1",
|
||||
"ol": "^10.9.0",
|
||||
"protobufjs": "^7.5.5",
|
||||
|
||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -61,9 +61,6 @@ importers:
|
||||
marked:
|
||||
specifier: ^18.0.0
|
||||
version: 18.0.0
|
||||
micron-parser:
|
||||
specifier: github:RFnexus/micron-parser-js
|
||||
version: git+https://github.com/RFnexus/micron-parser-js.git#aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9
|
||||
mitt:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
@@ -194,6 +191,9 @@ importers:
|
||||
jsdom:
|
||||
specifier: ^29.0.2
|
||||
version: 29.0.2
|
||||
micron-parser:
|
||||
specifier: github:RFnexus/micron-parser-js
|
||||
version: https://codeload.github.com/RFnexus/micron-parser-js/tar.gz/aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9
|
||||
postcss:
|
||||
specifier: ^8.5.10
|
||||
version: 8.5.10
|
||||
@@ -407,8 +407,8 @@ packages:
|
||||
resolution: {integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
resolution: {commit: 06b29aafb7708acef8b3669835c8a7857ebc92d2, repo: https://github.com/electron/node-gyp.git, type: git}
|
||||
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2}
|
||||
version: 10.2.0-electron.1
|
||||
engines: {node: '>=12.13.0'}
|
||||
hasBin: true
|
||||
@@ -2622,8 +2622,8 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
micron-parser@git+https://github.com/RFnexus/micron-parser-js.git#aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9:
|
||||
resolution: {commit: aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9, repo: https://github.com/RFnexus/micron-parser-js.git, type: git}
|
||||
micron-parser@https://codeload.github.com/RFnexus/micron-parser-js/tar.gz/aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9:
|
||||
resolution: {tarball: https://codeload.github.com/RFnexus/micron-parser-js/tar.gz/aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9}
|
||||
version: 0.0.0
|
||||
|
||||
mime-db@1.52.0:
|
||||
@@ -4403,7 +4403,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
exponential-backoff: 3.1.3
|
||||
@@ -4466,7 +4466,7 @@ snapshots:
|
||||
|
||||
'@electron/rebuild@3.7.2':
|
||||
dependencies:
|
||||
'@electron/node-gyp': git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2
|
||||
'@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2
|
||||
'@malept/cross-spawn-promise': 2.0.0
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.3
|
||||
@@ -6959,7 +6959,7 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.2
|
||||
|
||||
micron-parser@git+https://github.com/RFnexus/micron-parser-js.git#aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9: {}
|
||||
micron-parser@https://codeload.github.com/RFnexus/micron-parser-js/tar.gz/aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
|
||||
26
scripts/ensure-micron-parser-package.js
Normal file
26
scripts/ensure-micron-parser-package.js
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const packageDir = path.join(__dirname, "..", "node_modules", "micron-parser");
|
||||
const packageJsonPath = path.join(packageDir, "package.json");
|
||||
|
||||
if (!fs.existsSync(packageDir)) {
|
||||
console.log("micron-parser not installed, skipping package metadata normalization.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const packageJson = {
|
||||
name: "micron-parser",
|
||||
version: "0.0.0",
|
||||
main: "js/micron-parser.js",
|
||||
module: "js/micron-parser.js",
|
||||
};
|
||||
|
||||
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
||||
console.log("Created node_modules/micron-parser/package.json for electron-builder dependency scan.");
|
||||
Reference in New Issue
Block a user