feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops
- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help - Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete - Sigil scramble post-forge uniquification and Dispense Reveal ceremony - Full system check, desktop push, BITS/host-binary persistence, Path Tracer - Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav - README documents alerts, sigil scramble, and pack-usb workflow - USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
This commit is contained in:
@@ -3,10 +3,18 @@ import { NavLink, useLocation } from 'react-router-dom';
|
||||
import AmbientBackground from '../Ambient/AmbientBackground';
|
||||
import SystemStatusBar from '../Visual/SystemStatusBar';
|
||||
import { useWebSocket } from '../../hooks/useWebSocket';
|
||||
import { useIsMobileLayout } from '../../hooks/useMediaQuery';
|
||||
import MatrixRain from './MatrixRain';
|
||||
import afLogo from '../../assets/af-logo.png';
|
||||
import CursorFire from '../Visual/CursorFire';
|
||||
import SacredGeometryLayer from '../Visual/sacredGeometry/SacredGeometryLayer';
|
||||
import { SacredMotif } from '../Visual/sacredGeometry/motifs';
|
||||
import SetupBanner from '../SetupBanner';
|
||||
import { getSetupStatus } from '../../help/setupStatus';
|
||||
import { api } from '../../api/client';
|
||||
import type { ServerConfig } from '../../types';
|
||||
import './Layout.css';
|
||||
import './MobileNav.css';
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -20,8 +28,14 @@ const NAV = [
|
||||
{ to: '/builds', label: 'Builds', icon: 'builds' },
|
||||
{ to: '/guide', label: 'Field Guide', icon: 'guide' },
|
||||
{ to: '/settings', label: 'Calibrate', icon: 'gear' },
|
||||
{ to: '/pathtracer', label: 'Path Tracer', icon: 'trace' },
|
||||
] as const;
|
||||
|
||||
/** Primary tabs on iPhone bottom bar */
|
||||
const MOBILE_PRIMARY = NAV.slice(0, 4);
|
||||
/** Builds, Guide, Calibrate, Path Tracer — “More” sheet */
|
||||
const MOBILE_MORE = NAV.slice(4);
|
||||
|
||||
function NavIcon({ type }: { type: string }) {
|
||||
switch (type) {
|
||||
case 'deck':
|
||||
@@ -71,6 +85,16 @@ function NavIcon({ type }: { type: string }) {
|
||||
<path d="M10 12l1.5 2L14 11" />
|
||||
</svg>
|
||||
);
|
||||
case 'trace':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="4" cy="12" r="2" />
|
||||
<circle cx="12" cy="6" r="2" />
|
||||
<circle cx="20" cy="12" r="2" />
|
||||
<path d="M6 12h4l2-6 2 6h4" />
|
||||
<path d="M12 8v4" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
@@ -135,14 +159,59 @@ function FleetReadout() {
|
||||
);
|
||||
}
|
||||
|
||||
function MobileTopStats() {
|
||||
const { agents } = useWebSocket();
|
||||
const online = agents.filter((a) => a.status === 'online').length;
|
||||
const total = agents.length;
|
||||
const hr = agents.reduce((s, a) => s + (a.hashrate_15s ?? a.hashrate_15m ?? 0), 0);
|
||||
return (
|
||||
<div className="mobile-top-stats">
|
||||
<strong>
|
||||
{online}/{total} online
|
||||
</strong>
|
||||
<span>{hr > 0 ? formatHashrate(hr) : 'IDLE'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const location = useLocation();
|
||||
const isMobile = useIsMobileLayout();
|
||||
const [serverConfig, setServerConfig] = useState<ServerConfig | null>(null);
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.getConfig().then(setServerConfig).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setMoreOpen(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!moreOpen) return;
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => {
|
||||
document.body.style.overflow = prev;
|
||||
};
|
||||
}, [moreOpen]);
|
||||
|
||||
const setupStatus = getSetupStatus(serverConfig);
|
||||
const moreActive = MOBILE_MORE.some((item) => location.pathname === item.to);
|
||||
const mobileShortLabel: Record<string, string> = {
|
||||
'/dashboard': 'Deck',
|
||||
'/agents': 'Fleet',
|
||||
'/crucible': 'Ops',
|
||||
'/forge': 'Forge',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<CursorFire />
|
||||
<div className={`layout${isMobile ? ' layout--mobile' : ''}`}>
|
||||
{!isMobile && <CursorFire />}
|
||||
<AmbientBackground />
|
||||
<nav className="sidebar">
|
||||
<SacredGeometryLayer />
|
||||
<nav className="sidebar sidebar--desktop desktop-only">
|
||||
<div className="sidebar-header">
|
||||
<div className="logo">
|
||||
<div className="logo-emblem">
|
||||
@@ -172,6 +241,10 @@ export default function Layout({ children }: LayoutProps) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="sidebar-sacred-sigil" aria-hidden>
|
||||
<SacredMotif name="flower" opacity={0.6} />
|
||||
</div>
|
||||
|
||||
{/* Matrix rain log — fills the lower sidebar between nav and footer */}
|
||||
<MatrixRain />
|
||||
|
||||
@@ -184,10 +257,72 @@ export default function Layout({ children }: LayoutProps) {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="main-with-status">
|
||||
{isMobile && (
|
||||
<header className="mobile-top-bar">
|
||||
<img src={afLogo} alt="" className="mobile-top-logo" />
|
||||
<span className="mobile-top-title">AetherForge</span>
|
||||
<MobileTopStats />
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className={`main-with-status${isMobile ? ' main-with-status--mobile' : ''}`}>
|
||||
<SystemStatusBar />
|
||||
<SetupBanner status={setupStatus} />
|
||||
<main className="main-content">{children}</main>
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<>
|
||||
{moreOpen && (
|
||||
<div
|
||||
className="mobile-menu-backdrop"
|
||||
role="presentation"
|
||||
onClick={() => setMoreOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<div className={`mobile-more-sheet${moreOpen ? ' open' : ''}`}>
|
||||
{MOBILE_MORE.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) => `mobile-more-link${isActive ? ' active' : ''}`}
|
||||
onClick={() => setMoreOpen(false)}
|
||||
>
|
||||
<NavIcon type={item.icon} />
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
<nav className="mobile-bottom-nav" aria-label="Main navigation">
|
||||
{MOBILE_PRIMARY.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
`mobile-bottom-nav-item${isActive ? ' active' : ''}`
|
||||
}
|
||||
>
|
||||
<NavIcon type={item.icon} />
|
||||
{mobileShortLabel[item.to] ?? item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={`mobile-bottom-nav-item${moreActive ? ' active' : ''}`}
|
||||
aria-expanded={moreOpen}
|
||||
aria-label="More pages"
|
||||
onClick={() => setMoreOpen((o) => !o)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="12" cy="5" r="1.5" fill="currentColor" />
|
||||
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
|
||||
<circle cx="12" cy="19" r="1.5" fill="currentColor" />
|
||||
</svg>
|
||||
More
|
||||
</button>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user