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:
@@ -1,159 +1,216 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
|
||||
AETHERFORGE_CLIENT_HEADER,
|
||||
|
||||
AETHERFORGE_CLIENT_VALUE,
|
||||
|
||||
authHeaders,
|
||||
|
||||
clearStoredAuth,
|
||||
|
||||
consumeAuthExpiredFlag,
|
||||
|
||||
encodeBasicToken,
|
||||
|
||||
getStoredAuth,
|
||||
|
||||
setStoredAuth,
|
||||
|
||||
} from '../api/auth';
|
||||
|
||||
import { useSound } from '../context/SoundContext';
|
||||
|
||||
import { FlowerOfLifeWatermark, KnowledgeKey } from './Visual/sacredGeometry/motifs';
|
||||
|
||||
|
||||
|
||||
export default function SessionGate({ children }: { children: ReactNode }) {
|
||||
|
||||
const { play } = useSound();
|
||||
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const [authed, setAuthed] = useState(!!getStoredAuth());
|
||||
|
||||
const [degraded, setDegraded] = useState(false);
|
||||
|
||||
const [user, setUser] = useState('');
|
||||
|
||||
const [pass, setPass] = useState('');
|
||||
|
||||
const [err, setErr] = useState('');
|
||||
|
||||
const [sessionExpired, setSessionExpired] = useState(false);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const sync = () => {
|
||||
|
||||
const hasAuth = !!getStoredAuth();
|
||||
|
||||
setAuthed(hasAuth);
|
||||
|
||||
if (!hasAuth) {
|
||||
|
||||
setSessionExpired(consumeAuthExpiredFlag());
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
window.addEventListener('aetherforge-auth', sync);
|
||||
|
||||
return () => window.removeEventListener('aetherforge-auth', sync);
|
||||
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const token = getStoredAuth();
|
||||
|
||||
if (!token) {
|
||||
|
||||
setAuthed(false);
|
||||
|
||||
setSessionExpired(consumeAuthExpiredFlag());
|
||||
|
||||
setReady(true);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
fetch('/api/v1/config', { headers: authHeaders() })
|
||||
|
||||
.then((r) => {
|
||||
|
||||
if (r.status === 401) {
|
||||
|
||||
clearStoredAuth({ silent: true, expired: true });
|
||||
|
||||
setAuthed(false);
|
||||
|
||||
setSessionExpired(true);
|
||||
|
||||
} else if (!r.ok) {
|
||||
|
||||
// Server reachable but unhappy — keep saved credentials (degraded mode).
|
||||
|
||||
setAuthed(true);
|
||||
|
||||
setDegraded(true);
|
||||
|
||||
} else {
|
||||
|
||||
setAuthed(true);
|
||||
|
||||
setDegraded(false);
|
||||
|
||||
}
|
||||
|
||||
setReady(true);
|
||||
|
||||
})
|
||||
|
||||
.catch(() => {
|
||||
|
||||
// Network blip — trust stored credentials until the server responds.
|
||||
|
||||
setAuthed(true);
|
||||
|
||||
setDegraded(true);
|
||||
|
||||
setReady(true);
|
||||
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
setErr('');
|
||||
|
||||
setSessionExpired(false);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
|
||||
[AETHERFORGE_CLIENT_HEADER]: AETHERFORGE_CLIENT_VALUE,
|
||||
|
||||
Authorization: `Basic ${encodeBasicToken(user, pass)}`,
|
||||
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
const res = await fetch('/api/v1/config', { headers });
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
setErr('Login failed — check username and password.');
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import type { PublicBuildDTO } from '../types';
|
||||
import {
|
||||
AETHERFORGE_CLIENT_HEADER,
|
||||
AETHERFORGE_CLIENT_VALUE,
|
||||
authHeaders,
|
||||
clearStoredAuth,
|
||||
consumeAuthExpiredFlag,
|
||||
encodeBasicToken,
|
||||
getStoredAuth,
|
||||
setStoredAuth,
|
||||
} from '../api/auth';
|
||||
import { useSound } from '../context/SoundContext';
|
||||
import { FlowerOfLifeWatermark, KnowledgeKey } from './Visual/sacredGeometry/motifs';
|
||||
|
||||
export default function SessionGate({ children }: { children: ReactNode }) {
|
||||
const { play } = useSound();
|
||||
const [ready, setReady] = useState(false);
|
||||
const [authed, setAuthed] = useState(!!getStoredAuth());
|
||||
const [degraded, setDegraded] = useState(false);
|
||||
const [user, setUser] = useState('');
|
||||
const [pass, setPass] = useState('');
|
||||
const [err, setErr] = useState('');
|
||||
const [sessionExpired, setSessionExpired] = useState(false);
|
||||
const [publicOpen, setPublicOpen] = useState(false);
|
||||
const [publicBuilds, setPublicBuilds] = useState<PublicBuildDTO[]>([]);
|
||||
const [publicLoading, setPublicLoading] = useState(false);
|
||||
const [publicErr, setPublicErr] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const sync = () => {
|
||||
const hasAuth = !!getStoredAuth();
|
||||
setAuthed(hasAuth);
|
||||
if (!hasAuth) {
|
||||
setSessionExpired(consumeAuthExpiredFlag());
|
||||
}
|
||||
};
|
||||
window.addEventListener('aetherforge-auth', sync);
|
||||
return () => window.removeEventListener('aetherforge-auth', sync);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getStoredAuth();
|
||||
if (!token) {
|
||||
setAuthed(false);
|
||||
setSessionExpired(consumeAuthExpiredFlag());
|
||||
setReady(true);
|
||||
return;
|
||||
}
|
||||
fetch('/api/v1/config', { headers: authHeaders() })
|
||||
.then((r) => {
|
||||
if (r.status === 401) {
|
||||
clearStoredAuth({ silent: true, expired: true });
|
||||
setAuthed(false);
|
||||
setSessionExpired(true);
|
||||
} else if (!r.ok) {
|
||||
// Server reachable but unhappy — keep saved credentials (degraded mode).
|
||||
setAuthed(true);
|
||||
setDegraded(true);
|
||||
} else {
|
||||
setAuthed(true);
|
||||
setDegraded(false);
|
||||
}
|
||||
setReady(true);
|
||||
})
|
||||
.catch(() => {
|
||||
// Network blip — trust stored credentials until the server responds.
|
||||
setAuthed(true);
|
||||
setDegraded(true);
|
||||
setReady(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErr('');
|
||||
setSessionExpired(false);
|
||||
const headers: Record<string, string> = {
|
||||
[AETHERFORGE_CLIENT_HEADER]: AETHERFORGE_CLIENT_VALUE,
|
||||
Authorization: `Basic ${encodeBasicToken(user, pass)}`,
|
||||
};
|
||||
try {
|
||||
const res = await fetch('/api/v1/config', { headers });
|
||||
if (!res.ok) {
|
||||
setErr('Login failed — check username and password.');
|
||||
play('error');
|
||||
return;
|
||||
}
|
||||
setStoredAuth(user, pass);
|
||||
setAuthed(true);
|
||||
setDegraded(false);
|
||||
play('success');
|
||||
} catch {
|
||||
setErr('Cannot reach server — check that miner-server is running.');
|
||||
}
|
||||
};
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<div className="session-gate">
|
||||
<div className="session-gate-sacred-ring" aria-hidden>
|
||||
<FlowerOfLifeWatermark opacity={0.5} />
|
||||
</div>
|
||||
<p className="font-tech">Starting AetherForge…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const loadPublicBuilds = async () => {
|
||||
setPublicLoading(true);
|
||||
setPublicErr('');
|
||||
try {
|
||||
const res = await fetch('/api/v1/public/builds');
|
||||
if (!res.ok) throw new Error('unavailable');
|
||||
const data = (await res.json()) as { builds: PublicBuildDTO[] };
|
||||
setPublicBuilds(data.builds ?? []);
|
||||
setPublicOpen(true);
|
||||
} catch {
|
||||
setPublicErr('Public builds are not available yet — forge an installer first.');
|
||||
setPublicOpen(true);
|
||||
} finally {
|
||||
setPublicLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!authed) {
|
||||
return (
|
||||
<div className="session-gate">
|
||||
<div className="session-gate-sacred-ring" aria-hidden>
|
||||
<FlowerOfLifeWatermark opacity={0.5} />
|
||||
</div>
|
||||
<div className="session-gate-keys" aria-hidden>
|
||||
<div className="session-gate-key session-gate-key--tl">
|
||||
<KnowledgeKey opacity={0.55} />
|
||||
</div>
|
||||
<div className="session-gate-key session-gate-key--br">
|
||||
<KnowledgeKey opacity={0.45} />
|
||||
</div>
|
||||
</div>
|
||||
<form className="session-gate-card card" onSubmit={handleLogin}>
|
||||
<h1 className="font-display">AetherForge</h1>
|
||||
<p className="form-hint">Sign in to open the command deck.</p>
|
||||
{sessionExpired && (
|
||||
<p className="form-hint" style={{ color: 'var(--accent-red)' }}>
|
||||
Your session expired — please sign in again.
|
||||
</p>
|
||||
)}
|
||||
<label className="label" htmlFor="session-user">Username</label>
|
||||
<input id="session-user" className="input" value={user} onChange={(e) => setUser(e.target.value)} autoComplete="username" />
|
||||
<label className="label" htmlFor="session-pass">Password</label>
|
||||
<input
|
||||
id="session-pass"
|
||||
className="input"
|
||||
type="password"
|
||||
value={pass}
|
||||
onChange={(e) => setPass(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
{err && <p className="form-hint" style={{ color: 'var(--accent-red)' }}>{err}</p>}
|
||||
<button type="submit" className="btn btn-primary btn-lg">
|
||||
Enter Command Deck
|
||||
</button>
|
||||
<p className="session-gate-whisper" aria-hidden>
|
||||
ψ · the deck remembers every key
|
||||
</p>
|
||||
<div className="session-public-drawer" style={{ marginTop: '1.25rem', width: '100%' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline btn-sm"
|
||||
style={{ width: '100%' }}
|
||||
onClick={() => void loadPublicBuilds()}
|
||||
disabled={publicLoading}
|
||||
>
|
||||
{publicLoading ? 'Loading…' : 'Public builds (no login)'}
|
||||
</button>
|
||||
{publicOpen && (
|
||||
<div className="card" style={{ marginTop: '0.75rem', textAlign: 'left' }}>
|
||||
<p className="form-hint" style={{ marginTop: 0 }}>
|
||||
Pinned + latest forged installers — no credentials required.
|
||||
</p>
|
||||
{publicErr && <p className="form-hint" style={{ color: 'var(--accent-red)' }}>{publicErr}</p>}
|
||||
{publicBuilds.length === 0 && !publicErr && (
|
||||
<p className="form-hint">No public builds yet.</p>
|
||||
)}
|
||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||||
{publicBuilds.map((b) => (
|
||||
<li key={b.id} style={{ marginBottom: '0.5rem', fontSize: '0.85rem' }}>
|
||||
<strong>{b.worker_name}</strong>
|
||||
<span className="form-hint"> · {b.platform}</span>
|
||||
{b.pinned && <span> 📌</span>}
|
||||
<br />
|
||||
<a href={b.download_url} className="mono" style={{ fontSize: '0.75rem' }}>
|
||||
Download
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{degraded && (
|
||||
<div className="session-degraded-banner" role="status">
|
||||
Cannot reach server — using saved credentials. Some data may be stale until connectivity returns.
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user