Files
auto-charger/web/lib/db.ts
T
admin 81ef0a82ce fix: Web sieht Collector-Daten; Collector-Robustheit + DB-Pfad im Container
- web/lib/db.ts: pro Request frische readonly-Verbindung statt langlebigem
  Singleton. Eine gecachte WAL-Verbindung bekommt Writes des Collectors über
  den Docker-FUSE-Mount nicht mit -> Seite blieb leer.
- collector.py: connect_async + reconnect_delay_set statt connect(), damit ein
  nicht erreichbarer Broker keinen Container-Crash-Loop auslöst, sondern
  in-process mit Backoff neu versucht wird.
- docker-compose.yml: DB_PATH=/data/charges.db für den collector-Service
  erzwungen, damit ein relatives DB_PATH aus .env (lokaler Lauf) im Container
  nicht neben das gemountete Volume schreibt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 13:12:12 +02:00

89 lines
2.6 KiB
TypeScript

import Database from "better-sqlite3";
const DB_PATH = process.env.DB_PATH || "/data/charges.db";
export interface Charge {
id: number;
start_time: string;
end_time: string;
duration_s: number;
energy_wh: number;
}
export interface Totals {
month_wh: number;
year_wh: number;
total_wh: number;
install_date: string | null;
}
/**
* Öffnet die DB read-only und gibt sie an `fn`. Es wird bewusst pro Aufruf eine
* frische Verbindung geöffnet (und wieder geschlossen): Der Collector schreibt
* aus einem anderen Prozess/Container im WAL-Modus, und eine langlebige
* readonly-Verbindung bekommt diese Writes über den Docker-Desktop-FUSE-Mount
* nicht zuverlässig mit (inkohärenter WAL-Index im Shared-Memory). Eine frische
* Verbindung liest immer den aktuellen Stand.
*
* Gibt `fallback` zurück, wenn die Datei/das Schema noch nicht existiert
* (Collector legt sie beim ersten Start an) — die Seite bleibt dann leer statt
* zu crashen.
*/
function withDb<T>(fallback: T, fn: (conn: Database.Database) => T): T {
let conn: Database.Database | null = null;
try {
conn = new Database(DB_PATH, { readonly: true, fileMustExist: true });
const hasTable = conn
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='charges'")
.get();
if (!hasTable) return fallback;
return fn(conn);
} catch {
return fallback;
} finally {
conn?.close();
}
}
export function getCharges(): Charge[] {
return withDb<Charge[]>([], (conn) =>
conn
.prepare(
`SELECT id, start_time, end_time, duration_s, energy_wh
FROM charges ORDER BY start_time DESC`,
)
.all() as Charge[],
);
}
export function getTotals(): Totals {
const empty: Totals = { month_wh: 0, year_wh: 0, total_wh: 0, install_date: null };
return withDb<Totals>(empty, (conn) => {
const month = conn
.prepare(
`SELECT COALESCE(SUM(energy_wh), 0) AS s FROM charges
WHERE strftime('%Y-%m', start_time) = strftime('%Y-%m', 'now', 'localtime')`,
)
.get() as { s: number };
const year = conn
.prepare(
`SELECT COALESCE(SUM(energy_wh), 0) AS s FROM charges
WHERE strftime('%Y', start_time) = strftime('%Y', 'now', 'localtime')`,
)
.get() as { s: number };
const total = conn
.prepare(`SELECT COALESCE(SUM(energy_wh), 0) AS s FROM charges`)
.get() as { s: number };
const install = conn
.prepare(`SELECT value FROM meta WHERE key = 'install_date'`)
.get() as { value: string } | undefined;
return {
month_wh: month.s,
year_wh: year.s,
total_wh: total.s,
install_date: install?.value ?? null,
};
});
}