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:
parent
23063b3b7f
commit
49ee808d2b
22 changed files with 642 additions and 295 deletions
4
.replit
4
.replit
|
|
@ -60,6 +60,10 @@ externalPort = 3000
|
||||||
localPort = 40437
|
localPort = 40437
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 45997
|
||||||
|
externalPort = 3002
|
||||||
|
|
||||||
[deployment]
|
[deployment]
|
||||||
deploymentTarget = "autoscale"
|
deploymentTarget = "autoscale"
|
||||||
run = ["node", "dist/server/production.mjs"]
|
run = ["node", "dist/server/production.mjs"]
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,6 @@ import StaffLearningPortal from "./pages/staff/StaffLearningPortal";
|
||||||
import StaffPerformanceReviews from "./pages/staff/StaffPerformanceReviews";
|
import StaffPerformanceReviews from "./pages/staff/StaffPerformanceReviews";
|
||||||
import StaffProjectTracking from "./pages/staff/StaffProjectTracking";
|
import StaffProjectTracking from "./pages/staff/StaffProjectTracking";
|
||||||
import StaffTeamHandbook from "./pages/staff/StaffTeamHandbook";
|
import StaffTeamHandbook from "./pages/staff/StaffTeamHandbook";
|
||||||
import Overlay from "./pages/Overlay";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
@ -185,7 +184,6 @@ const App = () => (
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Subdomain Passport (aethex.me and aethex.space) handles its own redirect if not a subdomain */}
|
{/* Subdomain Passport (aethex.me and aethex.space) handles its own redirect if not a subdomain */}
|
||||||
<Route path="/" element={<SubdomainPassport />} />
|
<Route path="/" element={<SubdomainPassport />} />
|
||||||
<Route path="/overlay" element={<Overlay />} />
|
|
||||||
<Route path="/onboarding" element={<Onboarding />} />
|
<Route path="/onboarding" element={<Onboarding />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route
|
<Route
|
||||||
|
|
|
||||||
25
client/desktop-main.html
Normal file
25
client/desktop-main.html
Normal 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>
|
||||||
25
client/desktop-overlay.html
Normal file
25
client/desktop-overlay.html
Normal 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>
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import TitleBar from "./TitleBar";
|
import TitleBar from "./TitleBar";
|
||||||
import { ReactNode } from "react";
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -11,11 +16,11 @@ export default function DesktopShell({ children }: { children: ReactNode }) {
|
||||||
color: "#e5e7eb",
|
color: "#e5e7eb",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TitleBar />
|
<TitleBar title={title} />
|
||||||
<div style={{ flex: 1, overflow: "hidden" }}>{children}</div>
|
<div style={{ flex: 1, overflow: "auto" }}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
207
client/desktop/components/Overlay.tsx
Normal file
207
client/desktop/components/Overlay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 [pinned, setPinned] = useState(false);
|
||||||
|
|
||||||
const call = async (method: string) => {
|
useEffect(() => {
|
||||||
const api = (window as any)?.aeBridge;
|
const bridge = window.aeBridge;
|
||||||
if (!api || !api[method]) return;
|
if (bridge?.isPinned) {
|
||||||
const res = await api[method]();
|
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);
|
if (method === "togglePin") setPinned(res);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,20 +32,18 @@ export default function TitleBar() {
|
||||||
padding: "0 12px",
|
padding: "0 12px",
|
||||||
background: "#050814",
|
background: "#050814",
|
||||||
color: "#9ca3af",
|
color: "#9ca3af",
|
||||||
// @ts-ignore - Electron-specific property
|
|
||||||
WebkitAppRegion: "drag",
|
WebkitAppRegion: "drag",
|
||||||
borderBottom: "1px solid #0f172a",
|
borderBottom: "1px solid #0f172a",
|
||||||
letterSpacing: "0.08em",
|
letterSpacing: "0.08em",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
} as CSSProperties}
|
} as CSSProperties}
|
||||||
>
|
>
|
||||||
<div style={{ fontFamily: "Space Mono, monospace" }}>AeThex Terminal</div>
|
<div style={{ fontFamily: "Space Mono, monospace" }}>{title}</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "auto",
|
marginLeft: "auto",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
// @ts-ignore - Electron-specific property
|
|
||||||
WebkitAppRegion: "no-drag",
|
WebkitAppRegion: "no-drag",
|
||||||
} as CSSProperties}
|
} as CSSProperties}
|
||||||
>
|
>
|
||||||
|
|
@ -41,14 +52,14 @@ export default function TitleBar() {
|
||||||
style={btnStyle(pinned ? "#38bdf8" : "#1f2937")}
|
style={btnStyle(pinned ? "#38bdf8" : "#1f2937")}
|
||||||
title="Pin / Unpin"
|
title="Pin / Unpin"
|
||||||
>
|
>
|
||||||
{pinned ? "Pinned" : "Pin"}
|
{pinned ? "📌" : "Pin"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => call("minimize")}
|
onClick={() => call("minimize")}
|
||||||
style={btnStyle("#1f2937")}
|
style={btnStyle("#1f2937")}
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
>
|
>
|
||||||
_
|
—
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => call("maximize")}
|
onClick={() => call("maximize")}
|
||||||
|
|
@ -62,14 +73,14 @@ export default function TitleBar() {
|
||||||
style={btnStyle("#ef4444")}
|
style={btnStyle("#ef4444")}
|
||||||
title="Close"
|
title="Close"
|
||||||
>
|
>
|
||||||
X
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function btnStyle(bg: string) {
|
function btnStyle(bg: string): CSSProperties {
|
||||||
return {
|
return {
|
||||||
border: "1px solid #111827",
|
border: "1px solid #111827",
|
||||||
background: bg,
|
background: bg,
|
||||||
|
|
@ -78,7 +89,9 @@ function btnStyle(bg: string) {
|
||||||
padding: "4px 8px",
|
padding: "4px 8px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
minWidth: 46,
|
minWidth: 36,
|
||||||
} as React.CSSProperties;
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
3
client/desktop/components/index.ts
Normal file
3
client/desktop/components/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as TitleBar } from "./TitleBar";
|
||||||
|
export { default as DesktopShell } from "./DesktopShell";
|
||||||
|
export { default as Overlay } from "./Overlay";
|
||||||
23
client/desktop/desktop-main.tsx
Normal file
23
client/desktop/desktop-main.tsx
Normal 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>
|
||||||
|
);
|
||||||
17
client/desktop/desktop-overlay.tsx
Normal file
17
client/desktop/desktop-overlay.tsx
Normal 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
1
client/desktop/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./components";
|
||||||
23
client/desktop/types/preload.d.ts
vendored
Normal file
23
client/desktop/types/preload.d.ts
vendored
Normal 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 {};
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import Overlay from "./pages/Overlay";
|
|
||||||
import DesktopShell from "./components/DesktopShell";
|
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
|
|
@ -12,23 +10,10 @@ if (!container) {
|
||||||
|
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
const hash = typeof window !== "undefined" ? window.location.hash : "";
|
|
||||||
if (hash.startsWith("#/overlay")) {
|
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Overlay />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
root.render(
|
|
||||||
<StrictMode>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<DesktopShell>
|
|
||||||
<App />
|
<App />
|
||||||
</DesktopShell>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</StrictMode>,
|
</StrictMode>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
52
electron/ipc.js
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
148
electron/main.js
148
electron/main.js
|
|
@ -1,139 +1,35 @@
|
||||||
|
import { app, globalShortcut } from "electron";
|
||||||
import {
|
import {
|
||||||
app,
|
createMainWindow,
|
||||||
BrowserWindow,
|
createOverlayWindow,
|
||||||
ipcMain,
|
toggleMainVisibility,
|
||||||
globalShortcut,
|
} from "./windows.js";
|
||||||
clipboard,
|
import { registerIpcHandlers } from "./ipc.js";
|
||||||
} from "electron";
|
import { startClipboardSentinel, stopClipboardSentinel } from "./sentinel.js";
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
registerIpcHandlers();
|
||||||
|
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
createOverlayWindow();
|
createOverlayWindow();
|
||||||
|
|
||||||
startClipboardSentinel();
|
startClipboardSentinel();
|
||||||
|
|
||||||
// Global hotkey to toggle visibility
|
|
||||||
globalShortcut.register("Alt+Space", toggleMainVisibility);
|
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", () => {
|
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();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,19 @@ import { contextBridge, ipcRenderer } from "electron";
|
||||||
contextBridge.exposeInMainWorld("aeBridge", {
|
contextBridge.exposeInMainWorld("aeBridge", {
|
||||||
startWatcher: (dir) => ipcRenderer.invoke("watcher:start", dir),
|
startWatcher: (dir) => ipcRenderer.invoke("watcher:start", dir),
|
||||||
stopWatcher: () => ipcRenderer.invoke("watcher:stop"),
|
stopWatcher: () => ipcRenderer.invoke("watcher:stop"),
|
||||||
|
|
||||||
togglePin: () => ipcRenderer.invoke("window:toggle-pin"),
|
togglePin: () => ipcRenderer.invoke("window:toggle-pin"),
|
||||||
|
isPinned: () => ipcRenderer.invoke("window:is-pinned"),
|
||||||
close: () => ipcRenderer.invoke("window:close"),
|
close: () => ipcRenderer.invoke("window:close"),
|
||||||
minimize: () => ipcRenderer.invoke("window:minimize"),
|
minimize: () => ipcRenderer.invoke("window:minimize"),
|
||||||
maximize: () => ipcRenderer.invoke("window:maximize"),
|
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
33
electron/sentinel.js
Normal 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
92
electron/windows.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
"main": "electron/main.js",
|
"main": "electron/main.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"assets": [
|
"assets": [
|
||||||
"dist/spa/*"
|
"dist/spa/*",
|
||||||
|
"dist/desktop/*"
|
||||||
],
|
],
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"dist/server/**/*.js"
|
"dist/server/**/*.js"
|
||||||
|
|
@ -25,9 +26,10 @@
|
||||||
"format.fix": "prettier --write .",
|
"format.fix": "prettier --write .",
|
||||||
"typecheck": "tsc",
|
"typecheck": "tsc",
|
||||||
"desktop:dev": "concurrently -k \"npm:desktop:renderer\" \"npm:desktop:electron\"",
|
"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: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": {
|
"dependencies": {
|
||||||
"@builder.io/react": "^8.2.8",
|
"@builder.io/react": "^8.2.8",
|
||||||
|
|
|
||||||
20
replit.md
20
replit.md
|
|
@ -15,14 +15,30 @@ AeThex is a full-stack web application built with React, Vite, Express, and Supa
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
```
|
```
|
||||||
├── client/ # React frontend code
|
├── client/ # React frontend code (web)
|
||||||
│ ├── components/ # React components
|
│ ├── components/ # React components
|
||||||
│ ├── pages/ # Page components
|
│ ├── pages/ # Page components
|
||||||
│ ├── lib/ # Utility libraries
|
│ ├── lib/ # Utility libraries
|
||||||
│ ├── hooks/ # Custom React hooks
|
│ ├── 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
|
├── server/ # Express backend
|
||||||
│ └── index.ts # Main server file with API routes
|
│ └── 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
|
├── api/ # API route handlers
|
||||||
├── discord-bot/ # Discord bot integration
|
├── discord-bot/ # Discord bot integration
|
||||||
├── docs/ # Documentation files
|
├── docs/ # Documentation files
|
||||||
|
|
|
||||||
26
vite.desktop.config.ts
Normal file
26
vite.desktop.config.ts
Normal 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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue