modified: .replit
This commit is contained in:
parent
bce04f18cd
commit
82de4d9b41
17 changed files with 4918 additions and 430 deletions
4
.replit
4
.replit
|
|
@ -52,10 +52,6 @@ externalPort = 80
|
|||
localPort = 8044
|
||||
externalPort = 3003
|
||||
|
||||
[[ports]]
|
||||
localPort = 36771
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 38557
|
||||
externalPort = 3000
|
||||
|
|
|
|||
151
README.md
Normal file
151
README.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# AeThex Forge - Local Development Setup
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
This guide will help you set up and run the AeThex platform locally on your machine.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Node.js** (v18 or higher)
|
||||
- Download from: https://nodejs.org/
|
||||
- This will also install npm (Node Package Manager)
|
||||
|
||||
2. **Git** (optional, if you want to clone updates)
|
||||
- Download from: https://git-scm.com/
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install Node.js
|
||||
- Visit https://nodejs.org/ and download the LTS version
|
||||
- Run the installer and follow the setup wizard
|
||||
- Restart your terminal/PowerShell after installation
|
||||
|
||||
### 2. Verify Installation
|
||||
Open PowerShell or Command Prompt and run:
|
||||
```powershell
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
You should see version numbers (e.g., v20.x.x and 10.x.x)
|
||||
|
||||
### 3. Install Project Dependencies
|
||||
Navigate to the project folder and install dependencies:
|
||||
```powershell
|
||||
cd C:\Users\PCOEM\Downloads\aethex-forge\aethex-forge
|
||||
npm install
|
||||
```
|
||||
This may take a few minutes as it downloads all required packages.
|
||||
|
||||
### 4. Set Up Environment Variables
|
||||
Create a `.env` file in the root directory (`aethex-forge` folder) with the following variables:
|
||||
|
||||
**Minimum Required (to run the app):**
|
||||
```env
|
||||
# Supabase Configuration (Required)
|
||||
VITE_SUPABASE_URL=your_supabase_url_here
|
||||
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key_here
|
||||
SUPABASE_SERVICE_ROLE=your_service_role_key_here
|
||||
SUPABASE_URL=your_supabase_url_here
|
||||
|
||||
# API Base URL
|
||||
VITE_API_BASE=http://localhost:5000
|
||||
```
|
||||
|
||||
**Optional (for full functionality):**
|
||||
```env
|
||||
# Discord Integration
|
||||
DISCORD_CLIENT_ID=your_discord_client_id
|
||||
DISCORD_CLIENT_SECRET=your_discord_client_secret
|
||||
DISCORD_BOT_TOKEN=your_discord_bot_token
|
||||
DISCORD_PUBLIC_KEY=your_discord_public_key
|
||||
DISCORD_REDIRECT_URI=http://localhost:5000/api/discord/oauth/callback
|
||||
|
||||
# Foundation OAuth
|
||||
VITE_FOUNDATION_URL=https://aethex.foundation
|
||||
FOUNDATION_OAUTH_CLIENT_ID=your_foundation_client_id
|
||||
FOUNDATION_OAUTH_CLIENT_SECRET=your_foundation_client_secret
|
||||
|
||||
# Email Service (SMTP)
|
||||
SMTP_HOST=your_smtp_host
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_smtp_user
|
||||
SMTP_PASSWORD=your_smtp_password
|
||||
|
||||
# Other Services
|
||||
VITE_GHOST_API_URL=your_ghost_api_url
|
||||
GHOST_ADMIN_API_KEY=your_ghost_admin_key
|
||||
```
|
||||
|
||||
**Note:** You can start with just the Supabase variables to get the app running. Other features will work once you add their respective credentials.
|
||||
|
||||
### 5. Run the Development Server
|
||||
```powershell
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will start on **http://localhost:5000**
|
||||
|
||||
Open your browser and navigate to that URL to view the application.
|
||||
|
||||
## Available Commands
|
||||
|
||||
- `npm run dev` - Start development server (port 5000)
|
||||
- `npm run build` - Build for production
|
||||
- `npm start` - Start production server
|
||||
- `npm run typecheck` - Check TypeScript types
|
||||
- `npm test` - Run tests
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
aethex-forge/
|
||||
├── client/ # React frontend (pages, components)
|
||||
├── server/ # Express backend API
|
||||
├── api/ # API route handlers
|
||||
├── shared/ # Shared types between client/server
|
||||
├── discord-bot/ # Discord bot integration
|
||||
└── supabase/ # Database migrations
|
||||
```
|
||||
|
||||
## Getting Supabase Credentials
|
||||
|
||||
If you don't have Supabase credentials yet:
|
||||
|
||||
1. Go to https://supabase.com/
|
||||
2. Create a free account
|
||||
3. Create a new project
|
||||
4. Go to Project Settings → API
|
||||
5. Copy:
|
||||
- Project URL → `VITE_SUPABASE_URL` and `SUPABASE_URL`
|
||||
- `anon` `public` key → `VITE_SUPABASE_ANON_KEY`
|
||||
- `service_role` `secret` key → `SUPABASE_SERVICE_ROLE`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Already in Use
|
||||
If port 5000 is already in use, you can change it in `vite.config.ts`:
|
||||
```typescript
|
||||
server: {
|
||||
port: 5001, // Change to any available port
|
||||
}
|
||||
```
|
||||
|
||||
### Module Not Found Errors
|
||||
Try deleting `node_modules` and `package-lock.json`, then run `npm install` again:
|
||||
```powershell
|
||||
Remove-Item -Recurse -Force node_modules
|
||||
Remove-Item package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
### Environment Variables Not Loading
|
||||
- Make sure `.env` file is in the root `aethex-forge` directory
|
||||
- Restart the dev server after adding new environment variables
|
||||
- Variables starting with `VITE_` are exposed to the client
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check the `docs/` folder for detailed documentation
|
||||
- Review `AGENTS.md` for architecture details
|
||||
- See `replit.md` for deployment information
|
||||
|
||||
42
aethex-forge/.gitignore
vendored
Normal file
42
aethex-forge/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output
|
||||
|
||||
# Production
|
||||
dist/
|
||||
build/
|
||||
*.local
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# OS files
|
||||
Thumbs.db
|
||||
|
||||
|
|
@ -164,6 +164,7 @@ 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();
|
||||
|
||||
|
|
@ -184,6 +185,7 @@ 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
|
||||
|
|
|
|||
21
client/components/DesktopShell.tsx
Normal file
21
client/components/DesktopShell.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import TitleBar from "./TitleBar";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function DesktopShell({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
background: "#030712",
|
||||
color: "#e5e7eb",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<TitleBar />
|
||||
<div style={{ flex: 1, overflow: "hidden" }}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
308
client/components/Scene.tsx
Normal file
308
client/components/Scene.tsx
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||
import { Grid, OrbitControls, Text } from "@react-three/drei";
|
||||
import { MathUtils, Vector3 } from "three";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type Gateway = {
|
||||
label: string;
|
||||
color: string;
|
||||
route: string;
|
||||
angle: number; // radians
|
||||
};
|
||||
|
||||
const gateways: Gateway[] = [
|
||||
{
|
||||
label: "NEXUS",
|
||||
color: "#a855f7",
|
||||
route: "/dashboard/nexus",
|
||||
angle: MathUtils.degToRad(-50),
|
||||
},
|
||||
{
|
||||
label: "GAMEFORGE",
|
||||
color: "#22c55e",
|
||||
route: "/gameforge",
|
||||
angle: MathUtils.degToRad(-20),
|
||||
},
|
||||
{
|
||||
label: "FOUNDATION",
|
||||
color: "#ef4444",
|
||||
route: "/foundation",
|
||||
angle: MathUtils.degToRad(0),
|
||||
},
|
||||
{
|
||||
label: "LABS",
|
||||
color: "#eab308",
|
||||
route: "/dashboard/labs",
|
||||
angle: MathUtils.degToRad(20),
|
||||
},
|
||||
{
|
||||
label: "CORP",
|
||||
color: "#3b82f6",
|
||||
route: "/corp",
|
||||
angle: MathUtils.degToRad(50),
|
||||
},
|
||||
];
|
||||
|
||||
function CoreCube() {
|
||||
const ref = useRef<THREE.Mesh>(null);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (!ref.current) return;
|
||||
ref.current.rotation.x += delta * 0.45;
|
||||
ref.current.rotation.y += delta * 0.65;
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={ref}>
|
||||
<boxGeometry args={[1.4, 1.4, 1.4]} />
|
||||
<meshStandardMaterial
|
||||
emissive="#38bdf8"
|
||||
emissiveIntensity={1.5}
|
||||
color="#0ea5e9"
|
||||
metalness={0.6}
|
||||
roughness={0.2}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
function GatewayMesh({
|
||||
gateway,
|
||||
onHover,
|
||||
onClick,
|
||||
isActive,
|
||||
}: {
|
||||
gateway: Gateway;
|
||||
onHover: (label: string | null) => void;
|
||||
onClick: (gw: Gateway) => void;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const ref = useRef<THREE.Mesh>(null);
|
||||
const glow = useRef<THREE.Mesh>(null);
|
||||
|
||||
const position = useMemo(() => {
|
||||
const radius = 6;
|
||||
return new Vector3(
|
||||
Math.cos(gateway.angle) * radius,
|
||||
1.5,
|
||||
Math.sin(gateway.angle) * radius,
|
||||
);
|
||||
}, [gateway.angle]);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (ref.current) {
|
||||
const targetScale = isActive ? 1.22 : 1;
|
||||
ref.current.scale.lerp(
|
||||
new Vector3(targetScale, targetScale, targetScale),
|
||||
6 * delta,
|
||||
);
|
||||
}
|
||||
if (glow.current) {
|
||||
glow.current.rotation.y += delta * 0.8;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group position={position}>
|
||||
<mesh
|
||||
ref={ref}
|
||||
onPointerOver={() => onHover(gateway.label)}
|
||||
onPointerOut={() => onHover(null)}
|
||||
onClick={() => onClick(gateway)}
|
||||
>
|
||||
<torusKnotGeometry args={[0.5, 0.15, 120, 16]} />
|
||||
<meshStandardMaterial
|
||||
color={gateway.color}
|
||||
emissive={gateway.color}
|
||||
emissiveIntensity={isActive ? 2.4 : 1.2}
|
||||
roughness={0.25}
|
||||
metalness={0.7}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
<mesh ref={glow} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<ringGeometry args={[0.9, 1.2, 64]} />
|
||||
<meshBasicMaterial
|
||||
color={gateway.color}
|
||||
opacity={0.35}
|
||||
transparent
|
||||
side={2}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
<Text
|
||||
position={[0, 1.5, 0]}
|
||||
fontSize={0.6}
|
||||
color={gateway.color}
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
outlineColor="#0f172a"
|
||||
outlineWidth={0.01}
|
||||
>
|
||||
{gateway.label}
|
||||
</Text>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function CameraRig({ target }: { target: Gateway | null }) {
|
||||
const { camera } = useThree();
|
||||
const desired = useRef(new Vector3(0, 3, 12));
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (target) {
|
||||
const radius = 3.2;
|
||||
desired.current.set(
|
||||
Math.cos(target.angle) * radius,
|
||||
2.5,
|
||||
Math.sin(target.angle) * radius,
|
||||
);
|
||||
camera.lookAt(
|
||||
Math.cos(target.angle) * 6,
|
||||
1.5,
|
||||
Math.sin(target.angle) * 6,
|
||||
);
|
||||
} else {
|
||||
desired.current.set(0, 3, 12);
|
||||
camera.lookAt(0, 0, 0);
|
||||
}
|
||||
camera.position.lerp(desired.current, 3 * delta);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
function SceneContent() {
|
||||
const [hovered, setHovered] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<Gateway | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = (gw: Gateway) => {
|
||||
setSelected(gw);
|
||||
setTimeout(() => navigate(gw.route), 550);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<color attach="background" args={["#030712"]} />
|
||||
<fog attach="fog" args={["#030712", 15, 60]} />
|
||||
<hemisphereLight
|
||||
intensity={0.35}
|
||||
color="#4f46e5"
|
||||
groundColor="#0f172a"
|
||||
/>
|
||||
<spotLight
|
||||
position={[5, 12, 5]}
|
||||
intensity={1.5}
|
||||
angle={0.4}
|
||||
penumbra={0.7}
|
||||
/>
|
||||
<pointLight position={[-6, 6, -6]} intensity={1.2} color="#22d3ee" />
|
||||
|
||||
<Grid
|
||||
args={[100, 100]}
|
||||
sectionSize={2}
|
||||
sectionThickness={0.2}
|
||||
sectionColor="#0ea5e9"
|
||||
cellSize={0.5}
|
||||
cellThickness={0.1}
|
||||
cellColor="#1e293b"
|
||||
fadeDistance={30}
|
||||
fadeStrength={3}
|
||||
position={[0, -1, 0]}
|
||||
infiniteGrid
|
||||
/>
|
||||
|
||||
<CoreCube />
|
||||
|
||||
{gateways.map((gw) => (
|
||||
<GatewayMesh
|
||||
key={gw.label}
|
||||
gateway={gw}
|
||||
onHover={setHovered}
|
||||
onClick={handleClick}
|
||||
isActive={hovered === gw.label || selected?.label === gw.label}
|
||||
/>
|
||||
))}
|
||||
|
||||
<OrbitControls
|
||||
enablePan={false}
|
||||
minDistance={8}
|
||||
maxDistance={18}
|
||||
maxPolarAngle={Math.PI / 2.2}
|
||||
minPolarAngle={Math.PI / 3}
|
||||
enableDamping
|
||||
dampingFactor={0.08}
|
||||
/>
|
||||
|
||||
<CameraRig target={selected} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Scene() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
position: "relative",
|
||||
background: "#030712",
|
||||
}}
|
||||
>
|
||||
{/* HUD Overlay */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
left: 20,
|
||||
color: "#e5e7eb",
|
||||
fontFamily: "Inter, sans-serif",
|
||||
letterSpacing: "0.08em",
|
||||
fontSize: 14,
|
||||
zIndex: 10,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
AeThex OS v5.0
|
||||
</motion.div>
|
||||
|
||||
<motion.button
|
||||
initial={{ opacity: 0, y: -6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut", delay: 0.05 }}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 12,
|
||||
right: 20,
|
||||
padding: "10px 16px",
|
||||
borderRadius: 10,
|
||||
border: "1px solid #38bdf8",
|
||||
background: "rgba(14, 165, 233, 0.12)",
|
||||
color: "#e0f2fe",
|
||||
fontWeight: 600,
|
||||
fontFamily: "Inter, sans-serif",
|
||||
cursor: "pointer",
|
||||
zIndex: 10,
|
||||
backdropFilter: "blur(6px)",
|
||||
}}
|
||||
onClick={() => alert("Connect Passport")}
|
||||
>
|
||||
Connect Passport
|
||||
</motion.button>
|
||||
|
||||
<Canvas
|
||||
shadows
|
||||
camera={{ position: [0, 3, 12], fov: 50, near: 0.1, far: 100 }}
|
||||
gl={{ antialias: true }}
|
||||
>
|
||||
<SceneContent />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
82
client/components/TitleBar.tsx
Normal file
82
client/components/TitleBar.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { useState } from "react";
|
||||
|
||||
export default function TitleBar() {
|
||||
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]();
|
||||
if (method === "togglePin") setPinned(res);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 36,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "0 12px",
|
||||
background: "#050814",
|
||||
color: "#9ca3af",
|
||||
WebkitAppRegion: "drag",
|
||||
borderBottom: "1px solid #0f172a",
|
||||
letterSpacing: "0.08em",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontFamily: "Space Mono, monospace" }}>AeThex Terminal</div>
|
||||
<div
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
WebkitAppRegion: "no-drag",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => call("togglePin")}
|
||||
style={btnStyle(pinned ? "#38bdf8" : "#1f2937")}
|
||||
title="Pin / Unpin"
|
||||
>
|
||||
{pinned ? "Pinned" : "Pin"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => call("minimize")}
|
||||
style={btnStyle("#1f2937")}
|
||||
title="Minimize"
|
||||
>
|
||||
_
|
||||
</button>
|
||||
<button
|
||||
onClick={() => call("maximize")}
|
||||
style={btnStyle("#1f2937")}
|
||||
title="Maximize / Restore"
|
||||
>
|
||||
▢
|
||||
</button>
|
||||
<button
|
||||
onClick={() => call("close")}
|
||||
style={btnStyle("#ef4444")}
|
||||
title="Close"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function btnStyle(bg: string) {
|
||||
return {
|
||||
border: "1px solid #111827",
|
||||
background: bg,
|
||||
color: "#e5e7eb",
|
||||
borderRadius: 6,
|
||||
padding: "4px 8px",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
minWidth: 46,
|
||||
} as React.CSSProperties;
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
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");
|
||||
|
|
@ -9,10 +11,24 @@ if (!container) {
|
|||
}
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
const hash = typeof window !== "undefined" ? window.location.hash : "";
|
||||
if (hash.startsWith("#/overlay")) {
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<Overlay />
|
||||
</ErrorBoundary>
|
||||
</StrictMode>,
|
||||
);
|
||||
} else {
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<DesktopShell>
|
||||
<App />
|
||||
</DesktopShell>
|
||||
</ErrorBoundary>
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,414 +1,19 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import Layout from "@/components/Layout";
|
||||
import SEO from "@/components/SEO";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import LoadingScreen from "@/components/LoadingScreen";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
Zap,
|
||||
Target,
|
||||
Users,
|
||||
TrendingUp,
|
||||
LayoutDashboard,
|
||||
Microscope,
|
||||
IdCard,
|
||||
Briefcase,
|
||||
Code,
|
||||
BookOpen,
|
||||
Network,
|
||||
} from "lucide-react";
|
||||
import Scene from "@/components/Scene";
|
||||
|
||||
export default function Index() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [activeSection, setActiveSection] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1200);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActiveSection((prev) => (prev + 1) % 4);
|
||||
}, 4000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
type FeatureCard = {
|
||||
title: string;
|
||||
description: string;
|
||||
external?: boolean;
|
||||
icon: any;
|
||||
color: string;
|
||||
link?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
// Ecosystem Audience Navigation
|
||||
const audienceNav = [
|
||||
{
|
||||
title: "Solutions for Business",
|
||||
description:
|
||||
"Custom software development, consulting, and digital transformation",
|
||||
icon: Briefcase,
|
||||
color: "from-blue-500 to-cyan-500",
|
||||
link: "/corp",
|
||||
audience: "Enterprise clients",
|
||||
},
|
||||
{
|
||||
title: "Explore & Learn",
|
||||
description:
|
||||
"Open source projects, educational workshops, and community resources",
|
||||
icon: BookOpen,
|
||||
color: "from-red-500 to-pink-500",
|
||||
link: "https://aethex.foundation",
|
||||
audience: "Developers & learners",
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
title: "Join the Network",
|
||||
description:
|
||||
"Professional networking, job board, and portfolio showcase for Roblox devs",
|
||||
icon: Network,
|
||||
color: "from-cyan-500 to-blue-500",
|
||||
link: "/dev-link",
|
||||
audience: "Individual developers",
|
||||
},
|
||||
{
|
||||
title: "Careers & Innovation",
|
||||
description:
|
||||
"Join our team and work on cutting-edge R&D and experimental features",
|
||||
icon: Zap,
|
||||
color: "from-yellow-500 to-amber-500",
|
||||
link: "/labs",
|
||||
audience: "Top-tier talent",
|
||||
},
|
||||
];
|
||||
|
||||
// Platform Features (Dashboard, Feed, etc.)
|
||||
const platformFeatures: FeatureCard[] = [
|
||||
{
|
||||
title: "Dashboard",
|
||||
description: "Your projects, applications, and rewards — in one place",
|
||||
icon: LayoutDashboard,
|
||||
color: "from-rose-500 to-amber-500",
|
||||
link: "/dashboard",
|
||||
tags: ["Overview", "Rewards"],
|
||||
},
|
||||
{
|
||||
title: "Community Feed",
|
||||
description: "Share progress, discover collaborators, and stay updated",
|
||||
icon: Users,
|
||||
color: "from-indigo-500 to-cyan-500",
|
||||
link: "/feed",
|
||||
tags: ["Posts", "Collab"],
|
||||
},
|
||||
{
|
||||
title: "Developer Passport",
|
||||
description: "A public profile with verifiable achievements",
|
||||
icon: IdCard,
|
||||
color: "from-fuchsia-500 to-violet-600",
|
||||
link: "/passport/me",
|
||||
tags: ["Profile", "Badges"],
|
||||
},
|
||||
{
|
||||
title: "Docs & CLI",
|
||||
description: "Guides, API reference, and tooling to ship faster",
|
||||
icon: Microscope,
|
||||
color: "from-lime-500 to-emerald-600",
|
||||
link: "/docs",
|
||||
tags: ["Guides", "API"],
|
||||
},
|
||||
];
|
||||
|
||||
// Showcase wins from each division
|
||||
const showcaseWins = [
|
||||
{
|
||||
division: "Corp",
|
||||
title: "Enterprise Transformation",
|
||||
description: "Helped Fortune 500 company modernize their tech stack",
|
||||
metric: "$2.5M revenue impact",
|
||||
color: "bg-blue-500/10 border-blue-400/30",
|
||||
},
|
||||
{
|
||||
division: "Foundation",
|
||||
title: "Community Education",
|
||||
description: "Launched Roblox development workshop series",
|
||||
metric: "500+ developers trained",
|
||||
color: "bg-red-500/10 border-red-400/30",
|
||||
},
|
||||
{
|
||||
division: "Labs",
|
||||
title: "AI Innovation",
|
||||
description: "Breakthrough in procedural game content generation",
|
||||
metric: "Published research paper",
|
||||
color: "bg-yellow-500/10 border-yellow-400/30",
|
||||
},
|
||||
{
|
||||
division: "Dev-Link",
|
||||
title: "Network Growth",
|
||||
description: "Reached 10K+ Roblox developers on the platform",
|
||||
metric: "1000+ jobs posted",
|
||||
color: "bg-cyan-500/10 border-cyan-400/30",
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingScreen
|
||||
message="Initializing AeThex OS..."
|
||||
showProgress={true}
|
||||
duration={1200}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
pageTitle="Home"
|
||||
description="AeThex: Building the Future of Immersive Digital Experiences. Consulting, Open Source, Developer Network, and Innovation."
|
||||
pageTitle="AeThex | Immersive OS"
|
||||
description="AeThex OS — Cyberpunk Animus command center for Nexus, GameForge, Foundation, Labs, and Corp."
|
||||
canonical={
|
||||
typeof window !== "undefined"
|
||||
? window.location.href
|
||||
: (undefined as any)
|
||||
}
|
||||
/>
|
||||
<Layout hideFooter>
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-screen flex items-start justify-center overflow-hidden pt-24 sm:pt-36">
|
||||
{/* Geometric Background */}
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-aethex-900/50 via-background to-aethex-800/50" />
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="relative w-96 h-96 opacity-5">
|
||||
<img
|
||||
src="https://docs.aethex.tech/~gitbook/image?url=https%3A%2F%2F1143808467-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Forganizations%252FDhUg3jal6kdpG645FzIl%252Fsites%252Fsite_HeOmR%252Flogo%252FqxDYz8Oj2SnwUTa8t3UB%252FAeThex%2520Origin%2520logo.png%3Falt%3Dmedia%26token%3D200e8ea2-0129-4cbe-b516-4a53f60c512b&width=512&dpr=1&quality=100&sign=6c7576ce&sv=2"
|
||||
alt="Background"
|
||||
className="w-full h-full animate-float"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute bg-aethex-400/20 animate-float"
|
||||
style={{
|
||||
width: `${10 + Math.random() * 20}px`,
|
||||
height: `${10 + Math.random() * 20}px`,
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 5}s`,
|
||||
animationDuration: `${4 + Math.random() * 3}s`,
|
||||
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="container mx-auto px-4 relative z-10 pb-24 sm:pb-28">
|
||||
<div className="text-center space-y-12">
|
||||
{/* Title & Value Prop */}
|
||||
<div className="space-y-6 animate-scale-in">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-bold">
|
||||
<span className="text-gradient-purple">AeThex</span>
|
||||
</h1>
|
||||
<h2 className="text-2xl lg:text-3xl text-gradient animate-fade-in">
|
||||
Building the Future of Immersive Digital Experiences
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-3xl mx-auto animate-slide-up">
|
||||
Software innovation, enterprise consulting, open source
|
||||
education, and professional networking—all in one ecosystem.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Audience Navigation Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto animate-slide-up">
|
||||
{audienceNav.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link key={index} to={item.link} className="group">
|
||||
<Card
|
||||
className="relative overflow-hidden rounded-xl border border-border/30 hover:border-aethex-400/50 bg-card/60 backdrop-blur-sm hover:translate-y-[-4px] hover:shadow-[0_12px_40px_rgba(80,80,120,0.3)] transition-all duration-300 h-full cursor-pointer"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 bg-gradient-to-br from-white/6 via-transparent to-white/0" />
|
||||
<CardContent className="p-6 flex flex-col items-center text-center gap-4 h-full justify-between">
|
||||
<div
|
||||
className={`relative w-14 h-14 rounded-lg bg-gradient-to-r ${item.color} grid place-items-center shadow-inner`}
|
||||
>
|
||||
<Icon className="h-7 w-7 text-white drop-shadow" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg mb-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{item.description}
|
||||
</p>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{item.audience}
|
||||
</Badge>
|
||||
</div>
|
||||
<div
|
||||
className={`h-[2px] w-12 rounded-full bg-gradient-to-r ${item.color} opacity-60 group-hover:opacity-100 transition-opacity`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Division Showcase */}
|
||||
<div className="max-w-6xl mx-auto animate-slide-up mt-8">
|
||||
<h3 className="text-2xl font-bold mb-6">Recent Wins</h3>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{showcaseWins.map((win, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`border ${win.color} bg-opacity-50`}
|
||||
>
|
||||
<CardContent className="pt-6">
|
||||
<Badge variant="outline" className="mb-3 text-xs">
|
||||
{win.division}
|
||||
</Badge>
|
||||
<h4 className="font-semibold text-sm mb-2">
|
||||
{win.title}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
{win.description}
|
||||
</p>
|
||||
<p className="text-xs font-bold text-aethex-300">
|
||||
{win.metric}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Platform Features Section */}
|
||||
<div className="max-w-6xl mx-auto animate-slide-up mt-12">
|
||||
<h3 className="text-2xl font-bold mb-6">Your Platform</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{platformFeatures.map((feature, index) => {
|
||||
const Icon = feature.icon;
|
||||
const isActive = activeSection === index;
|
||||
return (
|
||||
<Card
|
||||
key={`platform-${index}`}
|
||||
className={`relative overflow-hidden rounded-xl border transition-all duration-500 group animate-fade-in ${
|
||||
isActive
|
||||
? "border-aethex-500/60 glow-blue"
|
||||
: "border-border/30 hover:border-aethex-400/50"
|
||||
} bg-card/60 backdrop-blur-sm hover:translate-y-[-2px] hover:shadow-[0_8px_30px_rgba(80,80,120,0.25)]`}
|
||||
style={{ animationDelay: `${(index + 4) * 0.08}s` }}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 bg-gradient-to-br from-white/6 via-transparent to-white/0" />
|
||||
<CardContent className="p-5 sm:p-6 flex flex-col items-center text-center gap-3">
|
||||
<div
|
||||
className={`relative w-12 h-12 rounded-lg bg-gradient-to-r ${feature.color} grid place-items-center shadow-inner`}
|
||||
>
|
||||
<Icon className="h-6 w-6 text-white drop-shadow" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm tracking-wide">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<div className="flex flex-wrap justify-center gap-2 min-h-[24px]">
|
||||
{(feature.tags || []).slice(0, 2).map((tag, i) => (
|
||||
<Badge
|
||||
key={i}
|
||||
variant="outline"
|
||||
className="border-white/10 text-xs text-foreground/80"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||
{feature.description}
|
||||
</p>
|
||||
<div
|
||||
className={`mt-1 h-[2px] w-16 rounded-full bg-gradient-to-r ${feature.color} opacity-60 group-hover:opacity-100 transition-opacity`}
|
||||
/>
|
||||
{feature.link ? (
|
||||
<div className="pt-1">
|
||||
{feature.external ? (
|
||||
<a
|
||||
href={feature.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs inline-flex items-center gap-1 text-aethex-300 hover:text-aethex-200"
|
||||
>
|
||||
Explore
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
to={feature.link}
|
||||
className="text-xs inline-flex items-center gap-1 text-aethex-300 hover:text-aethex-200"
|
||||
>
|
||||
Explore
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-6 animate-slide-up mt-8">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90 glow-blue hover-lift text-base sm:text-lg px-6 py-4 sm:px-8 sm:py-6"
|
||||
>
|
||||
<Link
|
||||
to="/onboarding"
|
||||
className="flex items-center space-x-2 group"
|
||||
>
|
||||
<Sparkles className="h-5 w-5" />
|
||||
<span>Get Started</span>
|
||||
<ArrowRight className="h-5 w-5 transition-transform group-hover:translate-x-2" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="border-aethex-400/50 hover:border-aethex-400 hover-lift text-base sm:text-lg px-6 py-4 sm:px-8 sm:py-6"
|
||||
>
|
||||
<Link to="/explore">Explore Platform</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
<Scene />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
107
client/pages/Overlay.tsx
Normal file
107
client/pages/Overlay.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
||||
17
electron-builder.yml
Normal file
17
electron-builder.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
appId: com.aethex.desktop
|
||||
productName: AeThex Desktop Terminal
|
||||
files:
|
||||
- "dist/**"
|
||||
- "electron/**"
|
||||
- "services/**"
|
||||
asar: true
|
||||
npmRebuild: false
|
||||
buildDependenciesFromSource: false
|
||||
win:
|
||||
target: nsis
|
||||
icon: build/icons/icon.ico
|
||||
mac:
|
||||
target: dmg
|
||||
category: public.app-category.developer-tools
|
||||
icon: build/icons/icon.icns
|
||||
|
||||
139
electron/main.js
Normal file
139
electron/main.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
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);
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
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();
|
||||
});
|
||||
|
||||
13
electron/preload.js
Normal file
13
electron/preload.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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"),
|
||||
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)),
|
||||
});
|
||||
|
||||
3963
package-lock.json
generated
3963
package-lock.json
generated
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
|
@ -2,6 +2,10 @@
|
|||
"name": "fusion-starter",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"description": "AeThex Terminal — desktop sidecar with overlay, watcher, and local sentinel.",
|
||||
"author": "AeThex",
|
||||
"main": "electron/main.js",
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"dist/spa/*"
|
||||
|
|
@ -19,13 +23,18 @@
|
|||
"start": "node dist/server/production.mjs",
|
||||
"test": "vitest --run",
|
||||
"format.fix": "prettier --write .",
|
||||
"typecheck": "tsc"
|
||||
"typecheck": "tsc",
|
||||
"desktop:dev": "concurrently -k \"npm:desktop:renderer\" \"npm:desktop:electron\"",
|
||||
"desktop:renderer": "npm run dev -- --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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@builder.io/react": "^8.2.8",
|
||||
"@discord/embedded-app-sdk": "^2.4.0",
|
||||
"@supabase/supabase-js": "^2.53.0",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"dotenv": "^17.2.0",
|
||||
"ethers": "^6.13.0",
|
||||
"express": "^4.18.2",
|
||||
|
|
@ -34,6 +43,10 @@
|
|||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^32.2.0",
|
||||
"electron-builder": "^25.1.8",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
|
|
@ -62,7 +75,7 @@
|
|||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@react-three/drei": "^10.1.2",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^8.18.0",
|
||||
"@swc/core": "^1.11.24",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
|
|
@ -83,7 +96,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"framer-motion": "^12.6.2",
|
||||
"framer-motion": "^12.23.25",
|
||||
"globals": "^15.9.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.462.0",
|
||||
|
|
|
|||
11
services/pii-scrub.js
Normal file
11
services/pii-scrub.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const patterns = [
|
||||
/api[_-]?key\s*=\s*["'][A-Za-z0-9_\-]{16,}["']/gi,
|
||||
/\b[A-F0-9]{32,64}\b/gi,
|
||||
/\b\d{3}-\d{2}-\d{4}\b/g,
|
||||
/\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b/g,
|
||||
];
|
||||
|
||||
export function scrubPII(input) {
|
||||
return patterns.reduce((out, p) => out.replace(p, "[REDACTED]"), input);
|
||||
}
|
||||
|
||||
36
services/watcher.js
Normal file
36
services/watcher.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import chokidar from "chokidar";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { scrubPII } from "./pii-scrub.js";
|
||||
|
||||
let watcher = null;
|
||||
|
||||
export async function startWatcher(dir) {
|
||||
if (watcher) await watcher.close();
|
||||
watcher = chokidar.watch(["**/*.lua", "**/*.cs", "**/*.ts", "**/*.tsx"], {
|
||||
cwd: dir,
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
watcher.on("change", (filePath) => {
|
||||
const full = path.join(dir, filePath);
|
||||
try {
|
||||
const content = fs.readFileSync(full, "utf-8");
|
||||
const safe = scrubPII(content);
|
||||
console.log(`[watcher] ${filePath} changed`);
|
||||
// TODO: route safe content to renderer or local analysis pipeline
|
||||
console.log(safe.slice(0, 400));
|
||||
} catch (err) {
|
||||
console.error("watcher read error", err);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function stopWatcher() {
|
||||
if (watcher) await watcher.close();
|
||||
watcher = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue