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:
@@ -442,39 +442,4 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
.logo-text-block,
|
||||
.nav-label,
|
||||
.fleet-readout,
|
||||
.sidebar-sig,
|
||||
.matrix-rain-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1rem 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
justify-content: center;
|
||||
padding: 0.85rem;
|
||||
}
|
||||
|
||||
.main-with-status {
|
||||
margin-left: 72px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
/* Mobile layout: bottom nav + top bar — see MobileNav.css and layout--mobile */
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
182
server/web/src/components/Layout/MobileNav.css
Normal file
182
server/web/src/components/Layout/MobileNav.css
Normal file
@@ -0,0 +1,182 @@
|
||||
/* Mobile top bar + bottom nav (see Layout.tsx) */
|
||||
|
||||
.mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-only {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-top-bar {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 110;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(8, 6, 4, 0.96);
|
||||
border-bottom: 1px solid var(--border-brass);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.layout--mobile .mobile-top-bar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobile-top-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mobile-top-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--brass-light);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.mobile-top-stats {
|
||||
margin-left: auto;
|
||||
font-family: var(--font-tech);
|
||||
font-size: 0.65rem;
|
||||
color: var(--neon-cyan);
|
||||
letter-spacing: 0.06em;
|
||||
text-align: right;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.mobile-top-stats span {
|
||||
display: block;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.layout--mobile .main-with-status {
|
||||
padding-top: 52px;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 110;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
gap: 0;
|
||||
background: rgba(8, 6, 4, 0.98);
|
||||
border-top: 1px solid var(--border-brass);
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.layout--mobile .mobile-bottom-nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.15rem;
|
||||
padding: 0.4rem 0.2rem;
|
||||
min-height: 52px;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav-item svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav-item.active,
|
||||
.mobile-bottom-nav-item:hover {
|
||||
color: var(--neon-cyan);
|
||||
}
|
||||
|
||||
.mobile-bottom-nav-item.active {
|
||||
background: rgba(0, 245, 255, 0.08);
|
||||
}
|
||||
|
||||
.mobile-menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 115;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.mobile-more-sheet {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(52px + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 120;
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: rgba(12, 10, 8, 0.98);
|
||||
border-top: 1px solid var(--border-brass);
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: transform 0.25s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.mobile-more-sheet.open {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mobile-more-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.mobile-more-link.active {
|
||||
background: rgba(0, 245, 255, 0.1);
|
||||
color: var(--neon-cyan);
|
||||
}
|
||||
|
||||
.mobile-more-link svg {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
Reference in New Issue
Block a user