Add administrator tools for managing architects and enhance OS widget functionality

Includes service worker registration for offline support, adds bulk actions and filtering to the admin architects page, and implements widget visibility controls and a mobile drawer for the OS.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: ad12b0de-1689-4465-b8e3-8b92d06f17d1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/4z9y3HV
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-21 21:55:24 +00:00
parent 9f20fd9b56
commit 50923682ad
5 changed files with 509 additions and 68 deletions

View file

@ -28,5 +28,14 @@
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW registration failed:', err));
});
}
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

67
client/public/sw.js Normal file
View file

@ -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;
})
);
});

View file

@ -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<any>(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [roleFilter, setRoleFilter] = useState<string>("all");
const [verifiedFilter, setVerifiedFilter] = useState<string>("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 */}
<div className="flex-1 overflow-auto">
<div className="p-8">
<div className="flex justify-between items-center mb-8">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
Architects
</h2>
<p className="text-muted-foreground text-sm mt-1">
{profiles?.length || 0} registered architects
{filteredProfiles.length} of {profiles?.length || 0} architects
</p>
</div>
{/* Search */}
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder="Search architects..."
value={searchQuery}
onChange={(e) => 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"
/>
<div className="flex items-center gap-4">
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="bg-card border border-white/10 px-3 py-2 text-sm text-white focus:border-primary/50 focus:outline-none"
data-testid="filter-role"
>
<option value="all">All Roles</option>
<option value="admin">Admin</option>
<option value="oversee">Overseer</option>
<option value="employee">Employee</option>
<option value="member">Member</option>
</select>
<select
value={verifiedFilter}
onChange={(e) => setVerifiedFilter(e.target.value)}
className="bg-card border border-white/10 px-3 py-2 text-sm text-white focus:border-primary/50 focus:outline-none"
data-testid="filter-verified"
>
<option value="all">All Status</option>
<option value="verified">Verified</option>
<option value="unverified">Unverified</option>
</select>
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder="Search architects..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
</div>
</div>
{selectedIds.size > 0 && (
<div className="flex items-center gap-4 mb-4 p-3 bg-primary/10 border border-primary/30 rounded">
<span className="text-sm text-white">{selectedIds.size} selected</span>
<button
onClick={() => bulkVerify(true)}
className="px-3 py-1 bg-green-500/20 text-green-400 text-sm rounded hover:bg-green-500/30 transition-colors"
data-testid="bulk-verify"
>
Verify Selected
</button>
<button
onClick={() => bulkVerify(false)}
className="px-3 py-1 bg-red-500/20 text-red-400 text-sm rounded hover:bg-red-500/30 transition-colors"
data-testid="bulk-unverify"
>
Revoke Selected
</button>
<button
onClick={() => setSelectedIds(new Set())}
className="px-3 py-1 text-muted-foreground text-sm hover:text-white transition-colors"
>
Clear Selection
</button>
<div className="flex-1" />
<button
onClick={exportToCSV}
className="flex items-center gap-2 px-3 py-1 bg-white/10 text-white text-sm rounded hover:bg-white/20 transition-colors"
data-testid="export-csv"
>
<Download className="w-4 h-4" /> Export CSV
</button>
</div>
)}
{selectedIds.size === 0 && (
<div className="flex justify-end mb-4">
<button
onClick={exportToCSV}
className="flex items-center gap-2 px-3 py-1 bg-white/10 text-white text-sm rounded hover:bg-white/20 transition-colors"
data-testid="export-csv-all"
>
<Download className="w-4 h-4" /> Export All to CSV
</button>
</div>
)}
{/* Table */}
<div className="bg-card/50 border border-white/10 overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-white/10 text-left">
<th className="p-4 w-10">
<button onClick={toggleSelectAll} className="text-muted-foreground hover:text-white transition-colors">
{selectedIds.size === filteredProfiles.length && filteredProfiles.length > 0 ? (
<CheckSquare className="w-5 h-5 text-primary" />
) : (
<Square className="w-5 h-5" />
)}
</button>
</th>
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">User</th>
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Role</th>
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Level</th>
@ -141,19 +285,28 @@ export default function AdminArchitects() {
<tbody>
{isLoading ? (
<tr>
<td colSpan={7} className="p-8 text-center text-muted-foreground">
<td colSpan={8} className="p-8 text-center text-muted-foreground">
Loading...
</td>
</tr>
) : filteredProfiles.length === 0 ? (
<tr>
<td colSpan={7} className="p-8 text-center text-muted-foreground">
<td colSpan={8} className="p-8 text-center text-muted-foreground">
No architects found
</td>
</tr>
) : (
filteredProfiles.map((profile: any) => (
<tr key={profile.id} className="border-b border-white/5 hover:bg-white/5 transition-colors">
<tr key={profile.id} className={`border-b border-white/5 hover:bg-white/5 transition-colors ${selectedIds.has(profile.id) ? 'bg-primary/5' : ''}`}>
<td className="p-4">
<button onClick={() => toggleSelect(profile.id)} className="text-muted-foreground hover:text-white transition-colors">
{selectedIds.has(profile.id) ? (
<CheckSquare className="w-5 h-5 text-primary" />
) : (
<Square className="w-5 h-5" />
)}
</button>
</td>
<td className="p-4">
<div className="flex items-center gap-3">
<img

View file

@ -1187,6 +1187,47 @@ function DesktopWidgets({ time, weather, notifications }: {
const saved = localStorage.getItem('aethex-widget-positions');
return saved ? JSON.parse(saved) : getDefaultWidgetPositions();
});
const [positionResetKey, setPositionResetKey] = useState(0);
const [widgetVisibility, setWidgetVisibility] = useState<Record<string, boolean>>(() => {
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: <Users className="w-3 h-3" /> };
};
return (
<div className="pointer-events-none absolute inset-0">
<DraggableWidget id="clock" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
<div className="p-3">
<div className="text-2xl font-mono text-white font-bold">
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
<div className="text-xs text-white/50 font-mono">
{time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })}
</div>
</div>
</DraggableWidget>
if (isMobile) {
return (
<>
<button
onClick={() => setMobileWidgetsOpen(!mobileWidgetsOpen)}
className="fixed top-4 right-4 z-50 w-10 h-10 bg-slate-900/90 backdrop-blur-xl border border-white/20 rounded-lg flex items-center justify-center text-white/70 hover:text-white transition-colors pointer-events-auto"
data-testid="mobile-widgets-toggle"
>
<BarChart3 className="w-5 h-5" />
</button>
<AnimatePresence>
{mobileWidgetsOpen && (
<motion.div
initial={{ opacity: 0, x: 300 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 300 }}
className="fixed top-0 right-0 bottom-12 w-72 bg-slate-900/95 backdrop-blur-xl border-l border-white/10 overflow-y-auto z-40 pointer-events-auto"
>
<div className="p-4 border-b border-white/10 flex items-center justify-between sticky top-0 bg-slate-900/95">
<span className="text-sm text-white/70 uppercase tracking-wider">Widgets</span>
<button onClick={() => setMobileWidgetsOpen(false)} className="text-white/50 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
{widgetVisibility.clock !== false && (
<div className="bg-white/5 rounded-lg p-3">
<div className="text-2xl font-mono text-white font-bold">
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
<div className="text-xs text-white/50 font-mono">
{time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })}
</div>
</div>
)}
{widgetVisibility.weather !== false && weather?.current_weather && (
<div className="bg-white/5 rounded-lg p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">Weather</div>
<div className="flex items-center gap-3">
<span className="text-2xl">{getWeatherIcon(weather.current_weather.weathercode)}</span>
<div>
<div className="text-xl font-mono text-white">{Math.round(weather.current_weather.temperature)}°F</div>
<div className="text-xs text-white/50">Wind: {weather.current_weather.windspeed} mph</div>
</div>
</div>
</div>
)}
{widgetVisibility.status !== false && metrics && (
<div className="bg-white/5 rounded-lg p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">System Status</div>
<div className="grid grid-cols-2 gap-2 text-xs font-mono">
<div className="flex justify-between">
<span className="text-white/60">Architects</span>
<span className="text-cyan-400">{metrics.totalProfiles || 0}</span>
</div>
<div className="flex justify-between">
<span className="text-white/60">Projects</span>
<span className="text-purple-400">{metrics.totalProjects || 0}</span>
</div>
<div className="flex justify-between">
<span className="text-white/60">Verified</span>
<span className="text-yellow-400">{metrics.verifiedUsers || 0}</span>
</div>
<div className="flex justify-between">
<span className="text-white/60">Online</span>
<span className="text-green-400">{metrics.onlineUsers || 0}</span>
</div>
</div>
</div>
)}
{widgetVisibility.notifications !== false && notifications && notifications.length > 0 && (
<div className="bg-white/5 rounded-lg p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">Notifications</div>
<div className="space-y-1.5 text-xs">
{notifications.slice(0, 4).map((n, i) => {
const cat = getNotificationCategory(n);
return (
<div key={i} className={`flex items-center gap-2 ${cat.color}`}>
{cat.icon}
<span className="truncate text-white/70">{n}</span>
</div>
);
})}
</div>
</div>
)}
{widgetVisibility.leaderboard !== false && leaderboard && leaderboard.length > 0 && (
<div className="bg-white/5 rounded-lg p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
<Award className="w-3 h-3 text-yellow-400" />
Top Architects
</div>
<div className="space-y-1.5 text-xs font-mono">
{leaderboard.map((arch: any, i: number) => (
<div key={arch.id} className="flex items-center gap-2">
<span className={`w-4 text-center ${i === 0 ? 'text-yellow-400' : i === 1 ? 'text-gray-300' : i === 2 ? 'text-amber-600' : 'text-white/40'}`}>
{i + 1}
</span>
<span className="flex-1 truncate text-white/80">{arch.username || arch.display_name}</span>
<span className="text-cyan-400">Lv{arch.level || 1}</span>
</div>
))}
</div>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}
{weather?.current_weather && (
<DraggableWidget id="weather" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
return (
<div className="pointer-events-none absolute inset-0 hidden md:block">
<button
onClick={() => setShowWidgetSettings(true)}
className="fixed top-4 left-4 z-50 w-8 h-8 bg-slate-900/80 backdrop-blur-xl border border-white/20 rounded-lg flex items-center justify-center text-white/50 hover:text-white transition-colors pointer-events-auto"
data-testid="widget-settings-btn"
>
<Settings className="w-4 h-4" />
</button>
<AnimatePresence>
{showWidgetSettings && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm pointer-events-auto"
onClick={() => setShowWidgetSettings(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-slate-900/95 backdrop-blur-xl border border-white/20 rounded-xl p-6 w-80"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-display uppercase tracking-wider">Widget Settings</h3>
<button onClick={() => setShowWidgetSettings(false)} className="text-white/50 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-2 mb-4">
{widgetOptions.map(opt => (
<label key={opt.id} className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer">
<input
type="checkbox"
checked={widgetVisibility[opt.id] !== false}
onChange={() => toggleWidgetVisibility(opt.id)}
className="w-4 h-4 rounded border-white/30 bg-white/10 text-cyan-500 focus:ring-cyan-500"
/>
<span className="text-white/80 text-sm">{opt.label}</span>
</label>
))}
</div>
<button
onClick={resetWidgetPositions}
className="w-full py-2 bg-white/10 hover:bg-white/20 text-white/80 rounded-lg text-sm transition-colors"
>
Reset Positions
</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{widgetVisibility.clock !== false && (
<DraggableWidget key={`clock-${positionResetKey}`} id="clock" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
<div className="p-3">
<div className="text-2xl font-mono text-white font-bold">
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
<div className="text-xs text-white/50 font-mono">
{time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })}
</div>
</div>
</DraggableWidget>
)}
{widgetVisibility.weather !== false && weather?.current_weather && (
<DraggableWidget key={`weather-${positionResetKey}`} id="weather" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
<div className="p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">Weather</div>
<div className="flex items-center gap-3">
@ -1262,8 +1472,8 @@ function DesktopWidgets({ time, weather, notifications }: {
</DraggableWidget>
)}
{metrics && (
<DraggableWidget id="status" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
{widgetVisibility.status !== false && metrics && (
<DraggableWidget key={`status-${positionResetKey}`} id="status" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
<div className="p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">System Status</div>
<div className="space-y-1.5 text-xs font-mono">
@ -1294,8 +1504,8 @@ function DesktopWidgets({ time, weather, notifications }: {
</DraggableWidget>
)}
{notifications && notifications.length > 0 && (
<DraggableWidget id="notifications" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
{widgetVisibility.notifications !== false && notifications && notifications.length > 0 && (
<DraggableWidget key={`notifications-${positionResetKey}`} id="notifications" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
<div className="p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">Notifications</div>
<div className="space-y-1.5 text-xs max-h-24 overflow-y-auto">
@ -1313,8 +1523,8 @@ function DesktopWidgets({ time, weather, notifications }: {
</DraggableWidget>
)}
{leaderboard && leaderboard.length > 0 && (
<DraggableWidget id="leaderboard" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
{widgetVisibility.leaderboard !== false && leaderboard && leaderboard.length > 0 && (
<DraggableWidget key={`leaderboard-${positionResetKey}`} id="leaderboard" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
<div className="p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
<Award className="w-3 h-3 text-yellow-400" />
@ -1335,8 +1545,8 @@ function DesktopWidgets({ time, weather, notifications }: {
</DraggableWidget>
)}
{metrics && (
<DraggableWidget id="pipeline" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
{widgetVisibility.pipeline !== false && metrics && (
<DraggableWidget key={`pipeline-${positionResetKey}`} id="pipeline" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
<div className="p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
<Layers className="w-3 h-3 text-purple-400" />
@ -1375,8 +1585,8 @@ function DesktopWidgets({ time, weather, notifications }: {
</DraggableWidget>
)}
{metrics && (
<DraggableWidget id="kpi" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
{widgetVisibility.kpi !== false && metrics && (
<DraggableWidget key={`kpi-${positionResetKey}`} id="kpi" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
<div className="p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
<BarChart3 className="w-3 h-3 text-cyan-400" />
@ -1404,30 +1614,32 @@ function DesktopWidgets({ time, weather, notifications }: {
</DraggableWidget>
)}
<DraggableWidget id="heartbeat" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
<div className="p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
<Activity className="w-3 h-3 text-red-400" />
Network Pulse
</div>
<div className="flex items-center justify-center py-2">
<motion.div
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"
>
{widgetVisibility.heartbeat !== false && (
<DraggableWidget key={`heartbeat-${positionResetKey}`} id="heartbeat" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
<div className="p-3">
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
<Activity className="w-3 h-3 text-red-400" />
Network Pulse
</div>
<div className="flex items-center justify-center py-2">
<motion.div
animate={{ scale: [1, 1.1, 1] }}
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", delay: 0.1 }}
className="w-4 h-4 rounded-full bg-red-500"
/>
</motion.div>
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"
>
<motion.div
animate={{ scale: [1, 1.1, 1] }}
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", delay: 0.1 }}
className="w-4 h-4 rounded-full bg-red-500"
/>
</motion.div>
</div>
<div className="text-center text-xs text-white/60 font-mono">
<span className="text-green-400"></span> All Systems Operational
</div>
</div>
<div className="text-center text-xs text-white/60 font-mono">
<span className="text-green-400"></span> All Systems Operational
</div>
</div>
</DraggableWidget>
</DraggableWidget>
)}
</div>
);
}