Conversation
Drops the verbose Z2M schema description text under each option row title in device state/action cards. Long descriptions (LED intensity / colour parameters on Inovelli switches, etc.) crowded the list and made it hard to scan dozens of similar rows. Fixes #13 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Z2M's schedule + unschedule OTA topics, AppStore optimistic state helpers, and ViewModel methods. Context menu now shows: - Check for Update (always, when device supports OTA) - Update Now (mains-powered, update available) - Schedule Update (battery-powered, update available) - Cancel Scheduled Update (when phase is .scheduled) The schedule path is the right primitive for sleepy battery devices — Z2M waits for the device to wake up and request the image rather than trying to push immediately. Fixes #15 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Behind a Developer Mode toggle in Settings → General, add a Developer section with an MQTT Inspector. Subscribe tab streams every inbound topic + payload (substring filter, pause, clear, 1000-message ring). Publish tab sends arbitrary topic + JSON or string payload, with a confirm prompt for bridge/request/* destinations. Z2MMessageRouter exposes decodeRaw() so the session controller can tap inbound messages for the inspector without breaking the typed routing. Fixes #21 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings → Tools → Backup. Sends bridge/request/backup, decodes the returned base64 zip from bridge/response/backup, writes it to a temp file, and presents iOS share sheet for save / AirDrop / iCloud Drive. Persists a metadata-only history list (timestamp + size, last 20 backups) — Shellbee does not retain backup files. Restore is intentionally out of scope: Z2M does not expose a restore API, restoration requires host access to the data directory. Links to the Z2M restore guide. Fixes #19 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#15 revision: Both 'Update Now' and 'Schedule Update' are now exposed on every OTA-supported device (not just battery). Power source informs ordering — battery devices list Schedule first as the recommended path, mains devices list Update Now first — but the user can pick either on any device. #12 implementation: - Device detail (...) menu: same Schedule / Update Now / Cancel Scheduled actions, ordered by power source. - 'Check All for Updates' bulk action: mains devices route to the rate-limited check queue; battery devices fire schedule requests directly (no rate-limit needed — Z2M defers them until each device wakes up). - Device row badge: shows 'Scheduled' instead of generic 'Preparing' when phase is .scheduled. Fixes #12 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hare preview
The 'Restore guide' link pointed at an outdated zigbee2mqtt.io URL that
404s. Replaced with an in-app sheet (RestoreGuideSheet) that explains
why Shellbee can't restore (no MQTT API for it, requires host access)
and walks through the steps to do it from the Z2M host. No external
links for the core content.
Backup files now write to Documents/Backups/ instead of temporaryDirectory.
The temp directory's sandboxed URLs caused the iOS share sheet to fall
back to a minimal set of receivers ("Save to Files" only — no AirDrop,
Mail, Messages, third-party apps). Documents/Backups/ is durable and
exposes the full receiver list. ShareLink also gets an explicit
SharePreview so the receiver shows a proper file label and zip glyph.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… CTA Three fixes: - State loss on tab switch. Subscribe/Publish were sibling subviews inside a switch — toggling destroyed the active view and lost the message buffer. Lifted into an @observable SubscribeStore owned by MQTTInspectorView so the buffer persists for the inspector lifetime. - Native chrome. Segmented control moved into the navigation toolbar (principal placement). Filter is now a .searchable bar. Pause / Clear moved to topBarTrailing icon buttons. List uses .plain style with a ContentUnavailableView for the empty state. Each message row gets a payload card with a tertiary fill background and a Show more / less toggle when content exceeds six lines. - Publish button. Was a plain Form row; now a borderedProminent large-control CTA pinned below the form on a .bar background, full width with a paperplane glyph. Topic field has submitLabel(.next) and hands focus to the payload editor on return. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes for the inspector chrome: - JSON syntax highlighting on subscribe payloads. Keys blue, string values green, numbers orange, booleans/null purple, structure muted secondary. Topic row also picks up the bridge/logging level color + glyph (red/yellow/blue/gray) when the message is a log entry, matching the raw logs view treatment. - Stable picker position. Switched from .frame(maxWidth:) to a fixed 220pt width on the segmented control, and consolidated trailing toolbar items to one per tab. Subscribe gets a single Menu (Pause + Clear inside); Publish gets a single Reset Form button. Picker stays in the same spot regardless of tab. - Unified Publish layout. Dropped the separate .bar bottom strip — the Publish button is now a borderedProminent CTA inside the Form's last Section with a clear row background, so the page reads as one continuous form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- OTA: "Disable Automatic Checks" → "Enable Automatic Checks" (positive
toggle, inverted binding; underlying disable_automatic_update_check
flag still written negated).
- OTA Transfer Timing: shorten labels so they no longer truncate
("Transfer Request Timeout" → "Request Timeout", "Delay Between
Blocks" → "Block Delay").
- MQTT: "Disable Message Retain" → "Retain Messages" (same negated-flag
pattern, inverted binding); footer copy updated.
- MQTT: "Max Packet Size (bytes)" now uses InlineIntField so the unit
renders alongside the value, matching every other numeric row.
- Add UI tests for each label change.
Fixes #28
Drops verb prefixes, removes redundant unit/qualifier words, fixes
sentence-case stragglers, flips one more negated toggle:
- AppGeneralView: "Recent Events on Home" → "Recent Events"
(section header already says Home); "Reconnect Attempts" →
"Reconnect Limit" (unit "attempts" was repeated);
"Automatically share crash reports" → Title Case.
- AppPerformanceView: "Concurrent Requests" → "Concurrency" (same
unit-repeat).
- AvailabilitySettingsView: both "Offline Timeout" rows → "Timeout"
(sections disambiguate); "Pause After Retries" → "Pause After".
- HealthSettingsView: section "Health Check Interval" → "Interval"
(row inside is "Check Interval").
- HomeAssistantSettingsView: drop "Use" prefix on toggles
("Use Legacy Action Sensor" → "Legacy Action Sensor", same for
Event Entities).
- NetworkSettingsView: section "Hardware Tuning" → "Adapter Tuning".
- SerialSettingsView: "Disable Adapter LED" → "Adapter LED" with
inverted binding (default ON, user disables); section "Adapter"
(containing Adapter Type / Baud Rate / RTS-CTS) → "Connection".
Adds UI tests for each rename.
Fixes #28
…e Performance App → General was doing three unrelated jobs under a misleading "Connection" header (Live Activity toggles + scheduled-OTA opt-in + reconnect retry limit) with a four-line footer trying to cover them all. Splits it apart into focused pages. - New AppLiveActivitiesView (linked from Application section between General and Notifications): three toggles "Connection", "OTA Updates", "Scheduled OTAs" across two sections, each with a one-line footer instead of the previous wall of text. - AppGeneralView slimmed: Appearance / Home / Connection (Reconnect Limit only) / Diagnostics / Advanced (Developer Mode now has a section header instead of floating). - AppPerformanceView renamed to "Bulk OTA" — the page only ever contained that one feature; the broad "Performance" title overpromised. Removed the redundant section header. Settings root link label updated to match. - Added UI tests covering the new navigation and the absence of the legacy labels. Fixes #29
… of OTA Updates Two sections made the three toggles look like two unrelated features. Collapses to one section with a combined footer; the disabled state on Scheduled OTAs (when OTA Updates is off) already communicates the parent/child relationship. Refs #29
Reorders the About page so the section the user actually came for — which version am I running, where are the device stats / credits — is the first thing they see. Bridge / Zigbee Network details follow. - New top "Shellbee" section: Version, Build, Device Statistics link, Acknowledgements link. Drops the previous unnamed footer-section that hosted the two nav links. - Acknowledgements: add Sentry Cocoa SDK (MIT). It's the only third-party SDK shipped with the app (per PRIVACY.md), and it belongs alongside the Z2M / zigbee2mqtt.io entries. Refs #29
About → new "Connect" section between Shellbee and Bridge, with two iOS-native rows: "Rate Shellbee" (star, pink) and "View on GitHub" (code chevron, label colour). Each uses the Settings-style coloured icon tile + trailing arrow.up.right indicator. App Store URL is a TBD placeholder until the App ID is assigned at first TestFlight. Footer copy fixes that became stale after recent label flips: - SerialSettingsView: footer was "Turns off the indicator LED…" but the toggle is now "Adapter LED" (default ON, user disables). Rewritten as "Controls the indicator LED on the Zigbee adapter, if supported." - AvailabilitySettingsView: said "Shellbee tracks…" but the bridge does the tracking; corrected to "the bridge tracks…". - OTASettingsView Transfer Timing: referenced the old "Delay" label; updated to match the renamed "Block Delay". Refs #29
Commit 478cb6b added AppLiveActivitiesView referencing ConnectionSessionController.otaScheduledLiveActivityEnabledKey but never landed the matching `static let` declaration, breaking Fast CI since. Adds the constant. Also restores a paste-corrupted extension declaration in InterviewActivityWidget that the same broken state would have masked behind the earlier compile failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related Z2M WebSocket robustness changes. Early auth rejection — Z2M completes the WS handshake first, then either streams the cached bridge state or closes the socket with 1008/policy-violation when the auth token is wrong. Without waiting for the first inbound frame we'd report "connected" and only fail on the next send. Now waits up to 5s for the first message, surfaces a clear "Server rejected the connection. Check the auth token." on 1008 closes, and replays the validated message into the stream so the session controller still sees it. Frame size — `bridge/response/backup` carries the full Z2M data folder as a base64 blob inside one JSON frame. The default 1 MB URLSessionWebSocketTask cap aborts on populated installs. Raises to 64 MB, enough headroom for typical mesh configs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the base64-decode + zip integrity check out of BackupView into a nonisolated BackupPayload enum so it's unit-testable without SwiftUI. Verifies the decoded file starts with the PK\\x03\\x04 zip magic before declaring success — base64 decoding is lenient enough that an HTML error page or truncated payload would silently produce a non-zip "backup". A failed verification deletes the bogus file and surfaces the failure to the user. UI: consolidates Create + Share into one section, shows the backup size next to "Share Backup" instead of in a status row, and uses a LabeledContent layout for history entries. Restore Guide row gets a chevron affordance and the share sheet routes through UIActivityViewController so the user can save to Files / iCloud / AirDrop without ShareLink's preview quirks on iPad. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Z2M's "scheduled" OTA phase (parked, waiting for the device to wake) was effectively invisible across the app — devices fell out of the Updates filter the moment they were scheduled, the badge ring went indeterminate, and Home alerts didn't differentiate scheduled vs. in-progress. - DeviceCondition.updatesAvailable now also matches scheduled / requested / updating phases via an optional otaStatus argument, so scheduled devices stay in the filter. - DeviceListRow swipe actions: a scheduled row exposes "Cancel" (calls unschedule). Battery devices get Schedule before Update; mains devices keep Update before Schedule. - DeviceUpgradeBadgeView renders a static ring + clock.badge glyph for scheduled — no spinner, since the device is parked. - DeviceListViewModel + DeviceDetailView re-issue an OTA check after unschedule. Z2M leaves update.state at "idle" otherwise, which drops the device out of the Updates filter entirely. - DeviceFirmwareMenu: drops the battery/mains split for the bulk "Check All" button. Z2M only offers a synchronous OTA check; routing battery devices through schedule was a workaround. Now every device goes through the rate-limited bulk queue, and sleepy devices that don't respond surface the standard error like windfront. Empty-state label flips to "No Updates" with a checkmark glyph. - HomeSnapshot adds scheduledUpdateDevices + updatingDevices counts; HomeDevicesCard renders dedicated alert rows for each. - OTAUpdateLiveActivityCoordinator gains an isScheduledEnabled flag (UserDefault, off by default) — scheduled OTAs can sit pending for hours, and most users don't want a Lock Screen surface for that. AppLiveActivitiesView's "Scheduled OTAs" toggle drives it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switching from a working server to one with bad/missing auth left hasBeenConnected == true from the prior session, which routed the new failure into the .lost branch — leaving the user on a stale homepage instead of bouncing back to the setup screen. connect() now drops hasBeenConnected, resets the store, and clears isConnected before kicking off the session, so a failure cleanly surfaces as .failed on a fresh attempt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fan extras section was rendering each Expose with a custom row (leading icon, custom paddings) and presenting indexed groups via a .sheet. Aligns with iOS Settings conventions instead. - New SettingsFormRow renders one Expose as a plain Form row: label on the left, value or control on the right, no leading icon, no chevron. Writable numerics push a detail screen with a slider. - Indexed groups now drill in through a NavigationLink to a FeatureGroupDetailView instead of bouncing through a sheet — matches the rest of the device detail navigation and keeps the back-stack coherent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes the Rate / GitHub group a labelled section instead of an unlabelled one, and applies .buttonStyle(.plain) so the rows don't pick up the default tinted button styling. The GitHub row icon shifts to .darkGray so it reads consistently in dark mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap the row HStack in `.contentShape(Rectangle())` so taps on the empty space between the label and the chevron register, matching the hit-test behaviour of native iOS Settings rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hero cards stay exactly as they are; everything they don't bind to a primary control drops down as native iOS Settings `Section`s beneath, grouped via `FeatureLayout` into Behaviour / Indicators / Maintenance / Status / More sections — same taxonomy fans already use. Writable numerics render their slider inline within the same `List` row (label + value on top, slider beneath) instead of pushing a near-empty detail screen with just a slider. `NumericDetailView` is gone. `linkquality`, `battery`, `last_seen`, `update`, `update_available`, and any `identify*` prefix are always hidden — surfaced on the device card or noisy diagnostics, never in a settings list. Light: removes the sunrise (Startup) and ellipsis (More) sheet buttons from inside the card and renders them as native sections beneath. Effects (sparkles) stays in the card — true light-specific control. Startup section sorts Power-On Behavior first, then state / brightness / color-temp / hue+sat / execute_if_off. The Color Temperature row now uses the same swatch + tinted slider as the hero card. `DeviceDetailView` reorganised around a single `heroAndSettingsSections(for:state:)` builder. Shared primitives: `SettingsFormRow`, `DeviceExtras`, `DeviceFeatureSectionRow`, `FeatureGroupDetailView` extracted into `Shared/Components/` so every category dispatches identically. Fixes #31 Fixes #32 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the v1.3.0 milestone. Twelve issues:
scheduleandunscheduletopics, AppStore optimistic helpers, ViewModel methods, plus integration in three places: (1) device list long-press menu, (2) device detail(...)menu (same battery-aware ordering), (3) 'Check All for Updates' bulk action — mains devices route through the existing rate-limited check queue; battery devices fire schedule requests directly. Device row badge shows 'Scheduled' for the scheduled phase.bridge/request/*destinations.bridge/request/backup, decodes the returned base64 zip, writes to a temp file, presents iOS share sheet for save / AirDrop / iCloud Drive. Persists a metadata-only history list. Restore explicitly out of scope.URLSessionWebSocketTask.maximumMessageSizefrom the 1 MB default to 64 MB so large real-world backups don't exceed the WS frame limit and disconnect, (2) decode base64 with.ignoreUnknownCharactersto tolerate whitespace/newlines that some MQTT serializers insert, (3) post-write integrity check — verify the file is non-empty and starts with the ZIP magic bytes (50 4B 03 04), surface afailedstatus and delete the partial file otherwise.didOpenand navigated to the homepage; the close arrived afterwards and was treated as a normal disconnect. NowZ2MWebSocketSessionDelegaterecords any close that arrives after the open, andZ2MWebSocketClientwaits a 600ms settle window post-handshake to surface the rejection as a connection failure with a clear "Check the auth token." message. The user stays on the connection screen.disable_automatic_update_check,force_disable_retain,disable_led) remain Z2M-canonical; only UI bindings flip.InlineIntField"Max Packet Size" with unitbytes.AppLiveActivitiesView) under Application, owns the three LA toggles ("Connection", "OTA Updates", "Scheduled OTAs") split across two sections each with a one-line focused footer.countdown_hours, timers) used to render as a row that pushed a near-empty detail screen containing only a slider. They now render inline within the sameListrow — label + value on top, slider beneath — matching the iOS Settings idiom (Display & Brightness, Accessibility → Display & Text Size). The hero card and indexed-group disclosure rows are untouched;NumericDetailViewis removed.Liston Fan screen — subsumed by Native iOS Settings sections beneath every category card for leftover exposes #32. Fan's grouped sections (and every other category's) now render as nativeListSections with system headers, dividers, dynamic-type, and VoiceOver behaviour, instead of hand-rolledVStack+secondarySystemGroupedBackgroundcards. The customsectionViewpath onFanControlCardis retained only for snapshot contexts (LogDetailView) where there's no surroundingList.Sections beneath.linkqualityandidentify*are always hidden (already surfaced on the device card / noisy diagnostics).Shared/Components/SettingsFormRow.swift(Expose-driven row with inline slider for writable numerics) andShared/Components/DeviceExtras.swift(eligibility filter withalwaysHiddenlist).LightFeatureSections,SwitchFeatureSections,ClimateFeatureSections,CoverFeatureSections. Fan's existingFanFeatureSectionskeeps its richer layout (indexed groups + bespoke filter card).rendersAdvancedSheetsInlineflag (defaulttruefor snapshot contexts).DeviceDetailViewpassesfalse, dropping the sunrise (Startup) and ellipsis (More) buttons from inside the card and rendering them as native sections beneath. Effects (sparkles) stays in the card — it's a real light-specific control, not configuration.FeatureLayoutinto proper Behaviour / Indicators / Maintenance / Status / More sections — same taxonomy fan already uses, no flat "Configuration" dump.DeviceDetailViewreorganised around a singleheroAndSettingsSections(for:state:)builder that dispatches per category — replaces the prior fan-vs-everything-elseif/else.LightControlContext.startupFeaturesnow sorts so Power-On Behavior leads (Z2M's headline startup setting), followed bystate_startup→current_level_startup→color_temp_startup→ hue/saturation →execute_if_off. Rows get explicit display labels ("Power-On Behavior", "Startup State") instead of the prettifier's title-cased default.LightTemperatureControlswatch + tinted slider as the hero card, instead of a plain numeric slider — adjusting startup temperature reads identically to adjusting the live one.Shared/Components/FeatureGroupDetailView.swift) and section-row dispatcher (DeviceFeatureSectionRow.swift) extracted from fan into shared components so every category renders identical "N members →" disclosures fortime1…time5-style indexed families.Version bumped to 1.3.0.
Closes
Test plan
testOTAAutomaticChecksLabelIsPositive)testOTATransferTimingLabelsVisible)testMQTTRetainLabelIsPositive,testMQTTMaxPacketSizeLabelHasNoParenthesisedUnit)testAdapterLEDLabelIsPositive)testHomeAssistantTogglesAreNouns)testNumericLabelsDoNotRepeatUnittestAvailabilityTimeoutRowsAreUnqualified)testLiveActivitiesHasOwnPage)testGeneralNoLongerHostsLiveActivities)testBulkOTAReplacesPerformance)Z2MWebSocketClientraisesmaximumMessageSizeto 64 MB on connect (unit test asserts task config)saveBackupdecodes base64 with embedded newlines/whitespace successfully (unit test feeds a hand-crafted base64 with\nseparators)saveBackuprejects a non-zip payload (wrong magic bytes) and surfacesfailedstatus, partial file is deleted (unit test)saveBackuprejects empty/zero-byte payload and surfacesfailed(unit test)seederto emit a >1 MB backup zip, confirm Shellbee receives it without disconnect and the saved file passesunzip -ttestFanWritableNumericRendersInline)NumericDetailViewis gone — no dedicated "Value" page is reachable from any fan settings rowtestLightAdvancedFeaturesRenderAsSettingsSections)FeatureLayoutFeatureLayoutsectionsFeatureLayoutsectiontestFeatureSectionsHideLinkqualityAndIdentify)🤖 Generated with Claude Code