feat(electron): enhance backend connection handling and UI updates

This commit is contained in:
Ivan
2026-04-16 23:32:54 -05:00
parent 0f5034642f
commit ebcd3c6acd
11 changed files with 454 additions and 90 deletions

View File

@@ -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");

View File

@@ -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);
}

View 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,
};
});

View File

@@ -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 {

View File

@@ -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");
},
});

View File

@@ -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

View File

@@ -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)"
},

View File

@@ -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",
},
});

View File

@@ -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
View File

@@ -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: {}

View 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.");