feat(stats): wire stats server, overlay, and CLI into main process

- Stats server auto-start on immersion tracker init
- Stats overlay toggle via keybinding and IPC
- Stats CLI command (subminer stats) with cleanup mode
- mpv plugin menu integration for stats toggle
- CLI args for --stats, --stats-cleanup, --stats-response-path
This commit is contained in:
2026-03-14 22:14:32 -07:00
parent ffe5c6e1c6
commit 26fb5b4162
37 changed files with 374 additions and 23 deletions

View File

@@ -143,6 +143,12 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => {
assert.equal(dictionaryTarget.dictionary, true);
assert.equal(dictionaryTarget.dictionaryTarget, '/tmp/example.mkv');
const stats = parseArgs(['--stats', '--stats-response-path', '/tmp/subminer-stats-response.json']);
assert.equal(stats.stats, true);
assert.equal(stats.statsResponsePath, '/tmp/subminer-stats-response.json');
assert.equal(hasExplicitCommand(stats), true);
assert.equal(shouldStartApp(stats), true);
const jellyfinLibraries = parseArgs(['--jellyfin-libraries']);
assert.equal(jellyfinLibraries.jellyfinLibraries, true);
assert.equal(hasExplicitCommand(jellyfinLibraries), true);

View File

@@ -29,6 +29,10 @@ export interface CliArgs {
anilistRetryQueue: boolean;
dictionary: boolean;
dictionaryTarget?: string;
stats: boolean;
statsCleanup?: boolean;
statsCleanupVocab?: boolean;
statsResponsePath?: string;
jellyfin: boolean;
jellyfinLogin: boolean;
jellyfinLogout: boolean;
@@ -97,6 +101,9 @@ export function parseArgs(argv: string[]): CliArgs {
anilistSetup: false,
anilistRetryQueue: false,
dictionary: false,
stats: false,
statsCleanup: false,
statsCleanupVocab: false,
jellyfin: false,
jellyfinLogin: false,
jellyfinLogout: false,
@@ -162,6 +169,15 @@ export function parseArgs(argv: string[]): CliArgs {
} else if (arg === '--dictionary-target') {
const value = readValue(argv[i + 1]);
if (value) args.dictionaryTarget = value;
} else if (arg === '--stats') args.stats = true;
else if (arg === '--stats-cleanup') args.statsCleanup = true;
else if (arg === '--stats-cleanup-vocab') args.statsCleanupVocab = true;
else if (arg.startsWith('--stats-response-path=')) {
const value = arg.split('=', 2)[1];
if (value) args.statsResponsePath = value;
} else if (arg === '--stats-response-path') {
const value = readValue(argv[i + 1]);
if (value) args.statsResponsePath = value;
} else if (arg === '--jellyfin') args.jellyfin = true;
else if (arg === '--jellyfin-login') args.jellyfinLogin = true;
else if (arg === '--jellyfin-logout') args.jellyfinLogout = true;
@@ -331,6 +347,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
args.anilistSetup ||
args.anilistRetryQueue ||
args.dictionary ||
args.stats ||
args.jellyfin ||
args.jellyfinLogin ||
args.jellyfinLogout ||
@@ -367,6 +384,7 @@ export function shouldStartApp(args: CliArgs): boolean {
args.markAudioCard ||
args.openRuntimeOptions ||
args.dictionary ||
args.stats ||
args.jellyfin ||
args.jellyfinPlay ||
args.texthooker
@@ -408,6 +426,7 @@ export function shouldRunSettingsOnlyStartup(args: CliArgs): boolean {
!args.anilistSetup &&
!args.anilistRetryQueue &&
!args.dictionary &&
!args.stats &&
!args.jellyfin &&
!args.jellyfinLogin &&
!args.jellyfinLogout &&

View File

@@ -18,6 +18,7 @@ test('printHelp includes configured texthooker port', () => {
assert.match(output, /--help\s+Show this help/);
assert.match(output, /default: 7777/);
assert.match(output, /--launch-mpv/);
assert.match(output, /--stats\s+Open the stats dashboard in your browser/);
assert.match(output, /--refresh-known-words/);
assert.match(output, /--setup\s+Open first-run setup window/);
assert.match(output, /--anilist-status/);

View File

@@ -14,6 +14,7 @@ ${B}Session${R}
--start Connect to mpv and launch overlay
--launch-mpv ${D}[targets...]${R} Launch mpv with the SubMiner mpv profile and exit
--stop Stop the running instance
--stats Open the stats dashboard in your browser
--texthooker Start texthooker server only ${D}(no overlay)${R}
${B}Overlay${R}