feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops

- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help
- Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete
- Sigil scramble post-forge uniquification and Dispense Reveal ceremony
- Full system check, desktop push, BITS/host-binary persistence, Path Tracer
- Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav
- README documents alerts, sigil scramble, and pack-usb workflow
- USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
This commit is contained in:
AetherForge
2026-06-03 20:32:59 -07:00
parent 03937edba7
commit d52479c9a6
139 changed files with 10611 additions and 369 deletions

View File

@@ -0,0 +1,79 @@
/**
* @vitest-environment happy-dom
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
HapticEngine,
loadSoundEnabled,
loadSoundVolume,
SFX_STORAGE_KEY,
SFX_VOLUME_KEY,
} from './hapticEngine';
describe('hapticEngine prefs', () => {
beforeEach(() => {
localStorage.clear();
});
it('defaults sound on and volume ~0.4', () => {
expect(loadSoundEnabled()).toBe(true);
expect(loadSoundVolume()).toBeCloseTo(0.4);
});
it('persists enabled flag', () => {
const e = new HapticEngine();
e.setEnabled(false);
expect(localStorage.getItem(SFX_STORAGE_KEY)).toBe('0');
expect(loadSoundEnabled()).toBe(false);
});
it('clamps volume', () => {
const e = new HapticEngine();
e.setVolume(2);
expect(e.getVolume()).toBe(1);
e.setVolume(-1);
expect(e.getVolume()).toBe(0);
expect(localStorage.getItem(SFX_VOLUME_KEY)).toBe('0');
});
});
describe('HapticEngine.play', () => {
const vibrate = vi.fn();
beforeEach(() => {
vibrate.mockClear();
Object.defineProperty(navigator, 'vibrate', {
value: vibrate,
configurable: true,
writable: true,
});
});
afterEach(() => {
vi.restoreAllMocks();
});
it('does not vibrate when disabled', () => {
const e = new HapticEngine();
e.setEnabled(false);
e.play('click');
expect(vibrate).not.toHaveBeenCalled();
});
it('vibrates when enabled', () => {
const e = new HapticEngine();
e.setEnabled(true);
e.play('success');
expect(vibrate).toHaveBeenCalled();
});
it('play does not throw without AudioContext', () => {
const prev = globalThis.AudioContext;
// @ts-expect-error test shim
delete globalThis.AudioContext;
const e = new HapticEngine();
e.setEnabled(true);
expect(() => e.play('click')).not.toThrow();
globalThis.AudioContext = prev;
});
});

View File

@@ -0,0 +1,228 @@
/** UI + fleet event cues — synthesized via Web Audio (no asset files). */
export type SoundCue =
| 'click'
| 'nav'
| 'success'
| 'error'
| 'alert'
| 'alertCritical'
| 'share'
| 'connect'
| 'disconnect'
| 'online'
| 'offline';
export const SFX_STORAGE_KEY = 'aetherforge-sfx';
export const SFX_VOLUME_KEY = 'aetherforge-sfx-volume';
export function loadSoundEnabled(): boolean {
try {
const v = localStorage.getItem(SFX_STORAGE_KEY);
return v === null ? true : v === '1';
} catch {
return true;
}
}
export function loadSoundVolume(): number {
try {
const v = localStorage.getItem(SFX_VOLUME_KEY);
if (v === null) return 0.4;
const n = parseFloat(v);
return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0.4;
} catch {
return 0.4;
}
}
function persistEnabled(enabled: boolean) {
try {
localStorage.setItem(SFX_STORAGE_KEY, enabled ? '1' : '0');
} catch {
/* ignore */
}
}
function persistVolume(volume: number) {
try {
localStorage.setItem(SFX_VOLUME_KEY, String(volume));
} catch {
/* ignore */
}
}
const VIBRATE: Partial<Record<SoundCue, number | number[]>> = {
click: 8,
nav: 12,
success: [12, 40, 18],
error: [30, 50, 80],
alert: [20, 30, 20],
alertCritical: [40, 60, 40, 80],
share: 14,
connect: [10, 25],
disconnect: [35, 20],
online: [15, 35],
offline: [25, 15],
};
type ToneSpec = {
freq: number;
duration: number;
type?: OscillatorType;
gain?: number;
delay?: number;
};
function vibrateFor(cue: SoundCue) {
if (typeof navigator === 'undefined' || !navigator.vibrate) return;
const pattern = VIBRATE[cue];
if (pattern !== undefined) navigator.vibrate(pattern);
}
export class HapticEngine {
private ctx: AudioContext | null = null;
private enabled = loadSoundEnabled();
private volume = loadSoundVolume();
private unlocked = false;
isEnabled() {
return this.enabled;
}
getVolume() {
return this.volume;
}
setEnabled(enabled: boolean) {
this.enabled = enabled;
persistEnabled(enabled);
}
setVolume(volume: number) {
this.volume = Math.min(1, Math.max(0, volume));
persistVolume(this.volume);
}
/** Browsers require a user gesture before audio plays. */
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 */
}
}
play(cue: SoundCue) {
if (!this.enabled) return;
this.unlock();
vibrateFor(cue);
const specs = cueSpecs(cue);
if (!specs.length) return;
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 master = ctx.createGain();
master.gain.value = this.volume;
master.connect(ctx.destination);
const now = ctx.currentTime;
for (const spec of specs) {
this.scheduleTone(ctx, master, spec, now);
}
} catch {
/* Audio blocked or unavailable */
}
}
private scheduleTone(ctx: AudioContext, dest: GainNode, spec: ToneSpec, base: number) {
const osc = ctx.createOscillator();
const g = ctx.createGain();
const t0 = base + (spec.delay ?? 0);
const dur = spec.duration;
const peak = (spec.gain ?? 0.12) * this.volume;
osc.type = spec.type ?? 'sine';
osc.frequency.setValueAtTime(spec.freq, t0);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(Math.max(peak, 0.0001), t0 + 0.008);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
osc.connect(g);
g.connect(dest);
osc.start(t0);
osc.stop(t0 + dur + 0.02);
}
}
function cueSpecs(cue: SoundCue): ToneSpec[] {
switch (cue) {
case 'click':
return [{ freq: 920, duration: 0.04, type: 'square', gain: 0.06 }];
case 'nav':
return [
{ freq: 440, duration: 0.05, gain: 0.07 },
{ freq: 660, duration: 0.06, delay: 0.04, gain: 0.06 },
];
case 'success':
return [
{ freq: 523, duration: 0.08, gain: 0.1 },
{ freq: 784, duration: 0.1, delay: 0.07, gain: 0.09 },
];
case 'error':
return [
{ freq: 180, duration: 0.12, type: 'sawtooth', gain: 0.11 },
{ freq: 140, duration: 0.14, delay: 0.1, type: 'sawtooth', gain: 0.09 },
];
case 'alert':
return [
{ freq: 740, duration: 0.07, type: 'triangle', gain: 0.09 },
{ freq: 620, duration: 0.08, delay: 0.09, type: 'triangle', gain: 0.08 },
];
case 'alertCritical':
return [
{ freq: 880, duration: 0.06, type: 'square', gain: 0.1 },
{ freq: 660, duration: 0.06, delay: 0.07, type: 'square', gain: 0.1 },
{ freq: 440, duration: 0.1, delay: 0.14, type: 'square', gain: 0.11 },
];
case 'share':
return [
{ freq: 1200, duration: 0.05, gain: 0.08 },
{ freq: 1600, duration: 0.06, delay: 0.05, gain: 0.07 },
];
case 'connect':
return [
{ freq: 330, duration: 0.07, gain: 0.08 },
{ freq: 495, duration: 0.09, delay: 0.06, gain: 0.08 },
];
case 'disconnect':
return [
{ freq: 400, duration: 0.1, gain: 0.08 },
{ freq: 260, duration: 0.12, delay: 0.08, gain: 0.07 },
];
case 'online':
return [
{ freq: 587, duration: 0.07, gain: 0.08 },
{ freq: 880, duration: 0.09, delay: 0.06, gain: 0.07 },
];
case 'offline':
return [
{ freq: 440, duration: 0.09, gain: 0.07 },
{ freq: 330, duration: 0.1, delay: 0.07, gain: 0.06 },
];
default:
return [];
}
}
export const hapticEngine = new HapticEngine();