-
Notifications
You must be signed in to change notification settings - Fork 0
feat: PopoverMainView redesign (#296) #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
086162e
b05078b
dce66a3
119cae7
93f2695
4556f24
70189fe
87fff9b
114cfa8
4a4a0c5
f998973
e8864f7
b29ac09
668224d
51aadb1
7368e8f
043d484
6c41d6b
0d20819
15021c0
07d6414
9e731bc
75550c9
e00ce1c
888288d
d70d046
43a214c
2da30b8
c8e8118
500bbd3
6a3fe6d
28d973c
0085057
1a143ce
3e1513c
751a4b7
e274658
0e387db
2d98df2
0ee6d84
779ade0
e592344
8a39ad9
8bcadaa
dd3718a
77a7d8e
7162557
4555aa4
8775c11
c80f4fc
64b0fdd
a052859
2668474
c2e3935
1e7c776
dc4a801
8c3619f
ddeb34f
4770a9f
6a6c330
900b8b3
1c79c90
ef19719
e8e3da8
4c5d8a3
6793202
b5f1150
fb2ee8e
dd95aa6
fbbc3e9
07f65bc
4067591
5222fc9
7a33f86
d34de9b
b01f70a
94a5a91
eb84978
1d56db5
f41d3ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| .build | ||
| dist |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||
| // swiftlint:disable opening_brace identifier_name missing_docs orphaned_doc_comment | ||||||||||||||||||||||||||||||||
| // swiftlint:disable opening_brace identifier_name orphaned_doc_comment missing_docs | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // MARK: - GroupStatus | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -20,10 +20,15 @@ enum GroupStatus { | |||||||||||||||||||||||||||||||
| /// Holds only the data needed for display and job fetching — deliberately | ||||||||||||||||||||||||||||||||
| /// minimal so the full job list lives on the parent ActionGroup instead. | ||||||||||||||||||||||||||||||||
| struct WorkflowRunRef: Identifiable { | ||||||||||||||||||||||||||||||||
| /// Unique GitHub run identifier. | ||||||||||||||||||||||||||||||||
| let id: Int | ||||||||||||||||||||||||||||||||
| let name: String // workflow file name, e.g. "SonarQube", "vitest" | ||||||||||||||||||||||||||||||||
| /// Workflow file name, e.g. "SonarQube", "vitest". | ||||||||||||||||||||||||||||||||
| let name: String | ||||||||||||||||||||||||||||||||
| /// Current run status: `queued`, `in_progress`, or `completed`. | ||||||||||||||||||||||||||||||||
| let status: String | ||||||||||||||||||||||||||||||||
| /// Final outcome when status is `completed`. | ||||||||||||||||||||||||||||||||
| let conclusion: String? | ||||||||||||||||||||||||||||||||
| /// URL to the run's page on github.com. | ||||||||||||||||||||||||||||||||
| let htmlUrl: String? | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -36,7 +41,7 @@ struct WorkflowRunRef: Identifiable { | |||||||||||||||||||||||||||||||
| /// Hierarchy: ActionGroup → jobs (flat across all sibling runs) → JobStep → log. | ||||||||||||||||||||||||||||||||
| /// `ActionDetailView` drills into the flat job list; `JobDetailView`/`StepLogView` | ||||||||||||||||||||||||||||||||
| /// are reused unchanged below that. | ||||||||||||||||||||||||||||||||
| struct ActionGroup: Identifiable { | ||||||||||||||||||||||||||||||||
| struct ActionGroup: Identifiable, Equatable { | ||||||||||||||||||||||||||||||||
| let headSha: String // head_sha — kept as the underlying group identity | ||||||||||||||||||||||||||||||||
| let label: String // "#1270" if PR, else "d6281b" (sha[:7]) | ||||||||||||||||||||||||||||||||
| let title: String // commit/PR message first line (≤40 chars) | ||||||||||||||||||||||||||||||||
|
|
@@ -58,6 +63,7 @@ struct ActionGroup: Identifiable { | |||||||||||||||||||||||||||||||
| /// Timestamps derived from job data, not run-level API fields. | ||||||||||||||||||||||||||||||||
| /// Mirrors ci-dash.py's `first_job_started_at` / `last_job_completed_at`. | ||||||||||||||||||||||||||||||||
| var firstJobStartedAt: Date? | ||||||||||||||||||||||||||||||||
| /// Last job completion time across all runs in this group. | ||||||||||||||||||||||||||||||||
| var lastJobCompletedAt: Date? | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// Fallback creation time from the representative run. | ||||||||||||||||||||||||||||||||
|
|
@@ -116,77 +122,57 @@ struct ActionGroup: Identifiable { | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// Name of the first in-progress job, or first queued, or "—". | ||||||||||||||||||||||||||||||||
| var currentJobName: String { | ||||||||||||||||||||||||||||||||
| if let j = jobs.first(where: { $0.status == "in_progress" }) { return j.name } | ||||||||||||||||||||||||||||||||
| if let j = jobs.first(where: { $0.status == "queued" }) { return j.name } | ||||||||||||||||||||||||||||||||
| if let job = jobs.first(where: { $0.status == "in_progress" }) { return job.name } | ||||||||||||||||||||||||||||||||
| if let job = jobs.first(where: { $0.status == "queued" }) { return job.name } | ||||||||||||||||||||||||||||||||
| return "—" | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// How long ago the group started, as a short human string, e.g. "3m ago", "1h ago". | ||||||||||||||||||||||||||||||||
| /// Uses `firstJobStartedAt` when available, falls back to `createdAt`. | ||||||||||||||||||||||||||||||||
| /// Returns "—" if neither timestamp is available. | ||||||||||||||||||||||||||||||||
| /// Delegates to `RelativeTimeFormatter` for testable, injectable formatting. | ||||||||||||||||||||||||||||||||
| var startedAgo: String { | ||||||||||||||||||||||||||||||||
| guard let ref = firstJobStartedAt ?? createdAt else { return "—" } | ||||||||||||||||||||||||||||||||
| return RelativeTimeFormatter.string(from: ref) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// Elapsed time derived from min(job.startedAt) → max(job.completedAt). | ||||||||||||||||||||||||||||||||
| var elapsed: String { | ||||||||||||||||||||||||||||||||
| if let start = firstJobStartedAt { | ||||||||||||||||||||||||||||||||
| let end = lastJobCompletedAt ?? Date() | ||||||||||||||||||||||||||||||||
| let sec = Int(end.timeIntervalSince(start)) | ||||||||||||||||||||||||||||||||
| guard sec >= 0 else { return "00:00" } | ||||||||||||||||||||||||||||||||
| let m = sec / 60; let s = sec % 60 | ||||||||||||||||||||||||||||||||
| return String(format: "%02d:%02d", m, s) | ||||||||||||||||||||||||||||||||
| let minutes = sec / 60; let seconds = sec % 60 | ||||||||||||||||||||||||||||||||
| return String(format: "%02d:%02d", minutes, seconds) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| guard let start = createdAt else { return "00:00" } | ||||||||||||||||||||||||||||||||
| let sec = Int(Date().timeIntervalSince(start)) | ||||||||||||||||||||||||||||||||
| guard sec >= 0 else { return "00:00" } | ||||||||||||||||||||||||||||||||
| let m = sec / 60; let s = sec % 60 | ||||||||||||||||||||||||||||||||
| return String(format: "%02d:%02d", m, s) | ||||||||||||||||||||||||||||||||
| let minutes = sec / 60; let seconds = sec % 60 | ||||||||||||||||||||||||||||||||
| return String(format: "%02d:%02d", minutes, seconds) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // MARK: - Codable helpers (private to this file) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private struct ActionRunsResponse: Codable { | ||||||||||||||||||||||||||||||||
| let workflowRuns: [RunPayload] | ||||||||||||||||||||||||||||||||
| enum CodingKeys: String, CodingKey { case workflowRuns = "workflow_runs" } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private struct RunPayload: Codable { | ||||||||||||||||||||||||||||||||
| let id: Int | ||||||||||||||||||||||||||||||||
| let name: String | ||||||||||||||||||||||||||||||||
| let status: String | ||||||||||||||||||||||||||||||||
| let conclusion: String? | ||||||||||||||||||||||||||||||||
| let headBranch: String? | ||||||||||||||||||||||||||||||||
| let headSha: String | ||||||||||||||||||||||||||||||||
| let displayTitle: String? | ||||||||||||||||||||||||||||||||
| let createdAt: String? | ||||||||||||||||||||||||||||||||
| let updatedAt: String? | ||||||||||||||||||||||||||||||||
| let htmlUrl: String? | ||||||||||||||||||||||||||||||||
| let headCommit: HeadCommit? | ||||||||||||||||||||||||||||||||
| let pullRequests: [PRRef]? | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| enum CodingKeys: String, CodingKey { | ||||||||||||||||||||||||||||||||
| case id, name, status, conclusion | ||||||||||||||||||||||||||||||||
| case headBranch = "head_branch" | ||||||||||||||||||||||||||||||||
| case headSha = "head_sha" | ||||||||||||||||||||||||||||||||
| case displayTitle = "display_title" | ||||||||||||||||||||||||||||||||
| case createdAt = "created_at" | ||||||||||||||||||||||||||||||||
| case updatedAt = "updated_at" | ||||||||||||||||||||||||||||||||
| case htmlUrl = "html_url" | ||||||||||||||||||||||||||||||||
| case headCommit = "head_commit" | ||||||||||||||||||||||||||||||||
| case pullRequests = "pull_requests" | ||||||||||||||||||||||||||||||||
| // MARK: - Progress fraction (model layer — keeps view bodies clean) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// Completion fraction 0.0–1.0 for the pie-progress dot, or `nil` (indeterminate) | ||||||||||||||||||||||||||||||||
| /// when status is queued or no job data is available. | ||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||
| /// - `nil` → indeterminate (queued / no jobs loaded yet) | ||||||||||||||||||||||||||||||||
| /// - `1.0` → completed | ||||||||||||||||||||||||||||||||
| /// - `0..<1` → partial (jobsDone / jobsTotal) | ||||||||||||||||||||||||||||||||
| var progressFraction: Double? { | ||||||||||||||||||||||||||||||||
| switch groupStatus { | ||||||||||||||||||||||||||||||||
| case .queued: return nil | ||||||||||||||||||||||||||||||||
| case .completed: return 1.0 | ||||||||||||||||||||||||||||||||
| case .inProgress: | ||||||||||||||||||||||||||||||||
| guard jobsTotal > 0 else { return nil } | ||||||||||||||||||||||||||||||||
| return Double(jobsDone) / Double(jobsTotal) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 ActionGroup.Equatable: runs compared by ID array only, not status/conclusion
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private struct HeadCommit: Codable { let message: String } | ||||||||||||||||||||||||||||||||
| private struct PRRef: Codable { let number: Int } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // MARK: - PR label | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 ActionGroup.Equatable conformance line exceeds line_length rule and is hard to maintain The custom == implementation packs four conditions onto one line, exceeding the configured line_length warning threshold and making it hard to verify correctness. The runs comparison uses map({ $0.id }) == which works but allocates two arrays on every equality check — common during SwiftUI diffing. A minor readability and allocation concern.
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| /// Derives the short identifier for an action group row. | ||||||||||||||||||||||||||||||||
| /// Priority: PR number → branch-embedded number → sha[:7]. | ||||||||||||||||||||||||||||||||
| private func prLabel(from run: RunPayload) -> String { | ||||||||||||||||||||||||||||||||
| if let pr = run.pullRequests?.first { return "#\(pr.number)" } | ||||||||||||||||||||||||||||||||
| if let branch = run.headBranch, | ||||||||||||||||||||||||||||||||
| let range = branch.range(of: #"/(\d+)/"#, options: .regularExpression) { | ||||||||||||||||||||||||||||||||
| let digits = branch[range].filter { $0.isNumber } | ||||||||||||||||||||||||||||||||
| return "#\(digits)" | ||||||||||||||||||||||||||||||||
| static func == (lhs: ActionGroup, rhs: ActionGroup) -> Bool { | ||||||||||||||||||||||||||||||||
| lhs.id == rhs.id && lhs.isDimmed == rhs.isDimmed && lhs.jobs == rhs.jobs && lhs.runs.map({ $0.id }) == rhs.runs.map({ $0.id }) | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 ActionGroup.== uses runs.map({$0.id}) comparison which ignores run status/conclusion changes The custom Equatable implementation compares lhs.id, isDimmed, jobs, and runs.map({$0.id}). It does NOT compare run status or conclusion. A run transitioning from in_progress to completed changes its status and conclusion but not its id — so lhs == rhs would return true and SwiftUI's .onChange(of: store.actions) in PopoverMainView would NOT fire, leaving visibleCount unreset and the dot color stale until the next full poll that changes the jobs array. Since ActionGroup.id is headSha and isDimmed captures dimming, consider adding run status/conclusion into the equality check or removing == entirely and relying on the default synthesised comparison (if all stored properties are Equatable).
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return String(run.headSha.prefix(7)) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 WorkflowRunRef missing Equatable — ActionGroup.== accesses run IDs but WorkflowRunRef has no conformance
Suggested change
AI Fix PromptThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 ActionGroup.== compares runs by ID array — order-sensitive, not set equality The Equatable implementation uses lhs.runs.map({ $0.id }) == rhs.runs.map({ $0.id }), which is order-sensitive array equality. If fetchActionGroups returns runs in a different order between polls (e.g. after server-side sorting changes), two logically identical groups will compare as not-equal, causing onChange to fire and visibleCount to reset unnecessarily.
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // MARK: - Fetch + Group | ||||||||||||||||||||||||||||||||
|
|
@@ -299,27 +285,27 @@ func fetchActionGroups(for scope: String, cache: [String: ActionGroup] = [:]) -> | |||||||||||||||||||||||||||||||
| // MARK: - Private helpers | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// Constructs an `ActiveJob` from a decoded `JobPayload`. | ||||||||||||||||||||||||||||||||
| func makeActiveJob(from j: JobPayload, iso: ISO8601DateFormatter, | ||||||||||||||||||||||||||||||||
| func makeActiveJob(from payload: JobPayload, iso: ISO8601DateFormatter, | ||||||||||||||||||||||||||||||||
| isDimmed: Bool = false) -> ActiveJob { | ||||||||||||||||||||||||||||||||
| let steps: [JobStep] = (j.steps ?? []).enumerated().map { idx, s in | ||||||||||||||||||||||||||||||||
| let steps: [JobStep] = (payload.steps ?? []).enumerated().map { idx, step in | ||||||||||||||||||||||||||||||||
| JobStep( | ||||||||||||||||||||||||||||||||
| id: idx + 1, | ||||||||||||||||||||||||||||||||
| name: s.name, | ||||||||||||||||||||||||||||||||
| status: s.status, | ||||||||||||||||||||||||||||||||
| conclusion: s.conclusion, | ||||||||||||||||||||||||||||||||
| startedAt: s.startedAt.flatMap { iso.date(from: $0) }, | ||||||||||||||||||||||||||||||||
| completedAt: s.completedAt.flatMap { iso.date(from: $0) } | ||||||||||||||||||||||||||||||||
| name: step.name, | ||||||||||||||||||||||||||||||||
| status: step.status, | ||||||||||||||||||||||||||||||||
| conclusion: step.conclusion, | ||||||||||||||||||||||||||||||||
| startedAt: step.startedAt, | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 JobStep.startedAt / completedAt silently changed from String? to Date? — makeActiveJob call in enrichStepsIfNeeded passes raw String fields In this diff, makeActiveJob now passes step.startedAt and step.completedAt directly (without iso.date(from:)) because the diff removes the flatMap conversion for JobStep fields. However, JobPayload.steps is typed as [JobStep]? and JobStep inherits its field types from the struct definition. If JobStep.startedAt and .completedAt were changed from String? to Date? (as suggested by the removal of iso.date(from:) calls), then JobPayload must also decode them as Date? — but JobPayload.steps is decoded from JSON which delivers ISO 8601 strings, not Date objects. This would cause a silent decode failure (steps array decoded as nil/empty) every time enrichStepsIfNeeded runs, causing step data to never populate. Verify that JobStep's Decodable conformance includes a custom date-decoding strategy, or revert the step field types to String? and restore the flatMap.
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| completedAt: step.completedAt | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 JobStep.startedAt/completedAt type change silently drops ISO parsing errors In makeActiveJob(), the step construction previously called iso.date(from: $0) on startedAt/completedAt strings (String? → Date?), matching JobStep's original field types. The diff now passes step.startedAt and step.completedAt directly without parsing. If JobStep.startedAt/completedAt were changed from String? to Date? in ActiveJob.swift (the JobPayload's steps are [JobStep]?), then JobPayload must also return Date? for those fields — but JobPayload.steps is typed as [JobStep]? and JobStep is Decodable. Since JobPayload decodes JSON from the GitHub API (which returns ISO strings), JobStep's Decodable conformance must now parse dates itself. If JobStep's CodingKeys / init(from:) don't implement custom ISO 8601 decoding, the dates will decode as nil silently, breaking elapsed time display and step progress in InlineJobRowView and StepLogView. Verify that JobStep has a custom Decodable implementation that handles the GitHub ISO 8601 format (including fractional seconds), or revert to explicit iso.date(from:) parsing in makeActiveJob(). AI Fix Prompt |
||||||||||||||||||||||||||||||||
| return ActiveJob( | ||||||||||||||||||||||||||||||||
| id: j.id, | ||||||||||||||||||||||||||||||||
| name: j.name, | ||||||||||||||||||||||||||||||||
| status: j.status, | ||||||||||||||||||||||||||||||||
| conclusion: j.conclusion, | ||||||||||||||||||||||||||||||||
| startedAt: j.startedAt.flatMap { iso.date(from: $0) }, | ||||||||||||||||||||||||||||||||
| createdAt: j.createdAt.flatMap { iso.date(from: $0) }, | ||||||||||||||||||||||||||||||||
| completedAt: j.completedAt.flatMap { iso.date(from: $0) }, | ||||||||||||||||||||||||||||||||
| htmlUrl: j.htmlUrl, | ||||||||||||||||||||||||||||||||
| id: payload.id, | ||||||||||||||||||||||||||||||||
| name: payload.name, | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 makeActiveJob step dates passed as String? instead of Date? after refactor In the refactored makeActiveJob, the step construction now passes
Suggested change
AI Fix PromptThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 JobStep date strings passed raw — ISO parsing removed only for steps makeActiveJob previously called iso.date(from:) on step.startedAt and step.completedAt before passing them to JobStep. This PR changes the call to pass step.startedAt and step.completedAt directly. However, JobStep stores these as String? (not Date?), so they are already raw strings — the iso.date(from:) calls were wrong before (JobStep doesn't have Date fields). Verify that JobStep.startedAt / JobStep.completedAt are indeed String? and that no elapsed-time computation relies on them being Date. If JobStep properties were Date?, this would silently store nil for all step timestamps.
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| status: payload.status, | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 JobStep.startedAt / completedAt type changed from String? to Date? — makeActiveJob skips ISO parsing In the diff, makeActiveJob now assigns step.startedAt and step.completedAt directly (without flatMap { iso.date(from: $0) }). This is only correct if JobStep.startedAt/completedAt are already typed as Date?. If they remain String? (as the diff for ActiveJob.swift shows JobPayload still has String? fields), this will be a type error at compile time — or worse, if JobStep was silently changed to Date? without updating Codable conformance, step timestamps will decode as nil from JSON. Verify that JobStep's stored type matches what makeActiveJob expects.
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| conclusion: payload.conclusion, | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 Step timestamps silently dropped — ISO parse removed from makeActiveJob In the refactored
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| startedAt: payload.startedAt.flatMap { iso.date(from: $0) }, | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 JobStep.startedAt / completedAt type change may silently drop timestamps In
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| createdAt: payload.createdAt.flatMap { iso.date(from: $0) }, | ||||||||||||||||||||||||||||||||
| completedAt: payload.completedAt.flatMap { iso.date(from: $0) }, | ||||||||||||||||||||||||||||||||
| htmlUrl: payload.htmlUrl, | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 JobStep.startedAt/completedAt type changed silently — verify callers handle Date? not String? In
Suggested change
AI Fix Prompt |
||||||||||||||||||||||||||||||||
| isDimmed: isDimmed, | ||||||||||||||||||||||||||||||||
| steps: steps | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
|
@@ -381,4 +367,4 @@ private func statusPriority(_ status: GroupStatus) -> Int { | |||||||||||||||||||||||||||||||
| case .completed: return 2 | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // swiftlint:enable opening_brace identifier_name missing_docs orphaned_doc_comment | ||||||||||||||||||||||||||||||||
| // swiftlint:enable opening_brace identifier_name orphaned_doc_comment missing_docs | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 ActionGroup.Equatable omits firstJobStartedAt — spurious equality causes stale inline job rows
The custom == implementation checks id, isDimmed, jobs, and runs.map(.id) but omits firstJobStartedAt (and lastJobCompletedAt). ActionRowView uses .onChange(of: store.actions) to reset visibleCount; if firstJobStartedAt changes (a new job starts timing) but no job is added/removed, the groups compare as equal. The bigger risk is that SwiftUI may skip re-rendering action rows whose timing data changed, keeping elapsed/startedAgo labels stale until the next structural change.
AI Fix Prompt