Compare commits

...

18 Commits

Author SHA1 Message Date
33707523f0 Add social SSO auth contract 2026-08-14 17:18:07 -07:00
308c60c93b Merge pull request 'Rename Session Coach feedback enums' (#323) from loewy/session-coach-feedback-enums into master
Reviewed-on: #323
2026-08-10 19:56:23 +00:00
19ba43ad6d Rename Session Coach feedback enums
All checks were successful
Tests / Tests (pull_request) Successful in 10s
2026-08-10 12:26:43 -07:00
03b565dc7d Merge pull request 'Let shot filters select a single player within a video' (#322) from dean/filter-by-player-index into master
Reviewed-on: #322
2026-08-06 20:21:37 +00:00
Dean Wenstrand
901f4222a4 Let shot filters select a single player within a video
All checks were successful
Tests / Tests (pull_request) Successful in 11s
Adds playerIndex to FilterInput, backing ShooterIdentity.player_index --
the cluster-assigned shooter, 0-based and stable only within one video.

Deliberately separate from userId, which is populated only once a cluster
has been confirmed against a registered account. Multiplayer sessions are
usually played against someone without a Railbird login, so filtering by
userId cannot express "the other player" in the common case; the cluster
index always can.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:14:00 -07:00
0f6e7b28d4 Merge pull request 'Return the shot's start frame image from getShotTableState' (#321) from ivan/shot-table-state-frame-image into master 2026-08-06 19:11:59 +00:00
9b6b08c1ec Return the shot's start frame image from getShotTableState
All checks were successful
Tests / Tests (pull_request) Successful in 12s
Shot Lab was showing the video's single thumbnail next to every shot's
layout. The backend now returns the frame the layout was detected in as
a base64 JPEG (frameImage), selected by the GetShotTableState operation.
Also dedupes ShareLinkGQL/CreateShotShareLinkInput, which appeared twice
in the committed schema after a bad merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:56:38 -07:00
0f01c71a43 Merge pull request 'Add Session Coach generation status query' (#320) from loewy/session-coach-status-polling-gql into master
Reviewed-on: #320
2026-08-06 06:40:20 +00:00
238ade1384 Add Session Coach generation status query
All checks were successful
Tests / Tests (pull_request) Successful in 40s
2026-08-05 12:08:51 -07:00
fe9b9ac9cc Merge pull request 'Select the detection audit payload on getTableState' (#315) from dean/shot-lab-detection-boxes-gql into master 2026-08-05 07:34:14 +00:00
0cf074b35b Merge pull request 'Revert premature Shot Lab GQL reconciliation (#317)' (#319) from loewy/revert-shot-boxes-gql-317 into master 2026-08-05 06:43:21 +00:00
e3d7a5aaac Revert "Merge pull request 'Restore Shot Lab detection box GraphQL fields' (#317) from loewy/fix-forward-shot-boxes-gql into master"
All checks were successful
Tests / Tests (pull_request) Successful in 1m8s
This reverts commit 65151d4b6b, reversing
changes made to 926b2aab7a.
2026-08-04 23:16:10 -07:00
65151d4b6b Merge pull request 'Restore Shot Lab detection box GraphQL fields' (#317) from loewy/fix-forward-shot-boxes-gql into master 2026-08-05 05:55:55 +00:00
15fe6e5c3d Restore Shot Lab detection box GraphQL fields
All checks were successful
Tests / Tests (pull_request) Successful in 1m39s
2026-08-04 22:45:33 -07:00
926b2aab7a Merge pull request 'Generate Session Coach feedback operations' (#313) from loewy/session-coach-feedback-gql into master
Reviewed-on: #313
2026-08-05 03:57:42 +00:00
bbe50cacb7 Generate Session Coach feedback operations
All checks were successful
Tests / Tests (pull_request) Successful in 29s
2026-08-04 15:07:56 -07:00
1953248639 Regenerate Session Coach feedback schema 2026-08-04 15:07:54 -07:00
594c89ab76 Select the detection audit payload on getTableState
All checks were successful
Tests / Tests (pull_request) Successful in 3m9s
Adds identifierToBox to TableStateGQL and selects it, alongside the
homography that was already in the schema but never requested, so the
photo path can draw a detection back over the image it came from. The
live and shot queries keep the layout-only selection: their frame lives
inside a video the app never holds, and one of them polls every five
seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:55:19 -07:00
4 changed files with 766 additions and 34 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,22 @@ query GetSessionCoach($videoId: Int!) {
}
}
query GetSessionCoachGenerationStatus($videoId: Int!, $generationId: ID!) {
getSessionCoachGenerationStatus(
videoId: $videoId
generationId: $generationId
) {
id
state
createdAt
updatedAt
startedAt
lastEnqueuedAt
enqueueCount
completedAt
}
}
mutation RequestSessionCoachGeneration($videoId: Int!) {
requestSessionCoachGeneration(videoId: $videoId) {
state
@@ -45,6 +61,21 @@ mutation RequestSessionCoachGeneration($videoId: Int!) {
}
}
mutation SubmitSessionCoachFeedback(
$videoId: Int!
$generationId: ID!
$response: SessionCoachFeedbackResponseEnum!
) {
submitSessionCoachFeedback(
videoId: $videoId
generationId: $generationId
response: $response
) {
response
updatedAt
}
}
fragment SessionCoachCandidateFields on SessionCoachCandidateGQL {
id
rank
@@ -117,4 +148,8 @@ fragment SessionCoachGenerationFields on SessionCoachGenerationGQL {
abstentionReason
synthesisRationale
}
feedback {
response
updatedAt
}
}

View File

@@ -1,3 +1,73 @@
fragment TableStateLayoutFields on TableStateGQL {
identifierToPosition
identifierToColor {
hex
}
}
fragment DetectionBoxFields on BoundingBoxGQL {
left
top
width
height
}
fragment DetectionPocketPointFields on PocketPointsGQL {
topLeft {
x
y
}
topSide {
x
y
}
topRight {
x
y
}
bottomLeft {
x
y
}
bottomSide {
x
y
}
bottomRight {
x
y
}
}
# Everything needed to redraw a detection over the image it came from:
# the detector's boxes plus the homography that turned them into plane
# positions. Boxes, pocket boxes, and source points share the crop's
# coordinate space; the crop locates that space in the full frame.
#
# Only the photo query selects this. The shot query returns its source
# frame (frameImage) but positions there come from the stored homography,
# so there are no fresh boxes to draw; the live query returns no image at
# all and is polled every five seconds.
fragment TableStateDetectionFields on TableStateGQL {
identifierToBox {
...DetectionBoxFields
}
homography {
crop {
...DetectionBoxFields
}
pockets {
...DetectionBoxFields
}
sourcePoints {
...DetectionPocketPointFields
}
destPoints {
...DetectionPocketPointFields
}
}
}
query GetTableState(
$b64Image: String!
$tableSize: Float
@@ -8,10 +78,8 @@ query GetTableState(
tableSize: $tableSize
useHomography: $useHomography
) {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
...TableStateDetectionFields
}
}
@@ -20,10 +88,7 @@ query GetLiveTableState($videoId: Int!) {
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
}
}
}
@@ -33,11 +98,11 @@ query GetShotTableState($shotId: Int!) {
shotId
videoId
frameIndex
# The b64 JPEG frame the layout was detected in, so the client can show
# the shot's actual starting frame instead of the video's thumbnail.
frameImage
tableState {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
}
}
}

View File

@@ -42,6 +42,25 @@ type AppleIapSubscriptionOptionsGQL {
productIds: [String!]!
}
type AuthBootstrapGQL {
status: AuthBootstrapStatusEnum!
user: UserGQL
}
enum AuthBootstrapStatusEnum {
REGISTERED
NEEDS_PROFILE
REGISTRATION_DISABLED
}
enum AuthRegistrationErrorEnum {
USERNAME_UNAVAILABLE
REGISTRATION_DISABLED
VERIFIED_EMAIL_REQUIRED
INVALID_USERNAME
UNSUPPORTED_PROVIDER
}
type BallTrajectoryGQL {
ballId: Int!
points: [TrajectoryPointGQL!]!
@@ -225,6 +244,11 @@ type CommentGQL {
replyToCommentId: Int
}
type CompleteAuthRegistrationGQL {
user: UserGQL
errorCode: AuthRegistrationErrorEnum
}
type CountLeaderboardGQL {
entries: [UserShotCountEntry!]!
}
@@ -250,11 +274,6 @@ input CreatePoolHallCameraInput {
claimTermsText: String = null
}
input CreateShotShareLinkInput {
shotIds: [Int!]!
paddingSeconds: Float = null
}
input CreatePoolHallInput {
name: String!
address: String = null
@@ -342,6 +361,7 @@ type DeployedConfigGQL {
storageLimitEnforcementEnabled: Boolean!
capabilityEnforcementEnabled: Boolean!
bannerMessages: [BannerGQL!]!
socialAuthProviderAvailability: [SocialAuthProviderAvailabilityGQL!]!
defaultAndroidRecordingFormat: StreamSegmentTypeEnum!
bucketUrl: String!
}
@@ -413,6 +433,7 @@ input FilterInput @oneOf {
userId: [Int!]
runId: [Int!]
username: [String!]
playerIndex: [Int!]
fargoRating: FloatRangeFilter
make: [Boolean!]
tags: [VideoTagInput!]
@@ -736,7 +757,16 @@ type Mutation {
shotId: Int!
fieldsToEdit: EditableShotFieldInputGQL!
): EditShotReturn!
submitSessionCoachFeedback(
videoId: Int!
generationId: ID!
response: SessionCoachFeedbackResponseEnum!
): SessionCoachFeedbackGQL!
requestSessionCoachGeneration(videoId: Int!): SessionCoachGQL!
completeAuthRegistration(
username: String!
agreesToMarketing: Boolean!
): CompleteAuthRegistrationGQL!
getProfileImageUploadLink(
fileExt: String = ".png"
): GetProfileUploadLinkReturn!
@@ -1076,6 +1106,10 @@ type Query {
limit: Int! = 500
countRespectsLimit: Boolean! = false
): GetRunsResult!
getSessionCoachGenerationStatus(
videoId: Int!
generationId: ID!
): SessionCoachGenerationStatusGQL
getSessionCoach(videoId: Int!): SessionCoachGQL!
videoPlayerClusters(videoId: Int!): [PlayerClusterGQL!]!
getShotAnnotationTypes(errorTypes: Boolean = false): [ShotAnnotationTypeGQL!]!
@@ -1114,6 +1148,7 @@ type Query {
): [ShotGQL!]!
getShotsByIds(ids: [Int!]!): [ShotGQL!]!
getUsageStats(days: Int! = 30): UsageStatsGQL!
getAuthBootstrap: AuthBootstrapGQL!
getUser(userId: Int!): UserGQL
doesUsernameExist(candidateUsername: String!): Boolean!
getLoggedInUser: UserGQL
@@ -1395,6 +1430,18 @@ type SessionCoachEvidenceGQL {
baselineMakePercentage: Float
}
type SessionCoachFeedbackGQL {
response: SessionCoachFeedbackResponseEnum!
updatedAt: DateTime!
}
enum SessionCoachFeedbackResponseEnum {
HELPFUL
NOT_RELEVANT
NOT_INSIGHTFUL
NOT_ENOUGH_EVIDENCE
}
type SessionCoachGQL {
videoId: Int!
processingId: Int
@@ -1446,6 +1493,7 @@ type SessionCoachGenerationGQL {
completedAt: DateTime
decision: SessionCoachDecisionGQL
diagnostics: SessionCoachGenerationDiagnosticsGQL
feedback: SessionCoachFeedbackGQL
}
enum SessionCoachGenerationStateEnum {
@@ -1455,6 +1503,17 @@ enum SessionCoachGenerationStateEnum {
FAILED
}
type SessionCoachGenerationStatusGQL {
id: ID!
state: SessionCoachGenerationStateEnum!
createdAt: DateTime!
updatedAt: DateTime!
startedAt: DateTime
lastEnqueuedAt: DateTime
enqueueCount: Int!
completedAt: DateTime
}
enum SessionCoachIneligibilityReasonEnum {
MULTIPLAYER
}
@@ -1540,6 +1599,7 @@ type ShotTableStateGQL {
videoId: Int!
frameIndex: Int!
tableState: TableStateGQL!
frameImage: String
}
input ShotsOrderingComponent @oneOf {
@@ -1585,6 +1645,24 @@ enum SimulationEventType {
BALL_STOP
}
enum SocialAuthPlatformEnum {
WEB
IOS
ANDROID
}
type SocialAuthProviderAvailabilityGQL {
provider: SocialAuthProviderEnum!
platform: SocialAuthPlatformEnum!
enabled: Boolean!
}
enum SocialAuthProviderEnum {
GOOGLE
APPLE
FACEBOOK
}
type SpinTypeBreakdownGQL {
draw: Int!
center: Int!
@@ -1721,6 +1799,7 @@ type TableStateGQL {
identifierToPosition: [[Float!]!]!
homography: HomographyInfoGQL
identifierToColor: [RGBColorGQL]!
identifierToBox: [BoundingBoxGQL!]!
}
type TagClassGQL {
@@ -1932,11 +2011,6 @@ type UserSubscriptionStatusGQL {
stripeSubscriptionId: String
}
type ShareLinkGQL {
slug: String!
url: String!
}
type VideoExportJobGQL {
id: Int!
videoId: Int!