Skip to content

feat(appearance): configurable display dimming - #334

Merged
jherforth merged 6 commits into
jherforth:mainfrom
adamecker:feature/display-dimming-settings
Oct 25, 2026
Merged

jherforth merged 6 commits into
jherforth:mainfrom
adamecker:feature/display-dimming-settings

Conversation

@adamecker

@adamecker adamecker commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Wall kiosk displays often cannot go dark enough: many panels' backlight floor is still too bright at night, and letting the device sleep blanks a dashboard the household still wants glanceable. This adds dimming as a first-class appearance setting — a CSS brightness filter over the rendered pixels, so the screen can go darker than the hardware allows while the image stays live (a dim-but-live image does not trip the panel's no-signal power save).

It is opt-in and off by default, and it lives on the existing appearance cascade: a household default plus a per-display override, like theme and auto-dark. So a wall display can dim while phones and other displays stay untouched.

Settings

New dimming object on the appearance settings, edited in Admin → Look, in its own Display dimming section between Appearance and Themes, with its own scope toggle, Use household switch, and Save button:

  • enabled (default false), day (default 0.7), night (default 0.03), after idle seconds (default 20), nightStart / nightEnd hours (default 0–6)
  • Values clamp on load (day/night 0–1, hours 0–23); invalid values fall back to defaults, never crash
  • Changes apply live with no reload; saving counts as activity, so freshly enabled dimming never fires instantly
  • Night start/end are chosen from a list of 24 hours labelled with their formatted time, and brightness headings carry the live percentage while the slider moves

Behavior: full brightness while the panel is in use, fading to the target after the idle delay, back to full brightness on the next interaction. While dimmed the page background is forced black and scrollbars are hidden (Chrome composites them outside the filtered layer, full-brightness), and a transparent shield catches the first tap over plugin iframes, whose pointer events never reach the document.

Testing

  • Client tests: 777/777 pass, build clean, translation check passed (es 1447/1447 keys).
  • Server: the inlined plugin-frame script now also reports pointer/key/wheel activity to the parent (throttled), so using a plugin holds the dim off; its tests pass 17/17.
  • Verified on a test instance: Look shows Appearance, Display dimming, and Themes as three sections in order, at desktop and 390 px widths; dimming does not fire while Admin is open and resumes after it closes; the night selects show formatted times; with Use household on, the card shows the household's values with controls disabled. Scope isolation (dimming save, then an Appearance save without reloading, leaves dimming intact) is covered by save-helper tests.
  • Not measured on hardware: frame rate on a low-end panel with an animated theme. The filter sits on and theme animations are transform/opacity-only (compositor work).
  • Per review, the kiosk URL parameters and the hgSetDim devtools hook were removed, as were the fork-only legacy keys and the repo screenshot.

Generated with Muse AI 🤖

adamecker added 2 commits October 25, 2026 17:14
Move the fork's kiosk dimming (CSS brightness filter, idle fade,
day/night targets) from URL params + localStorage into the appearance
cascade: a dimming object field with household default and per-display
override, normalized and clamped like autoDark. The old localStorage
keys seed a display override once via the legacy-migration pattern
(with their own marker, so a theme migrated long ago does not block
the dim seed). Kiosk URL params still override per-field for the
session, and saving in Admin applies live without a reload.

Generated with Muse AI 🤖
…tages live

Night start/end show their formatted time under the hour field, using
the app's standard time format (12-hour where that is the locale
convention, 24-hour elsewhere). Brightness headings now carry the live
percentage, so the value is visible while the slider moves, not only in
the drag bubble.
@adamecker
adamecker marked this pull request as ready for review October 25, 2026 18:33
@mrramam

mrramam commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Nice idea for panels whose backlight floor is too bright. One behavior issue and a few small things, from reading the code (not run):

  • Using a plugin doesn't count as activity. initDisplayDim listens on window, and the shield only exists while dimmed. Taps and scrolls inside a plugin frame never reach the parent, so someone voting in Polls or scrolling a plugin list gets dimmed after after seconds (20 by default) mid-use, and the next tap is swallowed by the shield. The SDK could post an activity ping to the parent on pointerdown/keydown/wheel, and poke could listen for it.
  • Legacy keys. hg-dim, hg-dim-night, hg-dim-after and LEGACY_DIMMING_MIGRATED_KEY seed from a build that only exists on your fork, so nobody upstream has them. Dropping them would simplify legacyLocalAppearance and the marker split in app.jsx.
  • Screenshot. screenshots/dimming-settings.png (188 KB) is in the diff only for the PR body. A drag-and-drop into the PR description keeps it out of the repo.
  • Night window clock. isNightAt uses the device's own hours. The rest of the client reads the household zone (getServerTimezoneSync). Probably fine for a wall panel, but worth matching.
  • Animated themes. A filter on html re-filters the whole page every frame under a moving background. Worth a frame-rate check on a low-end panel with an animated theme while dimmed.

@jherforth jherforth left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks, dimming below the backlight floor is a feature wall panels need, and keeping it in the appearance cascade (household default plus per-display override) is the right home for the data. The settings UI needs rework before this merges, and mrramam's review is still open.

Required: Display dimming gets its own section in Look

I rendered Admin → Look from this branch. "Display dimming" is a subtitle2 heading inside the Appearance card, directly under "Personalize". It's the same style as "Background" and "Card opacity", so it reads as a third Personalize item. It isn't aligned with the sections it should sit beside ("Appearance", "Themes"), and it shares the Appearance card's Save buttons.

1. Make it its own card. Move the dimming controls into a new DisplayDimmingSettings component, rendered as its own AdminFormSection in the Look tab, between Appearance and Themes:

{location.tab === 'look' && (
  <Card>
    <CardContent>
      <AppearanceSettings />
      <DisplayDimmingSettings />
      <ThemeLibrary />
    </CardContent>
  </Card>
)}

with

<AdminFormSection title={t('admin:appearance.dimming.heading')} subtitle={t('admin:appearance.dimming.help')}>
  …
</AdminFormSection>

That gives it the same h6 title and bordered box as "Appearance" and "Themes", aligned with them.

2. Give the card its own scope and Save.

  • Scope: its own "This display / Household" toggle, the "Use household" switch, and its own Save button.
  • Data: no change. dimming stays a field of the appearance setting, so the cascade and makeHouseholdDefault keep working.

3. Each card writes only its own fields. Today save() writes the whole household appearance object from the Appearance card's draft. With two cards, that would let one card overwrite the other's saved changes. For example, Appearance loaded at mount and saved later would put back the old dimming. So:

  • The dimming card writes only dimming, over what's stored now:
    • household: read the current appearance setting, replace dimming, write it back;
    • display: the current device overrides, plus dimming or without it when "Use household" is on.
  • The Appearance card stops writing dimming:
    • household: keep the stored dimming;
    • display: leave dimming out of the overrides it sends.
  • A shared helper that both use avoids getting this wrong twice, e.g. saveAppearanceFields(scope, partial), which reads the stored object and merges partial into it.

4. Match the label styles to the rest of Look.

  • The slider headings ("Daytime brightness — 70%", "Nighttime brightness — 3%") are bold body2. Use subtitle2, like "Card opacity", with the percentage after it.
  • Group the three number fields under a subtitle2 "Night hours" heading, with the night-hours help text under them, as now.

5. Night start and end are hours, so offer hours. Two number fields with the time as helper text ("0" with "12:00 AM" under it) make people translate in their head, and they accept values the normalizer then clamps. A Select of the 24 hours, labelled with formatHourOfDay(h), shows the time directly and can't be out of range. "Idle seconds before dimming" can stay a number field.

6. Don't dim while the Admin Panel is open. With the default 20 seconds, someone reading the dimming settings at night gets a 3%-bright Admin Panel mid-read, and their next tap is swallowed by the shield. Pause the idle timer while Admin is open.

Still open from mrramam's review

There are no commits since it, so all five points stand:

  • activity inside plugin frames (the SDK pinging the parent);
  • the fork-only legacy keys;
  • the screenshot in the repo;
  • the night window following the device's clock instead of the household zone;
  • the frame-rate check with an animated theme.

The legacy-keys point also applies to two more pieces that exist only for your fork, so please drop them too:

  • The kiosk URL parameters (?dim, ?dimNight, ?dimAfter, ?nightStart, ?nightEnd). The comment says they keep "existing kiosk launch URLs" working, but no upstream install has them. They also turn dimming on even when the setting is off, which the settings UI can't show.
  • window.hgSetDim: a global devtools hook.

Without them, resolveDimmingConfig reduces to normalizeDimming(settings).

Checks before re-review

  • Layout: Admin → Look shows three sections, Appearance, Display dimming and Themes, each with the same h6 title and bordered box, aligned on desktop and at phone width (390 px).
  • Scope: the dimming card's Save writes only dimming, at either scope. Save dimming, then save Appearance without reloading the panel, and the dimming setting survives. The same goes the other way round.
  • Use household: on a display with "Use household" on, the card shows the household's values, disabled.
  • Night hours: they're chosen from a list of times.
  • Admin open: the display doesn't dim while the Admin Panel is open, and does after it closes.
  • mrramam's points are addressed.
  • Client tests, check:i18n and the build pass.

…usehold timezone

Address jherforth's + mrramam's review on the dimming PR:

- Own card: dimming moves out of AppearanceSettings into
  DisplayDimmingSettings, rendered in the Look tab between Appearance
  and Themes as its own AdminFormSection with its own scope toggle,
  Use-household switch and Save button.
- Scope isolation: both cards save through the new appearanceSave
  helper, which re-reads the stored appearance and merges only its
  own fields (device saves delete fields switched to Use household
  instead of omitting them). Saving dimming, then Appearance without
  reloading, leaves dimming intact, and vice versa; Make-household-
  default preserves every display's dimming override.
- Labels: slider headings are subtitle2 with the live percentage
  after the label; the three number fields sit under a subtitle2
  Night hours heading (new en+es string).
- Night start/end are Selects of the 24 hours, labelled with
  formatHourOfDay; idle seconds stays a number field.
- Dimming pauses while the Admin Panel is open and resumes after a
  full idle delay on close.
- Dropped fork-only pieces: kiosk URL parameters (?dim, ?dimNight,
  ?dimAfter, ?nightStart, ?nightEnd), the window.hgSetDim hook, and
  the hg-dim/hg-dim-night/hg-dim-after legacy seeding with its
  dimmingMigrated marker (theme migration untouched).
- Plugin frames now report pointer/key/wheel use as
  homeglow:activity (always on, 1/sec throttle) and displayDim
  counts it as activity, so using a plugin holds the dim off.
- The night window is read in the household timezone
  (getServerTimezoneSync) via Intl, with a local-hours fallback for
  unknown zones.
- The PR-body screenshot is deleted from the repo.

No low-end-panel frame-rate measurement: the dimming filter sits
on <html> and theme animations are transform/opacity-only, so it
is addressed by mechanism, not measured on hardware.

Generated with Muse AI 🤖
@adamecker

Copy link
Copy Markdown
Contributor Author

Done in 83d14b7 — all of it, including mrramam's five points. The PR body is updated too (screenshot section removed, testing section refreshed).

Your six items

  1. Own card. The dimming controls moved out of AppearanceSettings into DisplayDimmingSettings, rendered as its own AdminFormSection in the Look tab between Appearance and Themes, with the dimming heading and help as its title/subtitle. Verified on a test instance: Look shows Appearance, Display dimming, and Themes as three sections in that order, at desktop and 390 px widths.
  2. Own scope and Save. The card has its own This display / Household toggle, its own Use household switch, and its own Save button.
  3. Each card writes only its own fields. New shared helper saveAppearanceFields(scope, partial) in client/src/utils/appearanceSave.js: it re-reads the stored appearance first and merges only its own fields (a device-scope field switched to Use household is dropped from the overrides instead of sent — this matters because the device PATCH replaces the whole appearance object). The Appearance card no longer writes dimming at all, and Make household default preserves the household's dimming and every display's dimming override. Covered by 5 new helper tests; the double-save check itself is covered there rather than by a UI-driven double-save.
  4. Label styles. Slider headings are now subtitle2 with the live percentage after the label, and the three fields sit under a subtitle2 Night hours heading with the help text beneath.
  5. Hours as a list. Night start/end are Selects of the 24 hours labelled with formatHourOfDay; idle seconds stays a number field. Verified live: the selects display 10:00 PM / 6:00 AM for a 22–6 window. One honesty note: I could not open the dropdown from synthetic test events, so the 24-option count comes from the source (24 MenuItems), while the formatted display values were verified in the browser.
  6. No dimming while Admin is open. setDimmingPaused is wired to the Admin Panel's open state: pausing undims immediately, and closing restarts the idle clock from zero. Verified live on the test instance: with a 2-second idle delay the display stayed undimmed while Admin was open, and dimmed after closing it.

Dropped fork-only pieces

  • Kiosk URL parameters (?dim, ?dimNight, ?dimAfter, ?nightStart, ?nightEnd) are gone; resolveDimmingConfig is deleted and the config is now just normalizeDimming(settings).
  • window.hgSetDim is gone.
  • The hg-dim / hg-dim-night / hg-dim-after legacy seeding and the migration marker split are gone (theme migration untouched). Grep confirms none of these remain in client or server code.

mrramam's five

  • Plugin-frame activity: the inlined plugin script (server/utils/pluginFrameTouch.js, the channel every plugin page gets — the separate SDK package was deliberately left alone) now posts a throttled homeglow:activity message on pointerdown/keydown/wheel, and displayDim counts it as activity. Tests 17/17; a live plugin-frame check was not run.
  • Screenshot: deleted from the repo, and the PR body's screenshot section is removed with it.
  • Night window clock: the window is now read in the household timezone via getServerTimezoneSync (Intl, with a local-hours fallback for unknown zones), with tests for the zone conversion.
  • Animated themes: no code change, as you framed it as a check. Addressed by mechanism, not measured on hardware: the filter sits on <html> and theme animations are transform/opacity-only (compositor work). No low-end-panel benchmark is claimed.

Checks

  • Client vitest 777/777, build clean, translation check passed (es 1447/1447).
  • Server: the touched file's suite passes 17/17. The full server suite was not run to completion (it is slow); nothing else server-side changed.
  • Use household: verified live — with it on, the card shows the household's values with controls disabled.

jherforth and others added 2 commits October 25, 2026 15:43
# Conflicts:
#	client/src/app.jsx
- "Idle seconds before dimming" moves up beside "Dim the display when
  idle": it's when dimming starts, not part of the night hours. Under
  "Night hours" are just its start and end.
- The "Night hours" heading gets space below it; the fields' floating
  labels overlapped it by 9 px.
- Idle seconds is saved as a number (editWholeNumber), not the field's
  text.
- homeglow:activity counts only from a frame on this page, like every
  other plugin message since jherforth#320.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@jherforth

Copy link
Copy Markdown
Owner

Thanks, this addressed everything from both reviews. In Edge, the card sits aligned between Appearance and Themes, and the two cards' saves leave each other alone in both directions. Night hours are a list of times, and dimming waits while Admin is open. I added two commits before merging:

Client tests, the Edge checks and CI pass.

@jherforth
jherforth merged commit 7f6fa12 into jherforth:main Oct 25, 2026
3 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in HomeGlow Kanban Oct 25, 2026
adamecker pushed a commit to adamecker/HomeGlow that referenced this pull request Oct 26, 2026
… revision counter jherforth#339, dimming jherforth#334, Save layout jherforth#336, chore perf jherforth#324, admin jherforth#330/jherforth#331/jherforth#332, PIN hardening jherforth#329

Conflicts resolved taking upstream's rewrite, preserving fork(prod) features:
- displayDim.js: upstream jherforth#334 base plus fork-only kiosk URL dim
  overrides and LCD pixel shift (both absent upstream)
- ChoreWidget/app.jsx/ChoreSchedulesTab: upstream jherforth#324/jherforth#336 rework
@jherforth jherforth added the enhancement New feature or request label Oct 27, 2026
@jherforth jherforth added this to the 1.11 milestone Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants