core: Energy monitoring - extend destroyStatesFrom (device) and wrapper (job) for partial recalc support - PR1 - #2528
Conversation
Introduces two low-level utilities that have no business logic and stand on their own, in preparation for upcoming energy-monitoring features. device.destroyStatesBetween(selector, from, to): - Deletes device feature states for a given selector between two dates (inclusive on both bounds). - Throws NotFoundError if the device feature does not exist. - Uses prepared statements for safe DuckDB DELETE. job.wrapperDetached(type, func): - Companion to job.wrapper that starts a job and runs it in background without awaiting completion (fire-and-forget). - Returns immediately with the created job, while the wrapped function runs detached and updates the job status to SUCCESS/FAILED on completion. - Internal try/catch ensures any error is caught and logged via job.finish(FAILED), and a secondary catch logs if job.finish itself fails. Test additions: - 4 tests for destroyStatesBetween (happy path + 2 boundary cases + not found error). - 3 tests for wrapperDetached (success, failure, finish-fail log path), plus 1 test on updateProgress that asserts it rejects when the job is missing. Coverage of new code: 100% lines/statements/functions on both files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR adds time-bounded device state deletion and a detached (fire-and-forget) job wrapper. ChangesDevice state destruction between timestamps
Detached job wrapper with background execution
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
server/test/lib/device/device.destroyStatesBetween.test.js (1)
79-99: ⚡ Quick winDocument inverted range behavior in JSDoc.
The test confirms that when
from > to, no states are deleted (silent no-op). While this is tested, it's not documented in the JSDoc. Consider adding a note to clarify this behavior for callers.📝 Suggested JSDoc enhancement
In
server/lib/device/device.destroyStatesBetween.js:/** * `@description` Destroy states between two dates. * `@param` {string} selector - Device feature selector. * `@param` {Date} from - The start date (inclusive). * `@param` {Date} to - The end date (inclusive). + * `@note` If from is after to, no states will be deleted. * `@returns` {Promise<void>} * `@example` * await gladys.device.destroyStatesBetween('kitchen-washer-consumption', new Date('2025-01-01'), new Date()); */🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/test/lib/device/device.destroyStatesBetween.test.js` around lines 79 - 99, The test shows Device.destroyStatesBetween silently no-ops when the 'from' date is after 'to'; update the JSDoc for the destroyStatesBetween method (in server/lib/device/device.destroyStatesBetween.js / Device class) to explicitly document this inverted-range behavior: state deletion will not occur and the method returns without error when from > to, include parameter descriptions for 'from' and 'to' and a short example or note about the no-op behavior so callers are aware.server/lib/device/device.destroyStatesBetween.js (1)
14-14: ⚡ Quick winConsider adding input type validation.
The function doesn't validate that
fromandtoare Date objects. If invalid types are passed,formatDateInUTCmay produce unexpected results or throw unclear errors. Consider adding validation to provide clearer error messages.🛡️ Suggested input validation
async function destroyStatesBetween(selector, from, to) { + if (!(from instanceof Date) || !(to instanceof Date)) { + throw new Error('Parameters from and to must be Date objects'); + } const existing = await db.DeviceFeature.findOne({ where: { selector } });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/lib/device/device.destroyStatesBetween.js` at line 14, Add input validation at the start of destroyStatesBetween: ensure both from and to are actual Date instances and are valid dates (e.g., instanceof Date and !isNaN(from.getTime()) / !isNaN(to.getTime())); if validation fails, throw a clear TypeError (e.g., "destroyStatesBetween: 'from' must be a valid Date") so callers get an explicit error instead of formatDateInUTC producing unclear failures; place these checks before any call to formatDateInUTC or other logic that assumes Date inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@server/lib/device/device.destroyStatesBetween.js`:
- Line 14: Add input validation at the start of destroyStatesBetween: ensure
both from and to are actual Date instances and are valid dates (e.g., instanceof
Date and !isNaN(from.getTime()) / !isNaN(to.getTime())); if validation fails,
throw a clear TypeError (e.g., "destroyStatesBetween: 'from' must be a valid
Date") so callers get an explicit error instead of formatDateInUTC producing
unclear failures; place these checks before any call to formatDateInUTC or other
logic that assumes Date inputs.
In `@server/test/lib/device/device.destroyStatesBetween.test.js`:
- Around line 79-99: The test shows Device.destroyStatesBetween silently no-ops
when the 'from' date is after 'to'; update the JSDoc for the
destroyStatesBetween method (in server/lib/device/device.destroyStatesBetween.js
/ Device class) to explicitly document this inverted-range behavior: state
deletion will not occur and the method returns without error when from > to,
include parameter descriptions for 'from' and 'to' and a short example or note
about the no-op behavior so callers are aware.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d2648a44-22d7-4ca2-bdd2-3be4b9684887
📒 Files selected for processing (6)
server/lib/device/device.destroyStatesBetween.jsserver/lib/device/index.jsserver/lib/job/index.jsserver/lib/job/job.wrapper.jsserver/test/lib/device/device.destroyStatesBetween.test.jsserver/test/lib/job/job.test.js
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2528 +/- ##
=======================================
Coverage 98.80% 98.81%
=======================================
Files 1018 1018
Lines 18305 18316 +11
=======================================
+ Hits 18087 18099 +12
+ Misses 218 217 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…SDoc Adds a one-line note in the JSDoc clarifying that calling destroyStatesBetween with from > to results in a silent no-op (consistent with the SQL semantics: created_at >= from AND created_at <= to is empty). Behavior was already covered by an explicit test; this just documents it for callers. Addresses CodeRabbit nitpick #1 on PR GladysAssistant#2528. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#4195 Bundle Size — 11.52MiB (0%).600f4ce(current) vs d8012f5 master#4188(baseline) Warning Bundle contains 2 duplicate packages – View duplicate packages Bundle metrics
|
| Current #4195 |
Baseline #4188 |
|
|---|---|---|
6.5MiB |
6.5MiB |
|
310.6KiB |
310.6KiB |
|
0% |
0% |
|
51 |
51 |
|
179 |
179 |
|
1643 |
1643 |
|
21 |
21 |
|
0.94% |
0.94% |
|
136 |
136 |
|
2 |
2 |
Bundle size by type no changes
| Current #4195 |
Baseline #4188 |
|
|---|---|---|
8.4MiB |
8.4MiB |
|
2.68MiB |
2.68MiB |
|
328.45KiB |
328.45KiB |
|
93.55KiB |
93.55KiB |
|
18.82KiB |
18.82KiB |
|
13.58KiB |
13.58KiB |
Bundle analysis report Branch Terdious:energy-recalc-pr1-utili... Project dashboard
Generated by RelativeCI Documentation Report issue
Pierre-Gilles
left a comment
There was a problem hiding this comment.
Thanks for separating the PR! I've added my feedback.
I'm not sure whether this was AI-invented or based on your own ideas, but this is typically the kind of change we should try to avoid with AI-assisted PRs: copying large blocks of code that were already working well and could have been extended more incrementally.
In this case, I think a smaller and more targeted change would have made the code easier to review and maintain, probably closer to what an experienced developer would naturally do.
|
Thanks for the review @Pierre-Gilles.
On the architectural choice (new functions vs. extending existing ones), this was my own design call, not an AI suggestion. The AI executed within constraints I had set; the strategy itself was deliberate. When I wrote the original energy PR (#2413) in January, I was operating from years of conversations with you about Gladys stability — your recurring theme that the calculation engine is sensitive and that modifications there carry validation cost.
With hindsight, your suggestion is cleaner here and I agree on the merits. Thanks again for the time on the review. |
…ing primitives Addresses review feedback on GladysAssistant#2528: rather than introducing two new sibling functions that largely duplicated existing logic, extend the existing primitives in place with an optional parameter. device.destroyStatesFrom(selector, from, to = new Date()): - Adds an optional `to` upper bound, defaulting to `now` so every pre-existing caller keeps the same effective behavior (the SQL goes from "DELETE WHERE >= from" to "DELETE WHERE >= from AND <= now", identical in practice). - The new bounded form replaces the standalone destroyStatesBetween() function and its dedicated file/test, which are removed. job.wrapper(type, func, { detached = false } = {}): - Adds an optional `{ detached }` option flag. - When false (default), the wrapper body is byte-for-byte identical to the previous implementation (awaits func, finishes the job, re-throws on error) — no behavior change for any existing caller. - When true, the wrapper starts the job, runs `func` in the background in a contained IIFE, returns the started job immediately, never re-throws to the caller, and falls back to logger.error if `finish` itself fails. This is the fire-and-forget mode needed by long "from beginning" recalculations that would otherwise hold the HTTP request open until completion. - Removes the standalone job.wrapperDetached() function and its prototype binding. Tests: - destroyStatesFrom.test.js: keeps all 5 master tests untouched, adds 2 new tests covering the bounded form (range cut-out + inverted from > to no-op). - job.test.js: the existing `describe('job.wrapperDetached')` block is rewritten as `describe('job.wrapper with { detached: true }')` with the same 3 assertions (success, failed job, finish-fail log path) expressed against the unified API; the new "should throw when job not found" test on updateProgress is preserved. Diff impact vs master: 219 insertions / 10 deletions across 4 files (down from 260 / 1 across 6 files), no new sibling function, no new prototype binding on DeviceManager or Job, no new public surface area. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…inning hardening
Updates the energy-monitoring test suite to cover the new selective
recalculation paths introduced in the previous commit, while keeping
all pre-existing legacy tests intact.
- Adapts every call site that targeted the old `(startAt, jobId)` /
`(jobId)` signatures of calculateCostFrom and FromBeginning
variants to the new `(startAt, featureSelectors, jobId)` /
`(featureSelectors, jobId)` shape. Where legacy tests passed a
jobId as the second positional argument, those calls continue to
work via the documented backward-compatibility branch in
calculateCostFrom, so the existing tests do not need to be
rewritten.
- calculateConsumptionFromIndex.test.js: new test asserting that
passing a whitelist of selectors filters out non-matching
consumption features, plus a test for the final progress update at
100% when a jobId is provided.
- calculateConsumptionFromIndexFromBeginning.test.js: new tests for
selective recalculation (whitelist), restoration of the
ENERGY_INDEX_LAST_PROCESSED cursor in both the partial-recalc and
the failed-window paths, mixed devices where some consumption
features lack a selector, and "no oldest state" early exit.
- calculateCostFrom.test.js: cleaned in the spirit of audit-driven
rigor:
- removed two stub-based tests that were duplicates of existing
master tests or that passed for the wrong reason (defensive
fallback already filtered upstream by the controller);
- rewrote remaining new tests with the real DuckDB pattern
(device.create + duckDbBatchInsertState + energyPrice.create)
used elsewhere in the file, instead of sinon stubs;
- added a dedicated test for the
`if (energyPricesForDate.length === 0)` branch that the existing
master test does not actually reach (its cost feature's
energy_parent_id mismatch makes the cost/consumption matching
fail earlier and short-circuits the code path).
- controller.test.js: covers feature_selectors validation
(non-array, empty-string item) on both `from-beginning` endpoints
and asserts the response now carries the started job_id. Updates
the "controller structure" tests to the new 3-route shape (the
two `*-range` endpoints stay out of this PR).
- Yesterday / EveryThirtyMinutes / IndexThirtyMinutes / FromBeginning
tests: signature adjustments only (one extra `null`/`undefined`
argument), no assertion was relaxed.
- Adds `server/test/utils/duckdb.js` (clearDuckDb helper) used to
reset relevant DuckDB tables between tests. This keeps the new
integration-style tests deterministic without relying on the
global bootstrap teardown.
- checks.test.js: extends the gladys.job mock with wrapperDetached
(introduced in PR GladysAssistant#2528) so EnergyMonitoringHandler can be
constructed when iterating over all services.
No production code is changed in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dysAssistant#2528 After GladysAssistant#2528 was refactored to expose a single job.wrapper(type, func, { detached }) primitive instead of a separate wrapperDetached function, update this PR's call sites accordingly: - services/energy-monitoring/lib/index.js: replace the two wrapperDetached registrations (cost FromBeginning, consumption FromBeginning) with wrapper(..., { detached: true }). - Energy-monitoring test mocks: drop the now-unused wrapperDetached helper from every gladys.job mock — wrapper alone is enough since the tests bypass the wrapping anyway. - test/services/checks.test.js: drop the wrapperDetached mock added in the previous commit; the file is now byte-identical to master. - calculateConsumptionFromIndexFromBeginning.test.js: drop the two defensive destroyStatesBetween stubs that no longer match the primitive surface, and switch the relevant assertion to destroyStatesFrom (which is the actual method used by the FromBeginning path). No behavior change in production code beyond the call-site rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ody for detached mode Follow-up to the previous refactor: instead of having two separate try/catch blocks for the legacy and detached paths, extract the shared "run func + finish job + handle error" body into a local IIFE (runAndFinish). The two modes now only differ on: - whether the wrapped function awaits the inner promise (legacy) or fires it and returns the started job immediately (detached); - whether errors thrown by func are re-thrown to the caller (legacy) or only reported on the job and swallowed (detached); - whether a secondary failure of `finish(FAILED)` re-throws (legacy, unchanged) or is caught and logged (detached only). Net effect: - The legacy path is byte-for-byte equivalent to master: same SQL sequence (start, await func, finish SUCCESS / catch, finish FAILED, throw), same return value (`res`), same thrown error. - The detached path runs `runAndFinish()` without awaiting and returns `job` immediately. Coverage on `job.wrapper.js`: 100% lines / statements / functions, 90% branches (one defensive `error && error.toString` guard, identical to master). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…inning hardening
Updates the energy-monitoring test suite to cover the new selective
recalculation paths introduced in the previous commit, while keeping
all pre-existing legacy tests intact.
- Adapts every call site that targeted the old `(startAt, jobId)` /
`(jobId)` signatures of calculateCostFrom and FromBeginning
variants to the new `(startAt, featureSelectors, jobId)` /
`(featureSelectors, jobId)` shape. Where legacy tests passed a
jobId as the second positional argument, those calls continue to
work via the documented backward-compatibility branch in
calculateCostFrom, so the existing tests do not need to be
rewritten.
- calculateConsumptionFromIndex.test.js: new test asserting that
passing a whitelist of selectors filters out non-matching
consumption features, plus a test for the final progress update at
100% when a jobId is provided.
- calculateConsumptionFromIndexFromBeginning.test.js: new tests for
selective recalculation (whitelist), restoration of the
ENERGY_INDEX_LAST_PROCESSED cursor in both the partial-recalc and
the failed-window paths, mixed devices where some consumption
features lack a selector, and "no oldest state" early exit.
- calculateCostFrom.test.js: cleaned in the spirit of audit-driven
rigor:
- removed two stub-based tests that were duplicates of existing
master tests or that passed for the wrong reason (defensive
fallback already filtered upstream by the controller);
- rewrote remaining new tests with the real DuckDB pattern
(device.create + duckDbBatchInsertState + energyPrice.create)
used elsewhere in the file, instead of sinon stubs;
- added a dedicated test for the
`if (energyPricesForDate.length === 0)` branch that the existing
master test does not actually reach (its cost feature's
energy_parent_id mismatch makes the cost/consumption matching
fail earlier and short-circuits the code path).
- controller.test.js: covers feature_selectors validation
(non-array, empty-string item) on both `from-beginning` endpoints
and asserts the response now carries the started job_id. Updates
the "controller structure" tests to the new 3-route shape (the
two `*-range` endpoints stay out of this PR).
- Yesterday / EveryThirtyMinutes / IndexThirtyMinutes / FromBeginning
tests: signature adjustments only (one extra `null`/`undefined`
argument), no assertion was relaxed.
- Adds `server/test/utils/duckdb.js` (clearDuckDb helper) used to
reset relevant DuckDB tables between tests. This keeps the new
integration-style tests deterministic without relying on the
global bootstrap teardown.
- checks.test.js: extends the gladys.job mock with wrapperDetached
(introduced in PR GladysAssistant#2528) so EnergyMonitoringHandler can be
constructed when iterating over all services.
No production code is changed in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dysAssistant#2528 After GladysAssistant#2528 was refactored to expose a single job.wrapper(type, func, { detached }) primitive instead of a separate wrapperDetached function, update this PR's call sites accordingly: - services/energy-monitoring/lib/index.js: replace the two wrapperDetached registrations (cost FromBeginning, consumption FromBeginning) with wrapper(..., { detached: true }). - Energy-monitoring test mocks: drop the now-unused wrapperDetached helper from every gladys.job mock — wrapper alone is enough since the tests bypass the wrapping anyway. - test/services/checks.test.js: drop the wrapperDetached mock added in the previous commit; the file is now byte-identical to master. - calculateConsumptionFromIndexFromBeginning.test.js: drop the two defensive destroyStatesBetween stubs that no longer match the primitive surface, and switch the relevant assertion to destroyStatesFrom (which is the actual method used by the FromBeginning path). No behavior change in production code beyond the call-site rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…over Addresses two CodeRabbit findings on PR GladysAssistant#2530: 1. front/src/config/i18n/{de,en,fr}.json — remove the two job-types translations "energy-monitoring-cost-calculation-range" and "energy-monitoring-consumption-from-index-range". The matching JOB_TYPES were already removed from server/utils/constants.js during the initial PR2 cleanup since the *-range backend is not in this PR; the i18n labels are therefore unused and were missed when the integration.energyMonitoring.* section was scrubbed. 2. server/services/energy-monitoring/lib/energy-monitoring.calculateCostFrom.js — remove parseDateWithBoundary() and its `|| new Date(0)` silent fallback to epoch. This helper was a leftover from the original range-mode draft that had to parse YYYY-MM-DD strings; in this PR every caller (internal: Yesterday/EveryThirtyMinutes/FromBeginning; external: the controller which no longer forwards start_date) always passes a Date object. Using `startAt` directly is the same behavior as master and avoids the silent epoch fallback CodeRabbit correctly flagged. The matching strict-validation defensive throw that CodeRabbit also suggested is deliberately not added — Gladys keeps low-level engine helpers free of input-type guards (mirrors the master pattern on `destroyStatesFrom`, also a reviewer preference on this PR's parent GladysAssistant#2528). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…backs CodeRabbit flagged a logical inconsistency on calculateConsumptionFromIndexFromBeginning.js: a consumption feature without `selector` could pass the whitelist filter (via the `f.selector || f.external_id || f.id` fallback) but would then be silently skipped during the per-feature `destroyStatesFrom` reset (because that loop guards on `if (!feature.selector) return;`), which would break idempotency of from-beginning recalculation. Verified that this scenario cannot actually occur in production: `t_device_feature.selector` is `allowNull: false, unique: true` in the Sequelize model (server/models/device_feature.js:37-41). A device feature with a missing selector cannot be persisted in the first place, so both halves of the inconsistency were guarding against an unreachable state. Aligned with the project's stance against defensive guards on code paths that the schema already excludes (mirrors the maintainer's feedback on `destroyStatesFrom` and `wrapper` in GladysAssistant#2528), this commit: - drops the `f.selector || f.external_id || f.id` fallback in `calculateConsumptionFromIndex.js`, `calculateConsumptionFromIndexFromBeginning.js` and `calculateCostFrom.js`. The whitelist match is now done directly on `f.selector` (cost feature on `ecf.consumptionCostFeature.selector`). - drops the `if (!feature.selector) return;` guard in `calculateConsumptionFromIndexFromBeginning.js`'s reset loop. - drops the three companion tests in `calculateConsumptionFromIndexFromBeginning.test.js` that fed fixture devices with `selector: null` and asserted the skip behavior; they were exercising the same unreachable branch. Net effect: - production diff vs master: shorter and easier to follow on the whitelist filter (one expression instead of three lines per call site). - tests pass at 116 (the three removed defensive tests are gone, the remaining coverage is preserved by the genuine real-DB test paths). - branch coverage on services/energy-monitoring rises from 93.84% to 96.19% precisely because the unreachable branches are no longer there to count against the denominator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@Pierre-Gilles update on testing: I've pushed a Docker test image I launched a full cost recalculation (costs only, not consumption) — I can let it run another day or two if you'd like more confidence on |
| * gladys.job.wrapper('long-running-recalc', func, { detached: true }); | ||
| */ | ||
| function wrapper(type, func) { | ||
| function wrapper(type, func, { detached = false } = {}) { |
There was a problem hiding this comment.
Je ne suis pas sûr de bien comprendre le besoin ni l’utilité de cette modification, j’avoue.
Tu mentionnes des requêtes HTTP qui bloquent, mais de mon côté il me semblait que ce problème avait déjà été corrigé. Le correctif est disponible ici : https://github.com/GladysAssistant/Gladys/blob/master/server/services/energy-monitoring/api/energy-monitoring.controller.js#L23
L’idée était justement de ne plus attendre la fin du traitement avant de répondre à l’appel API (suppression du await), afin que la requête soit résolue immédiatement.
Du coup, quel est exactement le bug que cette modification est censée résoudre ?
There was a problem hiding this comment.
Au passage, s’il reste effectivement un bug côté interface, je préférerais avoir une PR dédiée qui corrige le problème de bout en bout, plutôt qu’une PR qui mélange plusieurs sujets différents :)
…inning hardening
Updates the energy-monitoring test suite to cover the new selective
recalculation paths introduced in the previous commit, while keeping
all pre-existing legacy tests intact.
- Adapts every call site that targeted the old `(startAt, jobId)` /
`(jobId)` signatures of calculateCostFrom and FromBeginning
variants to the new `(startAt, featureSelectors, jobId)` /
`(featureSelectors, jobId)` shape. Where legacy tests passed a
jobId as the second positional argument, those calls continue to
work via the documented backward-compatibility branch in
calculateCostFrom, so the existing tests do not need to be
rewritten.
- calculateConsumptionFromIndex.test.js: new test asserting that
passing a whitelist of selectors filters out non-matching
consumption features, plus a test for the final progress update at
100% when a jobId is provided.
- calculateConsumptionFromIndexFromBeginning.test.js: new tests for
selective recalculation (whitelist), restoration of the
ENERGY_INDEX_LAST_PROCESSED cursor in both the partial-recalc and
the failed-window paths, mixed devices where some consumption
features lack a selector, and "no oldest state" early exit.
- calculateCostFrom.test.js: cleaned in the spirit of audit-driven
rigor:
- removed two stub-based tests that were duplicates of existing
master tests or that passed for the wrong reason (defensive
fallback already filtered upstream by the controller);
- rewrote remaining new tests with the real DuckDB pattern
(device.create + duckDbBatchInsertState + energyPrice.create)
used elsewhere in the file, instead of sinon stubs;
- added a dedicated test for the
`if (energyPricesForDate.length === 0)` branch that the existing
master test does not actually reach (its cost feature's
energy_parent_id mismatch makes the cost/consumption matching
fail earlier and short-circuits the code path).
- controller.test.js: covers feature_selectors validation
(non-array, empty-string item) on both `from-beginning` endpoints
and asserts the response now carries the started job_id. Updates
the "controller structure" tests to the new 3-route shape (the
two `*-range` endpoints stay out of this PR).
- Yesterday / EveryThirtyMinutes / IndexThirtyMinutes / FromBeginning
tests: signature adjustments only (one extra `null`/`undefined`
argument), no assertion was relaxed.
- Adds `server/test/utils/duckdb.js` (clearDuckDb helper) used to
reset relevant DuckDB tables between tests. This keeps the new
integration-style tests deterministic without relying on the
global bootstrap teardown.
- checks.test.js: extends the gladys.job mock with wrapperDetached
(introduced in PR GladysAssistant#2528) so EnergyMonitoringHandler can be
constructed when iterating over all services.
No production code is changed in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dysAssistant#2528 After GladysAssistant#2528 was refactored to expose a single job.wrapper(type, func, { detached }) primitive instead of a separate wrapperDetached function, update this PR's call sites accordingly: - services/energy-monitoring/lib/index.js: replace the two wrapperDetached registrations (cost FromBeginning, consumption FromBeginning) with wrapper(..., { detached: true }). - Energy-monitoring test mocks: drop the now-unused wrapperDetached helper from every gladys.job mock — wrapper alone is enough since the tests bypass the wrapping anyway. - test/services/checks.test.js: drop the wrapperDetached mock added in the previous commit; the file is now byte-identical to master. - calculateConsumptionFromIndexFromBeginning.test.js: drop the two defensive destroyStatesBetween stubs that no longer match the primitive surface, and switch the relevant assertion to destroyStatesFrom (which is the actual method used by the FromBeginning path). No behavior change in production code beyond the call-site rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…over Addresses two CodeRabbit findings on PR GladysAssistant#2530: 1. front/src/config/i18n/{de,en,fr}.json — remove the two job-types translations "energy-monitoring-cost-calculation-range" and "energy-monitoring-consumption-from-index-range". The matching JOB_TYPES were already removed from server/utils/constants.js during the initial PR2 cleanup since the *-range backend is not in this PR; the i18n labels are therefore unused and were missed when the integration.energyMonitoring.* section was scrubbed. 2. server/services/energy-monitoring/lib/energy-monitoring.calculateCostFrom.js — remove parseDateWithBoundary() and its `|| new Date(0)` silent fallback to epoch. This helper was a leftover from the original range-mode draft that had to parse YYYY-MM-DD strings; in this PR every caller (internal: Yesterday/EveryThirtyMinutes/FromBeginning; external: the controller which no longer forwards start_date) always passes a Date object. Using `startAt` directly is the same behavior as master and avoids the silent epoch fallback CodeRabbit correctly flagged. The matching strict-validation defensive throw that CodeRabbit also suggested is deliberately not added — Gladys keeps low-level engine helpers free of input-type guards (mirrors the master pattern on `destroyStatesFrom`, also a reviewer preference on this PR's parent GladysAssistant#2528). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…backs CodeRabbit flagged a logical inconsistency on calculateConsumptionFromIndexFromBeginning.js: a consumption feature without `selector` could pass the whitelist filter (via the `f.selector || f.external_id || f.id` fallback) but would then be silently skipped during the per-feature `destroyStatesFrom` reset (because that loop guards on `if (!feature.selector) return;`), which would break idempotency of from-beginning recalculation. Verified that this scenario cannot actually occur in production: `t_device_feature.selector` is `allowNull: false, unique: true` in the Sequelize model (server/models/device_feature.js:37-41). A device feature with a missing selector cannot be persisted in the first place, so both halves of the inconsistency were guarding against an unreachable state. Aligned with the project's stance against defensive guards on code paths that the schema already excludes (mirrors the maintainer's feedback on `destroyStatesFrom` and `wrapper` in GladysAssistant#2528), this commit: - drops the `f.selector || f.external_id || f.id` fallback in `calculateConsumptionFromIndex.js`, `calculateConsumptionFromIndexFromBeginning.js` and `calculateCostFrom.js`. The whitelist match is now done directly on `f.selector` (cost feature on `ecf.consumptionCostFeature.selector`). - drops the `if (!feature.selector) return;` guard in `calculateConsumptionFromIndexFromBeginning.js`'s reset loop. - drops the three companion tests in `calculateConsumptionFromIndexFromBeginning.test.js` that fed fixture devices with `selector: null` and asserted the skip behavior; they were exercising the same unreachable branch. Net effect: - production diff vs master: shorter and easier to follow on the whitelist filter (one expression instead of three lines per call site). - tests pass at 116 (the three removed defensive tests are gone, the remaining coverage is preserved by the genuine real-DB test paths). - branch coverage on services/energy-monitoring rises from 93.84% to 96.19% precisely because the unreachable branches are no longer there to count against the denominator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e-and-forget Following PR GladysAssistant#2528 review feedback: { detached } provides no functional benefit here. Progress %, FAILED-on-restart and FAILED-on-crash are all already handled by master's job pipeline regardless of { detached }, and the job_id returned to the frontend is only used in a dead truthiness check (the controller's await would already throw HTTP 500 if start() failed). - index.js: drop { detached: true } from the two from-beginning wrappers - controller: fire-and-forget like master (no await, no job_id in body) - controller.test.js: drop job_id assertions - EnergyMonitoring.jsx: drop !response.job_id check from the 3 handlers The detailed-job-info feature that originally introduced wrapperDetached (buildJobData + JobList display) will land in PR GladysAssistant#2412, end-to-end with its 3 missing components. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull Request check-list
To ensure your Pull Request can be accepted as fast as possible, make sure to review and check all of these items:
npm teston both front/server)npm run eslinton both front/server)npm run prettieron both front/server)NOTE: these things are not required to open a PR and can be done afterwards / while the PR is open.
Description of change
Summary
Extends two existing core primitives with an optional parameter, in preparation for upcoming energy-monitoring features (selective recalculation, date-range recalculation). The change is intentionally incremental rather than introducing sibling functions: every pre-existing caller keeps the same effective behavior, no new file is added, no new public surface area is exposed.
device.destroyStatesFrom(selector, from, to = new Date())— adds an optionaltoupper bound (inclusive) defaulting tonow.job.wrapper(type, func, { detached = false } = {})— adds an optional{ detached }option flag. When false (default), the wrapper body is byte-for-byte identical to master; when true, the wrapper starts the job, runsfuncin the background, returns the started job immediately and never re-throws to the caller.Details
device.destroyStatesFrom(selector, from, to = new Date())(selector, from).toparameter defaults tonew Date(), so every pre-existing caller keeps the same effective behavior: the SQL goes fromWHERE created_at >= fromtoWHERE created_at >= from AND created_at <= now, which is identical in practice.tois explicitly provided, the function deletes only states inside[from, to](both inclusive).from > to, the SQL is naturally empty and the function silently no-ops — documented in the JSDoc and covered by an explicit test.NotFoundError('DeviceFeature not found')for unknown selectors. Continues to use parameterized DuckDB DELETE (no string interpolation in SQL).job.wrapper(type, func, { detached = false } = {})(type, func).detached === false(default), the wrapper performs the exact same SQL sequence as on master (start→await func→finish(SUCCESS)/ on errorfinish(FAILED)then re-throw) and returns the same value (res). The shared body is now extracted into a local IIFE (runAndFinish) so the legacy and detached modes share it instead of duplicating it.detached === true, the wrapper callsthis.start(type), firesrunAndFinish()without awaiting it, and returns the startedjobimmediately. Errors thrown byfuncare reported on the job (status=FAILED) and never re-thrown to the caller; a secondarytry/catchfalls back tologger.error('job.wrapper: failed to finish job ...')iffinish(FAILED)itself rejects.Tests
server/test/lib/device/device.destroyStatesFrom.test.js:[from, to]deleted, states outside kept) and an inverted-range silent no-op.server/test/lib/job/job.test.js:describe('job.wrapper')legacy tests are unchanged,describe('job.wrapper with { detached: true }')block carries 3 tests covering success path, failure path, and the secondary log path whenjob.finishitself rejects,updateProgressasserts it rejects when the job id is unknown — easy win for coverage of a pre-existing branch.Coverage
device.destroyStatesFrom.js: 100% statements / lines / functions / branches.job.wrapper.js: 100% statements / lines / functions; branch coverage at 75% (one defensive guardif (error && error.toString)is not exercised — identical to the pre-existing situation on master, no new uncovered branch introduced).Tested manually
{ detached }mode and the reason this PR is the prerequisite for the follow-ups.Scope
Diff vs base branch (
master):Files touched:
server/lib/device/device.destroyStatesFrom.js—+23 / -4(add optionaltoparameter)server/lib/job/job.wrapper.js—+25 / -13(factor out shared body, add{ detached }option)server/test/lib/device/device.destroyStatesFrom.test.js—+87 / 0(2 new tests for the bounded form)server/test/lib/job/job.test.js—+80 / -6(3 new tests for the detached mode + 1 onupdateProgress)No new file, no new public function, no new prototype binding on
DeviceManagerorJob.Summary by CodeRabbit
New Features
Tests