One [chore] commit making make ios-lint pass --strict cleanly: a new .swiftlint.yml, identifier renames, three line reflows, and two scoped rule suppressions. No app behaviour changes.
Flux/.swiftlint.yml — the project had none, so SwiftLint was scanning test targets and .build-xc/ artifacts. It excludes build dirs and the three test targets.line_length region around release-note copy, and a per-file file_length disable matching the existing convention.SoCAlertsService.registerDeviceIfNeeded's tz: label is now timeZone: — all call sites (2 app delegates, internal calls, 11 test calls) updated.Ready to push
Mechanical lint cleanup with no logic change. SwiftLint --strict is green (0 of 166 files), the FluxCore package + 252 unit tests pass, and both make ios-build and make macos-build succeed. The one quality nit raised in review (inconsistent debug-log phrasing) was fixed and folded into the commit.
fab7d37 [chore]: make ios-lint pass --strict cleanly SwiftLint is a tool that checks Swift code against style rules (line length, variable-name length, file size, etc.). The Flux project was running it in strict mode (every warning is an error) but had no configuration file, so it was checking everything — including generated build folders and test code — and reporting dozens of errors that had been red since v1.4.
A red linter that everyone ignores stops being useful. This change makes the linter pass cleanly so a real new violation will actually stand out.
.swiftlint.yml): tells the linter which folders to skip (build output, tests).tz, d, s were renamed to timeZone, decoder, hhmm so they pass the "no cryptic abbreviations" rule and read better.SwiftLint runs from Flux/ via the ios-lint / macos-lint Make targets. A single new Flux/.swiftlint.yml governs both. Its excluded list mirrors .gitignore (build dirs) and adds the three test targets, dropping the linted file count from 260 to 166 and removing ~51 test-only violations.
HistoryDerivedState uses the same per-file // swiftlint:disable file_length that DayDetailView (417 lines) and APIModels (452 lines) already use, rather than introducing a competing global config bump.line_length disable in WhatsNewCatalogue is bounded by an explicit // swiftlint:enable right after the release-notes array (a blanket file-wide line_length disable is itself rejected by the blanket_disable_command rule).Renaming the public tz: label to timeZone: is an API change, acceptable for a two-user app and consistent with the project's documented Swift style. Excluding test targets entirely trades test-code linting for signal on production code — defensible here since the test violations were all style-noise (short fixture names, long literals).
Strict mode promotes the line_length warning (120) and file_length warning (400) to errors. The initial config attempt bumped file_length to 500, which surfaced two latent superfluous_disable_command errors: DayDetailView (417) and APIModels (452) already carried // swiftlint:disable file_length, and raising the limit made those disables superfluous. That revealed the established convention and the config bump was reverted in favour of a per-file disable on HistoryDerivedState (415), keeping the strict 400 default for the other 163 files.
None at runtime. The only semantic change is the tz:→timeZone: public label on SoCAlertsService.registerDeviceIfNeeded; the internal tuple field and all call sites were updated in lockstep, and the tzIdentifier backend field name was deliberately left untouched.
blanket_disable_command permits a blanket file_length disable (file-level rule, can't be line-scoped) but not line_length — hence the bounded enable/disable region in WhatsNewCatalogue.WidgetAccessibility reflow hoists watts(live.ppv)/watts(live.pload) into solar/load locals; call count is unchanged (verified).WhatsNewCoordinator were aligned (both drop the — skipping suffix) so the line-length fix didn't leave them inconsistent.Fully implemented: zero strict violations across 166 files; all production violations fixed or justifiably scoped; both platforms build; FluxCore tests pass. Partial / follow-up: a pre-existing HH:MM parsing duplication (see Open question) is untouched. Missing: nothing in scope.
Flux/.swiftlint.yml
Why it matters. Root cause of the noise. With no config, strict SwiftLint scanned generated build dirs and test code, inflating the count to 77 locally (99 in a built copy with .build-xc present). Excludes mirror .gitignore plus the three test targets.
What to look at. Flux/.swiftlint.yml:1-15
Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertsService.swift
Why it matters. Only semantic change in the diff. The argument label on a public method changed, so every call site had to move in lockstep or the build breaks.
What to look at. SoCAlertsService.swift:88 (+ tuple field :40, call sites in 2 app delegates and 11 test calls)
Flux/Flux/History/HistoryDerivedState.swift
Why it matters. A 415-line cohesive file 15 lines over the default 400. The choice was split-the-file vs config-bump vs per-file disable.
What to look at. HistoryDerivedState.swift:4-7
Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCatalogue.swift
Why it matters. 12 release-note strings exceed 120 (some 200+) chars. They are user-facing prose, not code, where line length is meaningless.
What to look at. WhatsNewCatalogue.swift:5-8, 130
Flux/Packages/FluxCore/Sources/FluxCore/Widget/WidgetAccessibility.swift
Why it matters. Kept the strict 120 default by reflowing rather than relaxing the rule: extracted solar/load locals, broke a trailing closure, and trimmed a debug-log clause.
What to look at. WidgetAccessibility.swift:40-44; DayInFiveBlocksPanel.swift:59-61; WhatsNewCoordinator.swift:58,62
The three test targets contributed ~51 of the violations, all style-noise (short fixture names, long literals, long test bodies). Excluding them wholesale is simpler than per-rule overrides and matches the user's intent to "exclude things that don't matter".
Bumping file_length to 500 globally made the existing disables on DayDetailView and APIModels superfluous (a strict-mode error) and relaxed the rule for every file. The per-file disable matches the established convention and keeps the 400 default everywhere else.
The project Swift style rules say to avoid abbreviations except well-known ones (URL, ID). tz isn't on that list, so it was renamed to timeZone across the public API and all call sites instead of being added to identifier_name.excluded.
The line-length fix removed — skipping from the "unparseable" log, leaving it inconsistent with the "missing" log. Re-adding the suffix would re-break the 120-char limit, so the "missing" log dropped its suffix too — both stay clear (the guard returns nil) and consistent.
| Severity | Area | Finding | Resolution |
|---|---|---|---|
| minor | WhatsNewCoordinator.swift:58,62 | After trimming the over-length 'unparseable' debug log, its phrasing no longer matched the sibling 'missing — skipping' log. | Dropped '— skipping' from the 'missing' log too so both are consistent; re-adding it to line 62 would have reintroduced the line_length violation. |
| minor | SoCAlertEditor.swift / SoCAlertRuleDraft.swift | PRE-EXISTING (not introduced here): HH:MM parse/validate helpers (dateFromHHMM, hhmmFromDate, isValidHHMM) overlap with FluxCore's DateFormatting.parseWindowTime(). This diff only renamed locals inside them. | Not changed. Out of scope for a lint commit, and not a safe drop-in: the editor helper uses Calendar.current for a local picker binding while parseWindowTime uses the Sydney calendar. Logged as a follow-up. |
| info | whole diff | Efficiency review found no regressions; the WidgetAccessibility local-extraction keeps watts(...) call count unchanged. | No action needed. |
Click to expand.
diff --git a/Flux/.swiftlint.yml b/Flux/.swiftlint.ymlnew file mode 100644index 0000000..49b3c89--- /dev/null+++ b/Flux/.swiftlint.yml@@ -0,0 +1,15 @@+# SwiftLint configuration for the Flux iOS/macOS app.+# Run from the Flux/ directory: `swiftlint lint --strict`+# (see the ios-lint / macos-lint targets in the Makefile).++excluded:+ # Build artifacts — mirror .gitignore so generated sources never get linted.+ - .build+ - .build-xc+ - DerivedData+ - Packages/FluxCore/.build+ # Test targets. Style rules (line length, identifier names, body length)+ # add noise without value in test code, so they are not linted.+ - FluxTests+ - FluxUITests+ - Packages/FluxCore/Tests
diff --git a/Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertsService.swift b/Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertsService.swiftindex 944b94a..f84e006 100644--- a/Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertsService.swift+++ b/Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertsService.swift@@ -37,7 +37,7 @@ public final class SoCAlertsService { /// Pending registration retained when the backend POST fails so the next /// foreground hook can replay it.- private var pendingRegistration: (token: Data?, tz: TimeZone)?+ private var pendingRegistration: (token: Data?, timeZone: TimeZone)? public init( deviceIdentifier: DeviceIdentifier = .shared,@@ -76,7 +76,7 @@ public final class SoCAlertsService { // Denied or undetermined-after-prompt: still POST a device row // (without a token) so the backend knows about the device and // the next foreground after granting can attach the token.- try await registerDeviceIfNeeded(token: nil, tz: .current)+ try await registerDeviceIfNeeded(token: nil, timeZone: .current) } } @@ -85,7 +85,7 @@ public final class SoCAlertsService { /// The env value comes from this build's `aps-environment` entitlement /// so a TestFlight install on the same backend as a Dev install /// registers under its own host.- public func registerDeviceIfNeeded(token: Data?, tz: TimeZone) async throws {+ public func registerDeviceIfNeeded(token: Data?, timeZone: TimeZone) async throws { guard let apiClient else { return } let tokenHex = token.map { hexString($0) } let env = APNsEnvironment.current()@@ -93,7 +93,7 @@ public final class SoCAlertsService { if let cached, cached.apnsToken == tokenHex, cached.apnsEnvironment == env,- cached.tzIdentifier == tz.identifier {+ cached.tzIdentifier == timeZone.identifier { // Nothing changed since the last successful POST. return }@@ -102,7 +102,7 @@ public final class SoCAlertsService { platform: currentPlatform, apnsToken: tokenHex, apnsEnvironment: env,- tzIdentifier: tz.identifier,+ tzIdentifier: timeZone.identifier, tzUpdatedAt: Int64(Date().timeIntervalSince1970) ) do {@@ -110,13 +110,13 @@ public final class SoCAlertsService { saveLastRegistration(LastRegistration( apnsToken: tokenHex, apnsEnvironment: env,- tzIdentifier: tz.identifier,+ tzIdentifier: timeZone.identifier, tzUpdatedAt: registration.tzUpdatedAt )) pendingRegistration = nil lastError = nil } catch {- pendingRegistration = (token, tz)+ pendingRegistration = (token, timeZone) lastError = error throw error }@@ -127,7 +127,7 @@ public final class SoCAlertsService { public func foregroundHook() async { if let pending = pendingRegistration { do {- try await registerDeviceIfNeeded(token: pending.token, tz: pending.tz)+ try await registerDeviceIfNeeded(token: pending.token, timeZone: pending.timeZone) } catch { // Stored already in lastError; nothing to do. }
diff --git a/Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertRuleDraft.swift b/Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertRuleDraft.swiftindex 4556651..de49963 100644--- a/Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertRuleDraft.swift+++ b/Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertRuleDraft.swift@@ -65,11 +65,11 @@ public struct SoCAlertRuleDraft: Sendable, Equatable { return nil } - private static func isValidHHMM(_ s: String) -> Bool {- let parts = s.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)+ private static func isValidHHMM(_ hhmm: String) -> Bool {+ let parts = hhmm.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) guard parts.count == 2,- let h = Int(parts[0]), let m = Int(parts[1]),- (0...23).contains(h), (0...59).contains(m)+ let hour = Int(parts[0]), let minute = Int(parts[1]),+ (0...23).contains(hour), (0...59).contains(minute) else { return false } return true }
diff --git a/Flux/Flux/Settings/SoCAlerts/SoCAlertEditor.swift b/Flux/Flux/Settings/SoCAlerts/SoCAlertEditor.swiftindex f8c99f1..4c6cedf 100644--- a/Flux/Flux/Settings/SoCAlerts/SoCAlertEditor.swift+++ b/Flux/Flux/Settings/SoCAlerts/SoCAlertEditor.swift@@ -106,17 +106,17 @@ struct SoCAlertEditor: View { ) } - private func dateFromHHMM(_ s: String) -> Date? {- let parts = s.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)- guard parts.count == 2, let h = Int(parts[0]), let m = Int(parts[1]) else { return nil }+ private func dateFromHHMM(_ hhmm: String) -> Date? {+ let parts = hhmm.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)+ guard parts.count == 2, let hour = Int(parts[0]), let minute = Int(parts[1]) else { return nil } var components = DateComponents()- components.hour = h- components.minute = m+ components.hour = hour+ components.minute = minute return Calendar.current.date(from: components) } - private func hhmmFromDate(_ d: Date) -> String {- let comps = Calendar.current.dateComponents([.hour, .minute], from: d)+ private func hhmmFromDate(_ date: Date) -> String {+ let comps = Calendar.current.dateComponents([.hour, .minute], from: date) return String(format: "%02d:%02d", comps.hour ?? 0, comps.minute ?? 0) } }
diff --git a/Flux/Packages/FluxCore/Sources/FluxCore/Networking/URLSessionAPIClient.swift b/Flux/Packages/FluxCore/Sources/FluxCore/Networking/URLSessionAPIClient.swiftindex 465edb6..412c910 100644--- a/Flux/Packages/FluxCore/Sources/FluxCore/Networking/URLSessionAPIClient.swift+++ b/Flux/Packages/FluxCore/Sources/FluxCore/Networking/URLSessionAPIClient.swift@@ -37,15 +37,15 @@ public final class URLSessionAPIClient: FluxAPIClient, Sendable { /// `Date` field (rule timestamps, future endpoints) lands on the /// success path. private static func makeDecoder() -> JSONDecoder {- let d = JSONDecoder()- d.dateDecodingStrategy = .iso8601- return d+ let decoder = JSONDecoder()+ decoder.dateDecodingStrategy = .iso8601+ return decoder } private static func makeEncoder() -> JSONEncoder {- let e = JSONEncoder()- e.dateEncodingStrategy = .iso8601- return e+ let encoder = JSONEncoder()+ encoder.dateEncodingStrategy = .iso8601+ return encoder } public func fetchStatus() async throws -> StatusResponse {
diff --git a/Flux/Packages/FluxCore/Sources/FluxCore/Widget/WidgetAccessibility.swift b/Flux/Packages/FluxCore/Sources/FluxCore/Widget/WidgetAccessibility.swiftindex 67da304..9dcc9c7 100644--- a/Flux/Packages/FluxCore/Sources/FluxCore/Widget/WidgetAccessibility.swift+++ b/Flux/Packages/FluxCore/Sources/FluxCore/Widget/WidgetAccessibility.swift@@ -38,7 +38,9 @@ public enum WidgetAccessibility { return "Battery \(socInt) percent, \(verb)." case .systemMedium, .systemLarge: if let live {- return "Battery \(socInt) percent, \(verb). Solar \(watts(live.ppv)), load \(watts(live.pload)), grid \(gridPhrase(live))."+ let solar = watts(live.ppv)+ let load = watts(live.pload)+ return "Battery \(socInt) percent, \(verb). Solar \(solar), load \(load), grid \(gridPhrase(live))." } return "Battery \(socInt) percent, \(verb)." default:
diff --git a/Flux/Flux/DayDetail/DayInFiveBlocksPanel.swift b/Flux/Flux/DayDetail/DayInFiveBlocksPanel.swiftindex 26a97ee..786161e 100644--- a/Flux/Flux/DayDetail/DayInFiveBlocksPanel.swift+++ b/Flux/Flux/DayDetail/DayInFiveBlocksPanel.swift@@ -56,7 +56,9 @@ struct DayInFiveBlocksPanel: View { .frame(maxHeight: .infinity) VStack(alignment: .leading, spacing: 2) { Text(label(for: block.kind))- .appFont { FluxTheme.Typography.touName(family: $0).weight(isHighlighted ? .semibold : .regular) }+ .appFont {+ FluxTheme.Typography.touName(family: $0).weight(isHighlighted ? .semibold : .regular)+ } .foregroundStyle(isHighlighted ? FluxTheme.Palette.offpeak : FluxTheme.Palette.primaryText) Text(timeRange(block)) .appFont(FluxTheme.Typography.touTime)
diff --git a/Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCoordinator.swift b/Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCoordinator.swiftindex 716b26c..4944a3b 100644--- a/Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCoordinator.swift+++ b/Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCoordinator.swift@@ -55,11 +55,11 @@ public struct WhatsNewCoordinator: Sendable { catalogue: [WhatsNewRelease] = WhatsNewCatalogue.releases ) -> WhatsNewCoordinator? { guard let raw = bundle.infoDictionary?["CFBundleShortVersionString"] as? String else {- Self.logger.debug("forCurrentInstall: CFBundleShortVersionString missing — skipping")+ Self.logger.debug("forCurrentInstall: CFBundleShortVersionString missing") return nil } guard let installed = WhatsNewVersion(raw) else {- Self.logger.debug("forCurrentInstall: CFBundleShortVersionString \(raw, privacy: .public) unparseable — skipping")+ Self.logger.debug("forCurrentInstall: CFBundleShortVersionString \(raw, privacy: .public) unparseable") return nil } return WhatsNewCoordinator(
diff --git a/Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCatalogue.swift b/Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCatalogue.swiftindex def10d4..ab02c46 100644--- a/Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCatalogue.swift+++ b/Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCatalogue.swift@@ -1,6 +1,10 @@ import Foundation public enum WhatsNewCatalogue {+ // Each release note is authored as a single line of prose so it stays easy+ // to read and edit. Line length is not meaningful for this copy, so the+ // rule is disabled across the catalogue and re-enabled right after it.+ // swiftlint:disable line_length public static let releases: [WhatsNewRelease] = [ WhatsNewRelease( version: "1.5",@@ -123,6 +127,7 @@ public enum WhatsNewCatalogue { ] ) ]+ // swiftlint:enable line_length private static func dateOf(year: Int, month: Int) -> Date { var components = DateComponents()
diff --git a/Flux/Flux/History/HistoryDerivedState.swift b/Flux/Flux/History/HistoryDerivedState.swiftindex ae7e2db..78644d1 100644--- a/Flux/Flux/History/HistoryDerivedState.swift+++ b/Flux/Flux/History/HistoryDerivedState.swift@@ -1,6 +1,11 @@ import FluxCore import Foundation +// The period-accumulator logic (DayRecordValue + Totals) is kept in one file+// alongside the derivations it serves; splitting it only to fit would fragment+// tightly-coupled code with intentionally file-private types.+// swiftlint:disable file_length+ extension HistoryViewModel { struct SolarEntry: Identifiable, Equatable { let date: Date
diff --git a/Flux/Flux/Mac/FluxAppDelegate.swift b/Flux/Flux/Mac/FluxAppDelegate.swiftindex 8bb9b60..6c9b296 100644--- a/Flux/Flux/Mac/FluxAppDelegate.swift+++ b/Flux/Flux/Mac/FluxAppDelegate.swift@@ -26,7 +26,7 @@ final class FluxAppDelegate: NSObject, NSApplicationDelegate { Task { @MainActor in try? await SoCAlertsService.shared.registerDeviceIfNeeded( token: deviceToken,- tz: .current+ timeZone: .current ) } }
diff --git a/Flux/Flux/iOS/FluxiOSAppDelegate.swift b/Flux/Flux/iOS/FluxiOSAppDelegate.swiftindex 8c0fbc4..7d46f4a 100644--- a/Flux/Flux/iOS/FluxiOSAppDelegate.swift+++ b/Flux/Flux/iOS/FluxiOSAppDelegate.swift@@ -26,7 +26,7 @@ final class FluxiOSAppDelegate: NSObject, UIApplicationDelegate { Task { @MainActor in try? await SoCAlertsService.shared.registerDeviceIfNeeded( token: deviceToken,- tz: .current+ timeZone: .current ) } }
diff --git a/Flux/Packages/FluxCore/Tests/FluxCoreTests/SoCAlertsServiceTests.swift b/Flux/Packages/FluxCore/Tests/FluxCoreTests/SoCAlertsServiceTests.swiftindex 8e1ec85..d4d1dfe 100644--- a/Flux/Packages/FluxCore/Tests/FluxCoreTests/SoCAlertsServiceTests.swift+++ b/Flux/Packages/FluxCore/Tests/FluxCoreTests/SoCAlertsServiceTests.swift@@ -10,7 +10,7 @@ struct SoCAlertsServiceTests { let api = TestAPIClient() let svc = makeService(api: api) - try await svc.registerDeviceIfNeeded(token: Data([0xde, 0xad, 0xbe, 0xef]), tz: TimeZone(identifier: "Australia/Sydney")!)+ try await svc.registerDeviceIfNeeded(token: Data([0xde, 0xad, 0xbe, 0xef]), timeZone: TimeZone(identifier: "Australia/Sydney")!) #expect(await api.registrationCalls.count == 1) let call = try #require(await api.registrationCalls.first) #expect(call.apnsToken == "deadbeef")@@ -23,8 +23,8 @@ struct SoCAlertsServiceTests { let svc = makeService(api: api) let token = Data([0xde, 0xad, 0xbe, 0xef])- try await svc.registerDeviceIfNeeded(token: token, tz: TimeZone(identifier: "Australia/Sydney")!)- try await svc.registerDeviceIfNeeded(token: token, tz: TimeZone(identifier: "Australia/Sydney")!)+ try await svc.registerDeviceIfNeeded(token: token, timeZone: TimeZone(identifier: "Australia/Sydney")!)+ try await svc.registerDeviceIfNeeded(token: token, timeZone: TimeZone(identifier: "Australia/Sydney")!) #expect(await api.registrationCalls.count == 1, "second call with same token+tz must not POST again") }@@ -34,8 +34,8 @@ struct SoCAlertsServiceTests { let api = TestAPIClient() let svc = makeService(api: api) - try await svc.registerDeviceIfNeeded(token: Data([0x01, 0x02]), tz: TimeZone(identifier: "Australia/Sydney")!)- try await svc.registerDeviceIfNeeded(token: Data([0x03, 0x04]), tz: TimeZone(identifier: "Australia/Sydney")!)+ try await svc.registerDeviceIfNeeded(token: Data([0x01, 0x02]), timeZone: TimeZone(identifier: "Australia/Sydney")!)+ try await svc.registerDeviceIfNeeded(token: Data([0x03, 0x04]), timeZone: TimeZone(identifier: "Australia/Sydney")!) #expect(await api.registrationCalls.count == 2) } @@ -43,7 +43,7 @@ struct SoCAlertsServiceTests { func registerDeviceIfNeededPostsWithoutToken() async throws { let api = TestAPIClient() let svc = makeService(api: api)- try await svc.registerDeviceIfNeeded(token: nil, tz: TimeZone(identifier: "Australia/Sydney")!)+ try await svc.registerDeviceIfNeeded(token: nil, timeZone: TimeZone(identifier: "Australia/Sydney")!) let call = try #require(await api.registrationCalls.first) #expect(call.apnsToken == nil, "denial path: backend gets the row without a token") }@@ -52,7 +52,7 @@ struct SoCAlertsServiceTests { func createRuleIsOptimisticAndAppendsToLocalRules() async throws { let api = TestAPIClient() let svc = makeService(api: api)- try await svc.registerDeviceIfNeeded(token: Data([0xab]), tz: TimeZone(identifier: "UTC")!)+ try await svc.registerDeviceIfNeeded(token: Data([0xab]), timeZone: TimeZone(identifier: "UTC")!) let draft = SoCAlertRuleDraft(thresholdPercent: 30, windowStart: "17:00", windowEnd: "18:00", enabled: true) let created = try await svc.create(draft)@@ -64,7 +64,7 @@ struct SoCAlertsServiceTests { let api = TestAPIClient() api.createRuleResult = .failure(FluxAPIError.ruleCapReached) let svc = makeService(api: api)- try await svc.registerDeviceIfNeeded(token: Data([0xab]), tz: TimeZone(identifier: "UTC")!)+ try await svc.registerDeviceIfNeeded(token: Data([0xab]), timeZone: TimeZone(identifier: "UTC")!) do { _ = try await svc.create(SoCAlertRuleDraft(thresholdPercent: 30, windowStart: "17:00", windowEnd: "18:00", enabled: true))@@ -82,7 +82,7 @@ struct SoCAlertsServiceTests { SoCAlertRule(id: "r1", thresholdPercent: 30, windowStart: "17:00", windowEnd: "18:00", enabled: true, label: nil, createdAt: now, updatedAt: now) ] let svc = makeService(api: api)- try await svc.registerDeviceIfNeeded(token: Data([0x01]), tz: TimeZone(identifier: "UTC")!)+ try await svc.registerDeviceIfNeeded(token: Data([0x01]), timeZone: TimeZone(identifier: "UTC")!) try await svc.refresh() #expect(svc.rules.count == 1) #expect(svc.rules.first?.id == "r1")@@ -94,7 +94,7 @@ struct SoCAlertsServiceTests { api.registerFailures = 1 let svc = makeService(api: api) do {- try await svc.registerDeviceIfNeeded(token: Data([0x01]), tz: TimeZone(identifier: "UTC")!)+ try await svc.registerDeviceIfNeeded(token: Data([0x01]), timeZone: TimeZone(identifier: "UTC")!) } catch { // expected }@@ -111,7 +111,7 @@ struct SoCAlertsServiceTests { let api = TestAPIClient() api.createRuleResult = .failure(.serverError) let svc = makeService(api: api)- try await svc.registerDeviceIfNeeded(token: Data([0x01]), tz: TimeZone(identifier: "UTC")!)+ try await svc.registerDeviceIfNeeded(token: Data([0x01]), timeZone: TimeZone(identifier: "UTC")!) _ = try? await svc.create(SoCAlertRuleDraft(thresholdPercent: 30, windowStart: "17:00", windowEnd: "18:00", enabled: true)) #expect(svc.lastError != nil) svc.clearError()
diff --git a/CHANGELOG.md b/CHANGELOG.mdindex dc332c0..056ad68 100644--- a/CHANGELOG.md+++ b/CHANGELOG.md@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [Unreleased]++### Changed++- **SwiftLint now passes `--strict` cleanly.** Added `Flux/.swiftlint.yml` (the project had none, so the linter was scanning test targets and `.build-xc/` artifacts): build directories (`.build`, `.build-xc`, `DerivedData`) and the `FluxTests` / `FluxUITests` / `FluxCore/Tests` targets are now excluded. The 26 remaining production violations were fixed rather than suppressed — short identifiers renamed to meaningful names (including the `registerDeviceIfNeeded` `tz:` → `timeZone:` label), three over-length lines reflowed, a scoped `line_length` exception around the release-note copy in `WhatsNewCatalogue`, and a per-file `file_length` disable on `HistoryDerivedState` matching the convention already used by `DayDetailView` and `APIModels`. No app behaviour changes.+ ## [1.5] - 2026-06-02 ### Fixed
Linting runs from the CLI via make ios-lint/macos-lint (cd Flux && swiftlint lint --strict), so the config at Flux/.swiftlint.yml is found automatically. If a Build Phase ever invokes SwiftLint from a different working directory, confirm it still resolves this config.
Worth a follow-up task: consolidate the HH:MM parse/validate helpers, being careful about the Calendar.current vs Sydney-calendar difference. Not done here to keep this commit purely lint cleanup.