Refactor desktop application and improve Electron integration

Restructure the Electron application by separating concerns into new modules (windows, ipc, sentinel), introduce TypeScript types for IPC, and update build configurations and entry points for desktop applications.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 714c0a0f-ae39-4276-a53a-1f68eb5443fa
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/CdxgfN4
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-05 22:36:13 +00:00
parent 23063b3b7f
commit 49ee808d2b
22 changed files with 642 additions and 295 deletions

View file

@ -60,6 +60,10 @@ externalPort = 3000
localPort = 40437
externalPort = 3001
[[ports]]
localPort = 45997
externalPort = 3002
[deployment]
deploymentTarget = "autoscale"
run = ["node", "dist/server/production.mjs"]

View file

@ -164,7 +164,6 @@ import StaffLearningPortal from "./pages/staff/StaffLearningPortal";
import StaffPerformanceReviews from "./pages/staff/StaffPerformanceReviews";
import StaffProjectTracking from "./pages/staff/StaffProjectTracking";
import StaffTeamHandbook from "./pages/staff/StaffTeamHandbook";
import Overlay from "./pages/Overlay";
const queryClient = new QueryClient();
@ -185,7 +184,6 @@ const App = () => (
<Routes>
{/* Subdomain Passport (aethex.me and aethex.space) handles its own redirect if not a subdomain */}
<Route path="/" element={<SubdomainPassport />} />
<Route path="/overlay" element={<Overlay />} />
<Route path="/onboarding" element={<Onboarding />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route

25
client/desktop-main.html Normal file
View file

@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://*.supabase.co wss://*.supabase.co" />
<title>AeThex</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: #030712;
color: #e5e7eb;
overflow: hidden;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./desktop/desktop-main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com" />
<title>AeThex Overlay</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: transparent;
color: #e5e7eb;
overflow: hidden;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./desktop/desktop-overlay.tsx"></script>
</body>
</html>

View file

@ -1,7 +1,12 @@
import TitleBar from "./TitleBar";
import { ReactNode } from "react";
export default function DesktopShell({ children }: { children: ReactNode }) {
interface DesktopShellProps {
children: ReactNode;
title?: string;
}
export default function DesktopShell({ children, title }: DesktopShellProps) {
return (
<div
style={{
@ -11,11 +16,11 @@ export default function DesktopShell({ children }: { children: ReactNode }) {
color: "#e5e7eb",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
<TitleBar />
<div style={{ flex: 1, overflow: "hidden" }}>{children}</div>
<TitleBar title={title} />
<div style={{ flex: 1, overflow: "auto" }}>{children}</div>
</div>
);
}

View file

@ -0,0 +1,207 @@
import { useState } from "react";
import "../types/preload";
interface OverlayProps {
defaultPath?: string;
}
export default function Overlay({ defaultPath = "" }: OverlayProps) {
const [dir, setDir] = useState(defaultPath);
const [status, setStatus] = useState<"idle" | "watching" | "error">("idle");
const [error, setError] = useState<string | null>(null);
const start = async () => {
try {
setError(null);
await window.aeBridge?.startWatcher(dir);
setStatus("watching");
} catch (e) {
const message = e instanceof Error ? e.message : "Failed to start watcher";
setError(message);
setStatus("error");
}
};
const stop = async () => {
try {
setError(null);
await window.aeBridge?.stopWatcher();
setStatus("idle");
} catch (e) {
const message = e instanceof Error ? e.message : "Failed to stop watcher";
setError(message);
}
};
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "rgba(3, 7, 18, 0.92)",
color: "#e5e7eb",
fontFamily: "Inter, sans-serif",
padding: 16,
backdropFilter: "blur(12px)",
}}
>
<div
style={{
marginBottom: 12,
fontSize: 11,
letterSpacing: "0.12em",
textTransform: "uppercase",
opacity: 0.7,
}}
>
AeThex Overlay
</div>
<div
style={{
border: "1px solid #38bdf8",
borderRadius: 12,
padding: 16,
background: "rgba(14, 165, 233, 0.06)",
}}
>
<div
style={{
fontWeight: 600,
marginBottom: 12,
fontSize: 14,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<span>📂</span> File Watcher
</div>
<label
style={{
display: "block",
fontSize: 11,
marginBottom: 6,
opacity: 0.8,
}}
>
Directory to watch
</label>
<input
value={dir}
onChange={(e) => setDir(e.target.value)}
placeholder="Enter folder path..."
style={{
width: "100%",
padding: 10,
borderRadius: 8,
border: "1px solid #1f2937",
background: "#0f172a",
color: "#e5e7eb",
marginBottom: 12,
fontSize: 13,
}}
/>
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<button
onClick={start}
disabled={status === "watching" || !dir}
style={{
padding: "10px 16px",
borderRadius: 8,
border: "1px solid #22c55e",
background:
status === "watching"
? "rgba(34, 197, 94, 0.3)"
: "rgba(34, 197, 94, 0.1)",
color: "#86efac",
cursor: status === "watching" || !dir ? "not-allowed" : "pointer",
opacity: status === "watching" || !dir ? 0.5 : 1,
fontSize: 13,
fontWeight: 500,
}}
>
Start
</button>
<button
onClick={stop}
disabled={status !== "watching"}
style={{
padding: "10px 16px",
borderRadius: 8,
border: "1px solid #ef4444",
background: "rgba(239, 68, 68, 0.1)",
color: "#fca5a5",
cursor: status !== "watching" ? "not-allowed" : "pointer",
opacity: status !== "watching" ? 0.5 : 1,
fontSize: 13,
fontWeight: 500,
}}
>
Stop
</button>
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
fontSize: 12,
}}
>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background:
status === "watching"
? "#22c55e"
: status === "error"
? "#ef4444"
: "#6b7280",
}}
/>
<span style={{ opacity: 0.8 }}>
{status === "watching"
? "Watching for changes..."
: status === "error"
? "Error"
: "Idle"}
</span>
</div>
{error && (
<div
style={{
marginTop: 8,
padding: 8,
borderRadius: 6,
background: "rgba(239, 68, 68, 0.1)",
border: "1px solid rgba(239, 68, 68, 0.3)",
color: "#fca5a5",
fontSize: 11,
}}
>
{error}
</div>
)}
<div
style={{
fontSize: 10,
marginTop: 12,
opacity: 0.5,
borderTop: "1px solid #1f2937",
paddingTop: 10,
}}
>
PII is scrubbed locally before processing
</div>
</div>
</div>
);
}

View file

@ -1,12 +1,25 @@
import { useState, CSSProperties } from "react";
import { useState, CSSProperties, useEffect } from "react";
import type { AeBridge } from "../types/preload";
export default function TitleBar() {
interface TitleBarProps {
title?: string;
}
export default function TitleBar({ title = "AeThex Terminal" }: TitleBarProps) {
const [pinned, setPinned] = useState(false);
const call = async (method: string) => {
const api = (window as any)?.aeBridge;
if (!api || !api[method]) return;
const res = await api[method]();
useEffect(() => {
const bridge = window.aeBridge;
if (bridge?.isPinned) {
bridge.isPinned().then(setPinned).catch(() => {});
}
}, []);
const call = async (method: keyof AeBridge) => {
const api = window.aeBridge;
if (!api || typeof api[method] !== "function") return;
const fn = api[method] as () => Promise<boolean>;
const res = await fn();
if (method === "togglePin") setPinned(res);
};
@ -19,20 +32,18 @@ export default function TitleBar() {
padding: "0 12px",
background: "#050814",
color: "#9ca3af",
// @ts-ignore - Electron-specific property
WebkitAppRegion: "drag",
borderBottom: "1px solid #0f172a",
letterSpacing: "0.08em",
fontSize: 12,
} as CSSProperties}
>
<div style={{ fontFamily: "Space Mono, monospace" }}>AeThex Terminal</div>
<div style={{ fontFamily: "Space Mono, monospace" }}>{title}</div>
<div
style={{
marginLeft: "auto",
display: "flex",
gap: 8,
// @ts-ignore - Electron-specific property
WebkitAppRegion: "no-drag",
} as CSSProperties}
>
@ -41,14 +52,14 @@ export default function TitleBar() {
style={btnStyle(pinned ? "#38bdf8" : "#1f2937")}
title="Pin / Unpin"
>
{pinned ? "Pinned" : "Pin"}
{pinned ? "📌" : "Pin"}
</button>
<button
onClick={() => call("minimize")}
style={btnStyle("#1f2937")}
title="Minimize"
>
_
</button>
<button
onClick={() => call("maximize")}
@ -62,14 +73,14 @@ export default function TitleBar() {
style={btnStyle("#ef4444")}
title="Close"
>
X
</button>
</div>
</div>
);
}
function btnStyle(bg: string) {
function btnStyle(bg: string): CSSProperties {
return {
border: "1px solid #111827",
background: bg,
@ -78,7 +89,9 @@ function btnStyle(bg: string) {
padding: "4px 8px",
cursor: "pointer",
fontSize: 12,
minWidth: 46,
} as React.CSSProperties;
minWidth: 36,
display: "flex",
alignItems: "center",
justifyContent: "center",
};
}

View file

@ -0,0 +1,3 @@
export { default as TitleBar } from "./TitleBar";
export { default as DesktopShell } from "./DesktopShell";
export { default as Overlay } from "./Overlay";

View file

@ -0,0 +1,23 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { DesktopShell } from "./components";
import ErrorBoundary from "../components/ErrorBoundary";
import App from "../App";
import "../global.css";
const container = document.getElementById("root");
if (!container) {
throw new Error("Root element not found");
}
const root = createRoot(container);
root.render(
<StrictMode>
<ErrorBoundary>
<DesktopShell title="AeThex">
<App />
</DesktopShell>
</ErrorBoundary>
</StrictMode>
);

View file

@ -0,0 +1,17 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Overlay } from "./components";
import "../global.css";
const container = document.getElementById("root");
if (!container) {
throw new Error("Root element not found");
}
const root = createRoot(container);
root.render(
<StrictMode>
<Overlay />
</StrictMode>
);

1
client/desktop/index.ts Normal file
View file

@ -0,0 +1 @@
export * from "./components";

23
client/desktop/types/preload.d.ts vendored Normal file
View file

@ -0,0 +1,23 @@
export interface ClipboardAlertPayload {
original: string;
scrubbed: string;
}
export interface AeBridge {
startWatcher: (dir: string) => Promise<boolean>;
stopWatcher: () => Promise<boolean>;
togglePin: () => Promise<boolean>;
isPinned: () => Promise<boolean>;
close: () => Promise<void>;
minimize: () => Promise<void>;
maximize: () => Promise<boolean>;
onClipboardAlert: (callback: (payload: ClipboardAlertPayload) => void) => () => void;
}
declare global {
interface Window {
aeBridge?: AeBridge;
}
}
export {};

View file

@ -1,8 +1,6 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import Overlay from "./pages/Overlay";
import DesktopShell from "./components/DesktopShell";
import ErrorBoundary from "./components/ErrorBoundary";
const container = document.getElementById("root");
@ -12,23 +10,10 @@ if (!container) {
const root = createRoot(container);
const hash = typeof window !== "undefined" ? window.location.hash : "";
if (hash.startsWith("#/overlay")) {
root.render(
root.render(
<StrictMode>
<ErrorBoundary>
<Overlay />
</ErrorBoundary>
</StrictMode>,
);
} else {
root.render(
<StrictMode>
<ErrorBoundary>
<DesktopShell>
<App />
</DesktopShell>
</ErrorBoundary>
</StrictMode>,
);
}
</StrictMode>
);

View file

@ -1,107 +0,0 @@
import { useState } from "react";
const defaultPath = "C:/Projects/Roblox"; // change as needed
export default function Overlay() {
const [dir, setDir] = useState(defaultPath);
const [status, setStatus] = useState<"idle" | "watching">("idle");
const start = async () => {
try {
await (window as any)?.aeBridge?.startWatcher(dir);
setStatus("watching");
} catch (e) {
console.error(e);
}
};
const stop = async () => {
try {
await (window as any)?.aeBridge?.stopWatcher();
setStatus("idle");
} catch (e) {
console.error(e);
}
};
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "rgba(3, 7, 18, 0.88)",
color: "#e5e7eb",
fontFamily: "Inter, sans-serif",
padding: 20,
backdropFilter: "blur(10px)",
}}
>
<div style={{ marginBottom: 12, fontSize: 12, letterSpacing: "0.1em" }}>
AeThex Overlay On-Top Sidecar
</div>
<div
style={{
border: "1px solid #38bdf8",
borderRadius: 12,
padding: 14,
background: "rgba(14, 165, 233, 0.07)",
}}
>
<div style={{ fontWeight: 700, marginBottom: 8, fontSize: 14 }}>
File Watcher
</div>
<label style={{ display: "block", fontSize: 12, marginBottom: 4 }}>
Folder to watch (.lua / .cs / .ts / .tsx)
</label>
<input
value={dir}
onChange={(e) => setDir(e.target.value)}
style={{
width: "100%",
padding: 10,
borderRadius: 8,
border: "1px solid #1f2937",
background: "#0f172a",
color: "#e5e7eb",
marginBottom: 10,
}}
/>
<div style={{ display: "flex", gap: 8 }}>
<button
onClick={start}
style={{
padding: "10px 14px",
borderRadius: 10,
border: "1px solid #38bdf8",
background: "rgba(56, 189, 248, 0.14)",
color: "#e0f2fe",
cursor: "pointer",
}}
>
Start
</button>
<button
onClick={stop}
style={{
padding: "10px 14px",
borderRadius: 10,
border: "1px solid #ef4444",
background: "rgba(239, 68, 68, 0.1)",
color: "#fecdd3",
cursor: "pointer",
}}
>
Stop
</button>
<span style={{ fontSize: 12, opacity: 0.8, alignSelf: "center" }}>
Status: {status}
</span>
</div>
<div style={{ fontSize: 11, marginTop: 10, opacity: 0.75 }}>
PII is scrubbed locally before any processing.
</div>
</div>
</div>
);
}

52
electron/ipc.js Normal file
View file

@ -0,0 +1,52 @@
import { ipcMain } from "electron";
import { getMainWindow } from "./windows.js";
import { startWatcher, stopWatcher } from "../services/watcher.js";
let pinned = false;
export function registerIpcHandlers() {
ipcMain.handle("watcher:start", async (_event, dir) => {
if (!dir || typeof dir !== "string") {
throw new Error("Invalid directory path");
}
await startWatcher(dir);
return true;
});
ipcMain.handle("watcher:stop", async () => {
await stopWatcher();
return true;
});
ipcMain.handle("window:toggle-pin", () => {
const mainWindow = getMainWindow();
pinned = !pinned;
mainWindow?.setAlwaysOnTop(pinned, "floating");
return pinned;
});
ipcMain.handle("window:close", () => {
const mainWindow = getMainWindow();
mainWindow?.close();
});
ipcMain.handle("window:minimize", () => {
const mainWindow = getMainWindow();
mainWindow?.minimize();
});
ipcMain.handle("window:maximize", () => {
const mainWindow = getMainWindow();
if (!mainWindow) return false;
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
return mainWindow.isMaximized();
});
ipcMain.handle("window:is-pinned", () => {
return pinned;
});
}

View file

@ -1,139 +1,35 @@
import { app, globalShortcut } from "electron";
import {
app,
BrowserWindow,
ipcMain,
globalShortcut,
clipboard,
} from "electron";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { startWatcher, stopWatcher } from "../services/watcher.js";
import { scrubPII } from "../services/pii-scrub.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let mainWindow = null;
let overlayWindow = null;
let pinned = false;
let sentinelInterval = null;
function getRendererUrl() {
return (
process.env.VITE_DEV_SERVER_URL ||
`file://${path.join(__dirname, "../dist/spa/index.html")}`
);
}
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
frame: false,
titleBarStyle: "hidden",
backgroundColor: "#030712",
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadURL(getRendererUrl());
}
function createOverlayWindow() {
overlayWindow = new BrowserWindow({
width: 420,
height: 640,
transparent: true,
frame: false,
alwaysOnTop: true,
resizable: true,
focusable: true,
backgroundColor: "#00000000",
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
overlayWindow.setAlwaysOnTop(true, "floating");
const base = getRendererUrl();
// Assumes your SPA has a route for /overlay
overlayWindow.loadURL(base + "#/overlay");
}
function toggleMainVisibility() {
if (!mainWindow) return;
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
}
function startClipboardSentinel() {
if (sentinelInterval) return;
let last = clipboard.readText();
sentinelInterval = setInterval(() => {
const now = clipboard.readText();
if (now !== last) {
last = now;
const scrubbed = scrubPII(now);
if (scrubbed !== now) {
mainWindow?.webContents.send("sentinel:clipboard-alert", now);
}
}
}, 1500);
}
createMainWindow,
createOverlayWindow,
toggleMainVisibility,
} from "./windows.js";
import { registerIpcHandlers } from "./ipc.js";
import { startClipboardSentinel, stopClipboardSentinel } from "./sentinel.js";
app.whenReady().then(() => {
registerIpcHandlers();
createMainWindow();
createOverlayWindow();
startClipboardSentinel();
// Global hotkey to toggle visibility
globalShortcut.register("Alt+Space", toggleMainVisibility);
ipcMain.handle("watcher:start", async (_e, dir) => {
await startWatcher(dir);
return true;
});
ipcMain.handle("watcher:stop", async () => {
await stopWatcher();
return true;
});
ipcMain.handle("window:toggle-pin", () => {
pinned = !pinned;
mainWindow?.setAlwaysOnTop(pinned, "floating");
return pinned;
});
ipcMain.handle("window:close", () => {
mainWindow?.close();
});
ipcMain.handle("window:minimize", () => {
mainWindow?.minimize();
});
ipcMain.handle("window:maximize", () => {
if (!mainWindow) return false;
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
return mainWindow.isMaximized();
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
stopClipboardSentinel();
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
createMainWindow();
});
app.on("will-quit", () => {
globalShortcut.unregisterAll();
stopClipboardSentinel();
});

View file

@ -3,11 +3,19 @@ import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("aeBridge", {
startWatcher: (dir) => ipcRenderer.invoke("watcher:start", dir),
stopWatcher: () => ipcRenderer.invoke("watcher:stop"),
togglePin: () => ipcRenderer.invoke("window:toggle-pin"),
isPinned: () => ipcRenderer.invoke("window:is-pinned"),
close: () => ipcRenderer.invoke("window:close"),
minimize: () => ipcRenderer.invoke("window:minimize"),
maximize: () => ipcRenderer.invoke("window:maximize"),
onClipboardAlert: (fn) =>
ipcRenderer.on("sentinel:clipboard-alert", (_e, payload) => fn(payload)),
});
onClipboardAlert: (callback) => {
ipcRenderer.on("sentinel:clipboard-alert", (_event, payload) =>
callback(payload)
);
return () => {
ipcRenderer.removeAllListeners("sentinel:clipboard-alert");
};
},
});

33
electron/sentinel.js Normal file
View file

@ -0,0 +1,33 @@
import { clipboard } from "electron";
import { getMainWindow } from "./windows.js";
import { scrubPII } from "../services/pii-scrub.js";
let sentinelInterval = null;
export function startClipboardSentinel() {
if (sentinelInterval) return;
let lastClipboard = clipboard.readText();
sentinelInterval = setInterval(() => {
const current = clipboard.readText();
if (current !== lastClipboard) {
lastClipboard = current;
const scrubbed = scrubPII(current);
if (scrubbed !== current) {
const mainWindow = getMainWindow();
mainWindow?.webContents.send("sentinel:clipboard-alert", {
original: current,
scrubbed: scrubbed,
});
}
}
}, 1500);
}
export function stopClipboardSentinel() {
if (sentinelInterval) {
clearInterval(sentinelInterval);
sentinelInterval = null;
}
}

92
electron/windows.js Normal file
View file

@ -0,0 +1,92 @@
import { BrowserWindow } from "electron";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let mainWindow = null;
let overlayWindow = null;
export function getMainWindow() {
return mainWindow;
}
export function getOverlayWindow() {
return overlayWindow;
}
export function getRendererUrl(entryFile = "desktop-main.html") {
if (process.env.VITE_DEV_SERVER_URL) {
return `${process.env.VITE_DEV_SERVER_URL}/${entryFile}`;
}
return `file://${path.join(__dirname, "../dist/desktop", entryFile)}`;
}
export function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 800,
minHeight: 600,
frame: false,
titleBarStyle: "hidden",
backgroundColor: "#030712",
show: false,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadURL(getRendererUrl("desktop-main.html"));
mainWindow.once("ready-to-show", () => {
mainWindow.show();
});
mainWindow.on("closed", () => {
mainWindow = null;
});
return mainWindow;
}
export function createOverlayWindow() {
overlayWindow = new BrowserWindow({
width: 380,
height: 320,
transparent: true,
frame: false,
alwaysOnTop: true,
resizable: true,
focusable: true,
skipTaskbar: true,
backgroundColor: "#00000000",
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
overlayWindow.setAlwaysOnTop(true, "floating");
overlayWindow.loadURL(getRendererUrl("desktop-overlay.html"));
overlayWindow.on("closed", () => {
overlayWindow = null;
});
return overlayWindow;
}
export function toggleMainVisibility() {
if (!mainWindow) return;
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
}

View file

@ -8,7 +8,8 @@
"main": "electron/main.js",
"pkg": {
"assets": [
"dist/spa/*"
"dist/spa/*",
"dist/desktop/*"
],
"scripts": [
"dist/server/**/*.js"
@ -25,9 +26,10 @@
"format.fix": "prettier --write .",
"typecheck": "tsc",
"desktop:dev": "concurrently -k \"npm:desktop:renderer\" \"npm:desktop:electron\"",
"desktop:renderer": "npm run dev -- --host --port 5173",
"desktop:renderer": "vite --config vite.desktop.config.ts --host --port 5173",
"desktop:electron": "cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .",
"desktop:build": "npm run build && electron-builder -c electron-builder.yml"
"desktop:build": "npm run build:desktop && electron-builder -c electron-builder.yml",
"build:desktop": "vite build --config vite.desktop.config.ts"
},
"dependencies": {
"@builder.io/react": "^8.2.8",

View file

@ -15,14 +15,30 @@ AeThex is a full-stack web application built with React, Vite, Express, and Supa
## Project Structure
```
├── client/ # React frontend code
├── client/ # React frontend code (web)
│ ├── components/ # React components
│ ├── pages/ # Page components
│ ├── lib/ # Utility libraries
│ ├── hooks/ # Custom React hooks
│ └── contexts/ # React contexts
│ ├── contexts/ # React contexts
│ ├── desktop/ # Desktop-specific React code
│ │ ├── components/ # TitleBar, DesktopShell, Overlay
│ │ ├── desktop-main.tsx # Desktop app entry point
│ │ └── desktop-overlay.tsx # Overlay window entry point
│ ├── main.tsx # Web app entry point
│ ├── desktop-main.html # Desktop HTML entry
│ └── desktop-overlay.html # Overlay HTML entry
├── electron/ # Electron main process
│ ├── main.js # Electron entry point
│ ├── preload.js # Secure IPC bridge
│ ├── windows.js # Window management
│ ├── ipc.js # IPC handlers
│ └── sentinel.js # Clipboard PII monitoring
├── server/ # Express backend
│ └── index.ts # Main server file with API routes
├── services/ # Backend services
│ ├── watcher.js # File watcher for dev workflow
│ └── pii-scrub.js # PII scrubbing utility
├── api/ # API route handlers
├── discord-bot/ # Discord bot integration
├── docs/ # Documentation files

26
vite.desktop.config.ts Normal file
View file

@ -0,0 +1,26 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
export default defineConfig({
root: "client",
base: "./",
build: {
outDir: "../dist/desktop",
emptyOutDir: true,
rollupOptions: {
input: {
main: path.resolve(__dirname, "client/desktop-main.html"),
overlay: path.resolve(__dirname, "client/desktop-overlay.html"),
},
},
},
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./client"),
"@shared": path.resolve(__dirname, "./shared"),
"@assets": path.resolve(__dirname, "./attached_assets"),
},
},
});