You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit df3e4c2
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cron): freshness gate uses the value source's own observation time, judged on age alone (codex P2 x2)
- Latest.observedAt = inverter collectionTime when the inverter's live points override the station fields, else station lastUpdateTime (toSec normalises s/ms); null when the source gave none — updatedAt stays display-only
- freeze.ts: frozen = observedAt missing OR now-observedAt > STALE_AFTER_S; no monotonic marker (a 4-day-old reading that advances 5 min is still 4 days old); last_reading_ts dropped
- tests updated (105 total)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reviewed incident guard, health reporting, operator purge and adjacent ingestion/recovery paths. No application source, production data, branch, deploy or purge was changed. Existing main untracked `.mcp.json.bak-httptest` preserved.
4
+
5
+
## P2: Gate on the timestamp of the data actually persisted
6
+
7
+
Location: `src/worker/index.ts:167` (integration with `src/worker/deye.ts:390-408`).
8
+
9
+
`Latest.updatedAt` always comes from station `lastUpdateTime`, or server `now` when absent. But `getLatestOpen` then overwrites watts/SOC with `/device/latest` values and never carries their `collectionTime` into that timestamp. The new guard therefore checks a different data source from the measurements it admits/rejects.
10
+
11
+
Actual `getLatest` -> `pollAndStore` -> in-memory SQLite reproductions at 2026-09-22 17:20 BKK:
12
+
13
+
- Station timestamp absent; inverter timestamp four days old with 2,631 W: writes a current `samples` row and `device_samples` row, deletes `frozen_since`, health healthy.
14
+
- Fresh station timestamp; same four-day-old inverter: same fabricated current rows and false recovery.
15
+
- Four-day-old station timestamp equal to marker; fresh inverter reports 4,000 W: drops the genuinely fresh sample and telemetry, remains frozen.
16
+
17
+
Carry trustworthy observation provenance from the selected source(s), retaining unknown as unknown instead of substituting poll time for freshness evidence. If values can be mixed between sources, do not label the whole bundle fresh merely because one timestamp is fresh. Test both divergence directions and station timestamp absent. Normalize timestamp units at the provenance boundary; a synthetic millisecond station timestamp also bypassed the current seconds-based age guard.
18
+
19
+
## P2: Reject known-stale observations even without a prior marker or when they advance
20
+
21
+
Location: `src/lib/freeze.ts:13`.
22
+
23
+
Known age >720 seconds is not sufficient to reject: rejection additionally requires a non-null stored marker and `readingTs <= lastStoredReadingTs`. Consequently:
24
+
25
+
- First poll after deploying this new metadata key accepts a four-day-old reading, stamps it as current in `samples`/`device_samples`, and clears frozen state.
26
+
- A delayed replay that advances from four days old to four days old +5 minutes also writes current rows and clears frozen state. A stream of delayed observations can continue doing this on each tick without ever being fresh.
27
+
28
+
Both reproduced through the real worker and SQLite, not just the helper. Treat freshness separately from monotonic advancement/deduplication: a known-old observation cannot be written under today's poll timestamp or prove recovery. If historical ingestion is desired, use the original observation time in an explicit history path. Test missing marker + stale observation, advancing-but-stale observation, and genuinely fresh recovery.
29
+
30
+
## Verified / non-blocking
31
+
32
+
- Fresh run: 105/105 tests (14 files), typecheck, build, and `git diff --check` passed. Existing tests cover only the helper, not source composition or the purge route.
33
+
- Established frozen station+device timestamps: no sample or telemetry written; repeated frozen ticks keep the first `frozen_since`; `_health` remains collector-healthy. Genuinely fresh recovery clears the marker and writes.
34
+
- Self-heal is reached on genuine recovery. Existing cap is **2 days**, so a four-day gap needs explicit older-history backfill if wanted. It did not fabricate frames when the mocked history was empty.
35
+
- Actual Hono purge endpoint: no login ->401; no configured PIN ->403; reversed or >31-day range ->400; default dry-run leaves data untouched; confirmed purge deletes only inclusive boundaries in both tables. Day/battery caches cleared; daily and year cache preserved.
36
+
- No production purge executed. After deploying a corrected admission policy, dry-run and confirm exact incident bounds; especially preserve the last valid reading / first real recovery sample if endpoints are inclusive.
37
+
38
+
Reproducer: `/home/l22/Desktop/deyecloud/.tmx-worktrees/codex/.summary/review-088cb16.cjs` (run with Node; transpiles actual TypeScript in memory, intercepts upstream requests, uses SQLite `:memory:`; external network forbidden).
console.warn("poll: frozen reading — not stored:",fz.reason);
168
+
console.warn("poll: stale/frozen reading — not stored:",fz.reason);
170
169
awaitenv.DB.batch([
171
170
env.DB.prepare("INSERT INTO meta (k,v) VALUES ('last_poll_ok',?) ON CONFLICT(k) DO UPDATE SET v=excluded.v").bind(String(ts)),
172
-
env.DB.prepare("INSERT INTO meta (k,v) VALUES ('frozen_since',?) ON CONFLICT(k) DO UPDATE SET v=v").bind(String(l.updatedAt)),// keep the FIRST frozen timestamp
171
+
env.DB.prepare("INSERT INTO meta (k,v) VALUES ('frozen_since',?) ON CONFLICT(k) DO UPDATE SET v=v").bind(String(l.observedAt??ts)),// keep the FIRST frozen timestamp
173
172
env.DB.prepare("DELETE FROM meta WHERE k='last_poll_error'"),
174
173
]);
175
174
// Alerts still run so the offline rule can fire from the device snapshot.
@@ -210,7 +209,6 @@ async function pollAndStore(env: Env) {
210
209
env.DB.prepare("DELETE FROM meta WHERE k='last_poll_error'"),
211
210
env.DB.prepare("DELETE FROM meta WHERE k='frozen_since'"),
212
211
env.DB.prepare("INSERT INTO meta (k,v) VALUES ('last_poll_ok',?) ON CONFLICT(k) DO UPDATE SET v=excluded.v").bind(String(ts)),
213
-
env.DB.prepare("INSERT INTO meta (k,v) VALUES ('last_reading_ts',?) ON CONFLICT(k) DO UPDATE SET v=excluded.v").bind(String(Number(l.updatedAt)||ts)),
0 commit comments