mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-27 18:12:05 -07:00
feat(stats): add v1 immersion stats dashboard (#19)
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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)',
|
||||
|
||||
11
src/config/definitions/defaults-stats.ts
Normal file
11
src/config/definitions/defaults-stats.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
@@ -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.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
39
src/config/definitions/options-stats.ts
Normal file
39
src/config/definitions/options-stats.ts
Normal 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.',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
Reference in New Issue
Block a user