diff --git a/electron/main.js b/electron/main.js index 24cf54b2d7..ec744af8e5 100644 --- a/electron/main.js +++ b/electron/main.js @@ -68,6 +68,32 @@ const getServerUrl = () => `http://localhost:${serverPort}`; function resolveNodeExecutable(env = process.env) { // #1081: Ensure Next.js standalone runs using Electron's Node runtime // instead of a randomly found system Node to prevent ABI architecture mismatches. + // + // On macOS packaged builds, process.execPath is the main Electron binary + // (e.g. OmniRoute.app/Contents/MacOS/OmniRoute). Spawning it with + // ELECTRON_RUN_AS_NODE causes macOS to show a second dock icon and/or + // flash a shell window. Use the Helper binary instead — macOS treats + // Helper processes as background tasks with no visible UI artifacts. + if (process.platform === "darwin" && !isDev) { + const helperPath = path.join(path.dirname(process.execPath), `${app.getName()} Helper`); + if (fs.existsSync(helperPath)) { + return helperPath; + } + // Electron \u003e= 20 may use "(Renderer)" / "(GPU)" / "(Plugin)" suffixed helpers. + // The unsuffixed Helper is the one suitable for ELECTRON_RUN_AS_NODE. + const frameworkHelper = path.join( + path.dirname(process.execPath), + "..", + "Frameworks", + `${app.getName()} Helper.app`, + "Contents", + "MacOS", + `${app.getName()} Helper` + ); + if (fs.existsSync(frameworkHelper)) { + return frameworkHelper; + } + } return process.execPath; } @@ -594,6 +620,8 @@ function startNextServer() { sendToRenderer("server-status", { status: "starting", port: serverPort }); // Fix #10: Use pipe instead of inherit for logging & readiness detection + // windowsHide prevents a visible console window from spawning alongside the GUI app. + // shell: false avoids launching via a shell wrapper which can flash a terminal on macOS. nextServer = spawn(nodeExecutable, [serverScript], { cwd: NEXT_SERVER_PATH, env: { @@ -605,6 +633,8 @@ function startNextServer() { NODE_PATH: resolveServerNodePath(serverEnv), }, stdio: "pipe", + windowsHide: true, + shell: false, }); // Capture server output for logging diff --git a/src/app/(dashboard)/dashboard/onboarding/page.tsx b/src/app/(dashboard)/dashboard/onboarding/page.tsx index fc99f0a89a..36ace2b1c1 100644 --- a/src/app/(dashboard)/dashboard/onboarding/page.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/page.tsx @@ -31,6 +31,7 @@ export default function OnboardingWizard() { const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [skipSecurity, setSkipSecurity] = useState(false); + const [capsLockOn, setCapsLockOn] = useState(false); // Provider step state const [selectedProvider, setSelectedProvider] = useState(null); @@ -325,6 +326,8 @@ export default function OnboardingWizard() { placeholder={t("enterPassword")} value={password} onChange={(e) => setPassword(e.target.value)} + onKeyDown={(e) => setCapsLockOn(e.getModifierState("CapsLock"))} + onKeyUp={(e) => setCapsLockOn(e.getModifierState("CapsLock"))} className="w-full px-4 py-2.5 bg-white/[0.04] border border-white/10 rounded-lg text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/40" /> setConfirmPassword(e.target.value)} + onKeyDown={(e) => setCapsLockOn(e.getModifierState("CapsLock"))} + onKeyUp={(e) => setCapsLockOn(e.getModifierState("CapsLock"))} className="w-full px-4 py-2.5 bg-white/[0.04] border border-white/10 rounded-lg text-text-main text-sm placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/40" /> + {capsLockOn && ( +
+ + Caps Lock is on +
+ )} {password && confirmPassword && password !== confirmPassword && ({t("passwordsMismatch")}
)} diff --git a/src/app/forgot-password/page.tsx b/src/app/forgot-password/page.tsx index 1d7984d6ef..d821f8fdf3 100644 --- a/src/app/forgot-password/page.tsx +++ b/src/app/forgot-password/page.tsx @@ -1,13 +1,15 @@ "use client"; import { useTranslations } from "next-intl"; +import { useState, useEffect } from "react"; +import { useIsElectron } from "@/shared/hooks/useElectron"; /** * Forgot Password Page — Phase 8.2 * - * Provides two recovery methods: - * 1. CLI reset via omniroute-reset-password command - * 2. Manual database reset instructions + * Provides recovery methods: + * - Web/CLI: CLI reset via omniroute-reset-password command + manual database reset + * - Electron: Data directory reset instructions */ import Link from "next/link"; @@ -15,6 +17,18 @@ import { Card } from "@/shared/components"; export default function ForgotPasswordPage() { const t = useTranslations("auth"); + const isElectron = useIsElectron(); + const [dataDir, setDataDir] = useState{t("resetDescription")}
{t("methodCliDescription")}
-npx omniroute reset-password
+ {isElectron ? (
+ <>
+ {/* Electron: App Reset Method */}
+ + Delete the settings file from the app data directory to reset your password: +
+settings.json{" "}
+ ({t("orRemovePasswordHashField")})
+ {t("methodCliHint")}
-{t("methodManualDescription")}
-.env{" "}
- {t("fileLabelSuffix")}
- + Set a new initial password via the server environment file: +
+server.env in
+ the data directory
+ {dataDir && (
+ settings.json{" "}
+ from the data directory
+ {t("methodCliDescription")}
+npx omniroute reset-password
- data/settings.json
- {" "}
- ({t("orRemovePasswordHashField")})
- {t("methodCliHint")}
+{t("methodManualDescription")}
+.env{" "}
+ {t("fileLabelSuffix")}
+
+ data/settings.json
+ {" "}
+ ({t("orRemovePasswordHashField")})
+ + + Caps Lock is on +
+ )} {error && (