diff --git a/.gitignore b/.gitignore index edd8c086..dd7c67fb 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,4 @@ gitbook/README.md open-sse.old/ .graphifyignore graphify-out/* +.next-analyze/* diff --git a/cli/cli.js b/cli/cli.js index 09057b2e..6a0fc064 100755 --- a/cli/cli.js +++ b/cli/cli.js @@ -4,8 +4,28 @@ const { spawn, exec, execSync } = require("child_process"); const path = require("path"); const fs = require("fs"); const https = require("https"); +const net = require("net"); const os = require("os"); +// Poll until the server accepts TCP connections on port, or timeout — avoids blind fixed waits. +function waitServerReady(port, { timeoutMs = 15000, intervalMs = 150 } = {}) { + const deadline = Date.now() + timeoutMs; + return new Promise((resolve) => { + const tryConnect = () => { + const socket = net.connect({ host: "127.0.0.1", port }, () => { + socket.destroy(); + resolve(true); + }); + socket.on("error", () => { + socket.destroy(); + if (Date.now() >= deadline) return resolve(false); + setTimeout(tryConnect, intervalMs); + }); + }; + tryConnect(); + }); +} + // Native spinner - no external dependency function createSpinner(text) { const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; @@ -212,17 +232,18 @@ function killCloudflaredByAppPort(appPort) { function killAllAppProcesses(appPort) { return new Promise((resolve) => { try { - // Kill MIT first (privileged process, needs special handling) - killProxyByPidFile(); - // Kill cloudflared/tailscale by PID file (precise, only this app's tunnel) - killTunnelByPidFile(); + // Background: MITM + tunnel/cloudflared run on separate ports/processes — + // killing them doesn't free the app port, so don't block the critical path. + // Server-side MITM manager has stale-lock recovery and starts deferred (~3s). + setImmediate(() => { + try { killProxyByPidFile(); } catch {} + try { killTunnelByPidFile(); } catch {} + try { killCloudflaredByAppPort(appPort); } catch {} + }); const platform = process.platform; let pids = []; - // Catch stale PID files: kill cloudflared bound to this app's port - pids.push(...killCloudflaredByAppPort(appPort)); - if (platform === "win32") { // Windows: use WMI to get full CommandLine (tasklist /V doesn't include it) try { @@ -499,14 +520,11 @@ if (!fs.existsSync(serverPath)) { process.exit(1); } -// Check for updates FIRST, then start server -checkForUpdate().then((latestVersion) => { - killAllAppProcesses(port).then(() => { - return killProcessOnPort(port); - }).then(() => { - startServer(latestVersion); - }); -}); +// Start server immediately; run update check in parallel (not on the critical path). +const updatePromise = checkForUpdate(); +killAllAppProcesses(port) + .then(() => killProcessOnPort(port)) + .then(() => startServer(updatePromise)); // Show interface selection menu async function showInterfaceMenu(latestVersion) { @@ -556,7 +574,9 @@ async function showInterfaceMenu(latestVersion) { const MAX_RESTARTS = 2; const RESTART_RESET_MS = 30000; // Reset counter if alive > 30s -function startServer(latestVersion) { +function startServer(updatePromise) { + // Accept either a Promise (parallel update check) or a resolved value. + const latestVersionPromise = Promise.resolve(updatePromise); const displayHost = getDisplayHost(); const url = `http://${displayHost}:${port}/dashboard`; // Surface real network exposure when bound to all interfaces (default 0.0.0.0). @@ -677,17 +697,19 @@ function startServer(latestVersion) { console.log(`\n🚀 ${pkg.name} v${pkg.version}`); console.log(`Server: http://${displayHost}:${port}`); - setTimeout(() => { + waitServerReady(port).then(() => { initTrayIcon(); console.log("\n💡 Router is now running in system tray. Close this terminal if you want."); console.log(" Right-click tray icon to open dashboard or quit.\n"); - }, 2000); + }); return; } // Wait for server to be ready, then show interface menu loop + tray - setTimeout(async () => { + waitServerReady(port).then(async () => { + // Resolve parallel update check (already running); don't block server start on it. + const latestVersion = await latestVersionPromise; // Start tray icon alongside TUI initTrayIcon(); @@ -772,7 +794,7 @@ function startServer(latestVersion) { cleanup(); process.exit(1); } - }, 3000); + }); function attachServerEvents() { server.on("error", (err) => { diff --git a/next.config.mjs b/next.config.mjs index 91f55af9..d017c157 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -30,6 +30,8 @@ const nextConfig = { proxyClientMaxBodySize, // Cache fetch responses across HMR refreshes for faster dev reloads. serverComponentsHmrCache: true, + // Tree-shake heavy barrel imports to cut compile + bundle size + optimizePackageImports: ["@xyflow/react", "@dnd-kit/core", "@dnd-kit/sortable", "material-symbols", "marked"], }, webpack: (config, { isServer }) => { // Ignore fs/path modules in browser bundle diff --git a/package.json b/package.json index b19693b8..d90a14dd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "9Router web dashboard", "private": true, "scripts": { - "dev": "next dev --webpack --port 20127", + "dev": "next dev --port 20127", + "dev:webpack": "next dev --webpack --port 20127", "build": "next build --webpack", "start": "next start --port 20127", "dev:bun": "bun --bun next dev --webpack --port 20127", diff --git a/src/app/globals.css b/src/app/globals.css index d96cac7e..fdc219ba 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,5 @@ -@import "tailwindcss"; +/* source() sets scan base to src/ for both webpack + Turbopack; auto-detection still skips binaries + gitignore */ +@import "tailwindcss" source("../../"); @custom-variant dark (&:where(.dark, .dark *)); diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index 607cbbc0..ecab40b1 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -14,7 +14,9 @@ import Badge from "./Badge"; import Card from "./Card"; import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards"; import UsageTable, { fmt, fmtTime } from "@/app/(dashboard)/dashboard/usage/components/UsageTable"; -import ProviderTopology from "@/app/(dashboard)/dashboard/usage/components/ProviderTopology"; +import dynamic from "next/dynamic"; +// Lazy-load: keeps @xyflow/react out of the shared bundle until topology renders +const ProviderTopology = dynamic(() => import("@/app/(dashboard)/dashboard/usage/components/ProviderTopology"), { ssr: false }); import UsageChart from "@/app/(dashboard)/dashboard/usage/components/UsageChart"; function timeAgo(timestamp) {