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>
This commit is contained in:
@@ -121,8 +121,13 @@ def main() -> None:
|
|||||||
client.on_disconnect = on_disconnect
|
client.on_disconnect = on_disconnect
|
||||||
client.on_message = on_message
|
client.on_message = on_message
|
||||||
|
|
||||||
|
# connect_async + loop_forever: bei nicht erreichbarem Broker wird
|
||||||
|
# in-process mit Backoff neu versucht, statt den Prozess zu beenden
|
||||||
|
# (kein Container-Crash-Loop). Verbindet sich selbst, sobald der
|
||||||
|
# Broker erreichbar ist.
|
||||||
log(f"Verbinde zu {MQTT_HOST}:{MQTT_PORT} ...")
|
log(f"Verbinde zu {MQTT_HOST}:{MQTT_PORT} ...")
|
||||||
client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
|
client.reconnect_delay_set(min_delay=1, max_delay=60)
|
||||||
|
client.connect_async(MQTT_HOST, MQTT_PORT, keepalive=60)
|
||||||
client.loop_forever(retry_first_connection=True)
|
client.loop_forever(retry_first_connection=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ services:
|
|||||||
container_name: auto-charger-collector
|
container_name: auto-charger-collector
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
# überschreibt ein evtl. relatives DB_PATH aus .env (lokaler Lauf) —
|
||||||
|
# im Container muss die DB im gemounteten Volume /data liegen.
|
||||||
|
- DB_PATH=/data/charges.db
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
|
||||||
|
|||||||
+25
-23
@@ -17,47 +17,48 @@ export interface Totals {
|
|||||||
install_date: string | null;
|
install_date: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let db: Database.Database | null = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Öffnet die DB read-only. Gibt null zurück, falls die Datei noch nicht
|
* Öffnet die DB read-only und gibt sie an `fn`. Es wird bewusst pro Aufruf eine
|
||||||
* existiert (Collector legt sie beim ersten Start an) oder noch kein Schema
|
* frische Verbindung geöffnet (und wieder geschlossen): Der Collector schreibt
|
||||||
* vorhanden ist — die Seite bleibt dann leer statt zu crashen.
|
* 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 getDb(): Database.Database | null {
|
function withDb<T>(fallback: T, fn: (conn: Database.Database) => T): T {
|
||||||
if (db) return db;
|
let conn: Database.Database | null = null;
|
||||||
try {
|
try {
|
||||||
const conn = new Database(DB_PATH, { readonly: true, fileMustExist: true });
|
conn = new Database(DB_PATH, { readonly: true, fileMustExist: true });
|
||||||
const hasTable = conn
|
const hasTable = conn
|
||||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='charges'")
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='charges'")
|
||||||
.get();
|
.get();
|
||||||
if (!hasTable) {
|
if (!hasTable) return fallback;
|
||||||
conn.close();
|
return fn(conn);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
db = conn;
|
|
||||||
return db;
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return fallback;
|
||||||
|
} finally {
|
||||||
|
conn?.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCharges(): Charge[] {
|
export function getCharges(): Charge[] {
|
||||||
const conn = getDb();
|
return withDb<Charge[]>([], (conn) =>
|
||||||
if (!conn) return [];
|
conn
|
||||||
return conn
|
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT id, start_time, end_time, duration_s, energy_wh
|
`SELECT id, start_time, end_time, duration_s, energy_wh
|
||||||
FROM charges ORDER BY start_time DESC`,
|
FROM charges ORDER BY start_time DESC`,
|
||||||
)
|
)
|
||||||
.all() as Charge[];
|
.all() as Charge[],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTotals(): Totals {
|
export function getTotals(): Totals {
|
||||||
const conn = getDb();
|
const empty: Totals = { month_wh: 0, year_wh: 0, total_wh: 0, install_date: null };
|
||||||
if (!conn) {
|
return withDb<Totals>(empty, (conn) => {
|
||||||
return { month_wh: 0, year_wh: 0, total_wh: 0, install_date: null };
|
|
||||||
}
|
|
||||||
const month = conn
|
const month = conn
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT COALESCE(SUM(energy_wh), 0) AS s FROM charges
|
`SELECT COALESCE(SUM(energy_wh), 0) AS s FROM charges
|
||||||
@@ -83,4 +84,5 @@ export function getTotals(): Totals {
|
|||||||
total_wh: total.s,
|
total_wh: total.s,
|
||||||
install_date: install?.value ?? null,
|
install_date: install?.value ?? null,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user