1. Stack & Architecture

  • Framework: SvelteKit (@sveltejs/kit)
  • Adapter: @sveltejs/adapter-node (Required for native OS file access to ~/.omp/stats.db).
  • Database: better-sqlite3 (Fast, synchronous, native SQLite bindings).
  • Architecture Pattern: Repository Pattern (SoC) -> Server-only Loaders -> Client UI.

2. Directory Structure

src/
β”œβ”€β”€ lib/
β”‚   └── server/
β”‚       β”œβ”€β”€ db.ts          # Singleton better-sqlite3 connection
β”‚       └── statsDao.ts    # Data Access Object for SQLite queries
β”œβ”€β”€ routes/
β”‚   β”œβ”€β”€ stats/
β”‚   β”‚   β”œβ”€β”€ +page.server.ts # Server-side loader (Initial Hydration)
β”‚   β”‚   └── +page.svelte    # Dashboard UI
β”‚   └── api/
β”‚       └── stats/
β”‚           └── +server.ts  # API Endpoint for Live Polling / SSE

3. Database Connection (lib/server/db.ts)

Must open SQLite in readonly mode to prevent locking conflicts with the active OMP logger. WAL mode should be assumed active.

import Database from "better-sqlite3"
import os from "os"
import path from "path"
 
const dbPath = path.join(os.homedir(), ".omp", "stats.db")
const db = new Database(dbPath, { readonly: true })
db.pragma("journal_mode = WAL")
 
export default db

4. Data Access Layer (lib/server/statsDao.ts)

Adheres to Single Responsibility Principle (SRP). Exposes pure aggregate queries.

import db from "./db"
 
export function getGlobalAggregates() {
  const stmt = db.prepare(`
    SELECT 
      SUM(total_tokens) as total_tokens,
      SUM(cost_total) as total_cost,
      COUNT(id) as total_requests
    FROM messages
  `)
  return stmt.get()
}
 
export function getUserSentimentAggregates() {
  const stmt = db.prepare(`
    SELECT 
      AVG(anguish) as avg_anguish,
      AVG(yelling) as avg_yelling,
      AVG(profanity) as avg_profanity
    FROM user_messages
  `)
  return stmt.get()
}
 
export function getLatestFileOffset() {
  const stmt = db.prepare(`SELECT MAX(last_modified) as last_mod FROM file_offsets`)
  return stmt.get().last_mod
}

5. Server Loaders (routes/stats/+page.server.ts)

Fetches initial SSR data so the page load is instantaneous.

import type { PageServerLoad } from "./$types"
import { getGlobalAggregates, getUserSentimentAggregates } from "$lib/server/statsDao"
 
export const load: PageServerLoad = async () => {
  return {
    aggregates: getGlobalAggregates(),
    sentiment: getUserSentimentAggregates(),
  }
}

6. Live Data Strategy

Since better-sqlite3 is synchronous and SQLite doesn’t natively push events to Node on external writes, we implement HTTP Polling combined with offset checking.

  • Client Polling (SWR): Use SvelteKit’s invalidate('omp:stats') in +page.svelte inside a setInterval. Fast, simple, leverages existing loaders.
  • Optimization: Check the file_offsets table first. Only re-run expensive SUM() and AVG() queries if last_modified or offset has increased since the last tick.

7. SOLID / ACID Validation

  • SRP & SoC: DB connection, raw SQL queries, and HTTP routing are strictly separated.
  • OCP: statsDao.ts can be extended with new granular queries without mutating base exports.
  • ACID: Enforced by SQLite natively. readonly: true ensures the node process cannot violate OMP’s write atomicity.