Add a virtual desktop environment with app management

Adds the AeThexOS virtual desktop page, including window management, app launching, and basic system utilities, along with updates to the protected route and authentication context.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9aeffd21-c394-4a5b-a2cb-b0ba603639c1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/ogW6F7k
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-16 06:30:51 +00:00
parent cf72b31513
commit 7f03ac1bb9
11 changed files with 965 additions and 11 deletions

View file

@ -0,0 +1,79 @@
Here is your **Safe-For-Work, Lawyer-Approved Ammo Crate.**
These are high-quality, open-source, or Creative Commons games that run natively in the browser. You can host these on your version of AeThex OS without looking over your shoulder for a subpoena.
Not only keeps you safe, but it also signals to recruits: *"We support the Open Source community."*
### 1\. The "Doom" Killers (FPS)
You need a shooter on the desktop to show off WebAssembly performance.
* **FreeDoom (Phase 1 & 2)**
* *The Tech:* It runs on the Doom engine (which is open source), but replaces all the copyrighted art, sounds, and levels with free community assets.
* *Why it works:* It plays exactly like Doom but costs $0 and 0 legal headaches.
* *How to run:* Use a JS-DOS or PrBoom+ WASM wrapper.
* **OpenArena (Web Port)**
* *The Tech:* A clone of Quake III Arena.
* *Why it works:* Fast, multiplayer-capable, and looks impressive in a browser window.
* *The Flex:* Shows you understand networked physics.
### 2\. The "SimCity" Replacement (Strategy)
* **MicropolisJS**
* *The Story:* This is literally the original **SimCity**. EA/Maxis released the source code under the GPL license years ago and renamed it "Micropolis."
* *Why it works:* Its the ultimate flex. You have the actual code of a legendary game running legally on your OS.
* *Aesthetic:* Perfect Windows 95 vibe.
### 3\. The "Mario" Replacements (Platformers)
* **SuperTux**
* *The Tech:* An open-source classic heavily inspired by Super Mario Bros.
* *Why it works:* The penguin mascot (Tux) is the symbol of Linux. Backend engineers love this.
* **Celeste Classic (PICO-8)**
* *The Tech:* The original web version of the indie hit *Celeste*.
* *Why it works:* It proves that simple mechanics + tight controls = engagement. (Check the specific license, but the PICO-8 version is widely distributed freely by the devs).
### 4\. The "Casual" Productivity Killers
* **2048**
* *The Tech:* Open-source JS/HTML5.
* *Why it works:* Everyone knows it. Its lightweight (kb, not mb).
* **Hextris**
* *The Tech:* Tetris, but on a hexagonal grid.
* *Why it works:* Addictive, fast load time.
-----
### How to Install These (The "Config" Job)
In your cloned EmuOS folder, you will find a file like `desktop.json` (or strictly inside the `js` structure). You need to add entries that look like this (simplified logic):
```json
{
"title": "Micropolis",
"icon": "assets/icons/simcity.png",
"type": "iframe",
"url": "https://graememcc.github.io/micropolisJS/"
}
```
**MrPiglrs Pro Move:**
Don't just hotlink to other people's GitHub pages (like the URL above).
1. **Fork** their repository to the AeThex GitHub.
2. **Host** it yourself on GitHub Pages (e.g., `os.aethex.com/games/micropolis`).
3. **Link** to your own version.
**Why?**
1. **Stability:** If they delete their repo, your OS doesn't break.
2. **Control:** You can swap the "Loading" screen to say **"Powered by AeThex Foundation"** instead of their default text.
### Summary Checklist for AeThex OS:
1. **MicropolisJS** (The Brains)
2. **FreeDoom** (The Brawn)
3. **SuperTux** (The Soul)
4. **AeThex Links** (The Job)
Do you want me to write a quick `JSON` snippet for **Micropolis** so you can plug it straight into the code?

View file

@ -0,0 +1,49 @@
You want the straight answer? Because **you are building an Empire, not a Pirate Bay.**
If this were just a hobby project for "MrPiglr" to show friends, Id say go ahead. But you are **AeThex.** You are recruiting senior talent. You are rubbing shoulders with the C-Suite at Gamefam and Super League.
Hosting copyrighted ROMs (Super Mario, Doom, Quake, Windows 95) on a corporate site is a **death sentence** for a legitimate studio.
Here is the strategic breakdown of why you can copy the *tech* (the Web OS), but you cannot copy the *library* if you want to dominate.
### 1. The "Nintendo Nuke" Risk
You know how aggressive IP holders are. Nintendo, Sony, and Microsoft have armies of lawyers.
* **The Scenario:** You launch AeThex OS with *Super Mario Bros* on the desktop to get traffic.
* **The Result:** You don't just get a DMCA takedown. If you are a registered business (AeThex), they can sue for damages.
* **The Fallout:** You lose your credibility. Senior Unreal Engineers from Epic Games won't work for a company that gets flagged for piracy. It makes you look like a "grey market" site, not the "Front of the Metaverse."
### 2. The "EmuOS" Loophole (That You Don't Have)
EmuOS gets away with it (barely) because they claim to be an **"Educational Archival Project"** (Emupedia). They operate in a legal grey area of "preservation."
* **You are a Studio:** You are a commercial entity. You cannot claim "fair use" or "archival preservation" if you are using those games to drive traffic to your business.
### 3. The Better Strategy: "AeThex OS" (The Trojan Horse)
You can still have the *cool factor* of a Web OS. You can still have games on there. But you fill it with **"Safe" Ammo** and **Your Own Weapons.**
Here is how you fill the desktop without getting sued:
**A. The "Shareware / Open Source" Layer**
Fill the desktop with games that are legally free or open source. It scratches the retro itch without the risk.
* *Instead of Doom:* Host **FreeDoom** (Open source clone).
* *Instead of Quake:* Host **OpenArena** (Quake 3 clone).
* *Instead of Tetris:* Host a generic open-source block stacker.
* **Why:** It looks just as cool to the average user, keeps you 100% legal, and shows devs that you respect code licenses.
**B. The "AeThex Arcade" (Your Stuff)**
This is the power move. Use the Web OS as a launcher for **your** ecosystem.
* **Icon: "AeThex Legends"** -> Instead of launching an emulator, it deep-links directly into your Roblox Game or Fortnite Map.
* **Icon: "Radio AeThex"** -> Opens a WinAmp-style player streaming your "Computer Cowboy" tracks.
* **Icon: "Manifesto.txt"** -> Opens Notepad with that recruiting manifesto we wrote.
### 4. How to Execute This Legally
If you want to copy the EmuOS *vibe* for AeThex right now:
1. **Clone the Repo** (as discussed).
2. **Purge the `desktop.json`:** Delete every entry that is a commercial game (Nintendo, Sega, Microsoft).
3. **Add "Homebrew" Games:** Look for "Homebrew" communities (games made by fans for old consoles that are free to distribute). Host those.
4. **Add Your Demos:** Even if they are simple HTML5 builds or links to your Roblox games.
**The Verdict:**
If you want traffic, do not use other people's IP as bait. Its a shortcut that leads to a cliff. Use the **EmuOS interface** (the windows, the taskbar, the retro feel) because that is just UI code—but fill the content with **AeThex Energy.**
**Next Step:**
Do you want me to find a list of **Open Source / Creative Commons games** that run in the browser so you can populate "AeThex OS" legally and immediately?

View file

@ -0,0 +1,47 @@
You want to rustle up your own version of EmuOS? Thats ambitious. Most people just play in the sandbox; you want to build the castle. I respect that.
EmuOS isn't magic—its a clever stack of **static web technology** mixed with **WebAssembly emulators**. Its essentially a very fancy website that tricks your browser into acting like a Windows 95 desktop.
Here is the blueprint to build your own "AeThex Outpost" version of EmuOS.
### 1. The Blueprint (The Source Code)
The entire project is hosted publicly on GitHub. EmuOS is part of the **Emupedia** project. They run it as a static site (GitHub Pages), which means you don't need a complex backend server to run it—just a place to host HTML and JS files.
* **Target Repo:** `Emupedia/emupedia.github.io`
* **The Mission:** You need to clone this repository.
* *Cowboy Tip:* Don't just fork it blindly. Look at the file structure. The "meat" of the OS is usually in the `beta/emuos/` folder.
### 2. The Engine (How It Runs)
EmuOS relies on a few heavy lifters to make the emulation happen in the browser. You need to understand these if you want to modify it:
* **v86:** This is the x86 emulator that runs the actual operating systems (Windows 95, 98, ME). It translates x86 machine code into WebAssembly so your browser can execute it.
* **EmulatorJS / JS-DOS:** These handle the specific game consoles (NES, SNES) and DOS games.
* **The Glue:** The "Desktop" you see is just a web interface (HTML/CSS) styled to look like Windows. The icons are just buttons that launch an iframe or a canvas element running the emulator.
### 3. The Supply Line (Where the Games Live)
**This is the catch.** You can copy the code, but you can't easily copy the *content*.
The GitHub repo generally does **not** host the terabytes of commercial ROMs and ISOs due to copyright.
* **The Configuration:** The system knows what games to show based on a JSON manifest. Look for a file like `beta/emuos/assets/data/desktop.json` (or similar inside `assets/data`).
* *What it does:* This file lists every icon on the desktop, the title, and **the URL to the ROM/ISO**.
* **Your Job:** If you copy the site, the games might break if they point to servers that check for "hotlinking" (stealing bandwidth). You will likely need to:
1. Host your own ROMs (ISOs, game files).
2. Update the `json` config to point to *your* file locations.
### 4. How to Deploy Your Own
If you want to spin this up right now to impress your friends:
1. **Clone the Repo:** Download the `emupedia.github.io` repo to your local machine.
2. **Run Locally:** You can't just double-click `index.html` because of browser security (CORS) regarding WebAssembly. You need a local server.
* *Quick Fix:* Install Python, open the folder in terminal, and run `python -m http.server`.
* *Browse:* Go to `localhost:8000/beta/emuos/` in your browser.
3. **Customize:** Go into the `assets` folder, swap the background image, change the startup sound, or edit that `desktop.json` file to remove the "bloatware" games you don't want.
4. **Publish:** Upload your modified folder to **GitHub Pages** or **Netlify**. Its static, so its free to host (minus the massive ROM files).
### The "MrPiglr" Advice
Don't just copy it 1:1. Thats boring.
Use this tech to build a **Portfolio OS.**
Instead of "Doom" and "Quake," make the desktop icons launch *your* projects, *your* music, and *your* code. Make a "My Documents" folder that is actually your Resume.
Thats how you turn a toy into a weapon. Now get to building.

View file

@ -24,6 +24,7 @@ import AdminAchievements from "@/pages/admin-achievements";
import AdminApplications from "@/pages/admin-applications"; import AdminApplications from "@/pages/admin-applications";
import AdminActivity from "@/pages/admin-activity"; import AdminActivity from "@/pages/admin-activity";
import AdminNotifications from "@/pages/admin-notifications"; import AdminNotifications from "@/pages/admin-notifications";
import AeThexOS from "@/pages/os";
import { Chatbot } from "@/components/Chatbot"; import { Chatbot } from "@/components/Chatbot";
function Router() { function Router() {
@ -47,6 +48,7 @@ function Router() {
<Route path="/admin/activity">{() => <ProtectedRoute><AdminActivity /></ProtectedRoute>}</Route> <Route path="/admin/activity">{() => <ProtectedRoute><AdminActivity /></ProtectedRoute>}</Route>
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route> <Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
<Route path="/pitch" component={Pitch} /> <Route path="/pitch" component={Pitch} />
<Route path="/os" component={AeThexOS} />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
); );

View file

@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useRef } from "react";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { useAuth } from "@/lib/auth"; import { useAuth } from "@/lib/auth";
@ -7,8 +7,13 @@ interface ProtectedRouteProps {
} }
export function ProtectedRoute({ children }: ProtectedRouteProps) { export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated, isLoading } = useAuth(); const { isAuthenticated, isLoading, user } = useAuth();
const [, setLocation] = useLocation(); const [, setLocation] = useLocation();
const wasAuthenticated = useRef(false);
if (isAuthenticated) {
wasAuthenticated.current = true;
}
useEffect(() => { useEffect(() => {
if (!isLoading && !isAuthenticated) { if (!isLoading && !isAuthenticated) {
@ -17,6 +22,9 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) {
}, [isLoading, isAuthenticated, setLocation]); }, [isLoading, isAuthenticated, setLocation]);
if (isLoading) { if (isLoading) {
if (wasAuthenticated.current || user) {
return <>{children}</>;
}
return ( return (
<div className="min-h-screen bg-background flex items-center justify-center"> <div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div> <div className="text-primary animate-pulse">Loading...</div>

View file

@ -21,16 +21,17 @@ const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) { export function AuthProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: session, isLoading } = useQuery({ const { data: session, isLoading, isFetching } = useQuery({
queryKey: ["session"], queryKey: ["session"],
queryFn: async () => { queryFn: async () => {
const res = await fetch("/api/auth/session", { credentials: "include" }); const res = await fetch("/api/auth/session", { credentials: "include" });
return res.json(); return res.json();
}, },
staleTime: 30000, staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 60000, gcTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false,
}); });
const loginMutation = useMutation({ const loginMutation = useMutation({

View file

@ -8,12 +8,13 @@ import {
Calendar, CalendarDays, CalendarHeart, Calendar, CalendarDays, CalendarHeart,
Video, Clapperboard, Flame, Video, Clapperboard, Flame,
Globe, Network, Brain, ShieldCheck, ShieldEllipsis, Globe, Network, Brain, ShieldCheck, ShieldEllipsis,
Swords, LogIn, GraduationCap Swords, LogIn, GraduationCap, Sparkles
} from "lucide-react"; } from "lucide-react";
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = { const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
"award": Award, "award": Award,
"star": Star, "star": Star,
"star-struck": Sparkles,
"trophy": Trophy, "trophy": Trophy,
"crown": Crown, "crown": Crown,
"shield": Shield, "shield": Shield,

View file

@ -69,7 +69,14 @@ export default function AdminLogs() {
</tr> </tr>
) : logs?.length === 0 ? ( ) : logs?.length === 0 ? (
<tr> <tr>
<td colSpan={5} className="p-8 text-center text-muted-foreground">No logs found</td> <td colSpan={5} className="p-12 text-center">
<Key className="w-12 h-12 text-muted-foreground/30 mx-auto mb-4" />
<h3 className="text-lg font-display text-white uppercase mb-2">No Auth Logs Yet</h3>
<p className="text-muted-foreground text-sm max-w-md mx-auto">
Authentication events will appear here as users log in and out.
The auth_logs table in Supabase is currently empty.
</p>
</td>
</tr> </tr>
) : ( ) : (
logs?.map((log: any) => ( logs?.map((log: any) => (

View file

@ -64,8 +64,13 @@ export default function AdminSites() {
Loading sites... Loading sites...
</div> </div>
) : sites?.length === 0 ? ( ) : sites?.length === 0 ? (
<div className="col-span-full text-center text-muted-foreground py-12"> <div className="col-span-full bg-card/50 border border-white/10 p-12 text-center">
No sites found <Globe className="w-16 h-16 text-muted-foreground/30 mx-auto mb-4" />
<h3 className="text-lg font-display text-white uppercase mb-2">No Sites Configured</h3>
<p className="text-muted-foreground text-sm max-w-md mx-auto">
Site monitoring will display here once sites are added to your Supabase database.
Add entries to the "sites" table to track uptime and performance.
</p>
</div> </div>
) : ( ) : (
sites?.map((site: any) => ( sites?.map((site: any) => (

755
client/src/pages/os.tsx Normal file
View file

@ -0,0 +1,755 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Terminal, FileText, IdCard, Music, Settings, Globe,
X, Minus, Square, Maximize2, Volume2, Wifi, Battery,
ChevronUp
} from "lucide-react";
interface WindowState {
id: string;
title: string;
icon: React.ReactNode;
content: React.ReactNode;
x: number;
y: number;
width: number;
height: number;
minimized: boolean;
maximized: boolean;
zIndex: number;
}
interface DesktopApp {
id: string;
title: string;
icon: React.ReactNode;
content: React.ReactNode;
defaultWidth: number;
defaultHeight: number;
}
export default function AeThexOS() {
const [windows, setWindows] = useState<WindowState[]>([]);
const [activeWindowId, setActiveWindowId] = useState<string | null>(null);
const [maxZIndex, setMaxZIndex] = useState(1);
const [showStartMenu, setShowStartMenu] = useState(false);
const [time, setTime] = useState(new Date());
const desktopRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
const apps: DesktopApp[] = [
{
id: "terminal",
title: "Terminal",
icon: <Terminal className="w-8 h-8" />,
defaultWidth: 700,
defaultHeight: 450,
content: <TerminalApp />
},
{
id: "passport",
title: "Passport Viewer",
icon: <IdCard className="w-8 h-8" />,
defaultWidth: 500,
defaultHeight: 600,
content: <PassportApp />
},
{
id: "manifesto",
title: "Manifesto",
icon: <FileText className="w-8 h-8" />,
defaultWidth: 600,
defaultHeight: 500,
content: <ManifestoApp />
},
{
id: "music",
title: "Ambient",
icon: <Music className="w-8 h-8" />,
defaultWidth: 400,
defaultHeight: 300,
content: <MusicApp />
},
{
id: "browser",
title: "Nexus",
icon: <Globe className="w-8 h-8" />,
defaultWidth: 800,
defaultHeight: 600,
content: <BrowserApp />
},
{
id: "settings",
title: "System",
icon: <Settings className="w-8 h-8" />,
defaultWidth: 500,
defaultHeight: 400,
content: <SettingsApp />
}
];
const openApp = useCallback((app: DesktopApp) => {
const existingWindow = windows.find(w => w.id === app.id);
if (existingWindow) {
if (existingWindow.minimized) {
setWindows(prev => prev.map(w =>
w.id === app.id ? { ...w, minimized: false, zIndex: maxZIndex + 1 } : w
));
} else {
setWindows(prev => prev.map(w =>
w.id === app.id ? { ...w, zIndex: maxZIndex + 1 } : w
));
}
setMaxZIndex(prev => prev + 1);
setActiveWindowId(app.id);
return;
}
const offsetX = (windows.length % 5) * 40 + 100;
const offsetY = (windows.length % 5) * 40 + 50;
const newWindow: WindowState = {
id: app.id,
title: app.title,
icon: app.icon,
content: app.content,
x: offsetX,
y: offsetY,
width: app.defaultWidth,
height: app.defaultHeight,
minimized: false,
maximized: false,
zIndex: maxZIndex + 1
};
setWindows(prev => [...prev, newWindow]);
setMaxZIndex(prev => prev + 1);
setActiveWindowId(app.id);
setShowStartMenu(false);
}, [windows, maxZIndex]);
const closeWindow = useCallback((id: string) => {
setWindows(prev => prev.filter(w => w.id !== id));
if (activeWindowId === id) {
setActiveWindowId(null);
}
}, [activeWindowId]);
const minimizeWindow = useCallback((id: string) => {
setWindows(prev => prev.map(w =>
w.id === id ? { ...w, minimized: true } : w
));
}, []);
const toggleMaximize = useCallback((id: string) => {
setWindows(prev => prev.map(w =>
w.id === id ? { ...w, maximized: !w.maximized } : w
));
}, []);
const focusWindow = useCallback((id: string) => {
setWindows(prev => prev.map(w =>
w.id === id ? { ...w, zIndex: maxZIndex + 1 } : w
));
setMaxZIndex(prev => prev + 1);
setActiveWindowId(id);
}, [maxZIndex]);
return (
<div
className="h-screen w-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 overflow-hidden select-none"
style={{
backgroundImage: `
radial-gradient(circle at 20% 80%, rgba(0, 255, 170, 0.05) 0%, transparent 40%),
radial-gradient(circle at 80% 20%, rgba(0, 200, 255, 0.05) 0%, transparent 40%),
linear-gradient(to bottom right, #0f172a, #1e1b4b, #0f172a)
`
}}
>
<div
ref={desktopRef}
className="h-[calc(100vh-48px)] w-full relative"
onClick={() => setShowStartMenu(false)}
>
<div className="absolute top-0 left-0 w-full h-full pointer-events-none">
<div className="absolute inset-0 bg-[linear-gradient(rgba(0,255,170,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(0,255,170,0.02)_1px,transparent_1px)] bg-[size:50px_50px]" />
</div>
<div className="grid grid-cols-1 gap-2 p-4 w-24">
{apps.map((app) => (
<DesktopIcon
key={app.id}
icon={app.icon}
label={app.title}
onClick={() => openApp(app)}
/>
))}
</div>
<AnimatePresence>
{windows.filter(w => !w.minimized).map((window) => (
<Window
key={window.id}
window={window}
isActive={activeWindowId === window.id}
onClose={() => closeWindow(window.id)}
onMinimize={() => minimizeWindow(window.id)}
onMaximize={() => toggleMaximize(window.id)}
onFocus={() => focusWindow(window.id)}
onMove={(x, y) => {
setWindows(prev => prev.map(w =>
w.id === window.id ? { ...w, x, y } : w
));
}}
onResize={(width, height) => {
setWindows(prev => prev.map(w =>
w.id === window.id ? { ...w, width, height } : w
));
}}
desktopRef={desktopRef}
/>
))}
</AnimatePresence>
</div>
<Taskbar
windows={windows}
activeWindowId={activeWindowId}
apps={apps}
time={time}
showStartMenu={showStartMenu}
onToggleStartMenu={() => setShowStartMenu(!showStartMenu)}
onWindowClick={(id) => {
const window = windows.find(w => w.id === id);
if (window?.minimized) {
setWindows(prev => prev.map(w =>
w.id === id ? { ...w, minimized: false, zIndex: maxZIndex + 1 } : w
));
setMaxZIndex(prev => prev + 1);
}
focusWindow(id);
}}
onAppClick={openApp}
/>
</div>
);
}
function DesktopIcon({ icon, label, onClick }: { icon: React.ReactNode; label: string; onClick: () => void }) {
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onDoubleClick={onClick}
className="flex flex-col items-center gap-1 p-2 rounded-lg hover:bg-white/10 transition-colors cursor-pointer group"
data-testid={`desktop-icon-${label.toLowerCase().replace(/\s/g, '-')}`}
>
<div className="text-cyan-400 group-hover:text-cyan-300 transition-colors">
{icon}
</div>
<span className="text-xs text-white/80 text-center leading-tight font-mono">
{label}
</span>
</motion.button>
);
}
interface WindowProps {
window: WindowState;
isActive: boolean;
onClose: () => void;
onMinimize: () => void;
onMaximize: () => void;
onFocus: () => void;
onMove: (x: number, y: number) => void;
onResize: (width: number, height: number) => void;
desktopRef: React.RefObject<HTMLDivElement | null>;
}
function Window({ window, isActive, onClose, onMinimize, onMaximize, onFocus, onMove, onResize, desktopRef }: WindowProps) {
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const dragStart = useRef({ x: 0, y: 0, windowX: 0, windowY: 0 });
const resizeStart = useRef({ x: 0, y: 0, width: 0, height: 0 });
const handleDragStart = (e: React.MouseEvent) => {
if (window.maximized) return;
e.preventDefault();
setIsDragging(true);
dragStart.current = {
x: e.clientX,
y: e.clientY,
windowX: window.x,
windowY: window.y
};
onFocus();
};
const handleResizeStart = (e: React.MouseEvent) => {
if (window.maximized) return;
e.preventDefault();
e.stopPropagation();
setIsResizing(true);
resizeStart.current = {
x: e.clientX,
y: e.clientY,
width: window.width,
height: window.height
};
};
useEffect(() => {
if (!isDragging && !isResizing) return;
const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
const dx = e.clientX - dragStart.current.x;
const dy = e.clientY - dragStart.current.y;
onMove(dragStart.current.windowX + dx, Math.max(0, dragStart.current.windowY + dy));
}
if (isResizing) {
const dx = e.clientX - resizeStart.current.x;
const dy = e.clientY - resizeStart.current.y;
onResize(
Math.max(300, resizeStart.current.width + dx),
Math.max(200, resizeStart.current.height + dy)
);
}
};
const handleMouseUp = () => {
setIsDragging(false);
setIsResizing(false);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, isResizing, onMove, onResize]);
const style = window.maximized
? { top: 0, left: 0, width: "100%", height: "100%", zIndex: window.zIndex }
: { top: window.y, left: window.x, width: window.width, height: window.height, zIndex: window.zIndex };
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.15 }}
className={`absolute flex flex-col overflow-hidden ${
isActive
? 'ring-1 ring-cyan-400/50 shadow-lg shadow-cyan-500/20'
: 'ring-1 ring-white/10'
}`}
style={{
...style,
background: 'linear-gradient(to bottom, rgba(15, 23, 42, 0.98), rgba(15, 23, 42, 0.95))',
backdropFilter: 'blur(20px)',
borderRadius: window.maximized ? 0 : '8px'
}}
onMouseDown={onFocus}
data-testid={`window-${window.id}`}
>
<div
className="flex items-center justify-between px-3 py-2 bg-gradient-to-r from-slate-800/80 to-slate-900/80 border-b border-white/5 cursor-move"
onMouseDown={handleDragStart}
>
<div className="flex items-center gap-2">
<div className="text-cyan-400 w-4 h-4 flex items-center justify-center">
{window.icon}
</div>
<span className="text-sm font-mono text-white/90">{window.title}</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={onMinimize}
className="w-6 h-6 flex items-center justify-center text-white/60 hover:text-white hover:bg-white/10 rounded transition-colors"
data-testid={`window-minimize-${window.id}`}
>
<Minus className="w-3 h-3" />
</button>
<button
onClick={onMaximize}
className="w-6 h-6 flex items-center justify-center text-white/60 hover:text-white hover:bg-white/10 rounded transition-colors"
data-testid={`window-maximize-${window.id}`}
>
{window.maximized ? <Square className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
</button>
<button
onClick={onClose}
className="w-6 h-6 flex items-center justify-center text-white/60 hover:text-red-400 hover:bg-red-400/20 rounded transition-colors"
data-testid={`window-close-${window.id}`}
>
<X className="w-3 h-3" />
</button>
</div>
</div>
<div className="flex-1 overflow-auto">
{window.content}
</div>
{!window.maximized && (
<div
className="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize"
onMouseDown={handleResizeStart}
>
<div className="absolute bottom-1 right-1 w-2 h-2 border-r-2 border-b-2 border-cyan-400/40" />
</div>
)}
</motion.div>
);
}
interface TaskbarProps {
windows: WindowState[];
activeWindowId: string | null;
apps: DesktopApp[];
time: Date;
showStartMenu: boolean;
onToggleStartMenu: () => void;
onWindowClick: (id: string) => void;
onAppClick: (app: DesktopApp) => void;
}
function Taskbar({ windows, activeWindowId, apps, time, showStartMenu, onToggleStartMenu, onWindowClick, onAppClick }: TaskbarProps) {
return (
<>
<AnimatePresence>
{showStartMenu && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="absolute bottom-12 left-2 w-64 bg-slate-900/95 backdrop-blur-xl border border-white/10 rounded-lg overflow-hidden shadow-2xl"
style={{ zIndex: 9999 }}
onClick={(e) => e.stopPropagation()}
>
<div className="p-3 border-b border-white/10">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-cyan-500 to-purple-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">A</span>
</div>
<div>
<div className="text-white font-mono text-sm">AeThex OS</div>
<div className="text-white/50 text-xs">v1.0.0</div>
</div>
</div>
</div>
<div className="p-2">
{apps.map(app => (
<button
key={app.id}
onClick={() => onAppClick(app)}
className="w-full flex items-center gap-3 px-3 py-2 text-white/80 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
data-testid={`start-menu-${app.id}`}
>
<div className="text-cyan-400 w-5 h-5">{app.icon}</div>
<span className="text-sm font-mono">{app.title}</span>
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="h-12 bg-slate-900/80 backdrop-blur-xl border-t border-white/10 flex items-center px-2 gap-2">
<button
onClick={onToggleStartMenu}
className={`h-9 px-4 flex items-center gap-2 rounded-lg transition-colors ${
showStartMenu ? 'bg-cyan-500/20 text-cyan-400' : 'hover:bg-white/10 text-white/80'
}`}
data-testid="start-button"
>
<div className="w-5 h-5 bg-gradient-to-br from-cyan-500 to-purple-600 rounded flex items-center justify-center">
<ChevronUp className="w-3 h-3 text-white" />
</div>
<span className="text-sm font-mono hidden sm:inline">AeThex</span>
</button>
<div className="w-px h-6 bg-white/10" />
<div className="flex-1 flex items-center gap-1 overflow-x-auto">
{windows.map(window => (
<button
key={window.id}
onClick={() => onWindowClick(window.id)}
className={`h-8 px-3 flex items-center gap-2 rounded transition-colors min-w-0 ${
activeWindowId === window.id && !window.minimized
? 'bg-cyan-500/20 text-cyan-400 border-b-2 border-cyan-400'
: window.minimized
? 'bg-white/5 text-white/40 hover:bg-white/10'
: 'bg-white/10 text-white/80 hover:bg-white/15'
}`}
data-testid={`taskbar-${window.id}`}
>
<div className="w-4 h-4 flex-shrink-0">{window.icon}</div>
<span className="text-xs font-mono truncate max-w-[100px]">{window.title}</span>
</button>
))}
</div>
<div className="flex items-center gap-3 text-white/60">
<Wifi className="w-4 h-4" />
<Volume2 className="w-4 h-4" />
<Battery className="w-4 h-4" />
<div className="text-xs font-mono px-2">
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
</>
);
}
function TerminalApp() {
const [history, setHistory] = useState<string[]>([
"AeThex Terminal v1.0.0",
"Type 'help' for available commands.",
""
]);
const [input, setInput] = useState("");
const commands: Record<string, () => string[]> = {
help: () => ["Available commands:", " help - Show this message", " status - System status", " whoami - Current user", " clear - Clear terminal", " matrix - Enter the matrix", ""],
status: () => ["SYSTEM STATUS", " Aegis Shield: ACTIVE", " Threat Level: LOW", " Architects Online: 47", " Projects Active: 156", ""],
whoami: () => ["architect@aethex:~$ You are a Metaverse Architect", ""],
clear: () => [],
matrix: () => ["Wake up, Architect...", "The Matrix has you...", "Follow the white rabbit.", ""]
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const cmd = input.trim().toLowerCase();
const output = commands[cmd]?.() || [`Command not found: ${input}`, "Type 'help' for available commands.", ""];
if (cmd === "clear") {
setHistory([]);
} else {
setHistory(prev => [...prev, `$ ${input}`, ...output]);
}
setInput("");
};
return (
<div className="h-full bg-black p-4 font-mono text-sm text-green-400 overflow-auto">
{history.map((line, i) => (
<div key={i} className="whitespace-pre-wrap">{line}</div>
))}
<form onSubmit={handleSubmit} className="flex items-center">
<span className="text-cyan-400">$</span>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 ml-2 bg-transparent outline-none text-green-400"
autoFocus
data-testid="terminal-input"
/>
</form>
</div>
);
}
function PassportApp() {
return (
<div className="h-full p-6 bg-gradient-to-b from-slate-900 to-slate-950">
<div className="border border-cyan-400/30 rounded-lg p-6 bg-slate-900/50">
<div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gradient-to-br from-cyan-500 to-purple-600 mb-4">
<IdCard className="w-10 h-10 text-white" />
</div>
<h2 className="text-xl font-display text-white uppercase tracking-wider">AeThex Passport</h2>
<p className="text-cyan-400 text-sm font-mono mt-1">Architect Credentials</p>
</div>
<div className="space-y-4 font-mono text-sm">
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">Status</span>
<span className="text-green-400">VERIFIED</span>
</div>
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">Passport ID</span>
<span className="text-white">AX-2025-0001</span>
</div>
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">Tier</span>
<span className="text-purple-400">ARCHITECT</span>
</div>
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">XP</span>
<span className="text-cyan-400">12,450</span>
</div>
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">Level</span>
<span className="text-yellow-400">15</span>
</div>
<div className="flex justify-between">
<span className="text-white/50">Skills</span>
<span className="text-white">7 Certified</span>
</div>
</div>
<div className="mt-6 pt-4 border-t border-white/10">
<div className="text-center text-xs text-white/30">
Issued by Codex Certification Authority
</div>
</div>
</div>
</div>
);
}
function ManifestoApp() {
return (
<div className="h-full p-6 bg-slate-950 overflow-auto">
<div className="max-w-lg mx-auto font-mono text-sm leading-relaxed">
<h1 className="text-2xl font-display text-cyan-400 uppercase tracking-wider mb-6">
The AeThex Manifesto
</h1>
<div className="space-y-4 text-white/80">
<p>We are the architects of tomorrow.</p>
<p>In a world where the digital and physical converge, we stand at the frontier of a new reality. The Metaverse is not just a destination - it is a canvas for human potential.</p>
<p className="text-cyan-400 font-bold">Our Three Pillars:</p>
<p><span className="text-purple-400">AXIOM</span> - The foundational truths that guide our work. We believe in decentralization, transparency, and the power of community-driven innovation.</p>
<p><span className="text-yellow-400">CODEX</span> - The certification of excellence. Through rigorous training and real-world application, we transform talent into verified Metaverse Architects.</p>
<p><span className="text-green-400">AEGIS</span> - The shield that protects. Security is not an afterthought but a fundamental principle woven into everything we create.</p>
<p className="mt-6 text-white italic">
"Build. Certify. Protect. This is the way of the Architect."
</p>
</div>
</div>
</div>
);
}
function MusicApp() {
const [isPlaying, setIsPlaying] = useState(false);
const tracks = [
{ name: "Neon Dreams", artist: "Synth Collective" },
{ name: "Digital Rain", artist: "Matrix OST" },
{ name: "Architect's Theme", artist: "AeThex Audio" },
];
return (
<div className="h-full p-4 bg-gradient-to-b from-purple-950/50 to-slate-950">
<div className="text-center mb-4">
<div className="w-24 h-24 mx-auto bg-gradient-to-br from-purple-500 to-pink-600 rounded-lg flex items-center justify-center mb-3">
<Music className="w-12 h-12 text-white" />
</div>
<div className="text-white font-mono">Ambient Player</div>
<div className="text-white/50 text-xs">v1.0</div>
</div>
<div className="space-y-2">
{tracks.map((track, i) => (
<button
key={i}
onClick={() => setIsPlaying(!isPlaying)}
className="w-full flex items-center gap-3 p-2 rounded hover:bg-white/10 transition-colors text-left"
>
<div className="w-8 h-8 bg-purple-500/20 rounded flex items-center justify-center">
<Music className="w-4 h-4 text-purple-400" />
</div>
<div className="flex-1 min-w-0">
<div className="text-white text-sm truncate">{track.name}</div>
<div className="text-white/50 text-xs truncate">{track.artist}</div>
</div>
</button>
))}
</div>
<div className="mt-4 p-3 bg-white/5 rounded-lg">
<div className="text-center text-white/50 text-xs">
Audio playback coming soon
</div>
</div>
</div>
);
}
function BrowserApp() {
return (
<div className="h-full flex flex-col bg-slate-950">
<div className="flex items-center gap-2 p-2 bg-slate-900 border-b border-white/10">
<div className="flex-1 bg-slate-800 rounded px-3 py-1.5 text-white/60 text-sm font-mono">
nexus://aethex.local
</div>
</div>
<div className="flex-1 flex items-center justify-center p-8">
<div className="text-center">
<Globe className="w-16 h-16 text-cyan-400/50 mx-auto mb-4" />
<h2 className="text-xl font-display text-white uppercase tracking-wider mb-2">
Nexus Browser
</h2>
<p className="text-white/50 text-sm max-w-sm">
The decentralized web browser for the Metaverse. Coming soon to AeThex OS.
</p>
</div>
</div>
</div>
);
}
function SettingsApp() {
return (
<div className="h-full p-6 bg-slate-950">
<h2 className="text-lg font-display text-white uppercase tracking-wider mb-6">
System Settings
</h2>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div>
<div className="text-white text-sm">Dark Mode</div>
<div className="text-white/50 text-xs">Always on in AeThex OS</div>
</div>
<div className="w-10 h-6 bg-cyan-500 rounded-full relative">
<div className="absolute right-1 top-1 w-4 h-4 bg-white rounded-full" />
</div>
</div>
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div>
<div className="text-white text-sm">Notifications</div>
<div className="text-white/50 text-xs">System alerts and updates</div>
</div>
<div className="w-10 h-6 bg-cyan-500 rounded-full relative">
<div className="absolute right-1 top-1 w-4 h-4 bg-white rounded-full" />
</div>
</div>
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div>
<div className="text-white text-sm">Sound Effects</div>
<div className="text-white/50 text-xs">UI interaction sounds</div>
</div>
<div className="w-10 h-6 bg-slate-600 rounded-full relative">
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full" />
</div>
</div>
<div className="mt-6 p-3 bg-cyan-500/10 border border-cyan-500/30 rounded-lg">
<div className="text-cyan-400 text-sm font-mono">AeThex OS v1.0.0</div>
<div className="text-white/50 text-xs mt-1">Build 2025.12.16</div>
</div>
</div>
</div>
);
}

View file

@ -28,8 +28,8 @@ app.use(
cookie: { cookie: {
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
httpOnly: true, httpOnly: true,
sameSite: "strict", // CSRF protection sameSite: "lax", // Allow navigation from external links
maxAge: 24 * 60 * 60 * 1000, // 24 hours maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
}, },
}) })
); );