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,43 @@
/**
* @vitest-environment happy-dom
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
AmbientMusicPlayer,
loadBgmEnabled,
loadBgmVolume,
BGM_STORAGE_KEY,
BGM_VOLUME_KEY,
AMBIENT_MUSIC_SRC,
} from './ambientMusic';
describe('ambientMusic prefs', () => {
beforeEach(() => {
localStorage.clear();
});
it('defaults music off and volume ~0.22', () => {
expect(loadBgmEnabled()).toBe(false);
expect(loadBgmVolume()).toBeCloseTo(0.22);
});
it('persists enabled flag', () => {
const p = new AmbientMusicPlayer();
p.setEnabled(true);
expect(localStorage.getItem(BGM_STORAGE_KEY)).toBe('1');
expect(loadBgmEnabled()).toBe(true);
});
it('clamps volume', () => {
const p = new AmbientMusicPlayer();
p.setVolume(3);
expect(p.getVolume()).toBe(1);
p.setVolume(-2);
expect(p.getVolume()).toBe(0);
expect(localStorage.getItem(BGM_VOLUME_KEY)).toBe('0');
});
it('points at public audio path', () => {
expect(AMBIENT_MUSIC_SRC).toBe('/audio/ambient.mp3');
});
});

View File

@@ -0,0 +1,152 @@
/**
* Looping background music — drop your MP3 at public/audio/ambient.mp3
* (MP3 preferred; OGG/WAV also work if you update AMBIENT_MUSIC_SRC).
*/
export const BGM_STORAGE_KEY = 'aetherforge-bgm';
export const BGM_VOLUME_KEY = 'aetherforge-bgm-volume';
/** Served from Vite public/ — place ambient.mp3 here before enabling in Settings. */
export const AMBIENT_MUSIC_SRC = '/audio/ambient.mp3';
export function loadBgmEnabled(): boolean {
try {
const v = localStorage.getItem(BGM_STORAGE_KEY);
return v === '1';
} catch {
return false;
}
}
export function loadBgmVolume(): number {
try {
const v = localStorage.getItem(BGM_VOLUME_KEY);
if (v === null) return 0.22;
const n = parseFloat(v);
return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0.22;
} catch {
return 0.22;
}
}
function persistBgmEnabled(enabled: boolean) {
try {
localStorage.setItem(BGM_STORAGE_KEY, enabled ? '1' : '0');
} catch {
/* ignore */
}
}
function persistBgmVolume(volume: number) {
try {
localStorage.setItem(BGM_VOLUME_KEY, String(volume));
} catch {
/* ignore */
}
}
export class AmbientMusicPlayer {
private audio: HTMLAudioElement | null = null;
private enabled = loadBgmEnabled();
private volume = loadBgmVolume();
private unlocked = false;
private playing = false;
private listeners = new Set<(playing: boolean) => void>();
isEnabled() {
return this.enabled;
}
isPlaying() {
return this.playing;
}
getVolume() {
return this.volume;
}
subscribe(fn: (playing: boolean) => void) {
this.listeners.add(fn);
return () => { this.listeners.delete(fn); };
}
private setPlaying(v: boolean) {
if (this.playing === v) return;
this.playing = v;
for (const fn of this.listeners) fn(v);
}
setEnabled(enabled: boolean) {
this.enabled = enabled;
persistBgmEnabled(enabled);
if (enabled) {
this.ensureAudio();
void this.tryPlay();
} else {
this.pause();
}
}
setVolume(volume: number) {
this.volume = Math.min(1, Math.max(0, volume));
persistBgmVolume(this.volume);
if (this.audio) this.audio.volume = this.volume;
}
/** Browsers block autoplay until a user gesture unlocks audio. */
unlock() {
if (this.unlocked) return;
this.unlocked = true;
this.ensureAudio();
if (this.enabled) void this.tryPlay();
}
togglePlay() {
if (this.playing) {
this.pause();
return false;
}
if (!this.enabled) {
this.setEnabled(true);
}
void this.tryPlay();
return true;
}
private ensureAudio() {
if (this.audio || typeof document === 'undefined') return;
const el = new Audio(AMBIENT_MUSIC_SRC);
el.loop = true;
el.preload = 'auto';
el.volume = this.volume;
el.addEventListener('play', () => this.setPlaying(true));
el.addEventListener('pause', () => this.setPlaying(false));
el.addEventListener('ended', () => this.setPlaying(false));
el.addEventListener('error', () => {
this.setPlaying(false);
});
this.audio = el;
}
private pause() {
if (!this.audio) return;
this.audio.pause();
this.setPlaying(false);
}
async tryPlay(): Promise<boolean> {
if (!this.enabled) return false;
this.ensureAudio();
if (!this.audio) return false;
this.audio.volume = this.volume;
try {
await this.audio.play();
this.setPlaying(true);
return true;
} catch {
this.setPlaying(false);
/* Autoplay policy — wait for Settings toggle or first click */
return false;
}
}
}
export const ambientMusicPlayer = new AmbientMusicPlayer();

View File

@@ -0,0 +1,48 @@
/**
* @vitest-environment happy-dom
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
HoverSfxEngine,
loadHoverEnabled,
loadHoverVolume,
HOVER_SFX_STORAGE_KEY,
HOVER_SFX_VOLUME_KEY,
} from './hoverSfx';
describe('hoverSfx prefs', () => {
beforeEach(() => {
localStorage.clear();
});
it('defaults hover on and volume ~0.25', () => {
expect(loadHoverEnabled()).toBe(true);
expect(loadHoverVolume()).toBeCloseTo(0.25);
});
it('persists enabled flag', () => {
const e = new HoverSfxEngine();
e.setEnabled(false);
expect(localStorage.getItem(HOVER_SFX_STORAGE_KEY)).toBe('0');
expect(loadHoverEnabled()).toBe(false);
});
it('clamps volume', () => {
const e = new HoverSfxEngine();
e.setVolume(2);
expect(e.getVolume()).toBe(1);
e.setVolume(-1);
expect(e.getVolume()).toBe(0);
expect(localStorage.getItem(HOVER_SFX_VOLUME_KEY)).toBe('0');
});
it('debounces rapid play calls', () => {
const e = new HoverSfxEngine();
e.setDebounceMs(200);
e.setEnabled(true);
expect(() => {
e.play(true);
e.play(true);
}).not.toThrow();
});
});

View File

@@ -0,0 +1,194 @@
/** Short dubstep/techno hover blips — Web Audio, no asset files. */
export const HOVER_SFX_STORAGE_KEY = 'aetherforge-hover-sfx';
export const HOVER_SFX_VOLUME_KEY = 'aetherforge-hover-volume';
export function loadHoverEnabled(): boolean {
try {
const v = localStorage.getItem(HOVER_SFX_STORAGE_KEY);
return v === null ? true : v === '1';
} catch {
return true;
}
}
export function loadHoverVolume(): number {
try {
const v = localStorage.getItem(HOVER_SFX_VOLUME_KEY);
if (v === null) return 0.25;
const n = parseFloat(v);
return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0.25;
} catch {
return 0.25;
}
}
function persistHoverEnabled(enabled: boolean) {
try {
localStorage.setItem(HOVER_SFX_STORAGE_KEY, enabled ? '1' : '0');
} catch {
/* ignore */
}
}
function persistHoverVolume(volume: number) {
try {
localStorage.setItem(HOVER_SFX_VOLUME_KEY, String(volume));
} catch {
/* ignore */
}
}
type HoverVariant = 'wub' | 'blip' | 'stab';
export class HoverSfxEngine {
private ctx: AudioContext | null = null;
private enabled = loadHoverEnabled();
private volume = loadHoverVolume();
private unlocked = false;
private lastPlayAt = 0;
private debounceMs = 140;
isEnabled() {
return this.enabled;
}
getVolume() {
return this.volume;
}
setEnabled(enabled: boolean) {
this.enabled = enabled;
persistHoverEnabled(enabled);
}
setVolume(volume: number) {
this.volume = Math.min(1, Math.max(0, volume));
persistHoverVolume(this.volume);
}
setDebounceMs(ms: number) {
this.debounceMs = Math.max(80, ms);
}
unlock() {
if (this.unlocked) return;
try {
const Ctx =
typeof window !== 'undefined'
? window.AudioContext ||
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext
: undefined;
if (!Ctx) return;
if (!this.ctx) this.ctx = new Ctx();
if (this.ctx.state === 'suspended') void this.ctx.resume();
this.unlocked = true;
} catch {
/* ignore */
}
}
/** Respects main SFX mute via `sfxEnabled` from caller. */
play(sfxEnabled = true) {
if (!sfxEnabled || !this.enabled) return;
const now = Date.now();
if (now - this.lastPlayAt < this.debounceMs) return;
this.lastPlayAt = now;
this.unlock();
try {
const Ctx = window.AudioContext ||
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!Ctx) return;
if (!this.ctx) this.ctx = new Ctx();
const ctx = this.ctx;
if (ctx.state === 'suspended') void ctx.resume();
const variant = pickVariant();
this.scheduleVariant(ctx, variant);
} catch {
/* Audio blocked */
}
}
preview(sfxEnabled = true) {
this.lastPlayAt = 0;
this.play(sfxEnabled);
}
private scheduleVariant(ctx: AudioContext, variant: HoverVariant) {
const master = ctx.createGain();
master.gain.value = this.volume;
master.connect(ctx.destination);
const t0 = ctx.currentTime;
switch (variant) {
case 'wub':
this.scheduleWub(ctx, master, t0);
break;
case 'blip':
this.scheduleBlip(ctx, master, t0);
break;
case 'stab':
this.scheduleStab(ctx, master, t0);
break;
}
}
private scheduleWub(ctx: AudioContext, dest: GainNode, t0: number) {
const osc = ctx.createOscillator();
const g = ctx.createGain();
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(420, t0);
filter.frequency.exponentialRampToValueAtTime(90, t0 + 0.1);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(95, t0);
osc.frequency.exponentialRampToValueAtTime(42, t0 + 0.09);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(0.09, t0 + 0.012);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.11);
osc.connect(filter);
filter.connect(g);
g.connect(dest);
osc.start(t0);
osc.stop(t0 + 0.13);
}
private scheduleBlip(ctx: AudioContext, dest: GainNode, t0: number) {
const freq = 280 + Math.random() * 180;
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(freq, t0);
osc.frequency.exponentialRampToValueAtTime(freq * 1.4, t0 + 0.04);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(0.05, t0 + 0.006);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.055);
osc.connect(g);
g.connect(dest);
osc.start(t0);
osc.stop(t0 + 0.07);
}
private scheduleStab(ctx: AudioContext, dest: GainNode, t0: number) {
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(62, t0);
osc.frequency.setValueAtTime(48, t0 + 0.03);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(0.07, t0 + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.08);
osc.connect(g);
g.connect(dest);
osc.start(t0);
osc.stop(t0 + 0.1);
}
}
function pickVariant(): HoverVariant {
const r = Math.random();
if (r < 0.45) return 'wub';
if (r < 0.8) return 'blip';
return 'stab';
}
export const hoverSfxEngine = new HoverSfxEngine();