Commit c5854942bc

c5854942bc4afa296913b0f1bab526dd68b0668e

parent: 6ba2e50325

Unsigned

cmc <hello@cleberg.net> · 2026-04-12 16:49 UTC

feat: add prioritization of polling hutch-stats for logged in user

Layout: unified · split

Hutch.xcodeproj/project.pbxproj +6 −6
@@ -532,7 +532,7 @@
532 "$(inherited)", 532 "$(inherited)",
533 "@executable_path/Frameworks", 533 "@executable_path/Frameworks",
534 ); 534 );
535 MARKETING_VERSION = 2.15.1; 535 MARKETING_VERSION = 2.15.2;
536 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; 536 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
537 PRODUCT_NAME = "$(TARGET_NAME)"; 537 PRODUCT_NAME = "$(TARGET_NAME)";
538 STRING_CATALOG_GENERATE_SYMBOLS = YES; 538 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -569,7 +569,7 @@
569 "$(inherited)", 569 "$(inherited)",
570 "@executable_path/Frameworks", 570 "@executable_path/Frameworks",
571 ); 571 );
572 MARKETING_VERSION = 2.15.1; 572 MARKETING_VERSION = 2.15.2;
573 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; 573 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
574 PRODUCT_NAME = "$(TARGET_NAME)"; 574 PRODUCT_NAME = "$(TARGET_NAME)";
575 STRING_CATALOG_GENERATE_SYMBOLS = YES; 575 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
632 APPLICATION_EXTENSION_API_ONLY = YES; 632 APPLICATION_EXTENSION_API_ONLY = YES;
633 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; 633 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
634 CODE_SIGN_STYLE = Automatic; 634 CODE_SIGN_STYLE = Automatic;
635 CURRENT_PROJECT_VERSION = 33; 635 CURRENT_PROJECT_VERSION = 34;
636 DEVELOPMENT_TEAM = ZCNAX3VL9D; 636 DEVELOPMENT_TEAM = ZCNAX3VL9D;
637 GENERATE_INFOPLIST_FILE = NO; 637 GENERATE_INFOPLIST_FILE = NO;
638 INFOPLIST_FILE = HutchWidgetExtension/Info.plist; 638 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
642 "@executable_path/Frameworks", 642 "@executable_path/Frameworks",
643 "@executable_path/../../Frameworks", 643 "@executable_path/../../Frameworks",
644 ); 644 );
645 MARKETING_VERSION = 2.15.1; 645 MARKETING_VERSION = 2.15.2;
646 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; 646 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
647 PRODUCT_NAME = "$(TARGET_NAME)"; 647 PRODUCT_NAME = "$(TARGET_NAME)";
648 SKIP_INSTALL = YES; 648 SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
661 APPLICATION_EXTENSION_API_ONLY = YES; 661 APPLICATION_EXTENSION_API_ONLY = YES;
662 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; 662 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
663 CODE_SIGN_STYLE = Automatic; 663 CODE_SIGN_STYLE = Automatic;
664 CURRENT_PROJECT_VERSION = 33; 664 CURRENT_PROJECT_VERSION = 34;
665 DEVELOPMENT_TEAM = ZCNAX3VL9D; 665 DEVELOPMENT_TEAM = ZCNAX3VL9D;
666 GENERATE_INFOPLIST_FILE = NO; 666 GENERATE_INFOPLIST_FILE = NO;
667 INFOPLIST_FILE = HutchWidgetExtension/Info.plist; 667 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
671 "@executable_path/Frameworks", 671 "@executable_path/Frameworks",
672 "@executable_path/../../Frameworks", 672 "@executable_path/../../Frameworks",
673 ); 673 );
674 MARKETING_VERSION = 2.15.1; 674 MARKETING_VERSION = 2.15.2;
675 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; 675 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
676 PRODUCT_NAME = "$(TARGET_NAME)"; 676 PRODUCT_NAME = "$(TARGET_NAME)";
677 SKIP_INSTALL = YES; 677 SKIP_INSTALL = YES;
Hutch/Networking/HutchStatsService.swift +14 −5
@@ -9,20 +9,23 @@ struct HutchStatsService: ContributionCalendarServing {
9 private let session: URLSession 9 private let session: URLSession
10 private let decoder: JSONDecoder 10 private let decoder: JSONDecoder
11 private let baseURL: URL 11 private let baseURL: URL
12 private let currentActor: String?
12 13
13 init( 14 init(
14 session: URLSession = .shared, 15 session: URLSession = .shared,
15 configuration: AppConfiguration 16 configuration: AppConfiguration,
17 currentActor: String? = nil
16 ) { 18 ) {
17 self.session = session 19 self.session = session
18 self.baseURL = configuration.hutchStatsBaseURL 20 self.baseURL = configuration.hutchStatsBaseURL
19 self.decoder = JSONDecoder() 21 self.decoder = JSONDecoder()
22 self.currentActor = currentActor
20 } 23 }
21 24
22 func fetchContributionCalendar(actor: String, endingOn endDate: Date) async throws -> ContributionCalendarResponse { 25 func fetchContributionCalendar(actor: String, endingOn endDate: Date) async throws -> ContributionCalendarResponse {
23 return try await fetch( 26 return try await fetch(
24 path: "api/contributions/\(actor)", 27 path: "api/contributions/\(actor)",
25 queryItems: trailingYearQueryItems(endingOn: endDate), 28 queryItems: contributionQueryItems(actor: actor, endingOn: endDate),
26 responseType: ContributionCalendarResponse.self 29 responseType: ContributionCalendarResponse.self
27 ) 30 )
28 } 31 }
@@ -30,7 +33,7 @@ struct HutchStatsService: ContributionCalendarServing {
30 func fetchContributionStats(actor: String, endingOn endDate: Date) async throws -> ContributionStatsResponse { 33 func fetchContributionStats(actor: String, endingOn endDate: Date) async throws -> ContributionStatsResponse {
31 return try await fetch( 34 return try await fetch(
32 path: "api/contributions/\(actor)/stats", 35 path: "api/contributions/\(actor)/stats",
33 queryItems: trailingYearQueryItems(endingOn: endDate), 36 queryItems: contributionQueryItems(actor: actor, endingOn: endDate),
34 responseType: ContributionStatsResponse.self 37 responseType: ContributionStatsResponse.self
35 ) 38 )
36 } 39 }
@@ -86,15 +89,21 @@ struct HutchStatsService: ContributionCalendarServing {
86 return normalizedStartDate...normalizedEndDate 89 return normalizedStartDate...normalizedEndDate
87 } 90 }
88 91
89 private func trailingYearQueryItems(endingOn endDate: Date) -> [URLQueryItem] { 92 func contributionQueryItems(actor: String, endingOn endDate: Date) -> [URLQueryItem] {
90 let range = trailingRange(endingOn: endDate) 93 let range = trailingRange(endingOn: endDate)
91 let start = Self.rangeFormatter.string(from: range.lowerBound) 94 let start = Self.rangeFormatter.string(from: range.lowerBound)
92 let end = Self.rangeFormatter.string(from: range.upperBound) 95 let end = Self.rangeFormatter.string(from: range.upperBound)
93 96
94 return [ 97 var queryItems = [
95 URLQueryItem(name: "from", value: start), 98 URLQueryItem(name: "from", value: start),
96 URLQueryItem(name: "to", value: end) 99 URLQueryItem(name: "to", value: end)
97 ] 100 ]
101
102 if actor == currentActor {
103 queryItems.append(URLQueryItem(name: "prioritize", value: "self"))
104 }
105
106 return queryItems
98 } 107 }
99 108
100 private static let rangeFormatter: DateFormatter = { 109 private static let rangeFormatter: DateFormatter = {
Hutch/Views/Lookup/UserProfileView.swift +4 −1
@@ -175,7 +175,10 @@ struct UserProfileView: View {
175 ownerUsername: owner, 175 ownerUsername: owner,
176 actor: actor, 176 actor: actor,
177 client: appState.client, 177 client: appState.client,
178 statsService: HutchStatsService(configuration: appState.configuration) 178 statsService: HutchStatsService(
179 configuration: appState.configuration,
180 currentActor: appState.currentUser?.canonicalName
181 )
179 ) 182 )
180 profileViewModel = newViewModel 183 profileViewModel = newViewModel
181 vm = newViewModel 184 vm = newViewModel
Hutch/Views/More/ProfileView.swift +4 −1
@@ -46,7 +46,10 @@ struct ProfileView: View {
46 ownerUsername: owner, 46 ownerUsername: owner,
47 actor: actor, 47 actor: actor,
48 client: appState.client, 48 client: appState.client,
49 statsService: HutchStatsService(configuration: appState.configuration) 49 statsService: HutchStatsService(
50 configuration: appState.configuration,
51 currentActor: appState.currentUser?.canonicalName
52 )
50 ) 53 )
51 contributionViewModel = newViewModel 54 contributionViewModel = newViewModel
52 vm = newViewModel 55 vm = newViewModel
HutchTests/ContributionCalendarTests.swift +24
@@ -93,6 +93,30 @@ struct ContributionCalendarTests {
93 #expect(range.upperBound == endDate) 93 #expect(range.upperBound == endDate)
94 } 94 }
95 95
96 @Test
97 func selfContributionRequestsIncludeExplicitPrioritySignal() {
98 let service = HutchStatsService(
99 configuration: AppConfiguration(),
100 currentActor: "~alice"
101 )
102 let endDate = ContributionDateParser.parse("2026-04-11")!
103 let queryItems = service.contributionQueryItems(actor: "~alice", endingOn: endDate)
104
105 #expect(queryItems.contains(URLQueryItem(name: "prioritize", value: "self")))
106 }
107
108 @Test
109 func otherContributionRequestsDoNotIncludePrioritySignal() {
110 let service = HutchStatsService(
111 configuration: AppConfiguration(),
112 currentActor: "~alice"
113 )
114 let endDate = ContributionDateParser.parse("2026-04-11")!
115 let queryItems = service.contributionQueryItems(actor: "~bob", endingOn: endDate)
116
117 #expect(queryItems.contains(URLQueryItem(name: "prioritize", value: "self")) == false)
118 }
119
96 @Test 120 @Test
97 func contributionIntensityBucketsMatchProductRules() { 121 func contributionIntensityBucketsMatchProductRules() {
98 #expect(ContributionIntensity(count: 0) == .empty) 122 #expect(ContributionIntensity(count: 0) == .empty)