Skip to content

core: Energy monitoring - extend destroyStatesFrom (device) and wrapper (job) for partial recalc support - PR1 - #2528

Draft
Terdious wants to merge 4 commits into
GladysAssistant:masterfrom
Terdious:energy-recalc-pr1-utilities
Draft

core: Energy monitoring - extend destroyStatesFrom (device) and wrapper (job) for partial recalc support - PR1#2528
Terdious wants to merge 4 commits into
GladysAssistant:masterfrom
Terdious:energy-recalc-pr1-utilities

Conversation

@Terdious

@Terdious Terdious commented May 18, 2026

Copy link
Copy Markdown
Contributor

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:

  • If your changes affect the code, did you write the tests?
  • Are tests passing? (npm test on both front/server)
  • Is the linter passing? (npm run eslint on both front/server)
  • Did you run prettier? (npm run prettier on both front/server)
  • Did you test this pull request in real life? With real devices? If this development is a big feature or a new service, we recommend that you provide a Docker image to the community (forum) for testing before merging.
  • If your changes modify the API (REST or Node.js), did you modify the API documentation? (Documentation is based on comments in code)

NOTE: these things are not required to open a PR and can be done afterwards / while the PR is open.

Description of change

Summary

This PR is the first independent slice extracted from #2413 as discussed
on https://community.gladysassistant.com/t/amelioration-re-calcul-suivi-de-lenergie/10069 ;
the rest of #2413 will follow in subsequent PRs only if this one is merged.

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 optional to upper bound (inclusive) defaulting to now.
  • 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, runs func in the background, returns the started job immediately and never re-throws to the caller.

Details

device.destroyStatesFrom(selector, from, to = new Date())

  • The existing function previously had the signature (selector, from).
  • The new optional to parameter defaults to new Date(), so every pre-existing caller keeps the same effective behavior: the SQL goes from WHERE created_at >= from to WHERE created_at >= from AND created_at <= now, which is identical in practice.
  • When to is explicitly provided, the function deletes only states inside [from, to] (both inclusive).
  • When from > to, the SQL is naturally empty and the function silently no-ops — documented in the JSDoc and covered by an explicit test.
  • Continues to throw NotFoundError('DeviceFeature not found') for unknown selectors. Continues to use parameterized DuckDB DELETE (no string interpolation in SQL).

job.wrapper(type, func, { detached = false } = {})

  • The existing function previously had the signature (type, func).
  • When detached === false (default), the wrapper performs the exact same SQL sequence as on master (startawait funcfinish(SUCCESS) / on error finish(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.
  • When detached === true, the wrapper calls this.start(type), fires runAndFinish() without awaiting it, and returns the started job immediately. Errors thrown by func are reported on the job (status=FAILED) and never re-thrown to the caller; a secondary try/catch falls back to logger.error('job.wrapper: failed to finish job ...') if finish(FAILED) itself rejects.
  • This is the fire-and-forget mode needed by long-running "from beginning" recalculations that would otherwise hold the HTTP request open until completion.

Tests

  • server/test/lib/device/device.destroyStatesFrom.test.js:
    • the 5 pre-existing master tests are unchanged,
    • 2 new tests cover the bounded form: a happy path with a range cut-out (states inside [from, to] deleted, states outside kept) and an inverted-range silent no-op.
  • server/test/lib/job/job.test.js:
    • the existing describe('job.wrapper') legacy tests are unchanged,
    • the new describe('job.wrapper with { detached: true }') block carries 3 tests covering success path, failure path, and the secondary log path when job.finish itself rejects,
    • a small additional test on updateProgress asserts 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 guard if (error && error.toString) is not exercised — identical to the pre-existing situation on master, no new uncovered branch introduced).

Tested manually

  • A long-running "from beginning" recalculation on my own production instance now returns the HTTP response immediately and runs as a background job (progress visible in the jobs page) — this is the actual functional motivation for the { detached } mode and the reason this PR is the prerequisite for the follow-ups.

Scope

Diff vs base branch (master):

Area Insertions Deletions
server (production) 67 13
server (tests) 148 6
Total 215 19

Files touched:

  • server/lib/device/device.destroyStatesFrom.js+23 / -4 (add optional to parameter)
  • 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 on updateProgress)

No new file, no new public function, no new prototype binding on DeviceManager or Job.

Summary by CodeRabbit

  • New Features

    • Jobs can now run in detached/background mode, enabling non-blocking job execution
    • Device state deletion now supports specifying an optional date range (from and to) for more precise cleanup
  • Tests

    • Added comprehensive test coverage for detached job execution and error handling
    • Added test cases for bounded date range state deletion, including edge cases

Review Change Stack

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>
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR adds time-bounded device state deletion and a detached (fire-and-forget) job wrapper. destroyStatesFrom gains an optional to parameter to limit deletions to an inclusive time range. The job wrapper gains a detached option: when enabled it starts a job, runs the wrapped function in background, updates job status to SUCCESS or FAILED with error details, and logs finalization errors. Tests added for both features.

Changes

Device state destruction between timestamps

Layer / File(s) Summary
Time-bounded state deletion
server/lib/device/device.destroyStatesFrom.js, server/test/lib/device/device.destroyStatesFrom.test.js
destroyStatesFrom accepts an optional upper-bound to date (defaults to new Date()), formats both dates to UTC, and deletes device feature states where created_at falls within the inclusive range. Tests verify in-range deletion and no-op behavior when from > to.

Detached job wrapper with background execution

Layer / File(s) Summary
Detached execution implementation
server/lib/job/job.wrapper.js
Wrapper now accepts { detached = false }. In detached mode it starts the job, invokes the wrapped func in a background async task, marks job SUCCESS or FAILED (with error_type and captured error string), and logs exceptions from job.finish via logger.error. Synchronous behavior preserved when detached is false.
Job wrapper tests and infrastructure
server/test/lib/job/job.test.js
Per-test Sinon sandbox, db.Job truncation before/after tests, added logger and stubbed event imports, updateProgress not-found test, and a detached-wrapper test suite that polls for terminal status, asserts success/failure terminal states and error capture, and verifies logger.error is called when job.finish rejects.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • Pierre-Gilles

Poem

I start the job and hop away,
A silent hare at work all day,
States vanish within the time,
Jobs succeed or fail in rhyme,
Logger keeps watch while I play. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions 'extend destroyStatesFrom' and 'wrapper' with 'partial recalc support', which aligns with the actual changes: adding optional to parameter to destroyStatesFrom and detached mode to job.wrapper for energy monitoring features.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Terdious Terdious changed the title feat(core): add destroyStatesBetween and job.wrapperDetached utilities core: Energy monitoring - add destroyStatesBetween (device) and wrapperDetached (job) utilities May 18, 2026
@Terdious Terdious changed the title core: Energy monitoring - add destroyStatesBetween (device) and wrapperDetached (job) utilities core: Energy monitoring - add destroyStatesBetween (device) and wrapperDetached (job) utilities - PR1 May 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
server/test/lib/device/device.destroyStatesBetween.test.js (1)

79-99: ⚡ Quick win

Document 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 win

Consider adding input type validation.

The function doesn't validate that from and to are Date objects. If invalid types are passed, formatDateInUTC may 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

📥 Commits

Reviewing files that changed from the base of the PR and between d8012f5 and 62c36bd.

📒 Files selected for processing (6)
  • server/lib/device/device.destroyStatesBetween.js
  • server/lib/device/index.js
  • server/lib/job/index.js
  • server/lib/job/job.wrapper.js
  • server/test/lib/device/device.destroyStatesBetween.test.js
  • server/test/lib/job/job.test.js

@codecov

codecov Bot commented May 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.81%. Comparing base (d8012f5) to head (600f4ce).
⚠️ Report is 191 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…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>
@relativeci

relativeci Bot commented May 18, 2026

Copy link
Copy Markdown

#4195 Bundle Size — 11.52MiB (0%).

600f4ce(current) vs d8012f5 master#4188(baseline)

Warning

Bundle contains 2 duplicate packages – View duplicate packages

Bundle metrics  no changes
                 Current
#4195
     Baseline
#4188
No change  Initial JS 6.5MiB 6.5MiB
No change  Initial CSS 310.6KiB 310.6KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 51 51
No change  Assets 179 179
No change  Modules 1643 1643
No change  Duplicate Modules 21 21
No change  Duplicate Code 0.94% 0.94%
No change  Packages 136 136
No change  Duplicate Packages 2 2
Bundle size by type  no changes
                 Current
#4195
     Baseline
#4188
No change  JS 8.4MiB 8.4MiB
No change  IMG 2.68MiB 2.68MiB
No change  CSS 328.45KiB 328.45KiB
No change  Fonts 93.55KiB 93.55KiB
No change  Other 18.82KiB 18.82KiB
No change  HTML 13.58KiB 13.58KiB

Bundle analysis reportBranch Terdious:energy-recalc-pr1-utili...Project dashboard


Generated by RelativeCIDocumentationReport issue

@Pierre-Gilles Pierre-Gilles left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread server/lib/device/device.destroyStatesBetween.js Outdated
Comment thread server/lib/job/job.wrapper.js Outdated
@Terdious

Copy link
Copy Markdown
Contributor Author

Thanks for the review @Pierre-Gilles.

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.

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.

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.
I totally agree with that and it would have been my initial choice actually. I was just afraid of getting negative feedback from you for fear of breaking the core (especially with the wrapper used in many places in the code).
Adding new isolated functions (destroyStatesBetween,
wrapperDetached) rather than modifying destroyStatesFrom and wrapper
felt like the safest path: zero impact on existing code, full backward
compatibility by construction, and a smaller blast radius if anything
went wrong. That was my reading of the room.

With hindsight, your suggestion is cleaner here and I agree on the merits.
I will refactor both functions along these lines and deploy the updates quickly.

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>
Terdious added a commit to Terdious/Gladys that referenced this pull request May 18, 2026
…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>
Terdious added a commit to Terdious/Gladys that referenced this pull request May 18, 2026
…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>
Terdious added a commit to Terdious/Gladys that referenced this pull request May 18, 2026
…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>
Terdious added a commit to Terdious/Gladys that referenced this pull request May 18, 2026
…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>
@Terdious
Terdious requested a review from Pierre-Gilles May 18, 2026 21:09
Terdious added a commit to Terdious/Gladys that referenced this pull request May 19, 2026
…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>
Terdious added a commit to Terdious/Gladys that referenced this pull request May 19, 2026
…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>
@Terdious Terdious changed the title core: Energy monitoring - add destroyStatesBetween (device) and wrapperDetached (job) utilities - PR1 core: Energy monitoring - extend destroyStatesFrom (device) and wrapper (job) for partial recalc support - PR1 May 20, 2026
@Terdious

Copy link
Copy Markdown
Contributor Author

@Pierre-Gilles update on testing:

I've pushed a Docker test image terdious/gladys:energy-monitoring-pr1
running since last night on an isolated copy of my prod DB (I dropped
data older than 2025 to keep the run manageable).

I launched a full cost recalculation (costs only, not consumption) —
completed in 1h30, logs are clean. I compared the recalculated values
against my actual prod month by month and year by year: results match.
The only delta is on yesterday's data, which is expected — it
corresponds to the gap between the DB snapshot and the test instance
startup.

I can let it run another day or two if you'd like more confidence on
the long-run incremental mode, or I can move on to preparing a similar
test image for PR2. Let me know what you prefer.

* gladys.job.wrapper('long-running-recalc', func, { detached: true });
*/
function wrapper(type, func) {
function wrapper(type, func, { detached = false } = {}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Terdious added a commit to Terdious/Gladys that referenced this pull request Jun 18, 2026
…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>
Terdious added a commit to Terdious/Gladys that referenced this pull request Jun 18, 2026
…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>
Terdious added a commit to Terdious/Gladys that referenced this pull request Jun 18, 2026
…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>
Terdious added a commit to Terdious/Gladys that referenced this pull request Jun 18, 2026
…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>
Terdious added a commit to Terdious/Gladys that referenced this pull request Jun 18, 2026
…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>
@Terdious Terdious assigned Terdious and unassigned Pierre-Gilles Jul 13, 2026
@Terdious
Terdious marked this pull request as draft July 17, 2026 14:27
@github-actions github-actions Bot added area:server Node.js server code type:feature New user-facing feature or improvement labels Jul 31, 2026
@Pierre-Gilles Pierre-Gilles added the needs:cursor-review Automated review by Cursor is needed label Aug 6, 2026 — with Cursor
@Pierre-Gilles Pierre-Gilles removed the needs:cursor-review Automated review by Cursor is needed label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:server Node.js server code type:feature New user-facing feature or improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants