Files
iOS/Sources/Extensions/Widgets/OpenPage/WidgetOpenPageProvider.swift
Copilot 0bb92dc9da Fix widget page identifiers causing duplicates and incorrect server names with multiple servers (#4051)
## Summary

Widget pages displayed wrong server names and showed blank names when >6
servers were configured. The issue had two root causes:

1. **Non-unique identifiers**: `IntentPanel` identifiers were set to
just `panel.path`, causing duplicate identifiers when multiple servers
shared the same panel path (e.g., "lovelace")
2. **Widget matching failure**: When widgets reloaded, the matching
logic couldn't handle backward compatibility, causing incorrect server
names to be displayed

**Changes Made:**

- Changed `IntentPanel` identifier from `panel.path` to
`"serverID-path"` format to ensure uniqueness across servers
- Added `extractedPath` computed property to `IntentPanel` for
consistent path extraction supporting both old and new identifier
formats
- Updated widget matching logic in `WidgetOpenPageProvider` to properly
match panels by server + path when reloading widgets
- Ensured backward compatibility with existing widget configurations
that use the old identifier format

**Before:**
```swift
// Server 1: identifier = "lovelace"
// Server 2: identifier = "lovelace"  // Duplicate!
// Widget matching: fails to match correct server
```

**After:**
```swift
// Server 1: identifier = "server1-lovelace"
// Server 2: identifier = "server2-lovelace"  // Unique!
// Widget matching: correctly matches by server + extracted path
```

The fix handles server IDs that contain hyphens and maintains backward
compatibility with widgets configured using the old identifier format.

## Screenshots

N/A - Fix resolves display issue shown in original bug report
screenshots

## Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#

## Any other notes

- Follows existing pattern in `PageAppEntity.swift` (line 69) for
consistency
- Path extraction logic is shared via `IntentPanel.extractedPath`
computed property to eliminate code duplication
- All linters passed (SwiftFormat, SwiftLint)

<!-- START COPILOT CODING AGENT SUFFIX -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>iOS widget showing wrong page and shows no pages if more
than 6 servers defined.</issue_title>
> <issue_description><!-- Please READ THIS FIRST
> If your issue relates to something not looking right on Home Assistant
within the Companion App, please check if the error is present in Safari
on iOS too. If the issue is also seen in Safari, please open an issue on
the frontend repo
(https://github.com/home-assistant/frontend/issues/new?labels=bug&template=BUG_REPORT.md)
instead -->
> 
> **iOS device model, version and app version**
> 
> Model Name: iPhone 16 Pro Max
> Software Version: 26.0.1
> App version: 2025.9.2 (2025.1423)
> 
> **Home Assistant Core Version**
> 2025.9.4
> 
> **Describe the bug**
> When adding multiple pages to the widget, the correct page description
isn't shown. When I have 5 servers (named Server 1 ... Server 5), and
have added the Overview page for each server, the widget shows, Server 1
Overview, Server 1 Overview, Server 3 Overview, Server 3 Overview, and
Server 5 Overview. Click on the second Server 1 Overview brings up
Server 1 Overview, but the widget configuration shows that its suppose
to bring up Server 2 Overview.
> 
> Additionally, if I also add Server 6 to the list of servers, all of
the page names are now blanks. It doesn't show Server 1 Overview, Server
2, Overview, etc. It would appear that having more than 5 servers
defined causes the widget to not display the names of any of the defined
pages.
> 
> 
> **To Reproduce**
> 
> Add up to 5 servers to the Companion App configuration. Then create a
HA Open Page widget and add the Overview page for the first two servers.
They will both be displayed with the first Server name.
> 
> **Expected behavior**
> 
> Adding the two server's Overview page should show Server 1 Overview
and Server 2 Overview in the widget.
> 
> **Screenshots**
> 
> <img width="660" height="1434" alt="Image"
src="https://github.com/user-attachments/assets/c421d9b4-d1d8-421b-b7b5-b50857a15973"
/>
> <img width="659" height="448" alt="Image"
src="https://github.com/user-attachments/assets/73ad6c2d-5f0c-4d8d-82c5-0006ea74855d"
/>
> <img width="660" height="442" alt="Image"
src="https://github.com/user-attachments/assets/1b891db3-c49a-469c-a82d-59c726964c01"
/>
> <img width="660" height="448" alt="Image"
src="https://github.com/user-attachments/assets/dc469e31-e588-4e24-9e01-09abca6caacc"
/>
> <img width="660" height="446" alt="Image"
src="https://github.com/user-attachments/assets/43e783a2-0f47-4ec2-a363-b6c45c3dab84"
/>
> <img width="660" height="453" alt="Image"
src="https://github.com/user-attachments/assets/92337fd4-1feb-4b3b-9ee9-1c0049cb3bb2"
/>
> <img width="660" height="449" alt="Image"
src="https://github.com/user-attachments/assets/a9d0548c-0e9d-496e-8616-84a6ffdc9fdc"
/>
> 
> **Additional context**
> This problem has been ongoing for a month or so.  
> </issue_description>
> 
> <agent_instructions>Check WidgetOpenPage, there must be some ID that
is not unique or such a thing.</agent_instructions>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>

- Fixes home-assistant/iOS#3876

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bgoncal <5808343+bgoncal@users.noreply.github.com>
2025-12-05 16:12:28 +01:00

69 lines
2.5 KiB
Swift

import PromiseKit
import Shared
import SwiftUI
import WidgetKit
struct WidgetOpenPageEntry: TimelineEntry {
var date = Date()
var pages: [IntentPanel] = []
}
struct WidgetOpenPageProvider: IntentTimelineProvider {
typealias Intent = WidgetOpenPageIntent
typealias Entry = WidgetOpenPageEntry
func placeholder(in context: Context) -> WidgetOpenPageEntry {
let count = WidgetFamilySizes.size(for: context.family)
let pages = stride(from: 0, to: count, by: 1).map { idx in
with(IntentPanel(identifier: "redacted\(idx)", display: "Redacted Text")) {
$0.icon = MaterialDesignIcons.bedEmptyIcon.name
}
}
return .init(pages: pages)
}
private func panels(
for context: Context,
updating existing: [IntentPanel],
timeout: Measurement<UnitDuration> = .init(value: 10.0, unit: .seconds),
completion: @escaping ([IntentPanel]) -> Void
) {
OpenPageIntentHandler.panels { panels in
var intentsToDisplay = panels
if !existing.isEmpty {
intentsToDisplay = existing.compactMap { existingValue in
intentsToDisplay.first { newPanel in
// Match by server and path, supporting both old and new identifier formats
newPanel.server == existingValue.server && newPanel.extractedPath == existingValue.extractedPath
}
}
}
completion(Array(intentsToDisplay.prefix(WidgetFamilySizes.size(for: context.family))))
}
}
func getSnapshot(for configuration: Intent, in context: Context, completion: @escaping (Entry) -> Void) {
panels(for: context, updating: configuration.pages ?? []) { panels in
completion(Entry(pages: Array(panels.prefix(WidgetFamilySizes.sizeForPreview(for: context.family)))))
}
}
private static var expiration: Measurement<UnitDuration> {
.init(value: 24, unit: .hours)
}
func getTimeline(for configuration: Intent, in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
func timeline(for pages: [IntentPanel]) -> Timeline<Entry> {
.init(
entries: [.init(pages: pages)],
policy: .after(Current.date().addingTimeInterval(Self.expiration.converted(to: .seconds).value))
)
}
panels(for: context, updating: configuration.pages ?? []) { panels in
completion(timeline(for: panels))
}
}
}