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:
AetherForge
2026-06-04 21:53:31 -07:00
parent 8466c7aa9b
commit 1551bd5dad
138 changed files with 7523 additions and 489 deletions

View File

@@ -0,0 +1,81 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import {
ambientMusicPlayer,
loadBgmEnabled,
loadBgmVolume,
} from '../audio/ambientMusic';
type AmbientMusicContextValue = {
enabled: boolean;
playing: boolean;
volume: number;
setEnabled: (v: boolean) => void;
setVolume: (v: number) => void;
togglePlay: () => void;
};
const AmbientMusicContext = createContext<AmbientMusicContextValue | null>(null);
export function AmbientMusicProvider({ children }: { children: React.ReactNode }) {
const [enabled, setEnabledState] = useState(loadBgmEnabled);
const [playing, setPlaying] = useState(() => ambientMusicPlayer.isPlaying());
const [volume, setVolumeState] = useState(loadBgmVolume);
const setEnabled = useCallback((v: boolean) => {
ambientMusicPlayer.setEnabled(v);
setEnabledState(v);
if (v) ambientMusicPlayer.unlock();
}, []);
const setVolume = useCallback((v: number) => {
ambientMusicPlayer.setVolume(v);
setVolumeState(ambientMusicPlayer.getVolume());
}, []);
const togglePlay = useCallback(() => {
ambientMusicPlayer.unlock();
ambientMusicPlayer.togglePlay();
setPlaying(ambientMusicPlayer.isPlaying());
setEnabledState(ambientMusicPlayer.isEnabled());
}, []);
useEffect(() => {
return ambientMusicPlayer.subscribe(setPlaying);
}, []);
useEffect(() => {
ambientMusicPlayer.setEnabled(enabled);
ambientMusicPlayer.setVolume(volume);
}, [enabled, volume]);
useEffect(() => {
const unlock = () => ambientMusicPlayer.unlock();
window.addEventListener('pointerdown', unlock, { once: true, passive: true });
window.addEventListener('keydown', unlock, { once: true });
return () => {
window.removeEventListener('pointerdown', unlock);
window.removeEventListener('keydown', unlock);
};
}, []);
const value = useMemo(
() => ({ enabled, playing, volume, setEnabled, setVolume, togglePlay }),
[enabled, playing, volume, setEnabled, setVolume, togglePlay]
);
return <AmbientMusicContext.Provider value={value}>{children}</AmbientMusicContext.Provider>;
}
const noopAmbient: AmbientMusicContextValue = {
enabled: false,
playing: false,
volume: 0,
setEnabled: () => {},
setVolume: () => {},
togglePlay: () => {},
};
export function useAmbientMusic() {
const ctx = useContext(AmbientMusicContext);
return ctx ?? noopAmbient;
}

View File

@@ -0,0 +1,35 @@
/**
* @vitest-environment happy-dom
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, fireEvent } from '@testing-library/react';
import { SoundProvider, SFX_INTERACTIVE_SELECTOR } from './SoundContext';
import * as hapticModule from '../audio/hapticEngine';
describe('SoundProvider click cues', () => {
const play = vi.fn();
beforeEach(() => {
play.mockClear();
vi.spyOn(hapticModule.hapticEngine, 'isEnabled').mockReturnValue(true);
vi.spyOn(hapticModule.hapticEngine, 'play').mockImplementation(play);
});
it('covers card-style interactive rows', () => {
expect(SFX_INTERACTIVE_SELECTOR).toContain('.agent-list-item.compact-row');
expect(SFX_INTERACTIVE_SELECTOR).toContain('.crucible-node-card');
expect(SFX_INTERACTIVE_SELECTOR).toContain('.pt-agent-card');
});
it('plays click on agent list row', () => {
render(
<SoundProvider>
<div className="agent-list-item compact-row" data-testid="row">
Fleet node
</div>
</SoundProvider>
);
fireEvent.click(document.querySelector('[data-testid="row"]')!);
expect(play).toHaveBeenCalledWith('click');
});
});

View File

@@ -1,20 +1,52 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { hapticEngine, loadSoundEnabled, loadSoundVolume, type SoundCue } from '../audio/hapticEngine';
import { hoverSfxEngine, loadHoverEnabled, loadHoverVolume } from '../audio/hoverSfx';
type SoundContextValue = {
enabled: boolean;
volume: number;
hoverEnabled: boolean;
hoverVolume: number;
setEnabled: (v: boolean) => void;
setVolume: (v: number) => void;
setHoverEnabled: (v: boolean) => void;
setHoverVolume: (v: number) => void;
play: (cue: SoundCue) => void;
preview: (cue?: SoundCue) => void;
previewHover: () => void;
};
const SoundContext = createContext<SoundContextValue | null>(null);
/** Elements that should emit the global UI click/nav cue (see SoundProvider listener). */
export const SFX_INTERACTIVE_SELECTOR = [
'button:not(:disabled)',
'.btn:not(:disabled)',
'[role="button"]:not([aria-disabled="true"])',
'.nav-link',
'.mobile-nav__link',
'.mobile-nav__more-btn',
'.agent-list-item.compact-row',
'.crucible-node-card',
'.crucible-group-item',
'.pt-agent-card:not([style*="cursor: not-allowed"])',
'.endpoint-chip:not(:disabled)',
'.fleet-group-chip:not(:disabled)',
].join(', ');
/** Elements that emit hover highlight SFX (debounced). */
export const HOVER_INTERACTIVE_SELECTOR = [
SFX_INTERACTIVE_SELECTOR,
'.neon-card',
'.card',
'a[href]:not([data-sfx="off"])',
].join(', ');
export function SoundProvider({ children }: { children: React.ReactNode }) {
const [enabled, setEnabledState] = useState(loadSoundEnabled);
const [volume, setVolumeState] = useState(loadSoundVolume);
const [hoverEnabled, setHoverEnabledState] = useState(loadHoverEnabled);
const [hoverVolume, setHoverVolumeState] = useState(loadHoverVolume);
const setEnabled = useCallback((v: boolean) => {
hapticEngine.setEnabled(v);
@@ -35,11 +67,31 @@ export function SoundProvider({ children }: { children: React.ReactNode }) {
hapticEngine.play(cue);
}, []);
const setHoverEnabled = useCallback((v: boolean) => {
hoverSfxEngine.setEnabled(v);
setHoverEnabledState(v);
}, []);
const setHoverVolume = useCallback((v: number) => {
hoverSfxEngine.setVolume(v);
setHoverVolumeState(hoverSfxEngine.getVolume());
}, []);
const previewHover = useCallback(() => {
hoverSfxEngine.unlock();
hoverSfxEngine.preview(enabled);
}, [enabled]);
useEffect(() => {
hapticEngine.setEnabled(enabled);
hapticEngine.setVolume(volume);
}, [enabled, volume]);
useEffect(() => {
hoverSfxEngine.setEnabled(hoverEnabled);
hoverSfxEngine.setVolume(hoverVolume);
}, [hoverEnabled, hoverVolume]);
useEffect(() => {
const unlock = () => hapticEngine.unlock();
window.addEventListener('pointerdown', unlock, { once: true, passive: true });
@@ -56,9 +108,7 @@ export function SoundProvider({ children }: { children: React.ReactNode }) {
const target = e.target as HTMLElement | null;
if (!target) return;
if (target.closest('[data-sfx="off"]')) return;
const interactive = target.closest(
'button:not(:disabled), .btn:not(:disabled), [role="button"]:not([aria-disabled="true"]), .nav-link, .mobile-nav__link, .mobile-nav__more-btn'
);
const interactive = target.closest(SFX_INTERACTIVE_SELECTOR);
if (!interactive) return;
const isNav =
interactive.classList.contains('nav-link') ||
@@ -69,9 +119,49 @@ export function SoundProvider({ children }: { children: React.ReactNode }) {
return () => document.removeEventListener('click', onClick, true);
}, [enabled]);
useEffect(() => {
const onMouseOver = (e: MouseEvent) => {
if (!hapticEngine.isEnabled() || !hoverSfxEngine.isEnabled()) return;
const target = e.target as HTMLElement | null;
if (!target) return;
if (target.closest('[data-sfx="off"]')) return;
const interactive = target.closest(HOVER_INTERACTIVE_SELECTOR);
if (!interactive) return;
const related = e.relatedTarget as Node | null;
if (related && interactive.contains(related)) return;
hoverSfxEngine.play(true);
};
document.addEventListener('mouseover', onMouseOver, true);
return () => document.removeEventListener('mouseover', onMouseOver, true);
}, [enabled, hoverEnabled]);
const value = useMemo(
() => ({ enabled, volume, setEnabled, setVolume, play, preview }),
[enabled, volume, setEnabled, setVolume, play, preview]
() => ({
enabled,
volume,
hoverEnabled,
hoverVolume,
setEnabled,
setVolume,
setHoverEnabled,
setHoverVolume,
play,
preview,
previewHover,
}),
[
enabled,
volume,
hoverEnabled,
hoverVolume,
setEnabled,
setVolume,
setHoverEnabled,
setHoverVolume,
play,
preview,
previewHover,
]
);
return <SoundContext.Provider value={value}>{children}</SoundContext.Provider>;
@@ -80,10 +170,15 @@ export function SoundProvider({ children }: { children: React.ReactNode }) {
const noopSound: SoundContextValue = {
enabled: false,
volume: 0,
hoverEnabled: false,
hoverVolume: 0,
setEnabled: () => {},
setVolume: () => {},
setHoverEnabled: () => {},
setHoverVolume: () => {},
play: () => {},
preview: () => {},
previewHover: () => {},
};
export function useSound() {