Compare commits

..

19 Commits

Author SHA1 Message Date
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
d7b0b21430 Merge pull request 'Use BigInt for video export file sizes' (#310) from agent/video-export-bigint into master
Reviewed-on: #310
2026-07-27 19:35:17 +00:00
f173466b2f Merge pull request 'Add createShotShareLink for short share links' (#311) from dean/short-share-links-gql into master
Reviewed-on: #311
2026-07-27 19:35:11 +00:00
Dean Wenstrand
df69d154a5 Merge master into short-share-links-gql
All checks were successful
Tests / Tests (pull_request) Successful in 34s
gql master added Shot Lab video feed scopes and pagination
(operations-only: recorded_shots.gql + codegen output) while this branch
added createShotShareLink. The two touch disjoint parts of the generated
index.tsx, so the merge is textual only.

Verified rather than trusted: re-ran graphql-codegen after the merge and
src/index.tsx came back byte-identical (same md5), so the merged
generated file is exactly what codegen produces. Reverted prettier's
schema.gql reformat, which CI's own prettier undoes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:34:22 -04:00
0bceb5d300 Merge pull request 'Add Shot Lab video feed scopes and pagination' (#312) from t3code/shot-lab-video-sources into master
Reviewed-on: #312
2026-07-26 09:31:20 +00:00
f58048768c Add Shot Lab video feed scopes and pagination
All checks were successful
Tests / Tests (pull_request) Successful in 12s
2026-07-26 02:26:39 -07:00
179e8ed4e1 Add createShotShareLink for short share links
All checks were successful
Tests / Tests (pull_request) Successful in 11s
index.tsx is graphql-codegen output. schema.gql carries only the new
type, input and mutation field, deliberately left in the file's existing
formatting: package.json pins prettier as ^3.2.5, so a local install and
CI's yarn install can resolve different 3.x versions that disagree about
GraphQL union indentation. Committing a locally-reformatted schema.gql
makes CI's 'graphql-codegen causes no changes' check fail when its
prettier reformats it back.

The schema is verified semantically identical to the backend export via
lexicographic_sort_schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:02:17 -07:00
3e6dabe12a Use BigInt for video export file size
All checks were successful
Tests / Tests (pull_request) Successful in 6m3s
2026-07-24 20:09:19 -07:00
9104a312b1 Merge pull request 'Expose Session Coach agent summary' (#305) from loewy/session-coach-real-agent into master
Reviewed-on: #305
2026-07-24 05:24:11 +00:00
43e944e82f Expose grounded Coach strengths
All checks were successful
Tests / Tests (pull_request) Successful in 40s
2026-07-23 11:53:03 -07:00
d6cc81ef9f Add multi-signal coach decision fields 2026-07-23 11:53:03 -07:00
5d775eac70 Expose Session Coach agent summary 2026-07-23 11:53:03 -07:00
b810907f10 Merge pull request 'Add getUploadLinks batch mutation (schema + operation)' (#309) from dean/batch-upload-links-gql into master
Reviewed-on: #309
2026-07-23 07:50:57 +00:00
Dean Wenstrand
0eb627479a Add getUploadLinks batch mutation (schema + operation)
All checks were successful
Tests / Tests (pull_request) Successful in 38s
Batched counterpart to getUploadLink: one validation pass server-side,
N locally-signed URLs per round-trip, already-acknowledged indexes
returned in alreadyUploaded. Schema grafted additively (a full
gql-export from BE master would drop schema entries belonging to
not-yet-merged BE branches).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 00:31:18 -07:00
1914b2f5c0 Merge pull request 'Re-add legacy shot-sim operations alongside GetShotLabVideos' (#308) from dean/readd-legacy-shotsim-ops into master
Reviewed-on: #308
2026-07-23 06:18:53 +00:00
Dean Wenstrand
722b2ca2b0 Re-add legacy shot-sim operations alongside GetShotLabVideos
All checks were successful
Tests / Tests (pull_request) Successful in 1m37s
Mobile master's shot-simulation-screen still uses LiveStreamPicker and
RecordedShotPicker (GetLiveStreams / GetRecordedStreams) while ALSO
using the video-first GetShotLabVideos picker — a transitional state.
1a95439 replaced the old operations in-place, so no gql commit carried
the union and mobile master's pointer could not satisfy its own code.
live_streams.gql is restored verbatim; GetRecordedStreams is re-added
in legacy_shotsim.gql so master's recorded_shots.gql (which now holds
GetShotLabVideos) stays untouched. Drop both when the old pickers are
deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 22:57:28 -07:00
79f38fa45f Merge pull request 'dean/nested-comment-replies' (#307) from dean/nested-comment-replies into master
Reviewed-on: #307
2026-07-23 03:24:38 +00:00
2886d2f5b8 Merge pull request 'Select stream.id in upload-status operations so the cache stays normalized' (#306) from dean/upload-stream-id-fields into master
Reviewed-on: #306
2026-07-23 02:38:07 +00:00
Dean Wenstrand
8441bb12e9 Select stream.id in upload-status operations so the cache stays normalized
All checks were successful
Tests / Tests (pull_request) Successful in 11s
UploadStreamWithDetails and the editUploadStream response selected
stream fields without id. Apollo cannot normalize an object with no id
selected, so every upload-service poll rewrote VideoGQL.stream as an
anonymous inline object — clobbering the id that feed/detail queries
select. Their cache reads went partial and useQuery silently fell back
to the last network result, rendered as the home feed collapsing to
the first page (and bouncing the viewport) whenever an upload was
polling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 19:27:25 -07:00
9 changed files with 1147 additions and 41 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
# Legacy shot-sim operations still used by mobile master's
# LiveStreamPicker / RecordedShotPicker; remove when those are deleted.
query GetRecordedStreams($limit: Int! = 25) {
getFeedVideos(
limit: $limit
filters: { isStreamCompleted: true, excludeVideosWithNoShots: true }
includePrivate: MINE
feedInput: { allUsers: true }
) {
videos {
id
name
createdAt
screenshotUri
framesPerSecond
totalShots
owner {
id
username
profileImageUri
}
}
}
}

View File

@@ -0,0 +1,34 @@
# Lightweight listing of live (in-progress) streams for pickers like the
# Shot Lab "follow a live stream" flow. Selects only what a picker card
# needs; intentionally avoids the heavy VideoCardFields fragment.
query GetLiveStreams($limit: Int! = 25) {
getFeedVideos(
limit: $limit
filters: { isStreamCompleted: false, requireCursorCompletion: false }
includePrivate: MINE
feedInput: { allUsers: true }
) {
videos {
id
name
startTime
createdAt
screenshotUri
elapsedTime
owner {
id
username
profileImageUri
}
stream {
id
isCompleted
lastSegmentUploadedAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}

View File

@@ -1,12 +1,19 @@
# Lightweight two-step picker data for Shot Lab's video-first source: the # Lightweight two-step picker data for Shot Lab's video-first source: recent
# caller's recent videos (completed or still uploading), then that video's # videos from the requested audience (mine, followed users, or public), then
# shots. The stream fields hint at "actively updating" without gating. # that video's shots. The stream fields hint at "actively updating" without
query GetShotLabVideos($userId: Int!, $limit: Int! = 25) { # gating.
query GetShotLabVideos(
$feedInput: VideoFeedInputGQL!
$includePrivate: IncludePrivateEnum!
$limit: Int! = 25
$after: String = null
) {
getFeedVideos( getFeedVideos(
limit: $limit limit: $limit
after: $after
includeCallersVideos: false includeCallersVideos: false
includePrivate: MINE includePrivate: $includePrivate
feedInput: { userId: $userId } feedInput: $feedInput
) { ) {
videos { videos {
id id
@@ -26,6 +33,10 @@ query GetShotLabVideos($userId: Int!, $limit: Int! = 25) {
lastSegmentUploadedAt lastSegmentUploadedAt
} }
} }
pageInfo {
hasNextPage
endCursor
}
} }
} }

View File

@@ -14,6 +14,16 @@ query GetSessionCoach($videoId: Int!) {
unknownOutcomeCount unknownOutcomeCount
analyzedShotCount analyzedShotCount
makePercentage makePercentage
longestMakeStreak
recentBaseline {
sessionCount
attemptCount
madeCount
makePercentage
}
positiveCandidates {
...SessionCoachCandidateFields
}
primaryCandidate { primaryCandidate {
...SessionCoachCandidateFields ...SessionCoachCandidateFields
} }
@@ -59,6 +69,10 @@ fragment SessionCoachCandidateFields on SessionCoachCandidateGQL {
patternShotCount patternShotCount
score score
rankReason rankReason
baselineSessionCount
baselineAttemptCount
baselineMadeCount
baselineMakePercentage
} }
} }
@@ -68,7 +82,13 @@ fragment SessionCoachGenerationFields on SessionCoachGenerationGQL {
createdAt createdAt
completedAt completedAt
decision { decision {
selectedPositiveCandidateId
positiveTitle
positiveBody
selectedCandidateId selectedCandidateId
supportingCandidateIds
supportingShotIds
sessionSummary
title title
body body
suggestedAction suggestedAction
@@ -95,5 +115,6 @@ fragment SessionCoachGenerationFields on SessionCoachGenerationGQL {
startedAt startedAt
lastEnqueuedAt lastEnqueuedAt
abstentionReason abstentionReason
synthesisRationale
} }
} }

View File

@@ -0,0 +1,8 @@
mutation CreateShotShareLink($shotIds: [Int!]!, $paddingSeconds: Float = null) {
createShotShareLink(
input: { shotIds: $shotIds, paddingSeconds: $paddingSeconds }
) {
slug
url
}
}

View File

@@ -1,3 +1,72 @@
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 live and shot queries read a
# frame out of a video the app never holds, so there is no image to
# overlay, and getLiveTableState is polled every five seconds.
fragment TableStateDetectionFields on TableStateGQL {
identifierToBox {
...DetectionBoxFields
}
homography {
crop {
...DetectionBoxFields
}
pockets {
...DetectionBoxFields
}
sourcePoints {
...DetectionPocketPointFields
}
destPoints {
...DetectionPocketPointFields
}
}
}
query GetTableState( query GetTableState(
$b64Image: String! $b64Image: String!
$tableSize: Float $tableSize: Float
@@ -8,10 +77,8 @@ query GetTableState(
tableSize: $tableSize tableSize: $tableSize
useHomography: $useHomography useHomography: $useHomography
) { ) {
identifierToPosition ...TableStateLayoutFields
identifierToColor { ...TableStateDetectionFields
hex
}
} }
} }
@@ -20,10 +87,7 @@ query GetLiveTableState($videoId: Int!) {
videoId videoId
frameIndex frameIndex
tableState { tableState {
identifierToPosition ...TableStateLayoutFields
identifierToColor {
hex
}
} }
} }
} }
@@ -34,10 +98,7 @@ query GetShotTableState($shotId: Int!) {
videoId videoId
frameIndex frameIndex
tableState { tableState {
identifierToPosition ...TableStateLayoutFields
identifierToColor {
hex
}
} }
} }
} }

View File

@@ -48,6 +48,52 @@ mutation GetUploadLink($videoId: Int!, $segmentIndex: Int!) {
} }
} }
stream { stream {
id
uploadCompletionCursor
}
}
}
mutation GetUploadLinks($videoId: Int!, $segmentIndexes: [Int!]!) {
getUploadLinks(videoId: $videoId, segmentIndexes: $segmentIndexes) {
value {
... on UploadLinkBatch {
links {
segmentIndex
uploadUrl
headers {
key
value
}
}
alreadyUploaded
}
... on GetUploadLinkErrors {
error {
... on MustHaveSetForUploadLinkErr {
resolution
framesPerSecond
}
... on ProcessingFailedErr {
processing {
status
errors {
message
}
}
}
... on StorageLimitExceededErr {
reason
tierName
retainedStorageUsedBytes
retainedStorageLimitBytes
remainingStorageBytes
}
}
}
}
stream {
id
uploadCompletionCursor uploadCompletionCursor
} }
} }
@@ -127,6 +173,7 @@ fragment UploadStreamWithDetails on VideoGQL {
name name
startTime startTime
stream { stream {
id
isCompleted isCompleted
lastIntendedSegmentBound lastIntendedSegmentBound
uploadCompletionCursor uploadCompletionCursor

View File

@@ -249,6 +249,11 @@ input CreatePoolHallCameraInput {
claimTermsText: String = null claimTermsText: String = null
} }
input CreateShotShareLinkInput {
shotIds: [Int!]!
paddingSeconds: Float = null
}
input CreatePoolHallInput { input CreatePoolHallInput {
name: String! name: String!
address: String = null address: String = null
@@ -526,6 +531,11 @@ type GetUploadLinkReturn {
stream: UploadStreamGQL stream: UploadStreamGQL
} }
type GetUploadLinksReturn {
value: UploadLinkBatchGetUploadLinkErrors!
stream: UploadStreamGQL
}
type HLSPlaylistGQL { type HLSPlaylistGQL {
videoId: Int! videoId: Int!
m3u8Text: String! m3u8Text: String!
@@ -701,6 +711,7 @@ type Mutation {
cancelCameraClaimSession(claimSessionId: ID!): CameraClaimSession! cancelCameraClaimSession(claimSessionId: ID!): CameraClaimSession!
endCameraLease(leaseId: ID!): CameraLease! endCameraLease(leaseId: ID!): CameraLease!
extendCameraLease(leaseId: ID!, additionalMinutes: Int! = 60): CameraLease! extendCameraLease(leaseId: ID!, additionalMinutes: Int! = 60): CameraLease!
createShotShareLink(input: CreateShotShareLinkInput!): ShareLinkGQL!
finalizePlayerAssignments( finalizePlayerAssignments(
input: FinalizePlayerAssignmentsInput! input: FinalizePlayerAssignmentsInput!
): [PlayerClusterGQL!]! ): [PlayerClusterGQL!]!
@@ -755,6 +766,7 @@ type Mutation {
expectedDurationSeconds: Float = null expectedDurationSeconds: Float = null
): CreateUploadStreamReturn! ): CreateUploadStreamReturn!
getUploadLink(videoId: Int!, segmentIndex: Int!): GetUploadLinkReturn! getUploadLink(videoId: Int!, segmentIndex: Int!): GetUploadLinkReturn!
getUploadLinks(videoId: Int!, segmentIndexes: [Int!]!): GetUploadLinksReturn!
getHlsInitUploadLink(videoId: Int!): GetUploadLinkReturn! getHlsInitUploadLink(videoId: Int!): GetUploadLinkReturn!
setSegmentDuration( setSegmentDuration(
videoId: Int! videoId: Int!
@@ -1289,11 +1301,25 @@ type SegmentAlreadyUploadedErr {
segmentId: Int! segmentId: Int!
} }
type SegmentUploadLink {
segmentIndex: Int!
uploadUrl: String!
headers: [Header]!
}
type SerializedShotPathsGQL { type SerializedShotPathsGQL {
b64EncodedBuffer: String b64EncodedBuffer: String
} }
type SessionCoachBaselineGQL {
sessionCount: Int!
attemptCount: Int!
madeCount: Int!
makePercentage: Float!
}
enum SessionCoachCandidateFamilyEnum { enum SessionCoachCandidateFamilyEnum {
OVERALL
AIMING AIMING
CUT_ANGLE CUT_ANGLE
SPIN SPIN
@@ -1317,13 +1343,22 @@ type SessionCoachCandidateGQL {
} }
enum SessionCoachCandidateKindEnum { enum SessionCoachCandidateKindEnum {
BASELINE_COMPARISON
SESSION_STREAK
STRENGTH_PATTERN
DIRECTIONAL_MISS DIRECTIONAL_MISS
OUTCOME_PATTERN OUTCOME_PATTERN
USAGE_PATTERN USAGE_PATTERN
} }
type SessionCoachDecisionGQL { type SessionCoachDecisionGQL {
selectedPositiveCandidateId: ID
positiveTitle: String
positiveBody: String
selectedCandidateId: ID selectedCandidateId: ID
supportingCandidateIds: [ID!]!
supportingShotIds: [Int!]!
sessionSummary: String
title: String title: String
body: String body: String
suggestedAction: String suggestedAction: String
@@ -1346,6 +1381,10 @@ type SessionCoachEvidenceGQL {
patternShotCount: Int! patternShotCount: Int!
score: Float! score: Float!
rankReason: String! rankReason: String!
baselineSessionCount: Int!
baselineAttemptCount: Int!
baselineMadeCount: Int!
baselineMakePercentage: Float
} }
type SessionCoachGQL { type SessionCoachGQL {
@@ -1363,6 +1402,9 @@ type SessionCoachGQL {
unknownOutcomeCount: Int! unknownOutcomeCount: Int!
analyzedShotCount: Int! analyzedShotCount: Int!
makePercentage: Float makePercentage: Float
longestMakeStreak: Int!
recentBaseline: SessionCoachBaselineGQL
positiveCandidates: [SessionCoachCandidateGQL!]!
primaryCandidate: SessionCoachCandidateGQL primaryCandidate: SessionCoachCandidateGQL
candidates: [SessionCoachCandidateGQL!]! candidates: [SessionCoachCandidateGQL!]!
agentGeneration: SessionCoachGenerationGQL agentGeneration: SessionCoachGenerationGQL
@@ -1386,6 +1428,7 @@ type SessionCoachGenerationDiagnosticsGQL {
startedAt: DateTime startedAt: DateTime
lastEnqueuedAt: DateTime lastEnqueuedAt: DateTime
abstentionReason: String abstentionReason: String
synthesisRationale: String
} }
type SessionCoachGenerationGQL { type SessionCoachGenerationGQL {
@@ -1665,6 +1708,7 @@ type TableStateGQL {
identifierToPosition: [[Float!]!]! identifierToPosition: [[Float!]!]!
homography: HomographyInfoGQL homography: HomographyInfoGQL
identifierToColor: [RGBColorGQL]! identifierToColor: [RGBColorGQL]!
identifierToBox: [BoundingBoxGQL!]!
} }
type TagClassGQL { type TagClassGQL {
@@ -1750,6 +1794,13 @@ type UploadLink {
headers: [Header]! headers: [Header]!
} }
type UploadLinkBatch {
links: [SegmentUploadLink!]!
alreadyUploaded: [Int!]!
}
union UploadLinkBatchGetUploadLinkErrors = UploadLinkBatch | GetUploadLinkErrors
union UploadLinkGetProfileUploadLinkErrors = union UploadLinkGetProfileUploadLinkErrors =
UploadLink UploadLink
| GetProfileUploadLinkErrors | GetProfileUploadLinkErrors
@@ -1869,6 +1920,11 @@ type UserSubscriptionStatusGQL {
stripeSubscriptionId: String stripeSubscriptionId: String
} }
type ShareLinkGQL {
slug: String!
url: String!
}
type VideoExportJobGQL { type VideoExportJobGQL {
id: Int! id: Int!
videoId: Int! videoId: Int!
@@ -1879,7 +1935,7 @@ type VideoExportJobGQL {
shotIds: [Int!] shotIds: [Int!]
runId: Int runId: Int
downloadUrl: String downloadUrl: String
fileSizeBytes: Int fileSizeBytes: BigInt
expiresAt: DateTime expiresAt: DateTime
createdAt: DateTime createdAt: DateTime
} }