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:
AetherForge
2026-06-03 20:32:59 -07:00
parent 03937edba7
commit d52479c9a6
139 changed files with 10611 additions and 369 deletions

View File

@@ -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 */

View File

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

View 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;
}