Skip to content

feat(macos): show 'Clock disabled on this server' when EMBER_CLOCK=off - #305

Merged
tarakanof merged 3 commits into
mainfrom
feat/278-clock-disabled
Oct 6, 2026
Merged

tarakanof merged 3 commits into
mainfrom
feat/278-clock-disabled

Conversation

@tarakanof

@tarakanof tarakanof commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Closes #278

Decodes disabled (omitted when false) in ClockHealth; with EMBER_CLOCK=off the server sends "disabled":true,"device":null.

  • Settings > Clock > Status: shows "Clock disabled on this server" instead of "The server can't reach the clock" + Find Clock.
  • Hardware > Clock: shows the same title instead of "No clock configured".
  • ClockDiscovery.serverLostClock returns false when disabled.
  • New string added to Localizable.xcstrings (EmberKit entry, like the others).

Evidence

  • New tests: clockHealthDecodesDisabled, serverLostClockIsFalseWhenTheServerDisabledTheClock, disabledClockTitleSaysTheServerDisabledIt; written first, failed to compile (no isDisabled), now green.
  • swift test --package-path macos: 891 tests passed.
  • xcodegen generate + unsigned Debug build OK (via scripts/strings.sh); scripts/strings.sh check clean after sync.
  • Live GET /v1/clock/health (read-only): clock enabled, so disabled is absent and device populated; omit-when-false decodes to not disabled. The disabled shape comes from clock_health_http.go (json:"disabled,omitempty").
  • Screenshots (Hardware pane, disabled; scratch bundle id, EMBER_HARDWARE_SNAPSHOTS): light, dark. Icon: clock.badge.xmark on all surfaces.

/v1/clock/health carries "disabled": true (omitted otherwise) with a null
device. The Settings Status section read that as a lost clock and offered
discovery; the Hardware pane showed "No clock configured". Decode the flag in
ClockHealth, stop serverLostClock from firing for it, and show the disabled
title in both places.

Closes #278
@tarakanof

Copy link
Copy Markdown
Owner Author

Independent review (WORKFLOW §4)

Reviewed f01484d against #278 and cmd/ember/clock_health_http.go (Disabled bool json:"disabled,omitempty"; probeClockHealthWithin returns nil when clockDisabled(), so disabled always comes with "device":null). Ran swift test --filter 'ClockDiscoveryTests|DashboardGoldenTests|StringCatalog' on the PR head: 48 passed. No screenshot taken.

Decode looks right: absent → nil → isDisabled == false, true → true, device: null decodes. The Settings Status and Hardware branches come before the lost/unconfigured branches, so the enabled, lost and unconfigured states still behave as before. The string goes through the EmberKit LocalizedStringResource + manual catalog entry route, the same one the other kit strings use. No code comments added.

No blockers.

should-fix

  1. No server→app contract golden for the disabled shape. cmd/ember/dashboard_http_test.go:214, macos/Tests/EmberKitTests/DashboardGoldenTests.swift:109. clock_health and clock_health_unreachable are pinned by goldens that the Go test writes and the Swift test decodes. The disabled case is only covered by a hand-written fragment in ClockDiscoveryTests.swift:152. Failure case: someone renames the key to clock_disabled, or moves it under publish. Go tests stay green and the Swift test still decodes its own literal, but the app silently goes back to "The server can't reach the clock" + Find Clock. Fix: t.Setenv("EMBER_CLOCK","off") → assertGolden(t, "clock_health_disabled", …), plus clockHealthDecodesDisabledGolden asserting isDisabled && device == nil.

  2. Status section still offers clock actions under "Clock disabled on this server". macos/Ember/Settings/Clock/ClockStatusSection.swift:83-89. The disabled label shows, but "Restart Clock…" (enabled once device settings load) goes to clockAccess.client → errClockDisabled. The user confirms, then gets the red "Couldn't restart the clock: …". "Discover Clocks…" hits handleDeviceDiscover, which returns 503 clock_disabled, while the Mac-side scan still offers "Use" for a clock the server will never drive. The footer copy ("Ember finds the clock on its own…") also contradicts the label. Fix: disable Restart/Discover when health?.isDisabled == true, or file a follow-up issue.

  3. Dashboard still says "Clock unreachable" for a disabled clock. macos/Ember/Dashboard/Cards/ClockCard.swift:28. With EMBER_CLOCK=off, /v1/screen has nothing, so the card shows "Clock unreachable" (wifi.slash) while Settings says "disabled". That's the same misreport app: show 'clock disabled' when the server runs with EMBER_CLOCK=off #278 fixes, on another surface. The dashboard already holds .clockHealth (DashboardWindow.swift:12), so the card could take isDisabled and show disabledTitle. If it's out of scope, open a follow-up issue.

nit

  1. Menu bar Clock menu ignores disabled. macos/Sources/EmberKit/Presentation/MenuRows.swift:299-305. With disabled, clockHealth.value != nil and matrixPower == nil, so the menu shows both "Turn Display Off" and "Turn Display On", next to Next/Previous/Dismiss. Every one of them fails with clock disabled. displayPower could return [] when clockHealth.value?.isDisabled == true (easy to unit-test in EmberKit). Follow-up is fine.

  2. Tautological test. ClockDiscoveryTests.swift:157-159 (disabledTitleSaysTheServerDisabledIt) asserts that a constant equals its own literal. It can't catch a wrong UI branch, and it only fails if someone edits the copy. The catalog presence is already enforced by StringCatalogTests. Drop it, or replace it with the golden from item 1.

  3. No hardware snapshot scenario for disabled. macos/Ember/Hardware/Preview/HardwareFixtures.swift:50. Add ("clock-disabled-light", …) with a ClockStats(configured: false…) and a health decoded from {"disabled":true,"device":null,…}. EMBER_HARDWARE_SNAPSHOTS would then render the new state offline, which gives the screenshot evidence the PR skipped without needing a scratch server.

  4. Public API surface. macos/Sources/EmberKit/Models/ClockHealth.swift:66. Both public var disabled: Bool? and isDisabled are exposed, so callers can pick either, and disabled == false vs nil is a distinction the server never makes. Consider making disabled private (or internal) and keeping only isDisabled.

Review of #305: pin the server shape with a clock_health_disabled golden,
disable Restart and Discover and swap the footer in the Status section, show
the disabled notice on the dashboard Clock card instead of "Clock unreachable",
hide the menu-bar display on/off rows, add a disabled hardware snapshot
scenario, and keep only isDisabled public. The notice comes from one
ClockHealthReadout.disabledNotice so every surface picks it the same way.
tarakanof added a commit that referenced this pull request Oct 6, 2026
@tarakanof

Copy link
Copy Markdown
Owner Author

Fixed in 1cd7e74:

  1. Fixed: clock_health_disabled golden (Go, EMBER_CLOCK=off) plus clockHealthDecodesDisabledGolden.
  2. Fixed: Restart/Discover disabled when disabled; footer explains EMBER_CLOCK=off.
  3. Fixed: dashboard Clock card shows the disabled notice (controls disabled) instead of "Clock unreachable".
  4. Fixed: MenuRows.displayPower returns [] when disabled; test displayPowerHiddenWhenTheClockIsDisabled.
  5. Fixed: tautological test replaced by disabledNoticeIsShownOnlyForADisabledClock on ClockHealthReadout.disabledNotice, which all surfaces use.
  6. Fixed: clock-disabled-light/dark scenarios in HardwareFixtures; rendered from a scratch bundle id build.
  7. Fixed: disabled is private; isDisabled is the only public API.

Hardware pane, disabled (light): https://raw.githubusercontent.com/tarakanof/Ember/b6aabe9/pr-305/clock-disabled-light.png
Dark: https://raw.githubusercontent.com/tarakanof/Ember/b6aabe9/pr-305/clock-disabled-dark.png

gofmt, go vet, go test ./... -race, swift test (893), unsigned build and strings.sh check pass.

The bare poweroff circle read as a stalled spinner.
tarakanof added a commit that referenced this pull request Oct 6, 2026
@tarakanof

Copy link
Copy Markdown
Owner Author

Re-review (1cd7e74, b5e5b87)

Ran these on origin/feat/278-clock-disabled (b5e5b87) in a temporary worktree: go test ./... all ok, swift test --package-path macos 893 passed.

Mutation check: I removed the isDisabled guard in MenuRows.displayPower and renamed the golden key to clock_disabled. That failed displayPowerHiddenWhenTheClockIsDisabled, clockHealthDecodesDisabledGolden and Go TestDashboardGolden, so the new tests do pin the fixes.

# Finding Status
1 Disabled contract golden Verified. The Go side writes clock_health_disabled.json with t.Setenv("EMBER_CLOCK","off") (the test isn't parallel, so Setenv is safe). The file has "disabled":true,"device":null, and the Swift side decodes it. Both sides fail on a key rename.
2 Status actions under disabled Verified. Discover and Restart are .disabled(disabledNotice != nil), the "Find Clock" row can't appear (else-branch), and the footer now explains EMBER_CLOCK=off. "Open Web UI" stays enabled, which is right: it talks to the clock directly. Restart's existing conditions are unchanged when not disabled. There's no view test (the repo has no view-test harness). The shared disabledNotice is unit-tested.
3 Dashboard "Clock unreachable" Verified. ClockCard shows the disabled notice ahead of the unreachable overlay and disables its controls. When the clock isn't disabled, disabledNotice is nil, so the card behaves as before.
4 Menu bar Clock menu Partly fixed (nit, follow-up OK). displayPower returns [] when health is loaded and disabled, and the test covers that. But .clockHealth is tier C, not in Feed.alwaysOn, and the menu bar doesn't track it. With only the menu bar open (Settings/Dashboard never opened this run), clockHealth stays .loading, usage is loaded, and the menu still lists both "Turn Display Off" and "Turn Display On" (MenuRows.swift:299-306, MenuBarContentView.swift:101-102). Next/Previous/Dismiss and "Show on Clock" also stay active and fail with clock disabled. A follow-up issue is enough.
5 Tautological test Verified. It's replaced by disabledNoticeIsShownOnlyForADisabledClock (disabled → notice, lost → nil, loading → nil), which tests the function that all three surfaces now share.
6 Hardware snapshot scenario Verified. clock-disabled-light/dark are in HardwareFixtures, and the light and dark renders are linked in the reply.
7 Public API Verified. disabled is private and isDisabled is the only public accessor.

New regressions: none found. Enabled, lost and unconfigured states go through the same branches as before. clock.badge.xmark is fine for the macOS 26 target. One optional nit: ClockHardwarePage.swift:63 wraps input.health in a synthetic .loaded(…, at: now) only to call disabledNotice. Using input.health?.isDisabled directly, or adding a ClockHealth? overload, would read more simply.

Verdict: no blockers, ready to merge once CI is green. Item 4's remaining menu-only case can go in a follow-up issue.

@tarakanof
tarakanof merged commit bbc6897 into main Oct 6, 2026
6 checks passed
@tarakanof
tarakanof deleted the feat/278-clock-disabled branch October 6, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

app: show 'clock disabled' when the server runs with EMBER_CLOCK=off

1 participant