Compare commits

..

27 Commits

Author SHA1 Message Date
Dean Wenstrand
53fa982bcf GetDrillRunLeaderboard: table/pocket filter vars + setup fields
All checks were successful
Tests / Tests (pull_request) Successful in 9s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:19:37 -07:00
Dean Wenstrand
7ec6ec9193 GetMyDrillRuns: include run date (video.createdAt)
All checks were successful
Tests / Tests (pull_request) Successful in 9s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 19:42:02 -07:00
Dean Wenstrand
0ce333525c Add GetMyDrillRuns query + generated types
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 19:33:56 -07:00
Dean Wenstrand
d1ed5b308b Add GetDrillRunLeaderboard query + generated types
All checks were successful
Tests / Tests (pull_request) Successful in 9s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:42:52 -07:00
af68bc3c60 Add Apple subscription sync schema
All checks were successful
Tests / Tests (pull_request) Successful in 10s
2026-06-11 18:40:54 -07:00
f12d2c5a1b Merge pull request 'Add Apple entitlement source enum' (#252) from apple-entitlement-source-enum into master
Reviewed-on: #252
2026-06-11 21:55:26 +00:00
4e3f649a2c Add Apple entitlement source enum
All checks were successful
Tests / Tests (pull_request) Successful in 28s
2026-06-11 12:43:03 -07:00
7cef75b2ca Merge pull request 'dean/get-video-card-query' (#250) from dean/get-video-card-query into master
Reviewed-on: #250
2026-06-09 05:05:52 +00:00
Dean Wenstrand
6d5cd9b1ed Add lean GetLastSessionDate + GetShotClipRanges queries
All checks were successful
Tests / Tests (pull_request) Successful in 24s
- GetLastSessionDate: most-recent session startTime only, for the Home
  recency nudge (was pulling the full VideoCardFields payload).
- GetShotClipRanges / ShotClipRange: per-shot frame/time window only, for
  the inline session player's condensed playback (was pulling
  ShotWithAllFeatures — features, serialized paths, annotations — x500).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:27:42 -07:00
Dean Wenstrand
d59e21c10e Add GetVideoCard query (single-video VideoCardFields)
Returns the full VideoCardFields fragment for one video id, so the
session-detail meta header can share one source of truth (and the
normalized Apollo cache) with the feed card instead of stitching
together GetVideoDetails + GetVideoSocialDetailsById.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:04:33 -07:00
9358205327 Merge pull request 'Add quotaEnforcementEnabled to deployed config' (#249) from loewy/expose-quota-enforcement-config into master
Reviewed-on: #249
2026-05-28 23:18:01 +00:00
88634a32e9 add quotaEnforcementEnabled to deployed config
All checks were successful
Tests / Tests (pull_request) Successful in 10s
2026-05-28 14:52:51 -07:00
f7200a2e9f Merge pull request 'Add upload quota status operation' (#248) from loewy/quota-status-buckets into master
Reviewed-on: #248
2026-05-28 21:46:56 +00:00
7d839c0fa6 Add upload quota status operation
All checks were successful
Tests / Tests (pull_request) Successful in 10s
2026-05-27 16:52:43 -07:00
ab1e604871 Merge pull request 'Add quota bucket status fields' (#247) from loewy/quota-status-buckets into master
Reviewed-on: #247
2026-05-26 02:47:40 +00:00
deb724b430 Add quota bucket status fields
All checks were successful
Tests / Tests (pull_request) Successful in 9s
2026-05-25 16:26:57 -07:00
c586bdf1a6 Merge pull request 'Add username + profileImageUri to PlayerClusterGQL' (#246) from dean/player-cluster-username into master
Reviewed-on: #246
2026-05-12 00:27:06 +00:00
Dean Wenstrand
9250e4c639 Add username + profileImageUri to PlayerClusterGQL
All checks were successful
Tests / Tests (pull_request) Successful in 9s
The labeling UI was falling back to "user N" for any assigned cluster
whose user wasn't the video owner — e.g. an admin re-labels a video
they don't own. With the resolver now resolving usernames for every
confirmed cluster, the FE can render real names regardless of who's
viewing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:25:34 -07:00
a12b3e1210 Merge pull request 'Add averageDifficulty to PlayerSummaryFields' (#245) from dean/player-summaries-avg-difficulty-types into master
Reviewed-on: #245
2026-05-11 22:41:52 +00:00
Dean Wenstrand
5cf2dbaf01 Add averageDifficulty to PlayerSummaryFields
All checks were successful
Tests / Tests (pull_request) Successful in 9s
Regenerated via `just gql` after BE added average_difficulty to
PlayerSummaryGQL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:34:25 -07:00
239a143554 Merge pull request 'Regenerate schema + add longestRun to PlayerSummaryFields' (#244) from dean/player-summaries-longest-run-types into master
Reviewed-on: #244
2026-05-11 20:29:39 +00:00
Dean Wenstrand
296522afb8 Regenerate schema + add longestRun to PlayerSummaryFields
All checks were successful
Tests / Tests (pull_request) Successful in 10s
Generated by `just gql` after BE added longest_run to PlayerSummaryGQL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:21:47 -07:00
f42579076e Merge pull request 'Add score to PlayerSummaryFields + PlayerClusterFields fragments' (#243) from dean/video-match-score-types into master
Reviewed-on: #243
2026-05-11 17:43:43 +00:00
Dean Wenstrand
0c9eb4945a Add score to PlayerSummaryFields + PlayerClusterFields fragments
All checks were successful
Tests / Tests (pull_request) Successful in 9s
Picks up the BE additions in railbird PR dean/video-match-score:
new `score` field on PlayerClusterGQL and PlayerSummaryGQL, and
a new `score` input field on ClusterAssignmentInput.

Generated by `just gql`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:22:58 -07:00
1182c15004 Merge pull request 'Add playerSummaries to schema + Video fragments' (#242) from dean/player-summaries-types into master
Reviewed-on: #242
2026-05-09 19:34:40 +00:00
Dean Wenstrand
755336b16a Add playerSummaries to schema + Video fragments
All checks were successful
Tests / Tests (pull_request) Successful in 10s
Backs the multi-player vs UI: feed cards and the detail page both
read `video.playerSummaries`, a per-cluster rollup with username,
profile image, representative full-frame URL, makes/total/percentage.

  - PlayerSummaryFields fragment in shooter.gql
  - VideoCardFields (feed) and GetVideoDetails (detail) include
    playerSummaries via the new fragment
  - VideoCardFields tag selection extended to include tagClasses,
    needed for the FE's player_count detection

Generated by `just gql` from the BE additions in railbird PR
dean/video-player-summaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:02:57 -07:00
c1efe9f5f2 Merge pull request 'Add VideoPlayerClusters query + FinalizePlayerAssignments mutation' (#241) from dean/player-labeling-ops into master
Reviewed-on: #241
2026-05-09 04:46:10 +00:00
9 changed files with 1270 additions and 3 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ query getDeployedConfig {
firebase
minimumAllowedAppVersion
subscriptionGatingEnabled
quotaEnforcementEnabled
bannerMessages {
color
dismissible

View File

@@ -47,6 +47,12 @@ fragment VideoCardFields on VideoGQL {
pocketSize
tags {
name
tagClasses {
name
}
}
playerSummaries {
...PlayerSummaryFields
}
currentProcessing {
id
@@ -95,6 +101,27 @@ query GetVideoFeedSessionCount(
}
}
# Minimal query for the Home recency nudge ("you haven't recorded in N days").
# Only the most recent session's start time — avoids pulling the full
# VideoCardFields payload (reactions, comments, player summaries, etc.).
query GetLastSessionDate(
$filters: VideoFilterInput = null
$includePrivate: IncludePrivateEnum = MINE
$feedInput: VideoFeedInputGQL = null
) {
getFeedVideos(
limit: 1
filters: $filters
includePrivate: $includePrivate
feedInput: $feedInput
) {
videos {
id
startTime
}
}
}
query GetVideoFeed(
$limit: Int! = 5
$after: String = null

View File

@@ -30,3 +30,56 @@ query GetRunsLeaderboard($interval: TimeInterval, $when: DateTime) {
}
}
}
query GetDrillRunLeaderboard(
$drillTag: String!
$interval: TimeInterval
$limit: Int = 50
$tableSizeMin: Float
$tableSizeMax: Float
$pocketSizeMin: Float
$pocketSizeMax: Float
) {
getDrillRunLeaderboard(
drillTag: $drillTag
interval: $interval
limit: $limit
tableSizeMin: $tableSizeMin
tableSizeMax: $tableSizeMax
pocketSizeMin: $pocketSizeMin
pocketSizeMax: $pocketSizeMax
) {
entries {
id
runLength
videoId
video {
tableSize
pocketSize
}
user {
id
username
profileImageUri
}
}
youRun {
id
runLength
videoId
}
youRank
totalPlayers
}
}
query GetMyDrillRuns($drillTag: String!, $limit: Int = 50) {
getMyDrillRuns(drillTag: $drillTag, limit: $limit) {
id
runLength
videoId
video {
createdAt
}
}
}

View File

@@ -1,3 +1,18 @@
fragment PlayerSummaryFields on PlayerSummaryGQL {
clusterId
userId
username
profileImageUri
representativeFullFrameUrl
totalShots
totalShotsMade
makePercentage
score
longestRun
averageDifficulty
averageTimeBetweenShots
}
fragment PlayerClusterShotFields on PlayerClusterShotGQL {
shotId
bboxX1
@@ -15,7 +30,10 @@ fragment PlayerClusterFields on PlayerClusterGQL {
clusterId
nShots
userId
username
profileImageUri
confirmed
score
shots {
...PlayerClusterShotFields
}

View File

@@ -132,6 +132,38 @@ query GetShotsByIds($ids: [Int!]!) {
}
}
# Lightweight clip boundaries for condensed session playback. The inline
# session player only needs each shot's frame/time window to seek between
# shots — this skips the heavy ShotWithAllFeatures payload (cue/pocketing
# features, serialized shot paths, annotations, nested video/playlist). The
# startTime/endTime @client resolvers derive their values from the frame
# fields + the video (looked up internally), so this is all they require.
fragment ShotClipRange on ShotGQL {
id
videoId
startFrame
endFrame
startTime @client
endTime @client
}
query GetShotClipRanges(
$filterInput: FilterInput!
$shotsOrdering: GetShotsOrdering
$limit: Int
) {
getOrderedShots(
filterInput: $filterInput
shotsOrdering: $shotsOrdering
limit: $limit
) {
count
shots {
...ShotClipRange
}
}
}
fragment ShotWithAllFeatures on ShotGQL {
id
videoId

View File

@@ -49,6 +49,39 @@ query GetUserPlayTime($userId: Int!, $filters: VideoFilterInput) {
}
}
query GetUploadQuotaStatus {
getQuotaStatus {
tierName
periodStart
periodEnd
durationUsedSeconds
durationLimitSeconds
maxVideoDurationSeconds
durationRemainingSeconds
canUpload
importQuotaBuckets {
quotaKey
appliesToUploadKind
periodStart
periodEnd
durationUsedSeconds
durationLimitSeconds
durationRemainingSeconds
canUpload
}
recordingQuotaBuckets {
quotaKey
appliesToUploadKind
periodStart
periodEnd
durationUsedSeconds
durationLimitSeconds
durationRemainingSeconds
canUpload
}
}
}
query getUsernames(
$matchString: String!
$limit: Int = null

View File

@@ -83,6 +83,9 @@ query GetVideoDetails($videoId: Int!) {
}
name
}
playerSummaries {
...PlayerSummaryFields
}
}
}
@@ -136,6 +139,15 @@ query GetVideoSocialDetailsById($videoId: Int!) {
}
}
# Full card payload for a single video — reuses the same VideoCardFields
# fragment the feed list uses, so the session-detail meta header shares one
# source of truth (and the normalized Apollo cache) with the feed card.
query GetVideoCard($videoId: Int!) {
getVideo(videoId: $videoId) {
...VideoCardFields
}
}
query GetVideos($videoIds: [Int!]!) {
getVideos(videoIds: $videoIds) {
...VideoStreamMetadata

View File

@@ -31,6 +31,16 @@ type Query {
limit: Int! = 50
requiredTags: [String!] = null
): RunLeaderboardGQL!
getDrillRunLeaderboard(
drillTag: String!
interval: TimeInterval = null
limit: Int! = 50
tableSizeMin: Float = null
tableSizeMax: Float = null
pocketSizeMin: Float = null
pocketSizeMax: Float = null
): DrillRunLeaderboardGQL!
getMyDrillRuns(drillTag: String!, limit: Int! = 50): [RunGQL!]!
getMakesLeaderboard(
interval: TimeInterval = null
when: DateTime = null
@@ -98,6 +108,7 @@ type Query {
): UserRelationshipsResult!
getAvailableSubscriptionOptions: StripeSubscriptionOptionsGQL!
getUserSubscriptionStatus: UserSubscriptionStatusGQL!
getAppleAppAccountToken: String!
getQuotaStatus: QuotaStatusGQL!
getPlayTime(userId: Int!, filters: VideoFilterInput = null): UserPlayTimeGQL!
getUserVideos(
@@ -406,6 +417,7 @@ type VideoGQL {
currentProcessing: VideoProcessingGQL
reactions: [ReactionGQL!]!
comments: [CommentGQL!]!
playerSummaries: [PlayerSummaryGQL!]!
}
type ShotGQL {
@@ -665,6 +677,21 @@ type CommentGQL {
replies: [CommentGQL!]!
}
type PlayerSummaryGQL {
clusterId: Int!
userId: Int
username: String
profileImageUri: String
representativeFullFrameUrl: String
totalShots: Int!
totalShotsMade: Int!
makePercentage: Float!
score: Int
longestRun: Int!
averageDifficulty: Float
averageTimeBetweenShots: Float
}
type DeployedConfigGQL {
allowNewUsers: Boolean!
firebase: Boolean!
@@ -672,6 +699,7 @@ type DeployedConfigGQL {
environment: String!
minimumAllowedAppVersion: String!
subscriptionGatingEnabled: Boolean!
quotaEnforcementEnabled: Boolean!
bannerMessages: [BannerGQL!]!
defaultAndroidRecordingFormat: StreamSegmentTypeEnum!
bucketUrl: String!
@@ -732,6 +760,13 @@ type RunLeaderboardGQL {
entries: [RunGQL!]!
}
type DrillRunLeaderboardGQL {
entries: [RunGQL!]!
youRun: RunGQL
youRank: Int
totalPlayers: Int!
}
type CountLeaderboardGQL {
entries: [UserShotCountEntry!]!
}
@@ -866,7 +901,10 @@ type PlayerClusterGQL {
clusterId: Int!
nShots: Int!
userId: Int
username: String
profileImageUri: String
confirmed: Boolean!
score: Int
shots: [PlayerClusterShotGQL!]!
}
@@ -1000,6 +1038,7 @@ enum EntitlementSourceTypeEnum {
ADMIN
MANUAL
STRIPE
APPLE
ALPHA_LEGACY
}
@@ -1021,6 +1060,19 @@ type QuotaStatusGQL {
durationUsedSeconds: Float!
durationLimitSeconds: Int
maxVideoDurationSeconds: Int
importQuotaBuckets: [QuotaBucketStatusGQL!]!
recordingQuotaBuckets: [QuotaBucketStatusGQL!]!
durationRemainingSeconds: Float
canUpload: Boolean!
}
type QuotaBucketStatusGQL {
quotaKey: String!
appliesToUploadKind: String!
periodStart: DateTime!
periodEnd: DateTime!
durationUsedSeconds: Float!
durationLimitSeconds: Int
durationRemainingSeconds: Float
canUpload: Boolean!
}
@@ -1142,6 +1194,9 @@ type Mutation {
retireTags(tagIds: [Int!]!): Boolean!
ensureStripeCustomerExists: UserGQL!
deleteUser: Boolean!
syncAppleSubscription(
input: SyncAppleSubscriptionInputGQL!
): SyncAppleSubscriptionResultGQL!
createSubscription(priceId: String!): CreateSubscriptionResultGQL!
createCustomerPortalSession: CreateCustomerPortalSessionResultGQL!
cancelSubscription: UserSubscriptionStatusGQL!
@@ -1199,6 +1254,7 @@ input FinalizePlayerAssignmentsInput {
input ClusterAssignmentInput {
clusterId: Int!
userId: Int = null
score: Int = null
}
input ShotMoveInput {
@@ -1294,6 +1350,26 @@ input EditUserInputGQL {
agreesToMarketing: Boolean = null
}
type SyncAppleSubscriptionResultGQL {
ok: Boolean!
errorCode: String
errorMessage: String
hasActiveSubscription: Boolean!
entitlementSource: EntitlementSourceTypeEnum
entitlementStartsAt: DateTime
entitlementEndsAt: DateTime
appleStatus: Int
originalTransactionId: String
latestTransactionId: String
productId: String
expiresAt: DateTime
}
input SyncAppleSubscriptionInputGQL {
signedTransactionInfo: String!
signedRenewalInfo: String = null
}
type CreateSubscriptionResultGQL {
checkoutUrl: String!
sessionId: String!