diff --git a/client/index.html b/client/index.html index 58728ca..cb1cd2b 100644 --- a/client/index.html +++ b/client/index.html @@ -28,5 +28,14 @@
+ diff --git a/client/public/opengraph.jpg b/client/public/opengraph.jpg index dd895df..93b52be 100644 Binary files a/client/public/opengraph.jpg and b/client/public/opengraph.jpg differ diff --git a/client/public/sw.js b/client/public/sw.js new file mode 100644 index 0000000..f2a4eda --- /dev/null +++ b/client/public/sw.js @@ -0,0 +1,67 @@ +const CACHE_NAME = 'aethex-os-v1'; +const STATIC_ASSETS = [ + '/', + '/manifest.json', + '/favicon.png' +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(STATIC_ASSETS); + }) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames + .filter((name) => name !== CACHE_NAME) + .map((name) => caches.delete(name)) + ); + }) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + if (request.method !== 'GET') return; + + if (url.pathname.startsWith('/api/')) { + event.respondWith( + fetch(request) + .then((response) => { + if (response.ok) { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(request, responseClone); + }); + } + return response; + }) + .catch(() => caches.match(request)) + ); + return; + } + + event.respondWith( + caches.match(request).then((cached) => { + const fetchPromise = fetch(request).then((response) => { + if (response.ok) { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(request, responseClone); + }); + } + return response; + }); + return cached || fetchPromise; + }) + ); +}); diff --git a/client/src/pages/admin-architects.tsx b/client/src/pages/admin-architects.tsx index f41a1f0..60c19a8 100644 --- a/client/src/pages/admin-architects.tsx +++ b/client/src/pages/admin-architects.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useMemo } from "react"; import { motion } from "framer-motion"; import { Link, useLocation } from "wouter"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; @@ -6,7 +6,8 @@ import { useAuth } from "@/lib/auth"; import { Users, FileCode, Shield, Activity, LogOut, Home, BarChart3, Settings, User, Search, - CheckCircle, XCircle, Eye, Edit, ChevronRight + CheckCircle, XCircle, Eye, Edit, ChevronRight, + Download, Trash2, Square, CheckSquare } from "lucide-react"; export default function AdminArchitects() { @@ -14,6 +15,9 @@ export default function AdminArchitects() { const [, setLocation] = useLocation(); const [searchQuery, setSearchQuery] = useState(""); const [selectedProfile, setSelectedProfile] = useState(null); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [roleFilter, setRoleFilter] = useState("all"); + const [verifiedFilter, setVerifiedFilter] = useState("all"); const queryClient = useQueryClient(); const { data: profiles, isLoading } = useQuery({ @@ -40,10 +44,70 @@ export default function AdminArchitects() { }, }); - const filteredProfiles = profiles?.filter((p: any) => - p.username?.toLowerCase().includes(searchQuery.toLowerCase()) || - p.email?.toLowerCase().includes(searchQuery.toLowerCase()) - ) || []; + const filteredProfiles = useMemo(() => { + if (!profiles) return []; + return profiles.filter((p: any) => { + const matchesSearch = p.username?.toLowerCase().includes(searchQuery.toLowerCase()) || + p.email?.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesRole = roleFilter === "all" || p.role === roleFilter; + const matchesVerified = verifiedFilter === "all" || + (verifiedFilter === "verified" && p.is_verified) || + (verifiedFilter === "unverified" && !p.is_verified); + return matchesSearch && matchesRole && matchesVerified; + }); + }, [profiles, searchQuery, roleFilter, verifiedFilter]); + + const toggleSelectAll = () => { + if (selectedIds.size === filteredProfiles.length) { + setSelectedIds(new Set()); + } else { + setSelectedIds(new Set(filteredProfiles.map((p: any) => p.id))); + } + }; + + const toggleSelect = (id: string) => { + const newSet = new Set(selectedIds); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + setSelectedIds(newSet); + }; + + const exportToCSV = () => { + const dataToExport = selectedIds.size > 0 + ? filteredProfiles.filter((p: any) => selectedIds.has(p.id)) + : filteredProfiles; + + const headers = ["Username", "Email", "Role", "Level", "XP", "Status", "Verified"]; + const csvContent = [ + headers.join(","), + ...dataToExport.map((p: any) => [ + p.username || "", + p.email || "", + p.role || "", + p.level || 0, + p.total_xp || 0, + p.status || "", + p.is_verified ? "Yes" : "No" + ].join(",")) + ].join("\n"); + + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `architects_${new Date().toISOString().split("T")[0]}.csv`; + link.click(); + }; + + const bulkVerify = async (verify: boolean) => { + const ids = Array.from(selectedIds); + for (const id of ids) { + await updateProfileMutation.mutateAsync({ id, updates: { is_verified: verify } }); + } + setSelectedIds(new Set()); + }; const handleLogout = async () => { await logout(); @@ -100,35 +164,115 @@ export default function AdminArchitects() { {/* Main Content */}
-
+

Architects

- {profiles?.length || 0} registered architects + {filteredProfiles.length} of {profiles?.length || 0} architects

- {/* Search */} -
- - setSearchQuery(e.target.value)} - className="bg-card border border-white/10 pl-10 pr-4 py-2 text-sm text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none w-64" - data-testid="input-search" - /> +
+ + + + +
+ + setSearchQuery(e.target.value)} + className="bg-card border border-white/10 pl-10 pr-4 py-2 text-sm text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none w-64" + data-testid="input-search" + /> +
+ {selectedIds.size > 0 && ( +
+ {selectedIds.size} selected + + + +
+ +
+ )} + + {selectedIds.size === 0 && ( +
+ +
+ )} + {/* Table */}
+ @@ -141,19 +285,28 @@ export default function AdminArchitects() { {isLoading ? ( - ) : filteredProfiles.length === 0 ? ( - ) : ( filteredProfiles.map((profile: any) => ( - + +
+ + User Role Level
+ Loading...
+ No architects found
+ +
>(() => { + const saved = localStorage.getItem('aethex-widget-visibility'); + return saved ? JSON.parse(saved) : { clock: true, weather: true, status: true, notifications: true, leaderboard: true, pipeline: true, kpi: true, heartbeat: true }; + }); + const [showWidgetSettings, setShowWidgetSettings] = useState(false); + const [mobileWidgetsOpen, setMobileWidgetsOpen] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth < 768); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + const toggleWidgetVisibility = (id: string) => { + setWidgetVisibility(prev => { + const updated = { ...prev, [id]: !prev[id] }; + localStorage.setItem('aethex-widget-visibility', JSON.stringify(updated)); + return updated; + }); + }; + + const resetWidgetPositions = () => { + const defaults = getDefaultWidgetPositions(); + setWidgetPositions(defaults); + setPositionResetKey(k => k + 1); + localStorage.setItem('aethex-widget-positions', JSON.stringify(defaults)); + }; + + const widgetOptions = [ + { id: 'clock', label: 'Clock' }, + { id: 'weather', label: 'Weather' }, + { id: 'status', label: 'System Status' }, + { id: 'notifications', label: 'Notifications' }, + { id: 'leaderboard', label: 'Leaderboard' }, + { id: 'pipeline', label: 'Pipeline' }, + { id: 'kpi', label: 'KPI Dashboard' }, + { id: 'heartbeat', label: 'Network Heartbeat' }, + ]; const { data: metrics } = useQuery({ queryKey: ['os-metrics'], @@ -1234,21 +1275,190 @@ function DesktopWidgets({ time, weather, notifications }: { return { color: 'text-cyan-400', icon: }; }; - return ( -
- -
-
- {time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -
-
- {time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })} -
-
-
+ if (isMobile) { + return ( + <> + + + {mobileWidgetsOpen && ( + +
+ Widgets + +
+
+ {widgetVisibility.clock !== false && ( +
+
+ {time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+ {time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })} +
+
+ )} + {widgetVisibility.weather !== false && weather?.current_weather && ( +
+
Weather
+
+ {getWeatherIcon(weather.current_weather.weathercode)} +
+
{Math.round(weather.current_weather.temperature)}°F
+
Wind: {weather.current_weather.windspeed} mph
+
+
+
+ )} + {widgetVisibility.status !== false && metrics && ( +
+
System Status
+
+
+ Architects + {metrics.totalProfiles || 0} +
+
+ Projects + {metrics.totalProjects || 0} +
+
+ Verified + {metrics.verifiedUsers || 0} +
+
+ Online + {metrics.onlineUsers || 0} +
+
+
+ )} + {widgetVisibility.notifications !== false && notifications && notifications.length > 0 && ( +
+
Notifications
+
+ {notifications.slice(0, 4).map((n, i) => { + const cat = getNotificationCategory(n); + return ( +
+ {cat.icon} + {n} +
+ ); + })} +
+
+ )} + {widgetVisibility.leaderboard !== false && leaderboard && leaderboard.length > 0 && ( +
+
+ + Top Architects +
+
+ {leaderboard.map((arch: any, i: number) => ( +
+ + {i + 1} + + {arch.username || arch.display_name} + Lv{arch.level || 1} +
+ ))} +
+
+ )} +
+
+ )} +
+ + ); + } - {weather?.current_weather && ( - + return ( +
+ + + + {showWidgetSettings && ( + setShowWidgetSettings(false)} + > + e.stopPropagation()} + > +
+

Widget Settings

+ +
+
+ {widgetOptions.map(opt => ( + + ))} +
+ +
+
+ )} +
+ + {widgetVisibility.clock !== false && ( + +
+
+ {time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+ {time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })} +
+
+
+ )} + + {widgetVisibility.weather !== false && weather?.current_weather && ( +
Weather
@@ -1262,8 +1472,8 @@ function DesktopWidgets({ time, weather, notifications }: { )} - {metrics && ( - + {widgetVisibility.status !== false && metrics && ( +
System Status
@@ -1294,8 +1504,8 @@ function DesktopWidgets({ time, weather, notifications }: { )} - {notifications && notifications.length > 0 && ( - + {widgetVisibility.notifications !== false && notifications && notifications.length > 0 && ( +
Notifications
@@ -1313,8 +1523,8 @@ function DesktopWidgets({ time, weather, notifications }: { )} - {leaderboard && leaderboard.length > 0 && ( - + {widgetVisibility.leaderboard !== false && leaderboard && leaderboard.length > 0 && ( +
@@ -1335,8 +1545,8 @@ function DesktopWidgets({ time, weather, notifications }: { )} - {metrics && ( - + {widgetVisibility.pipeline !== false && metrics && ( +
@@ -1375,8 +1585,8 @@ function DesktopWidgets({ time, weather, notifications }: { )} - {metrics && ( - + {widgetVisibility.kpi !== false && metrics && ( +
@@ -1404,30 +1614,32 @@ function DesktopWidgets({ time, weather, notifications }: { )} - -
-
- - Network Pulse -
-
- + {widgetVisibility.heartbeat !== false && ( + +
+
+ + Network Pulse +
+
- + animate={{ scale: [1, 1.2, 1] }} + transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }} + className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center" + > + + +
+
+ All Systems Operational +
-
- All Systems Operational -
-
- + + )}
); }