feat(stats): add v1 immersion stats dashboard (#19)

This commit is contained in:
2026-03-20 02:43:28 -07:00
committed by GitHub
parent 42abdd1268
commit 6749ff843c
555 changed files with 46356 additions and 2553 deletions

View File

@@ -9,12 +9,20 @@ export const IMMERSION_DEFAULT_CONFIG: Pick<ResolvedConfig, 'immersionTracking'>
queueCap: 1000,
payloadCapBytes: 256,
maintenanceIntervalMs: 24 * 60 * 60 * 1000,
retentionMode: 'preset',
retentionPreset: 'balanced',
retention: {
eventsDays: 7,
telemetryDays: 30,
dailyRollupsDays: 365,
monthlyRollupsDays: 5 * 365,
vacuumIntervalDays: 7,
eventsDays: 0,
telemetryDays: 0,
sessionsDays: 0,
dailyRollupsDays: 0,
monthlyRollupsDays: 0,
vacuumIntervalDays: 0,
},
lifetimeSummaries: {
global: true,
anime: true,
media: true,
},
},
};

View File

@@ -23,6 +23,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
},
tags: ['SubMiner'],
fields: {
word: 'Expression',
audio: 'ExpressionAudio',
image: 'Picture',
sentence: 'Sentence',
@@ -46,10 +47,19 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
animatedMaxWidth: 640,
animatedMaxHeight: undefined,
animatedCrf: 35,
syncAnimatedImageToWordAudio: true,
audioPadding: 0.5,
fallbackDuration: 3.0,
maxMediaDuration: 30,
},
knownWords: {
highlightEnabled: false,
refreshMinutes: 1440,
addMinedWordsImmediately: true,
matchMode: 'headword',
decks: {},
color: '#a6da95',
},
behavior: {
overwriteAudio: true,
overwriteImage: true,
@@ -59,13 +69,8 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
autoUpdateNewCards: true,
},
nPlusOne: {
highlightEnabled: false,
refreshMinutes: 1440,
matchMode: 'headword',
decks: [],
minSentenceWords: 3,
nPlusOne: '#c6a0f6',
knownWord: '#a6da95',
},
metadata: {
pattern: '[SubMiner] %f (%t)',

View File

@@ -0,0 +1,11 @@
import { ResolvedConfig } from '../../types.js';
export const STATS_DEFAULT_CONFIG: Pick<ResolvedConfig, 'stats'> = {
stats: {
toggleKey: 'Backquote',
markWatchedKey: 'KeyW',
serverPort: 6969,
autoStartServer: true,
autoOpenBrowser: true,
},
};

View File

@@ -48,35 +48,73 @@ export function buildImmersionConfigOptionRegistry(
defaultValue: defaultConfig.immersionTracking.maintenanceIntervalMs,
description: 'Maintenance cadence (prune + rollup + vacuum checks).',
},
{
path: 'immersionTracking.retentionMode',
kind: 'string',
defaultValue: defaultConfig.immersionTracking.retentionMode,
description: 'Retention mode (`preset` uses preset values, `advanced` uses explicit values).',
enumValues: ['preset', 'advanced'],
},
{
path: 'immersionTracking.retentionPreset',
kind: 'string',
defaultValue: defaultConfig.immersionTracking.retentionPreset,
description: 'Retention preset when `retentionMode` is `preset`.',
enumValues: ['minimal', 'balanced', 'deep-history'],
},
{
path: 'immersionTracking.retention.eventsDays',
kind: 'number',
defaultValue: defaultConfig.immersionTracking.retention.eventsDays,
description: 'Raw event retention window in days.',
description: 'Raw event retention window in days. Use 0 to keep all.',
},
{
path: 'immersionTracking.retention.telemetryDays',
kind: 'number',
defaultValue: defaultConfig.immersionTracking.retention.telemetryDays,
description: 'Telemetry retention window in days.',
description: 'Telemetry retention window in days. Use 0 to keep all.',
},
{
path: 'immersionTracking.retention.sessionsDays',
kind: 'number',
defaultValue: defaultConfig.immersionTracking.retention.sessionsDays,
description: 'Session retention window in days. Use 0 to keep all.',
},
{
path: 'immersionTracking.retention.dailyRollupsDays',
kind: 'number',
defaultValue: defaultConfig.immersionTracking.retention.dailyRollupsDays,
description: 'Daily rollup retention window in days.',
description: 'Daily rollup retention window in days. Use 0 to keep all.',
},
{
path: 'immersionTracking.retention.monthlyRollupsDays',
kind: 'number',
defaultValue: defaultConfig.immersionTracking.retention.monthlyRollupsDays,
description: 'Monthly rollup retention window in days.',
description: 'Monthly rollup retention window in days. Use 0 to keep all.',
},
{
path: 'immersionTracking.retention.vacuumIntervalDays',
kind: 'number',
defaultValue: defaultConfig.immersionTracking.retention.vacuumIntervalDays,
description: 'Minimum days between VACUUM runs.',
description: 'Minimum days between VACUUM runs. Use 0 to disable.',
},
{
path: 'immersionTracking.lifetimeSummaries.global',
kind: 'boolean',
defaultValue: defaultConfig.immersionTracking.lifetimeSummaries?.global,
description: 'Maintain global lifetime stats rows.',
},
{
path: 'immersionTracking.lifetimeSummaries.anime',
kind: 'boolean',
defaultValue: defaultConfig.immersionTracking.lifetimeSummaries?.anime,
description: 'Maintain per-anime lifetime stats rows.',
},
{
path: 'immersionTracking.lifetimeSummaries.media',
kind: 'boolean',
defaultValue: defaultConfig.immersionTracking.lifetimeSummaries?.media,
description: 'Maintain per-media lifetime stats rows.',
},
];
}

View File

@@ -51,6 +51,12 @@ export function buildIntegrationConfigOptionRegistry(
description:
'Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.',
},
{
path: 'ankiConnect.fields.word',
kind: 'string',
defaultValue: defaultConfig.ankiConnect.fields.word,
description: 'Card field for the mined word or expression text.',
},
{
path: 'ankiConnect.ai.enabled',
kind: 'boolean',
@@ -77,24 +83,37 @@ export function buildIntegrationConfigOptionRegistry(
runtime: runtimeOptionById.get('anki.autoUpdateNewCards'),
},
{
path: 'ankiConnect.nPlusOne.matchMode',
kind: 'enum',
enumValues: ['headword', 'surface'],
defaultValue: defaultConfig.ankiConnect.nPlusOne.matchMode,
description: 'Known-word matching strategy for N+1 highlighting.',
path: 'ankiConnect.media.syncAnimatedImageToWordAudio',
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.media.syncAnimatedImageToWordAudio,
description:
'For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio.',
},
{
path: 'ankiConnect.nPlusOne.highlightEnabled',
path: 'ankiConnect.knownWords.matchMode',
kind: 'enum',
enumValues: ['headword', 'surface'],
defaultValue: defaultConfig.ankiConnect.knownWords.matchMode,
description: 'Known-word matching strategy for subtitle annotations.',
},
{
path: 'ankiConnect.knownWords.highlightEnabled',
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.nPlusOne.highlightEnabled,
defaultValue: defaultConfig.ankiConnect.knownWords.highlightEnabled,
description: 'Enable fast local highlighting for words already known in Anki.',
},
{
path: 'ankiConnect.nPlusOne.refreshMinutes',
path: 'ankiConnect.knownWords.refreshMinutes',
kind: 'number',
defaultValue: defaultConfig.ankiConnect.nPlusOne.refreshMinutes,
defaultValue: defaultConfig.ankiConnect.knownWords.refreshMinutes,
description: 'Minutes between known-word cache refreshes.',
},
{
path: 'ankiConnect.knownWords.addMinedWordsImmediately',
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.knownWords.addMinedWordsImmediately,
description: 'Immediately append newly mined card words into the known-word cache.',
},
{
path: 'ankiConnect.nPlusOne.minSentenceWords',
kind: 'number',
@@ -102,10 +121,11 @@ export function buildIntegrationConfigOptionRegistry(
description: 'Minimum sentence word count required for N+1 targeting (default: 3).',
},
{
path: 'ankiConnect.nPlusOne.decks',
kind: 'array',
defaultValue: defaultConfig.ankiConnect.nPlusOne.decks,
description: 'Decks used for N+1 known-word cache scope. Supports one or more deck names.',
path: 'ankiConnect.knownWords.decks',
kind: 'object',
defaultValue: defaultConfig.ankiConnect.knownWords.decks,
description:
'Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }.',
},
{
path: 'ankiConnect.nPlusOne.nPlusOne',
@@ -114,10 +134,10 @@ export function buildIntegrationConfigOptionRegistry(
description: 'Color used for the single N+1 target token highlight.',
},
{
path: 'ankiConnect.nPlusOne.knownWord',
path: 'ankiConnect.knownWords.color',
kind: 'string',
defaultValue: defaultConfig.ankiConnect.nPlusOne.knownWord,
description: 'Color used for legacy known-word highlights.',
defaultValue: defaultConfig.ankiConnect.knownWords.color,
description: 'Color used for known-word highlights.',
},
{
path: 'ankiConnect.isKiku.fieldGrouping',

View File

@@ -0,0 +1,39 @@
import { ResolvedConfig } from '../../types.js';
import { ConfigOptionRegistryEntry } from './shared.js';
export function buildStatsConfigOptionRegistry(
defaultConfig: ResolvedConfig,
): ConfigOptionRegistryEntry[] {
return [
{
path: 'stats.toggleKey',
kind: 'string',
defaultValue: defaultConfig.stats.toggleKey,
description: 'Key code to toggle the stats overlay.',
},
{
path: 'stats.markWatchedKey',
kind: 'string',
defaultValue: defaultConfig.stats.markWatchedKey,
description: 'Key code to mark the current video as watched and advance to the next playlist entry.',
},
{
path: 'stats.serverPort',
kind: 'number',
defaultValue: defaultConfig.stats.serverPort,
description: 'Port for the stats HTTP server.',
},
{
path: 'stats.autoStartServer',
kind: 'boolean',
defaultValue: defaultConfig.stats.autoStartServer,
description: 'Automatically start the stats server on launch.',
},
{
path: 'stats.autoOpenBrowser',
kind: 'boolean',
defaultValue: defaultConfig.stats.autoOpenBrowser,
description: 'Automatically open the stats dashboard in a browser when the server starts.',
},
];
}

View File

@@ -21,15 +21,19 @@ export function buildRuntimeOptionRegistry(
},
{
id: 'subtitle.annotation.nPlusOne',
path: 'ankiConnect.nPlusOne.highlightEnabled',
path: 'ankiConnect.knownWords.highlightEnabled',
label: 'N+1 Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.ankiConnect.nPlusOne.highlightEnabled,
defaultValue: defaultConfig.ankiConnect.knownWords.highlightEnabled,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: () => ({}),
toAnkiPatch: (value) => ({
knownWords: {
highlightEnabled: value === true,
},
}),
},
{
id: 'subtitle.annotation.jlpt',
@@ -57,16 +61,16 @@ export function buildRuntimeOptionRegistry(
},
{
id: 'anki.nPlusOneMatchMode',
path: 'ankiConnect.nPlusOne.matchMode',
label: 'N+1 Match Mode',
path: 'ankiConnect.knownWords.matchMode',
label: 'Known Word Match Mode',
scope: 'ankiConnect',
valueType: 'enum',
allowedValues: ['headword', 'surface'],
defaultValue: defaultConfig.ankiConnect.nPlusOne.matchMode,
defaultValue: defaultConfig.ankiConnect.knownWords.matchMode,
requiresRestart: false,
formatValueForOsd: (value) => String(value),
toAnkiPatch: (value) => ({
nPlusOne: {
knownWords: {
matchMode: value === 'headword' || value === 'surface' ? value : 'headword',
},
}),

View File

@@ -176,6 +176,14 @@ const IMMERSION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
],
key: 'immersionTracking',
},
{
title: 'Stats Dashboard',
description: [
'Local immersion stats dashboard served on localhost and available as an in-app overlay.',
'Uses the immersion tracking database for overview, trends, sessions, and vocabulary views.',
],
key: 'stats',
},
];
export const CONFIG_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [