From 49ee808d2bf5b2e4e8799203e2c981f373d47a0c Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Fri, 5 Dec 2025 22:36:13 +0000 Subject: [PATCH] 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 --- .replit | 4 + client/App.tsx | 2 - client/desktop-main.html | 25 +++ client/desktop-overlay.html | 25 +++ .../{ => desktop}/components/DesktopShell.tsx | 13 +- client/desktop/components/Overlay.tsx | 207 ++++++++++++++++++ client/{ => desktop}/components/TitleBar.tsx | 45 ++-- client/desktop/components/index.ts | 3 + client/desktop/desktop-main.tsx | 23 ++ client/desktop/desktop-overlay.tsx | 17 ++ client/desktop/index.ts | 1 + client/desktop/types/preload.d.ts | 23 ++ client/main.tsx | 29 +-- client/pages/Overlay.tsx | 107 --------- electron/ipc.js | 52 +++++ electron/main.js | 148 ++----------- electron/preload.js | 14 +- electron/sentinel.js | 33 +++ electron/windows.js | 92 ++++++++ package.json | 8 +- replit.md | 40 +++- vite.desktop.config.ts | 26 +++ 22 files changed, 642 insertions(+), 295 deletions(-) create mode 100644 client/desktop-main.html create mode 100644 client/desktop-overlay.html rename client/{ => desktop}/components/DesktopShell.tsx (52%) create mode 100644 client/desktop/components/Overlay.tsx rename client/{ => desktop}/components/TitleBar.tsx (62%) create mode 100644 client/desktop/components/index.ts create mode 100644 client/desktop/desktop-main.tsx create mode 100644 client/desktop/desktop-overlay.tsx create mode 100644 client/desktop/index.ts create mode 100644 client/desktop/types/preload.d.ts delete mode 100644 client/pages/Overlay.tsx create mode 100644 electron/ipc.js create mode 100644 electron/sentinel.js create mode 100644 electron/windows.js create mode 100644 vite.desktop.config.ts diff --git a/.replit b/.replit index 76d2fa09..86074f7e 100644 --- a/.replit +++ b/.replit @@ -60,6 +60,10 @@ externalPort = 3000 localPort = 40437 externalPort = 3001 +[[ports]] +localPort = 45997 +externalPort = 3002 + [deployment] deploymentTarget = "autoscale" run = ["node", "dist/server/production.mjs"] diff --git a/client/App.tsx b/client/App.tsx index 986b320e..6384b617 100644 --- a/client/App.tsx +++ b/client/App.tsx @@ -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 = () => ( {/* Subdomain Passport (aethex.me and aethex.space) handles its own redirect if not a subdomain */} } /> - } /> } /> } /> + + + + + + AeThex + + + + + + +
+ + + diff --git a/client/desktop-overlay.html b/client/desktop-overlay.html new file mode 100644 index 00000000..b3a3686e --- /dev/null +++ b/client/desktop-overlay.html @@ -0,0 +1,25 @@ + + + + + + + AeThex Overlay + + + + + + +
+ + + diff --git a/client/components/DesktopShell.tsx b/client/desktop/components/DesktopShell.tsx similarity index 52% rename from client/components/DesktopShell.tsx rename to client/desktop/components/DesktopShell.tsx index 0045d146..fbbf5607 100644 --- a/client/components/DesktopShell.tsx +++ b/client/desktop/components/DesktopShell.tsx @@ -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 (
- -
{children}
+ +
{children}
); } - diff --git a/client/desktop/components/Overlay.tsx b/client/desktop/components/Overlay.tsx new file mode 100644 index 00000000..502241d7 --- /dev/null +++ b/client/desktop/components/Overlay.tsx @@ -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(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 ( +
+
+ AeThex Overlay +
+ +
+
+ 📂 File Watcher +
+ + + 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, + }} + /> + +
+ + +
+ +
+ + + {status === "watching" + ? "Watching for changes..." + : status === "error" + ? "Error" + : "Idle"} + +
+ + {error && ( +
+ {error} +
+ )} + +
+ PII is scrubbed locally before processing +
+
+
+ ); +} diff --git a/client/components/TitleBar.tsx b/client/desktop/components/TitleBar.tsx similarity index 62% rename from client/components/TitleBar.tsx rename to client/desktop/components/TitleBar.tsx index db68e46d..32f96848 100644 --- a/client/components/TitleBar.tsx +++ b/client/desktop/components/TitleBar.tsx @@ -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; + 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} > -
AeThex Terminal
+
{title}
@@ -41,14 +52,14 @@ export default function TitleBar() { style={btnStyle(pinned ? "#38bdf8" : "#1f2937")} title="Pin / Unpin" > - {pinned ? "Pinned" : "Pin"} + {pinned ? "📌" : "Pin"}
); } -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", + }; } - diff --git a/client/desktop/components/index.ts b/client/desktop/components/index.ts new file mode 100644 index 00000000..146336f7 --- /dev/null +++ b/client/desktop/components/index.ts @@ -0,0 +1,3 @@ +export { default as TitleBar } from "./TitleBar"; +export { default as DesktopShell } from "./DesktopShell"; +export { default as Overlay } from "./Overlay"; diff --git a/client/desktop/desktop-main.tsx b/client/desktop/desktop-main.tsx new file mode 100644 index 00000000..4e9ae227 --- /dev/null +++ b/client/desktop/desktop-main.tsx @@ -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( + + + + + + + +); diff --git a/client/desktop/desktop-overlay.tsx b/client/desktop/desktop-overlay.tsx new file mode 100644 index 00000000..94890d44 --- /dev/null +++ b/client/desktop/desktop-overlay.tsx @@ -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( + + + +); diff --git a/client/desktop/index.ts b/client/desktop/index.ts new file mode 100644 index 00000000..40b494c5 --- /dev/null +++ b/client/desktop/index.ts @@ -0,0 +1 @@ +export * from "./components"; diff --git a/client/desktop/types/preload.d.ts b/client/desktop/types/preload.d.ts new file mode 100644 index 00000000..a693d775 --- /dev/null +++ b/client/desktop/types/preload.d.ts @@ -0,0 +1,23 @@ +export interface ClipboardAlertPayload { + original: string; + scrubbed: string; +} + +export interface AeBridge { + startWatcher: (dir: string) => Promise; + stopWatcher: () => Promise; + togglePin: () => Promise; + isPinned: () => Promise; + close: () => Promise; + minimize: () => Promise; + maximize: () => Promise; + onClipboardAlert: (callback: (payload: ClipboardAlertPayload) => void) => () => void; +} + +declare global { + interface Window { + aeBridge?: AeBridge; + } +} + +export {}; diff --git a/client/main.tsx b/client/main.tsx index f8fa6ee8..f292d029 100644 --- a/client/main.tsx +++ b/client/main.tsx @@ -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( - - - - - , - ); -} else { - root.render( - - - - - - - , - ); -} +root.render( + + + + + +); diff --git a/client/pages/Overlay.tsx b/client/pages/Overlay.tsx deleted file mode 100644 index 76450ea5..00000000 --- a/client/pages/Overlay.tsx +++ /dev/null @@ -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 ( -
-
- AeThex Overlay — On-Top Sidecar -
-
-
- File Watcher -
- - setDir(e.target.value)} - style={{ - width: "100%", - padding: 10, - borderRadius: 8, - border: "1px solid #1f2937", - background: "#0f172a", - color: "#e5e7eb", - marginBottom: 10, - }} - /> -
- - - - Status: {status} - -
-
- PII is scrubbed locally before any processing. -
-
-
- ); -} - diff --git a/electron/ipc.js b/electron/ipc.js new file mode 100644 index 00000000..712cd0af --- /dev/null +++ b/electron/ipc.js @@ -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; + }); +} diff --git a/electron/main.js b/electron/main.js index 4ddb5781..74daf346 100644 --- a/electron/main.js +++ b/electron/main.js @@ -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(); +}); diff --git a/electron/preload.js b/electron/preload.js index 1925b7a2..0c1e8e79 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -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"); + }; + }, +}); diff --git a/electron/sentinel.js b/electron/sentinel.js new file mode 100644 index 00000000..face3480 --- /dev/null +++ b/electron/sentinel.js @@ -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; + } +} diff --git a/electron/windows.js b/electron/windows.js new file mode 100644 index 00000000..a169b0b2 --- /dev/null +++ b/electron/windows.js @@ -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(); + } +} diff --git a/package.json b/package.json index d914bbcd..c3be3f77 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/replit.md b/replit.md index ef98c8bc..4cd581cf 100644 --- a/replit.md +++ b/replit.md @@ -15,18 +15,34 @@ AeThex is a full-stack web application built with React, Vite, Express, and Supa ## Project Structure ``` -├── client/ # React frontend code -│ ├── components/ # React components -│ ├── pages/ # Page components -│ ├── lib/ # Utility libraries -│ ├── hooks/ # Custom React hooks -│ └── contexts/ # React contexts -├── server/ # Express backend -│ └── index.ts # Main server file with API routes -├── api/ # API route handlers -├── discord-bot/ # Discord bot integration -├── docs/ # Documentation files -└── shared/ # Shared code between client/server +├── client/ # React frontend code (web) +│ ├── components/ # React components +│ ├── pages/ # Page components +│ ├── lib/ # Utility libraries +│ ├── hooks/ # Custom React hooks +│ ├── 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 +└── shared/ # Shared code between client/server ``` diff --git a/vite.desktop.config.ts b/vite.desktop.config.ts new file mode 100644 index 00000000..3b50c30a --- /dev/null +++ b/vite.desktop.config.ts @@ -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"), + }, + }, +});