flux branch worktree-ios-lint-fixes commits 1 files 14 touched lines +77 / -42 lint 0 violations (was 77/99)

Pre-push review: ios-lint cleanup

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.

At a glance

  • Added 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.
  • Fixed all 26 production violations rather than suppressing them: 10 identifier renames, 3 line reflows.
  • Two scoped suppressions for cases where the rule does not apply: a bounded line_length region around release-note copy, and a per-file file_length disable matching the existing convention.
  • Public API change: SoCAlertsService.registerDeviceIfNeeded's tz: label is now timeZone: — all call sites (2 app delegates, internal calls, 11 test calls) updated.
  • No runtime behaviour change; verified by iOS + macOS builds and the full FluxCore test suite.

Verdict

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.

Review findings

3 raised · 1 fixed · 2 skipped

Jump to findings →

Commits

Three-level explanation

What changed

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.

Why it matters

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.

Key concepts

  • Config file (.swiftlint.yml): tells the linter which folders to skip (build output, tests).
  • Renaming: short names like tz, d, s were renamed to timeZone, decoder, hhmm so they pass the "no cryptic abbreviations" rule and read better.
  • Suppressions: for two cases where a rule genuinely doesn't fit (release-note text that is meant to be long; one cohesive file slightly over the size limit), the rule is switched off in just that spot with a comment explaining why.

Architecture

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.

Patterns

  • Fix > suppress. The 10 identifier and 3 line-length violations in production code were fixed in place. Suppression was reserved for two cases where the rule does not apply.
  • Match existing conventions. 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.
  • Minimal blast radius for suppressions. The 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).

Trade-offs

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).

Deep dive

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.

Architecture impact

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.

Edge cases / notes

  • 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.
  • The WidgetAccessibility reflow hoists watts(live.ppv)/watts(live.pload) into solar/load locals; call count is unchanged (verified).
  • The two debug-log lines in WhatsNewCoordinator were aligned (both drop the — skipping suffix) so the line-length fix didn't leave them inconsistent.

Completeness assessment

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.

Important changes — detailed

.swiftlint.yml: exclude build artifacts and test targets

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

Takeaway. When a strict linter has no config, it lints everything — generated output and tests included. A minimal excluded: list mirroring .gitignore is usually the first fix, not rule tuning.
Rationale. Test code legitimately uses short fixture names and long literals; linting it adds noise without value. Build dirs are generated. Both are excluded so the rules apply only to production source.

tz: -> timeZone: public API rename

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)

Takeaway. Renaming an external argument label is a source-breaking change; grep every call site (including tests, which still compile even when excluded from lint) before relying on the build.
Rationale. The project's Swift style rule says avoid abbreviations except URL/ID. Renaming is preferred over allow-listing tz in identifier_name config.

Per-file file_length disable on HistoryDerivedState

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

Takeaway. Match the codebase's existing escape hatch before inventing a new one — DayDetailView and APIModels already use a per-file file_length disable, so a third file in the same situation should too.
Rationale. A global limit bump made the two existing disables superfluous (a hard error) and weakened the rule everywhere; splitting would force widening intentionally file-private types. The per-file disable is consistent and keeps 400 strict elsewhere.

Bounded line_length region around release-note copy

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

Takeaway. SwiftLint's blanket_disable_command rejects a file-wide line_length disable but allows file_length; for line_length you must bound the region with a matching // swiftlint:enable.
Rationale. Reflowing prose with string concatenation is ugly and multiline literals would inject newlines into the displayed text. Scoping the disable to just the data array is the least-invasive correct option.

Three line reflows in production code

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

Takeaway. Most just-over-limit lines reflow cleanly (hoist locals, break the trailing closure) without weakening the global limit for 160+ already-compliant files.
Rationale. Keeping line_length at the default 120 preserves the signal the rest of the codebase already satisfies; the reflows are also marginally more readable.

Key decisions

Exclude test targets entirely rather than per-rule tuning.

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".

Per-file file_length disable over a global limit bump.

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.

Rename tz rather than allow-list it.

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.

Align both debug logs by dropping the suffix.

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.

Review findings

SeverityAreaFindingResolution
minorWhatsNewCoordinator.swift:58,62After 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.
minorSoCAlertEditor.swift / SoCAlertRuleDraft.swiftPRE-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.
infowhole diffEfficiency review found no regressions; the WidgetAccessibility local-extraction keeps watts(...) call count unchanged.No action needed.

Per-file diffs

Click to expand.

Flux/.swiftlint.yml Added +15 / -0
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
Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertsService.swift Modified +8 / -8
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.             }
Flux/Packages/FluxCore/Sources/FluxCore/Notifications/SoCAlertRuleDraft.swift Modified +4 / -4
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     }
Flux/Flux/Settings/SoCAlerts/SoCAlertEditor.swift Modified +7 / -7
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)     } }
Flux/Packages/FluxCore/Sources/FluxCore/Networking/URLSessionAPIClient.swift Modified +6 / -6
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 {
Flux/Packages/FluxCore/Sources/FluxCore/Widget/WidgetAccessibility.swift Modified +3 / -1
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:
Flux/Flux/DayDetail/DayInFiveBlocksPanel.swift Modified +3 / -1
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)
Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCoordinator.swift Modified +2 / -2
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(
Flux/Packages/FluxCore/Sources/FluxCore/WhatsNew/WhatsNewCatalogue.swift Modified +5 / -0
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()
Flux/Flux/History/HistoryDerivedState.swift Modified +5 / -0
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
Flux/Flux/Mac/FluxAppDelegate.swift Modified +1 / -1
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             )         }     }
Flux/Flux/iOS/FluxiOSAppDelegate.swift Modified +1 / -1
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             )         }     }
Flux/Packages/FluxCore/Tests/FluxCoreTests/SoCAlertsServiceTests.swift Modified +11 / -11
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()
CHANGELOG.md Modified +6 / -0
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

Things to double-check

Xcode picks up .swiftlint.yml.

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.

Pre-existing HH:MM duplication.

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.