Compare commits

...

8 Commits

Author SHA1 Message Date
Dean Wenstrand
eb5217e070 Add DrillResultGQL + drillResult on VideoCardFields
Session card drill outcome: longest run + owner's personal-best rank
(1/2/3 medal) among their sessions on the same drill + table/pocket setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:48:21 -07:00
bfaed33c89 Merge pull request 'feed: select uploadCompletionCursor on VideoCardFields stream' (#296) from dean/feed-upload-completion-cursor into master
Reviewed-on: #296
2026-07-14 18:24:54 +00:00
Dean Wenstrand
1ce3aaed1b feed: select uploadCompletionCursor on VideoCardFields stream
All checks were successful
Tests / Tests (pull_request) Successful in 24s
Lets the client distinguish a finalized-but-not-fully-uploaded stream
(is_completed set, but received cursor short of last_intended_segment_bound)
from one that is genuinely processing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:15:41 -07:00
1bd03f2bbe Merge pull request 'dean/unify-shotlab-cohorts' (#295) from dean/unify-shotlab-cohorts into master
Reviewed-on: #295
2026-07-13 01:08:48 +00:00
Dean Wenstrand
4a1c41aae1 Regenerate schema and hooks from backend master
All checks were successful
Tests / Tests (pull_request) Successful in 12s
Union of the cohort funnel, Shot Lab picker queries, and transient
homography IDs — converges the three divergent submodule pointers
(gql master, railbird master, railbird-mobile master).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:03:33 -07:00
Dean Wenstrand
4122284bbd Merge Shot Lab queries with cohort funnel 2026-07-12 17:59:33 -07:00
b30dbd01f5 Fetch shot paths and features for the Shot Lab picker
All checks were successful
Tests / Tests (pull_request) Successful in 11s
The recorded-shot picker now renders per-shot table diagrams, so
GetRecordedStreamShots needs serializedShotPaths plus the pocketing and
cue-object features that PlaneShotDiagram consumes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 11:13:17 -07:00
303261f2d7 Add recorded Shot Lab queries 2026-07-11 20:56:47 -07:00
5 changed files with 527 additions and 34 deletions

View File

@@ -393,6 +393,16 @@ export type DoesNotOwnShotErrOtherErrorNeedsNote =
| DoesNotOwnShotErr
| OtherErrorNeedsNote;
export type DrillResultGql = {
__typename?: "DrillResultGQL";
attemptCount: Scalars["Int"]["output"];
drillTag: Scalars["String"]["output"];
isPersonalBest: Scalars["Boolean"]["output"];
longestRun: Scalars["Int"]["output"];
longestRunId?: Maybe<Scalars["Int"]["output"]>;
prRank: Scalars["Int"]["output"];
};
export type DrillRunLeaderboardGql = {
__typename?: "DrillRunLeaderboardGQL";
entries: Array<RunGql>;
@@ -4155,6 +4165,7 @@ export type VideoGql = {
createdAt?: Maybe<Scalars["DateTime"]["output"]>;
currentHomography?: Maybe<HomographyInfoGql>;
currentProcessing?: Maybe<VideoProcessingGql>;
drillResult?: Maybe<DrillResultGql>;
elapsedTime?: Maybe<Scalars["Float"]["output"]>;
endTime?: Maybe<Scalars["DateTime"]["output"]>;
framesPerSecond: Scalars["Float"]["output"];
@@ -4784,6 +4795,7 @@ export type GetFeedQuery = {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
@@ -4794,6 +4806,15 @@ export type GetFeedQuery = {
name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>;
drillResult?: {
__typename?: "DrillResultGQL";
drillTag: string;
longestRun: number;
longestRunId?: number | null;
prRank: number;
isPersonalBest: boolean;
attemptCount: number;
} | null;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
@@ -4898,6 +4919,7 @@ export type VideoCardFieldsFragment = {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
@@ -4908,6 +4930,15 @@ export type VideoCardFieldsFragment = {
name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>;
drillResult?: {
__typename?: "DrillResultGQL";
drillTag: string;
longestRun: number;
longestRunId?: number | null;
prRank: number;
isPersonalBest: boolean;
attemptCount: number;
} | null;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
@@ -5045,6 +5076,7 @@ export type GetVideoFeedQuery = {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
@@ -5055,6 +5087,15 @@ export type GetVideoFeedQuery = {
name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>;
drillResult?: {
__typename?: "DrillResultGQL";
drillTag: string;
longestRun: number;
longestRunId?: number | null;
prRank: number;
isPersonalBest: boolean;
attemptCount: number;
} | null;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
@@ -6616,6 +6657,79 @@ export type ReactToVideoMutation = {
reactToVideo: boolean;
};
export type GetRecordedStreamsQueryVariables = Exact<{
limit?: Scalars["Int"]["input"];
}>;
export type GetRecordedStreamsQuery = {
__typename?: "Query";
getFeedVideos: {
__typename?: "VideoHistoryGQL";
videos: Array<{
__typename?: "VideoGQL";
id: number;
name?: string | null;
createdAt?: any | null;
screenshotUri?: string | null;
framesPerSecond: number;
totalShots: number;
owner?: {
__typename?: "UserGQL";
id: number;
username: string;
profileImageUri?: string | null;
} | null;
}>;
};
};
export type GetRecordedStreamShotsQueryVariables = Exact<{
videoId: Scalars["Int"]["input"];
limit?: Scalars["Int"]["input"];
}>;
export type GetRecordedStreamShotsQuery = {
__typename?: "Query";
getOrderedShots: {
__typename?: "GetShotsResult";
shots: Array<{
__typename?: "ShotGQL";
id: number;
videoId: number;
startFrame: number;
endFrame: number;
serializedShotPaths?: {
__typename?: "SerializedShotPathsGQL";
b64EncodedBuffer?: string | null;
} | null;
cueObjectFeatures?: {
__typename?: "CueObjectFeaturesGQL";
cueObjectDistance?: number | null;
cueObjectAngle?: number | null;
cueBallSpeed?: number | null;
shotDirection?: ShotDirectionEnum | null;
spinType?: SpinTypeEnum | null;
} | null;
pocketingIntentionFeatures?: {
__typename?: "PocketingIntentionFeaturesGQL";
make?: boolean | null;
targetPocketDistance?: number | null;
targetPocketAngle?: number | null;
targetPocketAngleDirection?: ShotDirectionEnum | null;
marginOfErrorInDegrees?: number | null;
intendedPocketType?: PocketEnum | null;
difficulty?: number | null;
} | null;
pocketingIntentionInfo?: {
__typename?: "PocketingIntentionInfoGQL";
ballId: number;
pocketId: PocketIdentifier;
pathMetadataIndex: number;
} | null;
}>;
};
};
export type GetRunsForHighlightsQueryVariables = Exact<{
filterInput: RunFilterInput;
runIds?: InputMaybe<Array<Scalars["Int"]["input"]> | Scalars["Int"]["input"]>;
@@ -6847,34 +6961,28 @@ export type GetLiveTableStateQuery = {
__typename?: "RGBColorGQL";
hex: string;
} | null>;
homography?: {
__typename?: "HomographyInfoGQL";
id?: number | null;
};
};
};
export type GetShotTableStateQueryVariables = Exact<{
shotId: Scalars["Int"]["input"];
}>;
export type GetShotTableStateQuery = {
__typename?: "Query";
getShotTableState: {
__typename?: "ShotTableStateGQL";
shotId: number;
videoId: number;
frameIndex: number;
crop: {
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
};
pockets: Array<{
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
}>;
sourcePoints: {
__typename?: "PocketPointsGQL";
topLeft: { __typename?: "IntPoint2D"; x: number; y: number };
topSide: { __typename?: "IntPoint2D"; x: number; y: number };
topRight: { __typename?: "IntPoint2D"; x: number; y: number };
bottomLeft: { __typename?: "IntPoint2D"; x: number; y: number };
bottomSide: { __typename?: "IntPoint2D"; x: number; y: number };
bottomRight: { __typename?: "IntPoint2D"; x: number; y: number };
};
} | null;
tableState: {
__typename?: "TableStateGQL";
identifierToPosition: Array<Array<number>>;
identifierToColor: Array<{
__typename?: "RGBColorGQL";
hex: string;
} | null>;
};
};
};
@@ -8126,6 +8234,7 @@ export type GetVideoCardQuery = {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
@@ -8136,6 +8245,15 @@ export type GetVideoCardQuery = {
name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>;
drillResult?: {
__typename?: "DrillResultGQL";
drillTag: string;
longestRun: number;
longestRunId?: number | null;
prRank: number;
isPersonalBest: boolean;
attemptCount: number;
} | null;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
@@ -8953,6 +9071,7 @@ export const VideoCardFieldsFragmentDoc = gql`
stream {
id
lastIntendedSegmentBound
uploadCompletionCursor
streamSegmentType
isCompleted
lastSegmentUploadedAt
@@ -8966,6 +9085,14 @@ export const VideoCardFieldsFragmentDoc = gql`
name
}
}
drillResult {
drillTag
longestRun
longestRunId
prRank
isPersonalBest
attemptCount
}
playerSummaries {
...PlayerSummaryFields
}
@@ -14232,6 +14359,203 @@ export type ReactToVideoMutationOptions = Apollo.BaseMutationOptions<
ReactToVideoMutation,
ReactToVideoMutationVariables
>;
export const GetRecordedStreamsDocument = gql`
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
}
}
}
}
`;
/**
* __useGetRecordedStreamsQuery__
*
* To run a query within a React component, call `useGetRecordedStreamsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRecordedStreamsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetRecordedStreamsQuery({
* variables: {
* limit: // value for 'limit'
* },
* });
*/
export function useGetRecordedStreamsQuery(
baseOptions?: Apollo.QueryHookOptions<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>(GetRecordedStreamsDocument, options);
}
export function useGetRecordedStreamsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>(GetRecordedStreamsDocument, options);
}
export function useGetRecordedStreamsSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>(GetRecordedStreamsDocument, options);
}
export type GetRecordedStreamsQueryHookResult = ReturnType<
typeof useGetRecordedStreamsQuery
>;
export type GetRecordedStreamsLazyQueryHookResult = ReturnType<
typeof useGetRecordedStreamsLazyQuery
>;
export type GetRecordedStreamsSuspenseQueryHookResult = ReturnType<
typeof useGetRecordedStreamsSuspenseQuery
>;
export type GetRecordedStreamsQueryResult = Apollo.QueryResult<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>;
export const GetRecordedStreamShotsDocument = gql`
query GetRecordedStreamShots($videoId: Int!, $limit: Int! = 200) {
getOrderedShots(
filterInput: { videoId: [$videoId] }
shotsOrdering: { orderings: [{ startFrame: { descending: false } }] }
limit: $limit
) {
shots {
id
videoId
startFrame
endFrame
serializedShotPaths {
b64EncodedBuffer
}
cueObjectFeatures {
cueObjectDistance
cueObjectAngle
cueBallSpeed
shotDirection
spinType
}
pocketingIntentionFeatures {
make
targetPocketDistance
targetPocketAngle
targetPocketAngleDirection
marginOfErrorInDegrees
intendedPocketType
difficulty
}
pocketingIntentionInfo {
ballId
pocketId
pathMetadataIndex
}
}
}
}
`;
/**
* __useGetRecordedStreamShotsQuery__
*
* To run a query within a React component, call `useGetRecordedStreamShotsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRecordedStreamShotsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetRecordedStreamShotsQuery({
* variables: {
* videoId: // value for 'videoId'
* limit: // value for 'limit'
* },
* });
*/
export function useGetRecordedStreamShotsQuery(
baseOptions: Apollo.QueryHookOptions<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>(GetRecordedStreamShotsDocument, options);
}
export function useGetRecordedStreamShotsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>(GetRecordedStreamShotsDocument, options);
}
export function useGetRecordedStreamShotsSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>(GetRecordedStreamShotsDocument, options);
}
export type GetRecordedStreamShotsQueryHookResult = ReturnType<
typeof useGetRecordedStreamShotsQuery
>;
export type GetRecordedStreamShotsLazyQueryHookResult = ReturnType<
typeof useGetRecordedStreamShotsLazyQuery
>;
export type GetRecordedStreamShotsSuspenseQueryHookResult = ReturnType<
typeof useGetRecordedStreamShotsSuspenseQuery
>;
export type GetRecordedStreamShotsQueryResult = Apollo.QueryResult<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>;
export const GetRunsForHighlightsDocument = gql`
query GetRunsForHighlights(
$filterInput: RunFilterInput!
@@ -14653,13 +14977,9 @@ export const GetLiveTableStateDocument = gql`
identifierToColor {
hex
}
homography {
...HomographyInfo
}
}
}
}
${HomographyInfoFragmentDoc}
`;
/**
@@ -14727,6 +15047,87 @@ export type GetLiveTableStateQueryResult = Apollo.QueryResult<
GetLiveTableStateQuery,
GetLiveTableStateQueryVariables
>;
export const GetShotTableStateDocument = gql`
query GetShotTableState($shotId: Int!) {
getShotTableState(shotId: $shotId) {
shotId
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
}
}
}
`;
/**
* __useGetShotTableStateQuery__
*
* To run a query within a React component, call `useGetShotTableStateQuery` and pass it any options that fit your needs.
* When your component renders, `useGetShotTableStateQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetShotTableStateQuery({
* variables: {
* shotId: // value for 'shotId'
* },
* });
*/
export function useGetShotTableStateQuery(
baseOptions: Apollo.QueryHookOptions<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>(GetShotTableStateDocument, options);
}
export function useGetShotTableStateLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>(GetShotTableStateDocument, options);
}
export function useGetShotTableStateSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>(GetShotTableStateDocument, options);
}
export type GetShotTableStateQueryHookResult = ReturnType<
typeof useGetShotTableStateQuery
>;
export type GetShotTableStateLazyQueryHookResult = ReturnType<
typeof useGetShotTableStateLazyQuery
>;
export type GetShotTableStateSuspenseQueryHookResult = ReturnType<
typeof useGetShotTableStateSuspenseQuery
>;
export type GetShotTableStateQueryResult = Apollo.QueryResult<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>;
export const SimulateShotDocument = gql`
query SimulateShot($simulationInput: SimulateShotInputGQL!) {
simulateShot(simulationInput: $simulationInput) {

View File

@@ -41,6 +41,7 @@ fragment VideoCardFields on VideoGQL {
stream {
id
lastIntendedSegmentBound
uploadCompletionCursor
streamSegmentType
isCompleted
lastSegmentUploadedAt
@@ -54,6 +55,14 @@ fragment VideoCardFields on VideoGQL {
name
}
}
drillResult {
drillTag
longestRun
longestRunId
prRank
isPersonalBest
attemptCount
}
playerSummaries {
...PlayerSummaryFields
}

View File

@@ -0,0 +1,62 @@
# Lightweight two-step picker data for loading a recorded shot into Shot Lab.
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
}
}
}
}
query GetRecordedStreamShots($videoId: Int!, $limit: Int! = 200) {
getOrderedShots(
filterInput: { videoId: [$videoId] }
shotsOrdering: { orderings: [{ startFrame: { descending: false } }] }
limit: $limit
) {
shots {
id
videoId
startFrame
endFrame
serializedShotPaths {
b64EncodedBuffer
}
cueObjectFeatures {
cueObjectDistance
cueObjectAngle
cueBallSpeed
shotDirection
spinType
}
pocketingIntentionFeatures {
make
targetPocketDistance
targetPocketAngle
targetPocketAngleDirection
marginOfErrorInDegrees
intendedPocketType
difficulty
}
pocketingIntentionInfo {
ballId
pocketId
pathMetadataIndex
}
}
}
}

View File

@@ -24,8 +24,19 @@ query GetLiveTableState($videoId: Int!) {
identifierToColor {
hex
}
homography {
...HomographyInfo
}
}
}
query GetShotTableState($shotId: Int!) {
getShotTableState(shotId: $shotId) {
shotId
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
}
}

View File

@@ -438,6 +438,7 @@ type VideoGQL {
reactions: [ReactionGQL!]!
comments: [CommentGQL!]!
playerSummaries: [PlayerSummaryGQL!]!
drillResult: DrillResultGQL
}
type ShotGQL {
@@ -721,6 +722,15 @@ type SpinTypeBreakdownGQL {
follow: Int!
}
type DrillResultGQL {
drillTag: String!
longestRun: Int!
longestRunId: Int
prRank: Int!
isPersonalBest: Boolean!
attemptCount: Int!
}
type DeployedConfigGQL {
allowNewUsers: Boolean!
firebase: Boolean!