Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Hemera/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ final class AppEnvironment {
} catch {
Log.error("ModelContainer creation failed — deleting store (HomeTile layout will be lost). Error: \(error)")
Self.deleteStore(at: modelConfig.url)
self.container = try! ModelContainer(for: schema, configurations: modelConfig)
do {
self.container = try ModelContainer(for: schema, configurations: modelConfig)
} catch {
Log.error("ModelContainer recreation failed after store deletion — falling back to in-memory container (no persistence this session). Error: \(error)")
let memoryConfig = ModelConfiguration(isStoredInMemoryOnly: true)
self.container = try! ModelContainer(for: schema, configurations: memoryConfig)
}
}

self.storage = SwiftDataStorage(context: container.mainContext)
Expand Down
25 changes: 16 additions & 9 deletions Hemera/HomeAssistant/MDISymbolMapper.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import HemeraLog

/// Maps Material Design Icon (MDI) names from Home Assistant to SF Symbol names.
///
Expand Down Expand Up @@ -31,18 +32,24 @@ enum MDISymbolMapper {

private static func loadMapping(from resource: String) -> [String: String] {
guard let url = Bundle.main.url(forResource: resource, withExtension: "json") else {
preconditionFailure("\(resource).json missing from bundle")
Log.error("\(resource).json missing from bundle — icon mapping disabled for \(resource)")
return [:]
}
let data = try! Data(contentsOf: url)
let entries = try! JSONDecoder().decode([Entry].self, from: data)

var map: [String: String] = [:]
for entry in entries {
for mdiName in entry.mdiNames {
map[mdiName] = entry.sfSymbol
do {
let data = try Data(contentsOf: url)
let entries = try JSONDecoder().decode([Entry].self, from: data)

var map: [String: String] = [:]
for entry in entries {
for mdiName in entry.mdiNames {
map[mdiName] = entry.sfSymbol
}
}
return map
} catch {
Log.error("Failed to load \(resource).json — icon mapping disabled", cause: error)
return [:]
}
return map
}
}

Expand Down
5 changes: 4 additions & 1 deletion Hemera/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ final class SessionManager: ConnectionRetrying {
Log.info("Starting demo session")

let demoConfig = ModelConfiguration(isStoredInMemoryOnly: true)
let demoContainer = try! ModelContainer(for: AppEnvironment.createSchema(), configurations: demoConfig)
guard let demoContainer = try? ModelContainer(for: AppEnvironment.createSchema(), configurations: demoConfig) else {
Log.error("Failed to create in-memory demo container — aborting demo session")
return
}
self.demoContainer = demoContainer
let demoContext = demoContainer.mainContext

Expand Down
15 changes: 12 additions & 3 deletions Hemera/UI/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@ struct RootView: View {
case .connecting:
ConnectingView(viewModel: ConnectingViewModel())
case .authenticated:
MainTabView(viewModel: MainTabViewModel())
.modelContainer(ServiceLocator.shared.session!.container)
.transaction { $0.animation = nil }
if let session = ServiceLocator.shared.session {
MainTabView(viewModel: MainTabViewModel())
.modelContainer(session.container)
.transaction { $0.animation = nil }
} else {
/**
Session torn down while destination is briefly still .authenticated.
Render nothing rather than crashing; the router handler will move
destination away from .authenticated on the same runloop turn.
*/
Color.clear
}
}
}
.environment(authManager)
Expand Down
15 changes: 10 additions & 5 deletions Hemera/UI/Settings/AuthenticatedWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,16 @@ struct AuthenticatedWebView: UIViewRepresentable {
let webView = WKWebView(frame: .zero, configuration: config)
context.coordinator.webView = webView

var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
components.queryItems = (components.queryItems ?? []) + [
URLQueryItem(name: "external_auth", value: "1")
]
webView.load(URLRequest(url: components.url!))
let requestURL: URL
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.queryItems = (components.queryItems ?? []) + [
URLQueryItem(name: "external_auth", value: "1")
]
requestURL = components.url ?? url
} else {
requestURL = url
}
webView.load(URLRequest(url: requestURL))
return webView
}

Expand Down
8 changes: 8 additions & 0 deletions HemeraTests/HomeAssistant/MDISymbolMapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ struct MDISymbolMapperTests {
}
}

@Test func bundledMaps_decodeToNonEmpty() throws {
for file in ["AreaMdiToSymbolMap", "EntityMdiToSymbolMap"] {
let entries = try loadEntries(from: file)
#expect(!entries.isEmpty)
#expect(entries.allSatisfy { !$0.mdiNames.isEmpty })
}
}

// MARK: - Helpers

private struct MappingEntry: Decodable {
Expand Down
13 changes: 13 additions & 0 deletions HemeraTests/Navigation/SessionManagerDemoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ struct SessionManagerTests {
#expect(ServiceLocator.shared.session != nil)
}

@Test
func inMemoryDemoContainer_withCurrentSchema_buildsAndIsQueryable() throws {
/**
Guards the graceful demo-container fallback in `startDemoSession`: if the
current schema ever stopped building an in-memory container, the demo path
would silently no-op. This fails loudly instead.
*/
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: AppEnvironment.createSchema(), configurations: config)
let count = try container.mainContext.fetchCount(FetchDescriptor<AreaEntity>())
#expect(count == 0)
}

// MARK: - tearDownDemoSession

@Test
Expand Down
Loading