obsidian-logfire/src/types.ts
tolvitty d5dfdd6b0d Kern-Typen: Event-System, Settings, Query-Interfaces
Alle Event-Typen (file, content, nav, editor, vault, plugin, system),
ContentSnapshot/Delta, QueryConfig, ProjectionTemplate,
LogfireSettings mit Defaults, deepMerge-Utility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 10:45:49 +01:00

348 lines
8.5 KiB
TypeScript

// ---------------------------------------------------------------------------
// Event System
// ---------------------------------------------------------------------------
export type EventCategory =
| 'file'
| 'content'
| 'navigation'
| 'editor'
| 'vault'
| 'plugin'
| 'system';
export type EventType =
// File events
| 'file:create'
| 'file:delete'
| 'file:rename'
| 'file:move'
| 'file:modify'
// Content events (semantic diffs)
| 'content:words-changed'
| 'content:link-added'
| 'content:link-removed'
| 'content:tag-added'
| 'content:tag-removed'
| 'content:frontmatter-changed'
| 'content:heading-changed'
| 'content:embed-added'
| 'content:embed-removed'
// Navigation events
| 'nav:file-open'
| 'nav:file-close'
| 'nav:active-leaf-change'
// Editor events
| 'editor:change'
| 'editor:selection'
| 'editor:paste'
// Vault structure events
| 'vault:folder-create'
| 'vault:folder-delete'
| 'vault:folder-rename'
// System events
| 'system:session-start'
| 'system:session-end'
| 'system:baseline-scan'
| 'system:rescan'
| 'system:maintenance'
// Plugin events
| 'plugin:command-executed'
// Custom events (from other plugins via API)
| `custom:${string}`;
export interface LogfireEvent {
id: string;
timestamp: number;
type: EventType;
category: EventCategory;
source: string;
target?: string;
payload: Record<string, unknown>;
session: string;
}
// ---------------------------------------------------------------------------
// Category mapping
// ---------------------------------------------------------------------------
const CATEGORY_MAP: Record<string, EventCategory> = {
file: 'file',
content: 'content',
nav: 'navigation',
editor: 'editor',
vault: 'vault',
plugin: 'plugin',
system: 'system',
custom: 'plugin',
};
export function categoryForType(type: EventType): EventCategory {
const prefix = type.split(':')[0];
return CATEGORY_MAP[prefix] ?? 'system';
}
// ---------------------------------------------------------------------------
// Content Analysis
// ---------------------------------------------------------------------------
export interface ContentSnapshot {
path: string;
wordCount: number;
charCount: number;
links: string[];
tags: string[];
headings: { level: number; text: string }[];
frontmatter: Record<string, unknown>;
embeds: string[];
}
export interface ContentDelta {
wordsAdded: number;
wordsRemoved: number;
linksAdded: string[];
linksRemoved: string[];
tagsAdded: string[];
tagsRemoved: string[];
headingsAdded: { level: number; text: string }[];
headingsRemoved: { level: number; text: string }[];
embedsAdded: string[];
embedsRemoved: string[];
frontmatterChanged: boolean;
frontmatterChanges: Record<string, { old: unknown; new: unknown }>;
}
// ---------------------------------------------------------------------------
// Query System
// ---------------------------------------------------------------------------
export type TimeRange =
| { type: 'relative'; value: 'today' | 'yesterday' | 'this-week' | 'this-month' | 'last-7-days' | 'last-30-days' }
| { type: 'absolute'; from: number; to: number }
| { type: 'session'; sessionId?: string };
export interface QueryConfig {
timeRange: TimeRange;
eventTypes?: EventType[];
categories?: EventCategory[];
filePaths?: string[];
groupBy?: 'file' | 'type' | 'hour' | 'day' | 'session';
orderBy?: 'timestamp' | 'count' | 'words';
orderDirection?: 'asc' | 'desc';
limit?: number;
}
// ---------------------------------------------------------------------------
// Projections
// ---------------------------------------------------------------------------
export interface ProjectionTemplate {
id: string;
name: string;
description: string;
enabled: boolean;
trigger: ProjectionTrigger;
output: OutputConfig;
sections: SectionConfig[];
}
export interface ProjectionTrigger {
type: 'interval' | 'on-session-end' | 'manual' | 'cron';
interval?: number;
cron?: string;
}
export interface OutputConfig {
folder: string;
filePattern: string;
mode: 'overwrite' | 'append';
frontmatter: Record<string, string>;
}
export interface SectionConfig {
id: string;
heading: string;
type: SectionType;
query: QueryConfig;
format: FormatConfig;
enabled: boolean;
}
export type SectionType = 'timeline' | 'table' | 'summary' | 'chart-data' | 'raw-events' | 'custom';
export interface FormatConfig {
type: 'builtin' | 'custom';
builtin?: BuiltinFormat;
customTemplate?: string;
}
export type BuiltinFormat =
| { name: 'timeline'; showTimestamp: boolean; showPayload: boolean }
| { name: 'table'; columns: ColumnDef[] }
| { name: 'summary'; metrics: MetricDef[] }
| { name: 'metric'; aggregate: 'count' | 'sum' | 'avg' | 'min' | 'max'; field?: string };
export interface ColumnDef {
header: string;
value: string;
align?: 'left' | 'right' | 'center';
}
export interface MetricDef {
label: string;
aggregate: 'count' | 'sum' | 'avg' | 'min' | 'max';
field?: string;
filter?: Partial<QueryConfig>;
}
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------
export interface LogfireSettings {
settingsVersion: number;
general: {
logFolder: string;
enabled: boolean;
pauseOnStartup: boolean;
};
tracking: {
fileEvents: boolean;
contentAnalysis: boolean;
navigation: boolean;
editorChanges: boolean;
commandTracking: boolean;
excludedFolders: string[];
excludedPatterns: string[];
};
projections: {
templates: ProjectionTemplate[];
};
advanced: {
debounceMs: number;
flushIntervalMs: number;
flushThreshold: number;
retention: RetentionSettings;
database: DatabaseSettings;
};
}
export interface RetentionSettings {
rawEventsDays: number;
dailyStatsDays: number;
monthlyStatsForever: boolean;
neverDeleteTypes: EventType[];
maintenanceOnStartup: boolean;
}
export interface DatabaseSettings {
cacheSizeMb: number;
walMode: boolean;
mmapSizeMb: number;
}
export const DEFAULT_SETTINGS: LogfireSettings = {
settingsVersion: 1,
general: {
logFolder: 'Logfire',
enabled: true,
pauseOnStartup: false,
},
tracking: {
fileEvents: true,
contentAnalysis: true,
navigation: true,
editorChanges: true,
commandTracking: true,
excludedFolders: ['.obsidian'],
excludedPatterns: [],
},
projections: {
templates: [],
},
advanced: {
debounceMs: 3000,
flushIntervalMs: 5000,
flushThreshold: 100,
retention: {
rawEventsDays: 30,
dailyStatsDays: 365,
monthlyStatsForever: true,
neverDeleteTypes: ['file:create', 'file:delete', 'file:rename'],
maintenanceOnStartup: true,
},
database: {
cacheSizeMb: 64,
walMode: true,
mmapSizeMb: 256,
},
},
};
// ---------------------------------------------------------------------------
// API types
// ---------------------------------------------------------------------------
export interface VaultStats {
eventsTotal: number;
wordsAdded: number;
wordsRemoved: number;
filesCreated: number;
filesModified: number;
filesDeleted: number;
sessionCount: number;
activeTimeMs: number;
}
export interface FileStats {
path: string;
eventsTotal: number;
wordsAdded: number;
wordsRemoved: number;
activeTimeMs: number;
lastModified: number;
editSessions: number;
}
// ---------------------------------------------------------------------------
// Utilities
// ---------------------------------------------------------------------------
export function deepMerge<T>(defaults: T, overrides: Partial<T>): T {
if (
typeof defaults !== 'object' || defaults === null ||
typeof overrides !== 'object' || overrides === null
) {
return (overrides ?? defaults) as T;
}
const result = { ...defaults } as Record<string, unknown>;
const src = overrides as Record<string, unknown>;
const def = defaults as Record<string, unknown>;
for (const key of Object.keys(src)) {
const defaultVal = def[key];
const overrideVal = src[key];
if (
defaultVal !== null && defaultVal !== undefined &&
overrideVal !== null && overrideVal !== undefined &&
typeof defaultVal === 'object' && !Array.isArray(defaultVal) &&
typeof overrideVal === 'object' && !Array.isArray(overrideVal)
) {
result[key] = deepMerge(defaultVal, overrideVal);
} else if (overrideVal !== undefined) {
result[key] = overrideVal;
}
}
return result as T;
}