Repository navigation
Releases: kherio/PowerSentinel
Releases · kherio/PowerSentinel
Release list
v4.16.1
- Bug fix:
keep_on_chargeno longer waits for unplug when the battery is already full - real device report: "aunque la bateria este al 100% cargada a veces parece que esta el dispositivo al 50%".keep_on_charge's gate inhandle_event()only checked whether a power source was connected (AC/USB/Wireless powered: truefromdumpsys battery) - a flag Android keepstruefor as long as the cable stays plugged in, well past the point the battery actually finishes charging (status becomes "Full", but the power-source flag never changes). Sokeep_on_chargegenuinely meant "until unplugged", never "until charged": a phone left on the charger overnight, picked up already at 100%, stayed throttled (capped CPU/cores, killed/reniced apps, WiFi off, etc. - whatever the active event applies) for no real reason until physically unplugged. Now also requiresDETECT_BATTERY_LEVEL < 100to hold the restrictions, mirroring the exact "battery topped off" idiom (level -eq 100)PowerSentinel-chargehealth.shalready used elsewhere - restrictions now lift the moment the battery reaches 100%, cable still connected or not. - New persisted test (
test_keep_on_charge_full_battery.sh, 4 cases: mid-charge holds as before, full-while-plugged-in now lifts, unplugged was already unaffected, and an unset/not-yet-refreshed battery-level reading fails toward NOT getting stuck) - confirmed to actually catch the original bug by reverting the fix and observing the exact failure before restoring it. 114 bash tests across 20 files and 33 frontend tests now pass.
v4.16.0
v4.16.0
- New mechanism: Data Saver / background data restriction - reviewed every real lever this module already pulls (CPU/cores, apps, GMS, WiFi, doze, refresh rate) looking specifically for "que haya un ahorro de bateria real y creible", and found one real, well-documented, system-wide drain this project had genuinely never touched: apps syncing, polling and waking the radio in the background over whichever network is active, regardless of
kill_wifi/handle_apps(WiFi off doesn't help on cellular; a merely-reniced app still syncs freely). Newrestrict_datafield toggles Android's own Data Saver (cmd netpolicy set restrict-background, the same thing Settings exposes as "Ahorro de datos") - genuinely complementary to everything else this daemon already does, same relationshipmax_cpu_freqalready has alongsidehandle_cores. Built to the exact same ownership contract already proven forkill_wifi/handle_gms/low_ram: the real original state is captured once (never re-recorded while already active), undo never turns it off if the user had it on themselves, and undo is composition-safe against another still-active event that also wants it on - the same three real bug classes those mechanisms already had to be fixed for, so this one launches without repeating any of them. Gated behind a newnetpolicy_restrictcapability (a harmless read-onlydumpsys netpolicyprobe, same reasoning asdoze_force) so a stripped ROM without the service just skips it with a clear warning instead of silently failing. Wired through the full stack: capabilities, event resolution/snapshot/reassert, the WebUI's config form (off by default in "Equilibrado", on in "Agresivo" - soadaptive_tier2/tier3andlow_power's own recommended defaults inherit it automatically), dashboard "active now" card, journal phrases, active-mode notification text, system health check, and the terminal wizard (PowerSentinelconf). - New persisted test (
test_restrict_data_ownership.sh) covering the capability gate, first-apply original-state capture, re-entry not overwriting that original, undo restoring it, the two-active-events composition case, and a user who already had Data Saver on themselves never getting it turned off - confirmed to actually catch a reintroduced composition bug by reverting the fix and observing the exactkill_wifi-class failure reappear before restoring it. 110 bash tests across 19 files and 33 frontend tests now pass.
v4.15.0
- CRITICAL FIX: searching for an event/app in Automatización was "impossible" - reported directly: the list flickered every few seconds and whatever had just been typed/filtered would disappear. Root cause:
refreshLiveStatus()'s 10s periodic refresh calledrenderVersionView()(which rebuilds the events list and global fields from scratch via a blanketinnerHTML = '') instead of the lightweightupdateActiveIndicators()it sits right next to - whose own comment already explains it was built specifically to AVOID this exact problem ("does NOT call the full renderEvents() on a timer - that rebuilds every card from scratch, destroying... any in-progress edit"), but the old destructive call was left running alongside it anyway, quietly defeating that fix on every tick. Now only callsrenderBasicMode()on the periodic refresh - the one piece that actually needs to reflect live data (battery, protected-app count), and which only ever sets specific properties on existing elements rather than rebuilding anything. - Moved "Modo adaptativo" to the top of the global settings list - found while investigating a report of not being able to find the toggle at all: it was there and working correctly, just 8th of 11 fields in a plain scrolling list, after several much lower-stakes settings (poll delay, log file/level, notification toggles, charge limit) - easy to miss without scrolling all the way down for a setting this consequential (it fully replaces every classic automatic event). Its own tier-threshold fields move with it, keeping the whole group together.
- Investigated a report of adaptive/classic events not showing "screen_off started" in the journal: confirmed adaptive mode and the classic per-event triggers (night, screen_off, charging, low_power) are mutually exclusive by design - enabling adaptive mode fully replaces the classic ones, which then never evaluate at all regardless of how they're configured. The journal-write path itself was confirmed correct and unconditional wherever an event genuinely starts.
- Investigated a report of a ~30s lag returning to normal performance after the screen turns on from a screen-off + power-save state: confirmed every restoration action (CPU frequency, cores, governor, doze, refresh rate) is a direct, immediate sysfs/system write with no sleep or wait anywhere in the undo path, and screen-state detection itself runs on a 2-second poll - no delay found in this project's own code. Likely either the person's own configured poll
delay(if raised well above the 3s default) or genuine Android/hardware-level ramp-up (CPU governor scaling, doze-exit bookkeeping) outside what these scripts directly control - flagged as inconclusive rather than guessed at.
v4.14.0
- New feature/design fix: "un móvil parado por la noche debería tener activado el modo Ahorro Extremo, con solo apps permitidas explícitamente pudiendo seguir funcionando" - checked this directly against the actual score weights first and confirmed it was architecturally impossible before this change: the maximum score achievable from battery+screen+night alone was 40+15+10=65, always short of tier3's default 70 threshold no matter how low the battery got - tier3 could only ever be reached through the temperature term, i.e. only when the phone was also genuinely hot. Added a large, mostly battery-independent bonus (+45) once the screen has been off a genuinely long stretch (30 minutes - distinct from the short debounce that only tells "locked a moment ago" from "actually locked") while it's night, guaranteeing tier3 even at a full, undrained battery - confirmed directly across battery levels from 10% to 100%, all correctly reaching tier3. Charging remains a deliberate exception (its own existing -40 term can still pull the total back below tier3 - no reason to restrict apps on a phone charging peacefully overnight), and the bonus never applies during the day even with a long screen-off stretch, confirmed both ways.
- Added recommended defaults for all three adaptive tiers - enabling adaptive mode had NO starting configuration at all for adaptive_tier1/2/3 before this; "Restaurar valores recomendados" only ever set up the classic events, leaving someone on adaptive mode with three empty, inert event blocks until they built each from scratch. Escalating mildest to harshest matching what the tier names promise: tier1 only nices background apps (nothing else touched), tier2 uses the same balanced preset night/screen_off already use, tier3 implements exactly what was asked - "solo la app que hayas permitido explícitamente puede seguir funcionando" via
handle_apps=suspend(freezes everything not on the allowlist - stronger than the aggressive preset's own "kill", which a killed app's own restart-on-launch can undo) pointing at a real allowlist file path the person still needs to create and populate themselves. - New persisted test (
test_adaptive_parked_at_night.sh) covering the battery-independence, the charging exception, and the daytime non-trigger. 96 bash tests across 18 files and 33 frontend tests now pass.
v4.13.0
- CRITICAL FIX: "Ahorro suave" was STILL flickering almost a week after v4.9.0's screen-off debounce - reported directly, with real frustration, asking for a full review. Found by re-reading every single term in
compute_pressure_score()from scratch rather than assuming the screen fix was the whole story: the CPU load term had the exact same shape of bug the screen-off term had -$DETECT_LOAD1(a 1-minute trailing average) crossing its whole-number boundaries at 1.0 or 2.0 changes the score by 10-20 points instantly, comfortably larger than the 5-point hysteresis margin - and a phone sitting idle overnight (exactly when "Noche"/tier1 is active) can easily have its 1-minute load average hover right around 1.0 from intermittent background activity (sync jobs, notification checks), crossing it repeatedly. Fixed with the exact same debounce pattern already used for screen-off, folded into the same state-update function rather than a second one, specifically to avoid the risk of a future round forgetting to wire up a second debounce function as carefully as the first one needed. Confirmed directly against a scenario that reproduced 39 tier transitions in 40 cycles under the old logic - down to 1 with the fix. Also fixed a related consistency bug found in the same pass:pressure_breakdown()(the WebUI's own expandable score explanation) still recomputed screen/load state instantly, completely bypassing the debounce - meaning the breakdown a person expands to understand their score could show a contribution the real score wasn't counting yet, disagreeing with the number right next to it. New persisted test (test_adaptive_load_debounce.sh), confirmed to actually catch the regression by reverting the fix and observing the exact reported flapping pattern reappear before restoring it. - Finished the pending dashboard review from the previous round: gave the battery card's "ritmo de descarga" comparison its own clear title and relabeled its two rows ("Últimas 6h" / "Media anterior") so it can no longer be confused with the hero card's own per-event drain comparison - both are real, different, legitimate comparisons, but the old generic labels ("Ahora" / "Tu media") made them look like the same stat shown twice. Moved "Encendidos nocturnos" out of the "Hoy" card into its own standalone card wit...
v4.15.0
v4.15.0
- CRITICAL FIX: searching for an event/app in Automatización was "impossible" - reported directly: the list flickered every few seconds and whatever had just been typed/filtered would disappear. Root cause:
refreshLiveStatus()'s 10s periodic refresh calledrenderVersionView()(which rebuilds the events list and global fields from scratch via a blanketinnerHTML = '') instead of the lightweightupdateActiveIndicators()it sits right next to - whose own comment already explains it was built specifically to AVOID this exact problem ("does NOT call the full renderEvents() on a timer - that rebuilds every card from scratch, destroying... any in-progress edit"), but the old destructive call was left running alongside it anyway, quietly defeating that fix on every tick. Now only callsrenderBasicMode()on the periodic refresh - the one piece that actually needs to reflect live data (battery, protected-app count), and which only ever sets specific properties on existing elements rather than rebuilding anything. - Moved "Modo adaptativo" to the top of the global settings list - found while investigating a report of not being able to find the toggle at all: it was there and working correctly, just 8th of 11 fields in a plain scrolling list, after several much lower-stakes settings (poll delay, log file/level, notification toggles, charge limit) - easy to miss without scrolling all the way down for a setting this consequential (it fully replaces every classic automatic event). Its own tier-threshold fields move with it, keeping the whole group together.
- Investigated a report of adaptive/classic events not showing "screen_off started" in the journal: confirmed adaptive mode and the classic per-event triggers (night, screen_off, charging, low_power) are mutually exclusive by design - enabling adaptive mode fully replaces the classic ones, which then never evaluate at all regardless of how they're configured. The journal-write path itself was confirmed correct and unconditional wherever an event genuinely starts.
- Investigated a report of a ~30s lag returning to normal performance after the screen turns on from a screen-off + power-save state: confirmed every restoration action (CPU frequency, cores, governor, doze, refresh rate) is a direct, immediate sysfs/system write with no sleep or wait anywhere in the undo path, and screen-state detection itself runs on a 2-second poll - no delay found in this project's own code. Likely either the person's own configured poll
delay(if raised well above the 3s default) or genuine Android/hardware-level ramp-up (CPU governor scaling, doze-exit bookkeeping) outside what these scripts directly control - flagged as inconclusive rather than guessed at.
v4.14.0
- New feature/design fix: "un móvil parado por la noche debería tener activado el modo Ahorro Extremo, con solo apps permitidas explícitamente pudiendo seguir funcionando" - checked this directly against the actual score weights first and confirmed it was architecturally impossible before this change: the maximum score achievable from battery+screen+night alone was 40+15+10=65, always short of tier3's default 70 threshold no matter how low the battery got - tier3 could only ever be reached through the temperature term, i.e. only when the phone was also genuinely hot. Added a large, mostly battery-independent bonus (+45) once the screen has been off a genuinely long stretch (30 minutes - distinct from the short debounce that only tells "locked a moment ago" from "actually locked") while it's night, guaranteeing tier3 even at a full, undrained battery - confirmed directly across battery levels from 10% to 100%, all correctly reaching tier3. Charging remains a deliberate exception (its own existing -40 term can still pull the total back below tier3 - no reason to restrict apps on a phone charging peacefully overnight), and the bonus never applies during the day even with a long screen-off stretch, confirmed both ways.
- Added recommended defaults for all three adaptive tiers - enabling adaptive mode had NO starting configuration at all for adaptive_tier1/2/3 before this; "Restaurar valores recomendados" only ever set up the classic events, leaving someone on adaptive mode with three empty, inert event blocks until they built each from scratch. Escalating mildest to harshest matching what the tier names promise: tier1 only nices background apps (nothing else touched), tier2 uses the same balanced preset night/screen_off already use, tier3 implements exactly what was asked - "solo la app que hayas permitido explícitamente puede seguir funcionando" via
handle_apps=suspend(freezes everything not on the allowlist - stronger than the aggressive preset's own "kill", which a killed app's own restart-on-launch can undo) pointing at a real allowlist file path the person still needs to create and populate themselves. - New persisted test (
test_adaptive_parked_at_night.sh) covering the battery-independence, the charging exception, and the daytime non-trigger. 96 bash tests across 18 files and 33 frontend tests now pass.
v4.13.0
- CRITICAL FIX: "Ahorro suave" was STILL flickering almost a week after v4.9.0's screen-off debounce - reported directly, with real frustration, asking for a full review. Found by re-reading every single term in
compute_pressure_score()from scratch rather than assuming the screen fix was the whole story: the CPU load term had the exact same shape of bug the screen-off term had -$DETECT_LOAD1(a 1-minute trailing average) crossing its whole-number boundaries at 1.0 or 2.0 changes the score by 10-20 points instantly, comfortably larger than the 5-point hysteresis margin - and a phone sitting idle overnight (exactly when "Noche"/tier1 is active) can easily have its 1-minute load average hover right around 1.0 from intermittent background activity (sync jobs, notification checks), crossing it repeatedly. Fixed with the exact same debounce pattern already used for screen-off, folded into the same state-update function rather than a second one, specifically to avoid the risk of a future round forgetting to wire up a second debounce function as carefully as the first one needed. Confirmed directly against a scenario that reproduced 39 tier transitions in 40 cycles under the old logic - down to 1 with the fix. Also fixed a related consistency bug found in the same pass:pressure_breakdown()(the WebUI's own expandable score explanation) still recomputed screen/load state instantly, completely bypassing the debounce - meaning the breakdown a person expands to understand their score could show a contribution the real score wasn't counting yet, disagreeing with the number right next to it. New persisted test (test_adaptive_load_debounce.sh), confirmed to actually catch the regression by reverting the fix and observing the exact reported flapping pattern reappear before restoring it. - Finished the pending dashboard review from the previous round: gave the battery card's "ritmo de descarga" comparison its own clear title and relabeled its two rows ("Últimas 6h" / "Media anterior") so it can no longer be confused with the hero card's own per-event drain comparison - both are real, different, legitimate comparisons, but the old generic labels ("Ahora" / "Tu media") made them look like the same stat shown twice. Moved "Encendidos nocturnos" out of the "Hoy" card into its own standalone card with a one-line note clarifying it's about last night, not today - the shared-visibility coordination function that existed only because the two cards used to share one outer container (and the real bug it once fixed, v3.62.0) is gone now that each manages its own visibility independently. Renamed the two identically-titled "Sistema" sections to "Salud del sistema" (the always-visible health-check card) and "Información del sistema" (the raw stats grid inside "Detalles técnicos"). Added a caption to classic mode's gauge percentage explaining what it actually measures ("% de tus mecanismos de ahorro activos ahora mismo") - adaptive mode already labaled its own score clearly, classic mode's bare percentage never did, despite classic being the mode most people are actually on.
- 88 bash tests across 17 files and 33 frontend tests across 7 files now pass.
v4.12.0
- Merged the hero gauge card and the separate "activo ahora" card into one, removing a real repetition found during a dashboard review: the mode name + "why" text in the hero already said, in slightly different words, exactly what the separate card's own title + "why" line said right underneath it - two stacked cards telling the same story twice. Moved that card's genuinely new content (which mechanisms are doing what, since when, the measured drain-rate comparison) directly into the hero card, right after its own "why" line, and dropped the separate card/title/duplicate "why" entirely. When more than one event is active simultaneously, mechanism categories they'd both otherwise show (e.g. both claim "Apps") now appear once, not duplicated - the first active event's value wins, matching the existing "first active event is primary" convention the "why" text itself already used; the "since" time anchors to whichever event started earliest; the drain comparison stays reserved for the primary event only, with a check against a since-changed primary before a slow background fetch can patch in stale text. New persisted frontend test (
test_render_active_now_merge.mjs) covering the empty case, the two-simultaneous-events dedup, and the single-event case. 33 frontend tests across 7 files now pass.
v4.11.0
- New persisted frontend test suite (
tests/frontend/,node tests/frontend/run.mjs) - the same fixtests/already gave the backend, for the frontend: every frontend bug found this session (the boot-restart-noise filter, the dashboard mode name reading real st...
v4.13.0
v4.13.0
- CRITICAL FIX: "Ahorro suave" was STILL flickering almost a week after v4.9.0's screen-off debounce - reported directly, with real frustration, asking for a full review. Found by re-reading every single term in
compute_pressure_score()from scratch rather than assuming the screen fix was the whole story: the CPU load term had the exact same shape of bug the screen-off term had -$DETECT_LOAD1(a 1-minute trailing average) crossing its whole-number boundaries at 1.0 or 2.0 changes the score by 10-20 points instantly, comfortably larger than the 5-point hysteresis margin - and a phone sitting idle overnight (exactly when "Noche"/tier1 is active) can easily have its 1-minute load average hover right around 1.0 from intermittent background activity (sync jobs, notification checks), crossing it repeatedly. Fixed with the exact same debounce pattern already used for screen-off, folded into the same state-update function rather than a second one, specifically to avoid the risk of a future round forgetting to wire up a second debounce function as carefully as the first one needed. Confirmed directly against a scenario that reproduced 39 tier transitions in 40 cycles under the old logic - down to 1 with the fix. Also fixed a related consistency bug found in the same pass:pressure_breakdown()(the WebUI's own expandable score explanation) still recomputed screen/load state instantly, completely bypassing the debounce - meaning the breakdown a person expands to understand their score could show a contribution the real score wasn't counting yet, disagreeing with the number right next to it. New persisted test (test_adaptive_load_debounce.sh), confirmed to actually catch the regression by reverting the fix and observing the exact reported flapping pattern reappear before restoring it. - Finished the pending dashboard review from the previous round: gave the battery card's "ritmo de descarga" comparison its own clear title and relabeled its two rows ("Últimas 6h" / "Media anterior") so it can no longer be confused with the hero card's own per-event drain comparison - both are real, different, legitimate comparisons, but the old generic labels ("Ahora" / "Tu media") made them look like the same stat shown twice. Moved "Encendidos nocturnos" out of the "Hoy" card into its own standalone card with a one-line note clarifying it's about last night, not today - the shared-visibility coordination function that existed only because the two cards used to share one outer container (and the real bug it once fixed, v3.62.0) is gone now that each manages its own visibility independently. Renamed the two identically-titled "Sistema" sections to "Salud del sistema" (the always-visible health-check card) and "Información del sistema" (the raw stats grid inside "Detalles técnicos"). Added a caption to classic mode's gauge percentage explaining what it actually measures ("% de tus mecanismos de ahorro activos ahora mismo") - adaptive mode already labaled its own score clearly, classic mode's bare percentage never did, despite classic being the mode most people are actually on.
- 88 bash tests across 17 files and 33 frontend tests across 7 files now pass.
v4.12.0
- Merged the hero gauge card and the separate "activo ahora" card into one, removing a real repetition found during a dashboard review: the mode name + "why" text in the hero already said, in slightly different words, exactly what the separate card's own title + "why" line said right underneath it - two stacked cards telling the same story twice. Moved that card's genuinely new content (which mechanisms are doing what, since when, the measured drain-rate comparison) directly into the hero card, right after its own "why" line, and dropped the separate card/title/duplicate "why" entirely. When more than one event is active simultaneously, mechanism categories they'd both otherwise show (e.g. both claim "Apps") now appear once, not duplicated - the first active event's value wins, matching the existing "first active event is primary" convention the "why" text itself already used; the "since" time anchors to whichever event started earliest; the drain comparison stays reserved for the primary event only, with a check against a since-changed primary before a slow background fetch can patch in stale text. New persisted frontend test (
test_render_active_now_merge.mjs) covering the empty case, the two-simultaneous-events dedup, and the single-event case. 33 frontend tests across 7 files now pass.
v4.11.0
- New persisted frontend test suite (
tests/frontend/,node tests/frontend/run.mjs) - the same fixtests/already gave the backend, for the frontend: every frontend bug found this session (the boot-restart-noise filter, the dashboard mode name reading real state instead of recomputing it, the inlinedisplay:blockoverride, the swipe animation's class handling, render()'s sub-renderer isolation) was verified with a hand-written, throwaway jsdom script that never outlived the conversation turn it was written in. New reusable harness (tests/frontend/lib/harness.mjs) formalizes that exact pattern - stub a module's imports, load the patched source, assert on the result - plusextractFunction/extractConstfor the rare case a function's own module has real side effects on import that make loading it whole impractical. 27 tests across 5 files, two of them confirmed to actually catch their bug by reverting the fix and observing the test fail before restoring it. - New functional self-test (
PowerSentinel-selftest.sh,run_functional_selftest()) - deliberately different from capability detection ("does this path/command exist on this device?"): this asks "does this function actually produce the right answer for the exact input it's built for?" - synthetic, safe inputs only, never touching this device's real state. Five checks:is_valid_bool_reading's true/false validation, the adaptive-tier thresholds' ascending order, the adaptive engine's hysteresis actually holding a tier in place against a small score drop, every summary function producing valid single-line JSON, and the process-monitor's state-file check matching the real on-disk shape - each one tied directly to a real bug this project already found and fixed (v3.55.0, v3.56.0/v4.6.0/v4.9.0, v3.67.0, v4.3.0). Runs once at every daemon boot (a genuine "fail" reaches the same critical-alert channel as safe mode; a "warn" is just logged) and on demand from the Diagnóstico panel, which now sources this same file rather than duplicating the checks. Confirmed each check actually catches its bug by reverting the corresponding fix and observing it fail.
v4.10.0
- CRITICAL FIX: Inicio's mode name stuck showing "Normal" even when something is genuinely active - reported directly. Root cause:
render()'s sequence of sub-renderers (renderBattery,renderChargeHealth,renderSystemHealth, thenrenderDashboard- the one that actually sets the mode name - and everything after) ran completely unguarded, one after another, with no error isolation at all. An exception thrown by ANY earlier renderer - a malformed or unexpected value in this specific device's own battery/charge-health/capabilities data, something impossible to reproduce or test against every real device's exact output - would abort the wholerender()call right there, meaningrenderDashboard()never even ran for that poll. If the same condition holds on every subsequent poll (very plausible for a device-specific data quirk), the mode name freezes at whatever it last showed and stays there indefinitely - exactly the reported symptom. Fixed by isolating each renderer in its own try/catch, logged to the console but never allowed to block the others - confirmed directly with a real crash in an earlier renderer (reproduced live) and verified the mode name still updated correctly to reflect the genuinely active tier afterward. Also wrapped the core-grid rendering section (a second, real crash surfaced by the same test) the same way.
v4.9.0
- CRITICAL FIX: "Ahorro suave" was STILL flickering every 30-100 seconds despite v4.6.0's fix - reported directly, with a screenshot showing the exact pattern. Traced the root cause much further back than the threshold logic itself:
compute_pressure_score()'s screen-off term gave its full +15 the instant it read the screen as off - completely ordinary phone use (checking it, locking it again every minute or two) swings the score by 15 points on every single toggle, comfortably clearing v4.6.0's 5-point hysteresis margin every time. No amount of threshold tuning downstream can fix a single input jumping by more than the margin in one step. Fixed at the actual source: the screen now has to have been continuously off for a real stretch (45s) before this term contributes anything at all, so a brief check-and-lock never moves the score in the first place. Added a second, independent layer on top for extra safety margin: a tier can still escalate immediately when conditions worsen, but can't de-escalate again until it's held the current tier for at least 2 minutes, regardless of what the score does in between. - Found and fixed a second real bug while building the above:
compute_pressure_score()is always called asx="$(compute_pressure_score)"by every caller (it needs to capture the numeric result) - command substitution runs the entire function in a subshell, so the screen-timing state it tried to update internally was silently discarded the moment that subshell exited, never once actually reaching the caller. The fix above would have done nothing at all without also catching this - split the state update into its own function, called as a plain statement (no subshell) immediately beforecompute_pressure_score()itself. - Verified with a direct simulation of the exact reported pattern (screen toggling every 40 seconds): ...
v4.12.0
v4.12.0
- Merged the hero gauge card and the separate "activo ahora" card into one, removing a real repetition found during a dashboard review: the mode name + "why" text in the hero already said, in slightly different words, exactly what the separate card's own title + "why" line said right underneath it - two stacked cards telling the same story twice. Moved that card's genuinely new content (which mechanisms are doing what, since when, the measured drain-rate comparison) directly into the hero card, right after its own "why" line, and dropped the separate card/title/duplicate "why" entirely. When more than one event is active simultaneously, mechanism categories they'd both otherwise show (e.g. both claim "Apps") now appear once, not duplicated - the first active event's value wins, matching the existing "first active event is primary" convention the "why" text itself already used; the "since" time anchors to whichever event started earliest; the drain comparison stays reserved for the primary event only, with a check against a since-changed primary before a slow background fetch can patch in stale text. New persisted frontend test (
test_render_active_now_merge.mjs) covering the empty case, the two-simultaneous-events dedup, and the single-event case. 33 frontend tests across 7 files now pass.
v4.11.0
- New persisted frontend test suite (
tests/frontend/,node tests/frontend/run.mjs) - the same fixtests/already gave the backend, for the frontend: every frontend bug found this session (the boot-restart-noise filter, the dashboard mode name reading real state instead of recomputing it, the inlinedisplay:blockoverride, the swipe animation's class handling, render()'s sub-renderer isolation) was verified with a hand-written, throwaway jsdom script that never outlived the conversation turn it was written in. New reusable harness (tests/frontend/lib/harness.mjs) formalizes that exact pattern - stub a module's imports, load the patched source, assert on the result - plusextractFunction/extractConstfor the rare case a function's own module has real side effects on import that make loading it whole impractical. 27 tests across 5 files, two of them confirmed to actually catch their bug by reverting the fix and observing the test fail before restoring it. - New functional self-test (
PowerSentinel-selftest.sh,run_functional_selftest()) - deliberately different from capability detection ("does this path/command exist on this device?"): this asks "does this function actually produce the right answer for the exact input it's built for?" - synthetic, safe inputs only, never touching this device's real state. Five checks:is_valid_bool_reading's true/false validation, the adaptive-tier thresholds' ascending order, the adaptive engine's hysteresis actually holding a tier in place against a small score drop, every summary function producing valid single-line JSON, and the process-monitor's state-file check matching the real on-disk shape - each one tied directly to a real bug this project already found and fixed (v3.55.0, v3.56.0/v4.6.0/v4.9.0, v3.67.0, v4.3.0). Runs once at every daemon boot (a genuine "fail" reaches the same critical-alert channel as safe mode; a "warn" is just logged) and on demand from the Diagnóstico panel, which now sources this same file rather than duplicating the checks. Confirmed each check actually catches its bug by reverting the corresponding fix and observing it fail.
v4.10.0
- CRITICAL FIX: Inicio's mode name stuck showing "Normal" even when something is genuinely active - reported directly. Root cause:
render()'s sequence of sub-renderers (renderBattery,renderChargeHealth,renderSystemHealth, thenrenderDashboard- the one that actually sets the mode name - and everything after) ran completely unguarded, one after another, with no error isolation at all. An exception thrown by ANY earlier renderer - a malformed or unexpected value in this specific device's own battery/charge-health/capabilities data, something impossible to reproduce or test against every real device's exact output - would abort the wholerender()call right there, meaningrenderDashboard()never even ran for that poll. If the same condition holds on every subsequent poll (very plausible for a device-specific data quirk), the mode name freezes at whatever it last showed and stays there indefinitely - exactly the reported symptom. Fixed by isolating each renderer in its own try/catch, logged to the console but never allowed to block the others - confirmed directly with a real crash in an earlier renderer (reproduced live) and verified the mode name still updated correctly to reflect the genuinely active tier afterward. Also wrapped the core-grid rendering section (a second, real crash surfaced by the same test) the same way.
v4.9.0
- CRITICAL FIX: "Ahorro suave" was STILL flickering every 30-100 seconds despite v4.6.0's fix - reported directly, with a screenshot showing the exact pattern. Traced the root cause much further back than the threshold logic itself:
compute_pressure_score()'s screen-off term gave its full +15 the instant it read the screen as off - completely ordinary phone use (checking it, locking it again every minute or two) swings the score by 15 points on every single toggle, comfortably clearing v4.6.0's 5-point hysteresis margin every time. No amount of threshold tuning downstream can fix a single input jumping by more than the margin in one step. Fixed at the actual source: the screen now has to have been continuously off for a real stretch (45s) before this term contributes anything at all, so a brief check-and-lock never moves the score in the first place. Added a second, independent layer on top for extra safety margin: a tier can still escalate immediately when conditions worsen, but can't de-escalate again until it's held the current tier for at least 2 minutes, regardless of what the score does in between. - Found and fixed a second real bug while building the above:
compute_pressure_score()is always called asx="$(compute_pressure_score)"by every caller (it needs to capture the numeric result) - command substitution runs the entire function in a subshell, so the screen-timing state it tried to update internally was silently discarded the moment that subshell exited, never once actually reaching the caller. The fix above would have done nothing at all without also catching this - split the state update into its own function, called as a plain statement (no subshell) immediately beforecompute_pressure_score()itself. - Verified with a direct simulation of the exact reported pattern (screen toggling every 40 seconds): 15 raw tier changes before any fix, down to 1 with the complete fix, and confirmed a genuine sustained screen-off still correctly escalates after 45 real seconds. Two new persisted tests (
test_adaptive_screen_debounce.sh) - one specifically calls the state-update function the same "plain statement before command substitution" way the real daemon does, so a regression back to updating state from inside the subshelled function would be caught immediately. 81 tests across 15 files now pass.
v4.8.0
- Felt swipe/tab-switch motion (feature request): switching tabs - by swipe, bottom-nav tap, or the "Más" sheet - now plays a brief slide+fade-in on the incoming view, from the direction of travel. Deliberately NOT the live-dragging, finger-following carousel this project already tried and abandoned once before (
initSwipeNav()'s own long-standing comment: three separate fix attempts, including a percentage-based one, all rendering wider than the screen on-device - a class of bug impossible to verify without a real browser in this dev environment). This is architecturally a different, much simpler thing: a single one-shot CSS@keyframesanimation on only the incoming view (the outgoing one still just disappears instantly exactly as before) - no absolute positioning, no two views ever visible at once, no pixel/percentage width math anywhere, so there's nothing for a stale inline style or a width miscalculation to get stuck in. The animation class is removed the instant it finishes so a later, unrelated display toggle of that view can never accidentally replay it. Respectsprefers-reduced-motion. Verified the class-toggling logic directly and captured a mid-animation frame showing the expected offset-and-fading intermediate state.
v4.7.0
- CRITICAL FIX: "Registro técnico" content still stopped partway down the screen - reported directly as the same symptom already fixed once for Actividad in v3.70.0, but persisting here. Real root cause was broader than the original CSS fix:
switchLogSubTab()setsstyle.display = 'block'directly (an inline style) whenever a person switches to a tab - and an inline style always wins over any stylesheet rule regardless of specificity, including thedisplay: flexv3.70.0 gave#l-view-log/#l-view-journalspecifically so their content could stretch to fill the screen. Actividad only ever looked fixed because it's the tab visible by default straight from the static HTML (no inline style to begin with) -switchLogSubTab()never touches it unless a person navigates away and back, which reintroduces the exact same bug there too, and affects "Energía" the same way once actually switched to. Fixed by clearing the property (setting it to'') for whichever view should be visible instead of forcing'block', letting the stylesheet's own rule take over correctly;'none'for hiding is unaffected, since there's no competing "should be visible" rule to fight there. Confirmed with real navigation sequences this time (switch to Registro técnico directly, and switch away to another tab and back to Actividad) rather than a static snapshot, which is exactly what let the original fix's verification miss this.
##...
v4.11.0
v4.11.0
- New persisted frontend test suite (
tests/frontend/,node tests/frontend/run.mjs) - the same fixtests/already gave the backend, for the frontend: every frontend bug found this session (the boot-restart-noise filter, the dashboard mode name reading real state instead of recomputing it, the inlinedisplay:blockoverride, the swipe animation's class handling, render()'s sub-renderer isolation) was verified with a hand-written, throwaway jsdom script that never outlived the conversation turn it was written in. New reusable harness (tests/frontend/lib/harness.mjs) formalizes that exact pattern - stub a module's imports, load the patched source, assert on the result - plusextractFunction/extractConstfor the rare case a function's own module has real side effects on import that make loading it whole impractical. 27 tests across 5 files, two of them confirmed to actually catch their bug by reverting the fix and observing the test fail before restoring it. - New functional self-test (
PowerSentinel-selftest.sh,run_functional_selftest()) - deliberately different from capability detection ("does this path/command exist on this device?"): this asks "does this function actually produce the right answer for the exact input it's built for?" - synthetic, safe inputs only, never touching this device's real state. Five checks:is_valid_bool_reading's true/false validation, the adaptive-tier thresholds' ascending order, the adaptive engine's hysteresis actually holding a tier in place against a small score drop, every summary function producing valid single-line JSON, and the process-monitor's state-file check matching the real on-disk shape - each one tied directly to a real bug this project already found and fixed (v3.55.0, v3.56.0/v4.6.0/v4.9.0, v3.67.0, v4.3.0). Runs once at every daemon boot (a genuine "fail" reaches the same critical-alert channel as safe mode; a "warn" is just logged) and on demand from the Diagnóstico panel, which now sources this same file rather than duplicating the checks. Confirmed each check actually catches its bug by reverting the corresponding fix and observing it fail.
v4.10.0
- CRITICAL FIX: Inicio's mode name stuck showing "Normal" even when something is genuinely active - reported directly. Root cause:
render()'s sequence of sub-renderers (renderBattery,renderChargeHealth,renderSystemHealth, thenrenderDashboard- the one that actually sets the mode name - and everything after) ran completely unguarded, one after another, with no error isolation at all. An exception thrown by ANY earlier renderer - a malformed or unexpected value in this specific device's own battery/charge-health/capabilities data, something impossible to reproduce or test against every real device's exact output - would abort the wholerender()call right there, meaningrenderDashboard()never even ran for that poll. If the same condition holds on every subsequent poll (very plausible for a device-specific data quirk), the mode name freezes at whatever it last showed and stays there indefinitely - exactly the reported symptom. Fixed by isolating each renderer in its own try/catch, logged to the console but never allowed to block the others - confirmed directly with a real crash in an earlier renderer (reproduced live) and verified the mode name still updated correctly to reflect the genuinely active tier afterward. Also wrapped the core-grid rendering section (a second, real crash surfaced by the same test) the same way.
v4.9.0
- CRITICAL FIX: "Ahorro suave" was STILL flickering every 30-100 seconds despite v4.6.0's fix - reported directly, with a screenshot showing the exact pattern. Traced the root cause much further back than the threshold logic itself:
compute_pressure_score()'s screen-off term gave its full +15 the instant it read the screen as off - completely ordinary phone use (checking it, locking it again every minute or two) swings the score by 15 points on every single toggle, comfortably clearing v4.6.0's 5-point hysteresis margin every time. No amount of threshold tuning downstream can fix a single input jumping by more than the margin in one step. Fixed at the actual source: the screen now has to have been continuously off for a real stretch (45s) before this term contributes anything at all, so a brief check-and-lock never moves the score in the first place. Added a second, independent layer on top for extra safety margin: a tier can still escalate immediately when conditions worsen, but can't de-escalate again until it's held the current tier for at least 2 minutes, regardless of what the score does in between. - Found and fixed a second real bug while building the above:
compute_pressure_score()is always called asx="$(compute_pressure_score)"by every caller (it needs to capture the numeric result) - command substitution runs the entire function in a subshell, so the screen-timing state it tried to update internally was silently discarded the moment that subshell exited, never once actually reaching the caller. The fix above would have done nothing at all without also catching this - split the state update into its own function, called as a plain statement (no subshell) immediately beforecompute_pressure_score()itself. - Verified with a direct simulation of the exact reported pattern (screen toggling every 40 seconds): 15 raw tier changes before any fix, down to 1 with the complete fix, and confirmed a genuine sustained screen-off still correctly escalates after 45 real seconds. Two new persisted tests (
test_adaptive_screen_debounce.sh) - one specifically calls the state-update function the same "plain statement before command substitution" way the real daemon does, so a regression back to updating state from inside the subshelled function would be caught immediately. 81 tests across 15 files now pass.
v4.8.0
- Felt swipe/tab-switch motion (feature request): switching tabs - by swipe, bottom-nav tap, or the "Más" sheet - now plays a brief slide+fade-in on the incoming view, from the direction of travel. Deliberately NOT the live-dragging, finger-following carousel this project already tried and abandoned once before (
initSwipeNav()'s own long-standing comment: three separate fix attempts, including a percentage-based one, all rendering wider than the screen on-device - a class of bug impossible to verify without a real browser in this dev environment). This is architecturally a different, much simpler thing: a single one-shot CSS@keyframesanimation on only the incoming view (the outgoing one still just disappears instantly exactly as before) - no absolute positioning, no two views ever visible at once, no pixel/percentage width math anywhere, so there's nothing for a stale inline style or a width miscalculation to get stuck in. The animation class is removed the instant it finishes so a later, unrelated display toggle of that view can never accidentally replay it. Respectsprefers-reduced-motion. Verified the class-toggling logic directly and captured a mid-animation frame showing the expected offset-and-fading intermediate state.
v4.7.0
- CRITICAL FIX: "Registro técnico" content still stopped partway down the screen - reported directly as the same symptom already fixed once for Actividad in v3.70.0, but persisting here. Real root cause was broader than the original CSS fix:
switchLogSubTab()setsstyle.display = 'block'directly (an inline style) whenever a person switches to a tab - and an inline style always wins over any stylesheet rule regardless of specificity, including thedisplay: flexv3.70.0 gave#l-view-log/#l-view-journalspecifically so their content could stretch to fill the screen. Actividad only ever looked fixed because it's the tab visible by default straight from the static HTML (no inline style to begin with) -switchLogSubTab()never touches it unless a person navigates away and back, which reintroduces the exact same bug there too, and affects "Energía" the same way once actually switched to. Fixed by clearing the property (setting it to'') for whichever view should be visible instead of forcing'block', letting the stylesheet's own rule take over correctly;'none'for hiding is unaffected, since there's no competing "should be visible" rule to fight there. Confirmed with real navigation sequences this time (switch to Registro técnico directly, and switch away to another tab and back to Actividad) rather than a static snapshot, which is exactly what let the original fix's verification miss this.
v4.6.0
- CRITICAL FIX: "Ahorro suave" (adaptive_tier1) flickering in and out of Actividad continuously - reported directly, along with wanting to see the current mode on Inicio. Root cause:
pressure_tier_for_score()had no hysteresis at all - a straight threshold comparison, recomputed from scratch every daemon cycle, against a score that's genuinely noisy moment to moment (the 1-minute load average alone swings it 10-20 points crossing its own breakpoints; a screen on/off toggle swings it 15). Tier1's default threshold of 20 is the easiest one to sit near, which is exactly the tier that was reported flapping. Fixed with a standard sticky-threshold margin: escalating to a higher tier is still immediate (reacting fast to genuinely worsening conditions is the safe direction to be quick about), but de-escalating now requires the score to drop meaningfully below the current tier's own entry threshold, not merely dip a fraction under it. Confirmed with the exact reported scenario (a score hovering a couple of points under the threshold no longer drops the tier) and that a genuine large drop (e.g. plugging in the charger) still lands directly at the right tier in one step rather than creeping down. - Found and fixed a second, related bug while looking into "cómo saber en qué modo está realmente": Inic...
v4.10.0
v4.10.0
- CRITICAL FIX: Inicio's mode name stuck showing "Normal" even when something is genuinely active - reported directly. Root cause:
render()'s sequence of sub-renderers (renderBattery,renderChargeHealth,renderSystemHealth, thenrenderDashboard- the one that actually sets the mode name - and everything after) ran completely unguarded, one after another, with no error isolation at all. An exception thrown by ANY earlier renderer - a malformed or unexpected value in this specific device's own battery/charge-health/capabilities data, something impossible to reproduce or test against every real device's exact output - would abort the wholerender()call right there, meaningrenderDashboard()never even ran for that poll. If the same condition holds on every subsequent poll (very plausible for a device-specific data quirk), the mode name freezes at whatever it last showed and stays there indefinitely - exactly the reported symptom. Fixed by isolating each renderer in its own try/catch, logged to the console but never allowed to block the others - confirmed directly with a real crash in an earlier renderer (reproduced live) and verified the mode name still updated correctly to reflect the genuinely active tier afterward. Also wrapped the core-grid rendering section (a second, real crash surfaced by the same test) the same way.
v4.9.0
- CRITICAL FIX: "Ahorro suave" was STILL flickering every 30-100 seconds despite v4.6.0's fix - reported directly, with a screenshot showing the exact pattern. Traced the root cause much further back than the threshold logic itself:
compute_pressure_score()'s screen-off term gave its full +15 the instant it read the screen as off - completely ordinary phone use (checking it, locking it again every minute or two) swings the score by 15 points on every single toggle, comfortably clearing v4.6.0's 5-point hysteresis margin every time. No amount of threshold tuning downstream can fix a single input jumping by more than the margin in one step. Fixed at the actual source: the screen now has to have been continuously off for a real stretch (45s) before this term contributes anything at all, so a brief check-and-lock never moves the score in the first place. Added a second, independent layer on top for extra safety margin: a tier can still escalate immediately when conditions worsen, but can't de-escalate again until it's held the current tier for at least 2 minutes, regardless of what the score does in between. - Found and fixed a second real bug while building the above:
compute_pressure_score()is always called asx="$(compute_pressure_score)"by every caller (it needs to capture the numeric result) - command substitution runs the entire function in a subshell, so the screen-timing state it tried to update internally was silently discarded the moment that subshell exited, never once actually reaching the caller. The fix above would have done nothing at all without also catching this - split the state update into its own function, called as a plain statement (no subshell) immediately beforecompute_pressure_score()itself. - Verified with a direct simulation of the exact reported pattern (screen toggling every 40 seconds): 15 raw tier changes before any fix, down to 1 with the complete fix, and confirmed a genuine sustained screen-off still correctly escalates after 45 real seconds. Two new persisted tests (
test_adaptive_screen_debounce.sh) - one specifically calls the state-update function the same "plain statement before command substitution" way the real daemon does, so a regression back to updating state from inside the subshelled function would be caught immediately. 81 tests across 15 files now pass.
v4.8.0
- Felt swipe/tab-switch motion (feature request): switching tabs - by swipe, bottom-nav tap, or the "Más" sheet - now plays a brief slide+fade-in on the incoming view, from the direction of travel. Deliberately NOT the live-dragging, finger-following carousel this project already tried and abandoned once before (
initSwipeNav()'s own long-standing comment: three separate fix attempts, including a percentage-based one, all rendering wider than the screen on-device - a class of bug impossible to verify without a real browser in this dev environment). This is architecturally a different, much simpler thing: a single one-shot CSS@keyframesanimation on only the incoming view (the outgoing one still just disappears instantly exactly as before) - no absolute positioning, no two views ever visible at once, no pixel/percentage width math anywhere, so there's nothing for a stale inline style or a width miscalculation to get stuck in. The animation class is removed the instant it finishes so a later, unrelated display toggle of that view can never accidentally replay it. Respectsprefers-reduced-motion. Verified the class-toggling logic directly and captured a mid-animation frame showing the expected offset-and-fading intermediate state.
v4.7.0
- CRITICAL FIX: "Registro técnico" content still stopped partway down the screen - reported directly as the same symptom already fixed once for Actividad in v3.70.0, but persisting here. Real root cause was broader than the original CSS fix:
switchLogSubTab()setsstyle.display = 'block'directly (an inline style) whenever a person switches to a tab - and an inline style always wins over any stylesheet rule regardless of specificity, including thedisplay: flexv3.70.0 gave#l-view-log/#l-view-journalspecifically so their content could stretch to fill the screen. Actividad only ever looked fixed because it's the tab visible by default straight from the static HTML (no inline style to begin with) -switchLogSubTab()never touches it unless a person navigates away and back, which reintroduces the exact same bug there too, and affects "Energía" the same way once actually switched to. Fixed by clearing the property (setting it to'') for whichever view should be visible instead of forcing'block', letting the stylesheet's own rule take over correctly;'none'for hiding is unaffected, since there's no competing "should be visible" rule to fight there. Confirmed with real navigation sequences this time (switch to Registro técnico directly, and switch away to another tab and back to Actividad) rather than a static snapshot, which is exactly what let the original fix's verification miss this.
v4.6.0
- CRITICAL FIX: "Ahorro suave" (adaptive_tier1) flickering in and out of Actividad continuously - reported directly, along with wanting to see the current mode on Inicio. Root cause:
pressure_tier_for_score()had no hysteresis at all - a straight threshold comparison, recomputed from scratch every daemon cycle, against a score that's genuinely noisy moment to moment (the 1-minute load average alone swings it 10-20 points crossing its own breakpoints; a screen on/off toggle swings it 15). Tier1's default threshold of 20 is the easiest one to sit near, which is exactly the tier that was reported flapping. Fixed with a standard sticky-threshold margin: escalating to a higher tier is still immediate (reacting fast to genuinely worsening conditions is the safe direction to be quick about), but de-escalating now requires the score to drop meaningfully below the current tier's own entry threshold, not merely dip a fraction under it. Confirmed with the exact reported scenario (a score hovering a couple of points under the threshold no longer drops the tier) and that a genuine large drop (e.g. plugging in the charger) still lands directly at the right tier in one step rather than creeping down. - Found and fixed a second, related bug while looking into "cómo saber en qué modo está realmente": Inicio's own mode-name display (the word next to the gauge) was recomputing its own tier independently from the raw score, via a plain no-hysteresis comparison completely separate from the daemon's real decision - meaning even after the backend fix above, the displayed name could still have flickered on its own, potentially disagreeing with what Actividad's timeline said actually happened. Now reads the real, authoritative state directly (
sys.activeEvents, the exact same data driving the journal) instead of recalculating a possibly-different answer - the gauge's mode name is the "what mode is it in right now" indicator being asked for, now made trustworthy rather than added as a new element. - New persisted test (
test_adaptive_tier_hysteresis.sh), confirmed to actually catch the regression by reverting the fix and observing the exact reported flapping reappear before restoring it. 76 tests across 13 files now pass.
v4.5.0
- New, opt-in status notification ("Mostrar modo actual en notificaciones", off by default): shows which mode is active and what it's actually doing, e.g. "PowerSentinel — Noche / CPU en ahorro · apps en 2º plano ralentizadas · CPU al 50% · pantalla a 60Hz". Deliberately does NOT reintroduce the old noisy per-change notification behavior
alertbridge.shwas specifically rewritten to stop doing (its own header comment already says so) - posted with a fixed tag so every update REPLACES the previous notification in place rather than stacking a new one, only re-posts when the text actually changed, and is entirely opt-in so anyone who doesn't ask for it sees no difference at all. Built fromactive_mechanisms_snapshot()- the exact same resolved data the WebUI's own "active now" card already uses - so the notification can never say something different from what the app itself shows. - Real, honest limitation surfaced rather than glossed over:
cmd notification post(confirmed against its own AOSP source) has no flag for importance, silent, or ongoing/non-dismissible - the first time this notification appears it will likely make the normal notification sound. The setting's own help text tells the perso...
v4.9.0
v4.9.0
- CRITICAL FIX: "Ahorro suave" was STILL flickering every 30-100 seconds despite v4.6.0's fix - reported directly, with a screenshot showing the exact pattern. Traced the root cause much further back than the threshold logic itself:
compute_pressure_score()'s screen-off term gave its full +15 the instant it read the screen as off - completely ordinary phone use (checking it, locking it again every minute or two) swings the score by 15 points on every single toggle, comfortably clearing v4.6.0's 5-point hysteresis margin every time. No amount of threshold tuning downstream can fix a single input jumping by more than the margin in one step. Fixed at the actual source: the screen now has to have been continuously off for a real stretch (45s) before this term contributes anything at all, so a brief check-and-lock never moves the score in the first place. Added a second, independent layer on top for extra safety margin: a tier can still escalate immediately when conditions worsen, but can't de-escalate again until it's held the current tier for at least 2 minutes, regardless of what the score does in between. - Found and fixed a second real bug while building the above:
compute_pressure_score()is always called asx="$(compute_pressure_score)"by every caller (it needs to capture the numeric result) - command substitution runs the entire function in a subshell, so the screen-timing state it tried to update internally was silently discarded the moment that subshell exited, never once actually reaching the caller. The fix above would have done nothing at all without also catching this - split the state update into its own function, called as a plain statement (no subshell) immediately beforecompute_pressure_score()itself. - Verified with a direct simulation of the exact reported pattern (screen toggling every 40 seconds): 15 raw tier changes before any fix, down to 1 with the complete fix, and confirmed a genuine sustained screen-off still correctly escalates after 45 real seconds. Two new persisted tests (
test_adaptive_screen_debounce.sh) - one specifically calls the state-update function the same "plain statement before command substitution" way the real daemon does, so a regression back to updating state from inside the subshelled function would be caught immediately. 81 tests across 15 files now pass.
v4.8.0
- Felt swipe/tab-switch motion (feature request): switching tabs - by swipe, bottom-nav tap, or the "Más" sheet - now plays a brief slide+fade-in on the incoming view, from the direction of travel. Deliberately NOT the live-dragging, finger-following carousel this project already tried and abandoned once before (
initSwipeNav()'s own long-standing comment: three separate fix attempts, including a percentage-based one, all rendering wider than the screen on-device - a class of bug impossible to verify without a real browser in this dev environment). This is architecturally a different, much simpler thing: a single one-shot CSS@keyframesanimation on only the incoming view (the outgoing one still just disappears instantly exactly as before) - no absolute positioning, no two views ever visible at once, no pixel/percentage width math anywhere, so there's nothing for a stale inline style or a width miscalculation to get stuck in. The animation class is removed the instant it finishes so a later, unrelated display toggle of that view can never accidentally replay it. Respectsprefers-reduced-motion. Verified the class-toggling logic directly and captured a mid-animation frame showing the expected offset-and-fading intermediate state.
v4.7.0
- CRITICAL FIX: "Registro técnico" content still stopped partway down the screen - reported directly as the same symptom already fixed once for Actividad in v3.70.0, but persisting here. Real root cause was broader than the original CSS fix:
switchLogSubTab()setsstyle.display = 'block'directly (an inline style) whenever a person switches to a tab - and an inline style always wins over any stylesheet rule regardless of specificity, including thedisplay: flexv3.70.0 gave#l-view-log/#l-view-journalspecifically so their content could stretch to fill the screen. Actividad only ever looked fixed because it's the tab visible by default straight from the static HTML (no inline style to begin with) -switchLogSubTab()never touches it unless a person navigates away and back, which reintroduces the exact same bug there too, and affects "Energía" the same way once actually switched to. Fixed by clearing the property (setting it to'') for whichever view should be visible instead of forcing'block', letting the stylesheet's own rule take over correctly;'none'for hiding is unaffected, since there's no competing "should be visible" rule to fight there. Confirmed with real navigation sequences this time (switch to Registro técnico directly, and switch away to another tab and back to Actividad) rather than a static snapshot, which is exactly what let the original fix's verification miss this.
v4.6.0
- CRITICAL FIX: "Ahorro suave" (adaptive_tier1) flickering in and out of Actividad continuously - reported directly, along with wanting to see the current mode on Inicio. Root cause:
pressure_tier_for_score()had no hysteresis at all - a straight threshold comparison, recomputed from scratch every daemon cycle, against a score that's genuinely noisy moment to moment (the 1-minute load average alone swings it 10-20 points crossing its own breakpoints; a screen on/off toggle swings it 15). Tier1's default threshold of 20 is the easiest one to sit near, which is exactly the tier that was reported flapping. Fixed with a standard sticky-threshold margin: escalating to a higher tier is still immediate (reacting fast to genuinely worsening conditions is the safe direction to be quick about), but de-escalating now requires the score to drop meaningfully below the current tier's own entry threshold, not merely dip a fraction under it. Confirmed with the exact reported scenario (a score hovering a couple of points under the threshold no longer drops the tier) and that a genuine large drop (e.g. plugging in the charger) still lands directly at the right tier in one step rather than creeping down. - Found and fixed a second, related bug while looking into "cómo saber en qué modo está realmente": Inicio's own mode-name display (the word next to the gauge) was recomputing its own tier independently from the raw score, via a plain no-hysteresis comparison completely separate from the daemon's real decision - meaning even after the backend fix above, the displayed name could still have flickered on its own, potentially disagreeing with what Actividad's timeline said actually happened. Now reads the real, authoritative state directly (
sys.activeEvents, the exact same data driving the journal) instead of recalculating a possibly-different answer - the gauge's mode name is the "what mode is it in right now" indicator being asked for, now made trustworthy rather than added as a new element. - New persisted test (
test_adaptive_tier_hysteresis.sh), confirmed to actually catch the regression by reverting the fix and observing the exact reported flapping reappear before restoring it. 76 tests across 13 files now pass.
v4.5.0
- New, opt-in status notification ("Mostrar modo actual en notificaciones", off by default): shows which mode is active and what it's actually doing, e.g. "PowerSentinel — Noche / CPU en ahorro · apps en 2º plano ralentizadas · CPU al 50% · pantalla a 60Hz". Deliberately does NOT reintroduce the old noisy per-change notification behavior
alertbridge.shwas specifically rewritten to stop doing (its own header comment already says so) - posted with a fixed tag so every update REPLACES the previous notification in place rather than stacking a new one, only re-posts when the text actually changed, and is entirely opt-in so anyone who doesn't ask for it sees no difference at all. Built fromactive_mechanisms_snapshot()- the exact same resolved data the WebUI's own "active now" card already uses - so the notification can never say something different from what the app itself shows. - Real, honest limitation surfaced rather than glossed over:
cmd notification post(confirmed against its own AOSP source) has no flag for importance, silent, or ongoing/non-dismissible - the first time this notification appears it will likely make the normal notification sound. The setting's own help text tells the person to long-press it once and set it to "Silent" from Android's own settings; every update after that stays quiet. No amount of additional code changes what the shell command itself can't do. - Found and fixed a real gap while building this:
max_cpu_freq/max_refresh_rate(v4.1.0's CPU speed and refresh rate limits) were never added toactive_mechanisms_snapshot()or the WebUI's ownmechanismRows()- meaning Inicio's "active now" card has silently never shown either mechanism even when genuinely active, since the day they shipped. Fixed in both places. - Also corrected the existing
notifysetting's help text, which still described the old "a notification for every change" behavior this project deliberately moved away from, not what it actually does today (critical alerts only). - New persisted test (
test_notify_active_mode.sh) covering the text builder - the empty case, a single event with several mechanisms, and two simultaneous events combining into one notification without duplicating phrases. 68 tests across 12 files now pass.
v4.4.0
- CRITICAL FIX: "Arranque" (boot) appeared to flicker in and out of Actividad continuously - reported directly, along with asking for a way to actually know which mode is active. Root cause isn't in the boot-event logic itself: "boot" only ever activates once, in
init_v2()at daemon startup,...
v4.8.0
v4.8.0
- Felt swipe/tab-switch motion (feature request): switching tabs - by swipe, bottom-nav tap, or the "Más" sheet - now plays a brief slide+fade-in on the incoming view, from the direction of travel. Deliberately NOT the live-dragging, finger-following carousel this project already tried and abandoned once before (
initSwipeNav()'s own long-standing comment: three separate fix attempts, including a percentage-based one, all rendering wider than the screen on-device - a class of bug impossible to verify without a real browser in this dev environment). This is architecturally a different, much simpler thing: a single one-shot CSS@keyframesanimation on only the incoming view (the outgoing one still just disappears instantly exactly as before) - no absolute positioning, no two views ever visible at once, no pixel/percentage width math anywhere, so there's nothing for a stale inline style or a width miscalculation to get stuck in. The animation class is removed the instant it finishes so a later, unrelated display toggle of that view can never accidentally replay it. Respectsprefers-reduced-motion. Verified the class-toggling logic directly and captured a mid-animation frame showing the expected offset-and-fading intermediate state.
v4.7.0
- CRITICAL FIX: "Registro técnico" content still stopped partway down the screen - reported directly as the same symptom already fixed once for Actividad in v3.70.0, but persisting here. Real root cause was broader than the original CSS fix:
switchLogSubTab()setsstyle.display = 'block'directly (an inline style) whenever a person switches to a tab - and an inline style always wins over any stylesheet rule regardless of specificity, including thedisplay: flexv3.70.0 gave#l-view-log/#l-view-journalspecifically so their content could stretch to fill the screen. Actividad only ever looked fixed because it's the tab visible by default straight from the static HTML (no inline style to begin with) -switchLogSubTab()never touches it unless a person navigates away and back, which reintroduces the exact same bug there too, and affects "Energía" the same way once actually switched to. Fixed by clearing the property (setting it to'') for whichever view should be visible instead of forcing'block', letting the stylesheet's own rule take over correctly;'none'for hiding is unaffected, since there's no competing "should be visible" rule to fight there. Confirmed with real navigation sequences this time (switch to Registro técnico directly, and switch away to another tab and back to Actividad) rather than a static snapshot, which is exactly what let the original fix's verification miss this.
v4.6.0
- CRITICAL FIX: "Ahorro suave" (adaptive_tier1) flickering in and out of Actividad continuously - reported directly, along with wanting to see the current mode on Inicio. Root cause:
pressure_tier_for_score()had no hysteresis at all - a straight threshold comparison, recomputed from scratch every daemon cycle, against a score that's genuinely noisy moment to moment (the 1-minute load average alone swings it 10-20 points crossing its own breakpoints; a screen on/off toggle swings it 15). Tier1's default threshold of 20 is the easiest one to sit near, which is exactly the tier that was reported flapping. Fixed with a standard sticky-threshold margin: escalating to a higher tier is still immediate (reacting fast to genuinely worsening conditions is the safe direction to be quick about), but de-escalating now requires the score to drop meaningfully below the current tier's own entry threshold, not merely dip a fraction under it. Confirmed with the exact reported scenario (a score hovering a couple of points under the threshold no longer drops the tier) and that a genuine large drop (e.g. plugging in the charger) still lands directly at the right tier in one step rather than creeping down. - Found and fixed a second, related bug while looking into "cómo saber en qué modo está realmente": Inicio's own mode-name display (the word next to the gauge) was recomputing its own tier independently from the raw score, via a plain no-hysteresis comparison completely separate from the daemon's real decision - meaning even after the backend fix above, the displayed name could still have flickered on its own, potentially disagreeing with what Actividad's timeline said actually happened. Now reads the real, authoritative state directly (
sys.activeEvents, the exact same data driving the journal) instead of recalculating a possibly-different answer - the gauge's mode name is the "what mode is it in right now" indicator being asked for, now made trustworthy rather than added as a new element. - New persisted test (
test_adaptive_tier_hysteresis.sh), confirmed to actually catch the regression by reverting the fix and observing the exact reported flapping reappear before restoring it. 76 tests across 13 files now pass.
v4.5.0
- New, opt-in status notification ("Mostrar modo actual en notificaciones", off by default): shows which mode is active and what it's actually doing, e.g. "PowerSentinel — Noche / CPU en ahorro · apps en 2º plano ralentizadas · CPU al 50% · pantalla a 60Hz". Deliberately does NOT reintroduce the old noisy per-change notification behavior
alertbridge.shwas specifically rewritten to stop doing (its own header comment already says so) - posted with a fixed tag so every update REPLACES the previous notification in place rather than stacking a new one, only re-posts when the text actually changed, and is entirely opt-in so anyone who doesn't ask for it sees no difference at all. Built fromactive_mechanisms_snapshot()- the exact same resolved data the WebUI's own "active now" card already uses - so the notification can never say something different from what the app itself shows. - Real, honest limitation surfaced rather than glossed over:
cmd notification post(confirmed against its own AOSP source) has no flag for importance, silent, or ongoing/non-dismissible - the first time this notification appears it will likely make the normal notification sound. The setting's own help text tells the person to long-press it once and set it to "Silent" from Android's own settings; every update after that stays quiet. No amount of additional code changes what the shell command itself can't do. - Found and fixed a real gap while building this:
max_cpu_freq/max_refresh_rate(v4.1.0's CPU speed and refresh rate limits) were never added toactive_mechanisms_snapshot()or the WebUI's ownmechanismRows()- meaning Inicio's "active now" card has silently never shown either mechanism even when genuinely active, since the day they shipped. Fixed in both places. - Also corrected the existing
notifysetting's help text, which still described the old "a notification for every change" behavior this project deliberately moved away from, not what it actually does today (critical alerts only). - New persisted test (
test_notify_active_mode.sh) covering the text builder - the empty case, a single event with several mechanisms, and two simultaneous events combining into one notification without duplicating phrases. 68 tests across 12 files now pass.
v4.4.0
- CRITICAL FIX: "Arranque" (boot) appeared to flicker in and out of Actividad continuously - reported directly, along with asking for a way to actually know which mode is active. Root cause isn't in the boot-event logic itself: "boot" only ever activates once, in
init_v2()at daemon startup, andstate_reconcile()correctly undoes whatever the previous run left active before that - individually, both are exactly right. But if the daemon itself is dying and getting relaunched repeatedly (a crash, an OOM kill -service.sh's own 60-second watchdog catches this and relaunches it every time it happens), every relaunch produces this exact "boot ended" (state_reconcile undoing the crashed run's stale boot) immediately followed by a fresh "boot started" - daemon-internal bookkeeping noise from an invisible restart, not a meaningful mode change. Filters out any "boot ended" immediately followed by a "boot started" within 15 real seconds from every view built from journal entries (Actividad, Inicio's recent-activity list, and today's intervention count, which was also being inflated by the same noise) - confirmed with the exact restart case, an isolated genuine boot, and a same-pair-but-too-slow-to-be-a-restart case, all handled correctly. - New diagnostics check: counts "boot started" journal entries in the last 2 hours - 1 is completely normal, several means the daemon is actually crash-looping and the watchdog is masking it by relaunching every ~60s. Gives a direct, unambiguous answer to "is something actually wrong" instead of inferring it from timeline noise.
v4.3.0
handle_proc's background monitor has likely never actually worked, for any user, since it was written - found while re-auditing the whole module for hang/resource-degradation risks. Its while-loop's own continuation check compared the real, on-disk state file (an object keyed by event name,{"night": <timestamp>, ...}) using a filter that iterates values, not keys - so the check was false unconditionally, every single time, for every real state file this daemon has ever produced. The loop exited on its very first condition check, meaning this feature has silently done nothing since it shipped. Fixed to check the key directly.- Fixing that exposed a second, real problem it had been masking:
reassert_active_events()callsenable_pwr_save()(and thereforeaction_proc_apply()) for every still-active event whenever ANY event ends. With no de-duplication, once the monitor loop actually persists (as the fix above makes it do), every such reassert would have spawned another redundant background loop for the same event - an unbounded, e...
v4.7.0
v4.7.0
- CRITICAL FIX: "Registro técnico" content still stopped partway down the screen - reported directly as the same symptom already fixed once for Actividad in v3.70.0, but persisting here. Real root cause was broader than the original CSS fix:
switchLogSubTab()setsstyle.display = 'block'directly (an inline style) whenever a person switches to a tab - and an inline style always wins over any stylesheet rule regardless of specificity, including thedisplay: flexv3.70.0 gave#l-view-log/#l-view-journalspecifically so their content could stretch to fill the screen. Actividad only ever looked fixed because it's the tab visible by default straight from the static HTML (no inline style to begin with) -switchLogSubTab()never touches it unless a person navigates away and back, which reintroduces the exact same bug there too, and affects "Energía" the same way once actually switched to. Fixed by clearing the property (setting it to'') for whichever view should be visible instead of forcing'block', letting the stylesheet's own rule take over correctly;'none'for hiding is unaffected, since there's no competing "should be visible" rule to fight there. Confirmed with real navigation sequences this time (switch to Registro técnico directly, and switch away to another tab and back to Actividad) rather than a static snapshot, which is exactly what let the original fix's verification miss this.
v4.6.0
- CRITICAL FIX: "Ahorro suave" (adaptive_tier1) flickering in and out of Actividad continuously - reported directly, along with wanting to see the current mode on Inicio. Root cause:
pressure_tier_for_score()had no hysteresis at all - a straight threshold comparison, recomputed from scratch every daemon cycle, against a score that's genuinely noisy moment to moment (the 1-minute load average alone swings it 10-20 points crossing its own breakpoints; a screen on/off toggle swings it 15). Tier1's default threshold of 20 is the easiest one to sit near, which is exactly the tier that was reported flapping. Fixed with a standard sticky-threshold margin: escalating to a higher tier is still immediate (reacting fast to genuinely worsening conditions is the safe direction to be quick about), but de-escalating now requires the score to drop meaningfully below the current tier's own entry threshold, not merely dip a fraction under it. Confirmed with the exact reported scenario (a score hovering a couple of points under the threshold no longer drops the tier) and that a genuine large drop (e.g. plugging in the charger) still lands directly at the right tier in one step rather than creeping down. - Found and fixed a second, related bug while looking into "cómo saber en qué modo está realmente": Inicio's own mode-name display (the word next to the gauge) was recomputing its own tier independently from the raw score, via a plain no-hysteresis comparison completely separate from the daemon's real decision - meaning even after the backend fix above, the displayed name could still have flickered on its own, potentially disagreeing with what Actividad's timeline said actually happened. Now reads the real, authoritative state directly (
sys.activeEvents, the exact same data driving the journal) instead of recalculating a possibly-different answer - the gauge's mode name is the "what mode is it in right now" indicator being asked for, now made trustworthy rather than added as a new element. - New persisted test (
test_adaptive_tier_hysteresis.sh), confirmed to actually catch the regression by reverting the fix and observing the exact reported flapping reappear before restoring it. 76 tests across 13 files now pass.
v4.5.0
- New, opt-in status notification ("Mostrar modo actual en notificaciones", off by default): shows which mode is active and what it's actually doing, e.g. "PowerSentinel — Noche / CPU en ahorro · apps en 2º plano ralentizadas · CPU al 50% · pantalla a 60Hz". Deliberately does NOT reintroduce the old noisy per-change notification behavior
alertbridge.shwas specifically rewritten to stop doing (its own header comment already says so) - posted with a fixed tag so every update REPLACES the previous notification in place rather than stacking a new one, only re-posts when the text actually changed, and is entirely opt-in so anyone who doesn't ask for it sees no difference at all. Built fromactive_mechanisms_snapshot()- the exact same resolved data the WebUI's own "active now" card already uses - so the notification can never say something different from what the app itself shows. - Real, honest limitation surfaced rather than glossed over:
cmd notification post(confirmed against its own AOSP source) has no flag for importance, silent, or ongoing/non-dismissible - the first time this notification appears it will likely make the normal notification sound. The setting's own help text tells the person to long-press it once and set it to "Silent" from Android's own settings; every update after that stays quiet. No amount of additional code changes what the shell command itself can't do. - Found and fixed a real gap while building this:
max_cpu_freq/max_refresh_rate(v4.1.0's CPU speed and refresh rate limits) were never added toactive_mechanisms_snapshot()or the WebUI's ownmechanismRows()- meaning Inicio's "active now" card has silently never shown either mechanism even when genuinely active, since the day they shipped. Fixed in both places. - Also corrected the existing
notifysetting's help text, which still described the old "a notification for every change" behavior this project deliberately moved away from, not what it actually does today (critical alerts only). - New persisted test (
test_notify_active_mode.sh) covering the text builder - the empty case, a single event with several mechanisms, and two simultaneous events combining into one notification without duplicating phrases. 68 tests across 12 files now pass.
v4.4.0
- CRITICAL FIX: "Arranque" (boot) appeared to flicker in and out of Actividad continuously - reported directly, along with asking for a way to actually know which mode is active. Root cause isn't in the boot-event logic itself: "boot" only ever activates once, in
init_v2()at daemon startup, andstate_reconcile()correctly undoes whatever the previous run left active before that - individually, both are exactly right. But if the daemon itself is dying and getting relaunched repeatedly (a crash, an OOM kill -service.sh's own 60-second watchdog catches this and relaunches it every time it happens), every relaunch produces this exact "boot ended" (state_reconcile undoing the crashed run's stale boot) immediately followed by a fresh "boot started" - daemon-internal bookkeeping noise from an invisible restart, not a meaningful mode change. Filters out any "boot ended" immediately followed by a "boot started" within 15 real seconds from every view built from journal entries (Actividad, Inicio's recent-activity list, and today's intervention count, which was also being inflated by the same noise) - confirmed with the exact restart case, an isolated genuine boot, and a same-pair-but-too-slow-to-be-a-restart case, all handled correctly. - New diagnostics check: counts "boot started" journal entries in the last 2 hours - 1 is completely normal, several means the daemon is actually crash-looping and the watchdog is masking it by relaunching every ~60s. Gives a direct, unambiguous answer to "is something actually wrong" instead of inferring it from timeline noise.
v4.3.0
handle_proc's background monitor has likely never actually worked, for any user, since it was written - found while re-auditing the whole module for hang/resource-degradation risks. Its while-loop's own continuation check compared the real, on-disk state file (an object keyed by event name,{"night": <timestamp>, ...}) using a filter that iterates values, not keys - so the check was false unconditionally, every single time, for every real state file this daemon has ever produced. The loop exited on its very first condition check, meaning this feature has silently done nothing since it shipped. Fixed to check the key directly.- Fixing that exposed a second, real problem it had been masking:
reassert_active_events()callsenable_pwr_save()(and thereforeaction_proc_apply()) for every still-active event whenever ANY event ends. With no de-duplication, once the monitor loop actually persists (as the fix above makes it do), every such reassert would have spawned another redundant background loop for the same event - an unbounded, ever-accumulating resource leak the longer a device stays up with multiple simultaneoushandle_procevents, spending real CPU on a mechanism meant to save it. Fixed by tracking the PID of the currently-running monitor per event and refusing to start a second one while it's still alive. Confirmed with real, live background processes: a monitor started once stays alive and does NOT duplicate across repeated calls while the event stays active, correctly exits on its own once the event ends, and a genuine reactivation afterward starts a fresh one. - Added a small defense-in-depth protection: a user's denylist (the one path that can name a system package at all - the normal scan is third-party apps only) could previously name something like
com.android.systemuiwith nothing stopping it from being force-stopped. Not a permanent hang (Android restarts SystemUI on its own), but a visible full-UI crash/flicker for something that should never have been a valid target - now explicitly refused, same as dialer/SMS/emergency/doze-whitelisted apps already were. - Reviewed every other mechanism that touches the system (apps, GMS, WiFi, Doze, low-RAM, cores, CPU frequency) for anything of comparable severity and found nothing else at this level - all already ...