Les observations du conseiller cessent de s evaporer

Ses helpers sont ecrases toutes les quinze minutes : un signalement grave
emis a 3 h du matin avait disparu au reveil, et rien dans la memoire du
projet n en gardait trace. C est precisement ce que le proprietaire
demandait quand il a dit vouloir un outil pour garder une trace des
evolutions — voir un comportement idiot du systeme ne sert a rien si
personne ne peut le relire le lendemain.

Une table `observation`, alimentee par le service d historisation, clef sur
l horodatage du marqueur du conseiller : rejouer un cycle n invente rien et
un conseiller a l arret n ecrit pas la meme ligne toutes les minutes. La
confiance est extraite de `_reason` (« [NN%] la piste ») pour rester
interrogeable sans decouper une chaine en SQL.

`node tools/enquete.mjs observations [heures] [gravite]` ne montre par
defaut que ce qui a ete signale, et compte a part les cycles calmes : un
observateur qui fonctionne est silencieux la plupart du temps, et noyer les
signalements sous des centaines de « rien a dire » les rendrait invisibles.

Essai a blanc : observation=1, le service ecrirait bien sa ligne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-13 11:30:18 +02:00
parent e95ea15516
commit 2345b87d04
2 changed files with 78 additions and 3 deletions

View File

@@ -263,6 +263,37 @@ async function ecrireBilan(sql, st) {
return rowCount;
}
// Une observation par run du conseiller. La cle est l'horodatage de SON
// marqueur : rejouer un cycle n'invente rien, et un conseiller a l'arret
// n'ecrit pas la meme observation toutes les minutes.
async function ecrireObservation(sql, st) {
const marqueur = st.get('input_text.evcs_advisor_last_run');
if (!marqueur || !txt(marqueur.state)) return 0;
const g = (id) => txt(st.get(id)?.state);
// `_reason` porte « [NN%] la piste ». On separe pour que la confiance
// reste interrogeable sans avoir a decouper une chaine en SQL.
const reason = g('input_text.evcs_advisor_reason') || '';
const m = reason.match(/^\[(\d+)%\]\s*(.*)$/s);
const { rowCount } = await sql(
`INSERT INTO observation (ts, statut, gravite, constat, ce_qui_cloche,
piste, modele, soc_pct, brut)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
ON CONFLICT (ts) DO NOTHING`,
[
marqueur.last_updated,
g('input_text.evcs_advisor_status'),
g('input_text.evcs_advisor_gravite'),
g('input_text.evcs_advisor_constat'),
g('input_text.evcs_advisor_ce_qui_cloche'),
m ? m[2] : reason,
g('input_text.evcs_advisor_model'),
num(st.get('sensor.victron_battery_soc')?.state),
JSON.stringify({ confiance_pct: m ? Number(m[1]) : null, reason_brut: reason }),
]
);
return rowCount;
}
async function ecrireDecision(sql, st) {
const etat = st.get('input_text.victron_evcs_surplus_state');
if (!etat) return 0;
@@ -509,10 +540,32 @@ async function migrer(client) {
)`);
await client.query(
'CREATE INDEX IF NOT EXISTS cellule_module_ts_idx ON cellule (module, ts DESC)');
// Ce que le CONSEILLER a observe. Ses helpers sont ecrases toutes les
// quinze minutes : sans cette table, un signalement grave emis a 3 h du
// matin a disparu au reveil. C'est precisement la trace que le proprietaire
// demandait — voir un comportement idiot ne sert a rien si personne ne peut
// le relire le lendemain.
await client.query(`
CREATE TABLE IF NOT EXISTS observation (
ts timestamptz PRIMARY KEY,
statut text,
gravite text,
constat text,
ce_qui_cloche text,
piste text,
modele text,
soc_pct double precision,
brut jsonb
)`);
// On interroge cette table par gravite bien plus souvent que par date :
// « montre-moi tous les signalements graves de la semaine ».
await client.query(
'CREATE INDEX IF NOT EXISTS observation_gravite_idx ON observation (gravite, ts DESC)');
// Le role de lecture doit voir la nouvelle table. S'il n'existe pas encore,
// on ne fait pas echouer le demarrage pour autant.
await client.query(`DO $$ BEGIN
GRANT SELECT ON session_charge TO evcs_lecture;
GRANT SELECT ON session_charge, cellule, observation TO evcs_lecture;
EXCEPTION WHEN undefined_object THEN NULL; END $$`);
}
@@ -806,6 +859,7 @@ async function cycle(client) {
mesures: await ecrireMesures(sql, st),
bilan: await ecrireBilan(sql, st),
decision: await ecrireDecision(sql, st),
observation: await ecrireObservation(sql, st),
prevision: await ecrirePrevision(sql, st),
session: await ecrireSession(sql, client, st),
evenement: await detecterEvenements(sql, st),
@@ -820,12 +874,13 @@ async function cycle(client) {
return 0;
}),
};
const total = r.mesures + r.bilan + r.decision + r.prevision + r.evenement + r.session + r.cellules + (r.rapport || 0);
const total = r.mesures + r.bilan + r.decision + r.prevision + r.evenement + r.session + r.cellules + r.observation + (r.rapport || 0);
if (total > 0 || ONCE) {
console.log(
`${new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })} ` +
`${DRY ? '[BLANC] ' : ''}mesures=${r.mesures} bilan=${r.bilan} ` +
`decision=${r.decision} prevision=${r.prevision} session=${r.session} evenement=${r.evenement} cellules=${r.cellules}`
`decision=${r.decision} prevision=${r.prevision} session=${r.session} evenement=${r.evenement} ` +
`observation=${r.observation} cellules=${r.cellules}`
);
}
}