Compare commits

..

6 Commits

Author SHA1 Message Date
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
6b8fdd4262 Add per-session Coach GraphQL contract
All checks were successful
Tests / Tests (pull_request) Successful in 10s
2026-07-16 16:45:55 -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
5 changed files with 674 additions and 76 deletions

View File

@@ -3088,6 +3088,7 @@ export type Query = {
getQuotaStatus: QuotaStatusGql;
getResolvedTier: ResolvedTierGql;
getRuns: GetRunsResult;
getSessionCoach: SessionCoachGql;
getShotAnnotationTypes: Array<ShotAnnotationTypeGql>;
getShotTableState: ShotTableStateGql;
getShots: Array<ShotGql>;
@@ -3231,6 +3232,10 @@ export type QueryGetRunsArgs = {
runsOrdering?: InputMaybe<GetRunsOrdering>;
};
export type QueryGetSessionCoachArgs = {
videoId: Scalars["Int"]["input"];
};
export type QueryGetShotAnnotationTypesArgs = {
errorTypes?: InputMaybe<Scalars["Boolean"]["input"]>;
};
@@ -3546,6 +3551,84 @@ export type SerializedShotPathsGql = {
b64EncodedBuffer?: Maybe<Scalars["String"]["output"]>;
};
export enum SessionCoachCandidateFamilyEnum {
Aiming = "AIMING",
BackCut = "BACK_CUT",
CueToObjectDistance = "CUE_TO_OBJECT_DISTANCE",
CutAngle = "CUT_ANGLE",
ObjectToPocketDistance = "OBJECT_TO_POCKET_DISTANCE",
Spin = "SPIN",
}
export type SessionCoachCandidateGql = {
__typename?: "SessionCoachCandidateGQL";
action: Scalars["String"]["output"];
body: Scalars["String"]["output"];
drillId?: Maybe<Scalars["String"]["output"]>;
evidence: SessionCoachEvidenceGql;
family: SessionCoachCandidateFamilyEnum;
id: Scalars["ID"]["output"];
kind: SessionCoachCandidateKindEnum;
limitations: Array<Scalars["String"]["output"]>;
rank: Scalars["Int"]["output"];
supportingShotIds: Array<Scalars["Int"]["output"]>;
title: Scalars["String"]["output"];
};
export enum SessionCoachCandidateKindEnum {
DirectionalMiss = "DIRECTIONAL_MISS",
OutcomePattern = "OUTCOME_PATTERN",
UsagePattern = "USAGE_PATTERN",
}
export type SessionCoachEvidenceGql = {
__typename?: "SessionCoachEvidenceGQL";
attemptCount: Scalars["Int"]["output"];
bucketKey: Scalars["String"]["output"];
bucketLabel: Scalars["String"]["output"];
comparisonAttemptCount: Scalars["Int"]["output"];
comparisonMadeCount: Scalars["Int"]["output"];
comparisonMakePercentage?: Maybe<Scalars["Float"]["output"]>;
madeCount: Scalars["Int"]["output"];
makePercentage: Scalars["Float"]["output"];
missedCount: Scalars["Int"]["output"];
patternShotCount: Scalars["Int"]["output"];
rankReason: Scalars["String"]["output"];
score: Scalars["Float"]["output"];
};
export type SessionCoachGql = {
__typename?: "SessionCoachGQL";
analysisVersion: Scalars["String"]["output"];
analyzedShotCount: Scalars["Int"]["output"];
candidates: Array<SessionCoachCandidateGql>;
generatedAt: Scalars["DateTime"]["output"];
ineligibilityReason?: Maybe<SessionCoachIneligibilityReasonEnum>;
madeCount: Scalars["Int"]["output"];
makePercentage?: Maybe<Scalars["Float"]["output"]>;
missedCount: Scalars["Int"]["output"];
primaryCandidate?: Maybe<SessionCoachCandidateGql>;
processingId?: Maybe<Scalars["Int"]["output"]>;
processingStatus?: Maybe<Scalars["String"]["output"]>;
shotCount: Scalars["Int"]["output"];
state: SessionCoachStateEnum;
summary?: Maybe<Scalars["String"]["output"]>;
unknownOutcomeCount: Scalars["Int"]["output"];
videoId: Scalars["Int"]["output"];
};
export enum SessionCoachIneligibilityReasonEnum {
Multiplayer = "MULTIPLAYER",
}
export enum SessionCoachStateEnum {
Failed = "FAILED",
Ineligible = "INELIGIBLE",
NoTrackedShots = "NO_TRACKED_SHOTS",
Processing = "PROCESSING",
Ready = "READY",
}
export type ShotAnnotationGql = {
__typename?: "ShotAnnotationGQL";
createdAt?: Maybe<Scalars["DateTime"]["output"]>;
@@ -4784,6 +4867,7 @@ export type GetFeedQuery = {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
@@ -4898,6 +4982,7 @@ export type VideoCardFieldsFragment = {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
@@ -5045,6 +5130,7 @@ export type GetVideoFeedQuery = {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
@@ -6763,6 +6849,116 @@ export type GetRunsWithTimestampsQuery = {
};
};
export type GetSessionCoachQueryVariables = Exact<{
videoId: Scalars["Int"]["input"];
}>;
export type GetSessionCoachQuery = {
__typename?: "Query";
getSessionCoach: {
__typename?: "SessionCoachGQL";
videoId: number;
processingId?: number | null;
analysisVersion: string;
state: SessionCoachStateEnum;
generatedAt: any;
processingStatus?: string | null;
ineligibilityReason?: SessionCoachIneligibilityReasonEnum | null;
summary?: string | null;
shotCount: number;
madeCount: number;
missedCount: number;
unknownOutcomeCount: number;
analyzedShotCount: number;
makePercentage?: number | null;
primaryCandidate?: {
__typename?: "SessionCoachCandidateGQL";
id: string;
rank: number;
family: SessionCoachCandidateFamilyEnum;
kind: SessionCoachCandidateKindEnum;
title: string;
body: string;
action: string;
supportingShotIds: Array<number>;
drillId?: string | null;
limitations: Array<string>;
evidence: {
__typename?: "SessionCoachEvidenceGQL";
bucketKey: string;
bucketLabel: string;
attemptCount: number;
madeCount: number;
missedCount: number;
makePercentage: number;
comparisonAttemptCount: number;
comparisonMadeCount: number;
comparisonMakePercentage?: number | null;
patternShotCount: number;
score: number;
rankReason: string;
};
} | null;
candidates: Array<{
__typename?: "SessionCoachCandidateGQL";
id: string;
rank: number;
family: SessionCoachCandidateFamilyEnum;
kind: SessionCoachCandidateKindEnum;
title: string;
body: string;
action: string;
supportingShotIds: Array<number>;
drillId?: string | null;
limitations: Array<string>;
evidence: {
__typename?: "SessionCoachEvidenceGQL";
bucketKey: string;
bucketLabel: string;
attemptCount: number;
madeCount: number;
missedCount: number;
makePercentage: number;
comparisonAttemptCount: number;
comparisonMadeCount: number;
comparisonMakePercentage?: number | null;
patternShotCount: number;
score: number;
rankReason: string;
};
}>;
};
};
export type SessionCoachCandidateFieldsFragment = {
__typename?: "SessionCoachCandidateGQL";
id: string;
rank: number;
family: SessionCoachCandidateFamilyEnum;
kind: SessionCoachCandidateKindEnum;
title: string;
body: string;
action: string;
supportingShotIds: Array<number>;
drillId?: string | null;
limitations: Array<string>;
evidence: {
__typename?: "SessionCoachEvidenceGQL";
bucketKey: string;
bucketLabel: string;
attemptCount: number;
madeCount: number;
missedCount: number;
makePercentage: number;
comparisonAttemptCount: number;
comparisonMadeCount: number;
comparisonMakePercentage?: number | null;
patternShotCount: number;
score: number;
rankReason: string;
};
};
export type PlayerSummaryFieldsFragment = {
__typename?: "PlayerSummaryGQL";
clusterId: number;
@@ -6920,7 +7116,6 @@ export type GetLiveTableStateQuery = {
__typename?: "RGBColorGQL";
hex: string;
} | null>;
<<<<<<< HEAD
};
};
};
@@ -6943,36 +7138,6 @@ export type GetShotTableStateQuery = {
__typename?: "RGBColorGQL";
hex: string;
} | null>;
=======
homography?: {
__typename?: "HomographyInfoGQL";
id?: number | null;
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;
>>>>>>> 6d13dc3
};
};
};
@@ -8224,6 +8389,7 @@ export type GetVideoCardQuery = {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
@@ -8865,6 +9031,7 @@ export type GetUploadLinkMutation = {
};
stream?: {
__typename?: "UploadStreamGQL";
id: string;
uploadCompletionCursor: number;
} | null;
};
@@ -8963,6 +9130,7 @@ export type UploadStreamWithDetailsFragment = {
startTime?: any | null;
stream?: {
__typename?: "UploadStreamGQL";
id: string;
isCompleted: boolean;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
@@ -8987,6 +9155,7 @@ export type GetUploadStreamsWithDetailsQuery = {
startTime?: any | null;
stream?: {
__typename?: "UploadStreamGQL";
id: string;
isCompleted: boolean;
lastIntendedSegmentBound?: number | null;
uploadCompletionCursor: number;
@@ -9051,6 +9220,7 @@ export const VideoCardFieldsFragmentDoc = gql`
stream {
id
lastIntendedSegmentBound
uploadCompletionCursor
streamSegmentType
isCompleted
lastSegmentUploadedAt
@@ -9238,6 +9408,34 @@ export const PocketingIntentionFragmentFragmentDoc = gql`
difficulty
}
`;
export const SessionCoachCandidateFieldsFragmentDoc = gql`
fragment SessionCoachCandidateFields on SessionCoachCandidateGQL {
id
rank
family
kind
title
body
action
supportingShotIds
drillId
limitations
evidence {
bucketKey
bucketLabel
attemptCount
madeCount
missedCount
makePercentage
comparisonAttemptCount
comparisonMadeCount
comparisonMakePercentage
patternShotCount
score
rankReason
}
}
`;
export const PlayerClusterShotFieldsFragmentDoc = gql`
fragment PlayerClusterShotFields on PlayerClusterShotGQL {
shotId
@@ -9497,6 +9695,7 @@ export const UploadStreamWithDetailsFragmentDoc = gql`
name
startTime
stream {
id
isCompleted
lastIntendedSegmentBound
uploadCompletionCursor
@@ -14726,6 +14925,99 @@ export type GetRunsWithTimestampsQueryResult = Apollo.QueryResult<
GetRunsWithTimestampsQuery,
GetRunsWithTimestampsQueryVariables
>;
export const GetSessionCoachDocument = gql`
query GetSessionCoach($videoId: Int!) {
getSessionCoach(videoId: $videoId) {
videoId
processingId
analysisVersion
state
generatedAt
processingStatus
ineligibilityReason
summary
shotCount
madeCount
missedCount
unknownOutcomeCount
analyzedShotCount
makePercentage
primaryCandidate {
...SessionCoachCandidateFields
}
candidates {
...SessionCoachCandidateFields
}
}
}
${SessionCoachCandidateFieldsFragmentDoc}
`;
/**
* __useGetSessionCoachQuery__
*
* To run a query within a React component, call `useGetSessionCoachQuery` and pass it any options that fit your needs.
* When your component renders, `useGetSessionCoachQuery` 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 } = useGetSessionCoachQuery({
* variables: {
* videoId: // value for 'videoId'
* },
* });
*/
export function useGetSessionCoachQuery(
baseOptions: Apollo.QueryHookOptions<
GetSessionCoachQuery,
GetSessionCoachQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<GetSessionCoachQuery, GetSessionCoachQueryVariables>(
GetSessionCoachDocument,
options,
);
}
export function useGetSessionCoachLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetSessionCoachQuery,
GetSessionCoachQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetSessionCoachQuery,
GetSessionCoachQueryVariables
>(GetSessionCoachDocument, options);
}
export function useGetSessionCoachSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetSessionCoachQuery,
GetSessionCoachQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetSessionCoachQuery,
GetSessionCoachQueryVariables
>(GetSessionCoachDocument, options);
}
export type GetSessionCoachQueryHookResult = ReturnType<
typeof useGetSessionCoachQuery
>;
export type GetSessionCoachLazyQueryHookResult = ReturnType<
typeof useGetSessionCoachLazyQuery
>;
export type GetSessionCoachSuspenseQueryHookResult = ReturnType<
typeof useGetSessionCoachSuspenseQuery
>;
export type GetSessionCoachQueryResult = Apollo.QueryResult<
GetSessionCoachQuery,
GetSessionCoachQueryVariables
>;
export const VideoPlayerClustersDocument = gql`
query VideoPlayerClusters($videoId: Int!) {
videoPlayerClusters(videoId: $videoId) {
@@ -19187,6 +19479,7 @@ export const GetUploadLinkDocument = gql`
}
}
stream {
id
uploadCompletionCursor
}
}

View File

@@ -41,6 +41,7 @@ fragment VideoCardFields on VideoGQL {
stream {
id
lastIntendedSegmentBound
uploadCompletionCursor
streamSegmentType
isCompleted
lastSegmentUploadedAt

View File

@@ -0,0 +1,51 @@
query GetSessionCoach($videoId: Int!) {
getSessionCoach(videoId: $videoId) {
videoId
processingId
analysisVersion
state
generatedAt
processingStatus
ineligibilityReason
summary
shotCount
madeCount
missedCount
unknownOutcomeCount
analyzedShotCount
makePercentage
primaryCandidate {
...SessionCoachCandidateFields
}
candidates {
...SessionCoachCandidateFields
}
}
}
fragment SessionCoachCandidateFields on SessionCoachCandidateGQL {
id
rank
family
kind
title
body
action
supportingShotIds
drillId
limitations
evidence {
bucketKey
bucketLabel
attemptCount
madeCount
missedCount
makePercentage
comparisonAttemptCount
comparisonMadeCount
comparisonMakePercentage
patternShotCount
score
rankReason
}
}

View File

@@ -48,6 +48,7 @@ mutation GetUploadLink($videoId: Int!, $segmentIndex: Int!) {
}
}
stream {
id
uploadCompletionCursor
}
}
@@ -127,6 +128,7 @@ fragment UploadStreamWithDetails on VideoGQL {
name
startTime
stream {
id
isCompleted
lastIntendedSegmentBound
uploadCompletionCursor

View File

@@ -1,5 +1,7 @@
type Query {
getAggregatedShotMetrics(aggregateInput: AggregateInputGQL!): [AggregateResultGQL!]!
getAggregatedShotMetrics(
aggregateInput: AggregateInputGQL!
): [AggregateResultGQL!]!
getBucketSet(keyName: String!): BucketSetGQL
challenges(includeDismissed: Boolean! = false): [Challenge!]!
myDismissedChallenges: [Challenge!]!
@@ -11,14 +13,44 @@ type Query {
myChallengeEntries: [ChallengeEntry!]!
getDeployedConfig: DeployedConfigGQL!
waitFor(duration: Float!): Float!
getFeedVideos(limit: Int! = 5, after: String = null, includePrivate: IncludePrivateEnum! = MINE, includeCallersVideos: Boolean = true, filters: VideoFilterInput = null, feedInput: VideoFeedInputGQL = null): VideoHistoryGQL!
getVideoMakePercentageIntervals(videoId: ID!, intervalDuration: Int! = 300): [MakePercentageIntervalGQL!]!
getLongestRunsLeaderboard(interval: TimeInterval = null, when: DateTime = null, 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!
getFeedVideos(
limit: Int! = 5
after: String = null
includePrivate: IncludePrivateEnum! = MINE
includeCallersVideos: Boolean = true
filters: VideoFilterInput = null
feedInput: VideoFeedInputGQL = null
): VideoHistoryGQL!
getVideoMakePercentageIntervals(
videoId: ID!
intervalDuration: Int! = 300
): [MakePercentageIntervalGQL!]!
getLongestRunsLeaderboard(
interval: TimeInterval = null
when: DateTime = null
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): CountLeaderboardGQL!
getMakesLeaderboard(
interval: TimeInterval = null
when: DateTime = null
): CountLeaderboardGQL!
getMedals(scope: MedalScope!, userId: Int = null): RequestedMedalsGQL!
notifications(limit: Int! = 20, offset: Int! = 0, filters: NotificationFilters = null): NotificationConnection!
notifications(
limit: Int! = 20
offset: Int! = 0
filters: NotificationFilters = null
): NotificationConnection!
unreadNotificationCount: Int!
poolHalls: [PoolHall!]!
claimablePoolHalls: [PoolHall!]!
@@ -26,25 +58,70 @@ type Query {
claimableCameras(poolHallId: ID!): [PoolHallCamera!]!
cameraClaimSession(id: ID!): CameraClaimSession
activeCameraLease: CameraLease
getRuns(filterInput: RunFilterInput!, runIds: [Int!] = null, runsOrdering: GetRunsOrdering = null, limit: Int! = 500, countRespectsLimit: Boolean! = false): GetRunsResult!
getRuns(
filterInput: RunFilterInput!
runIds: [Int!] = null
runsOrdering: GetRunsOrdering = null
limit: Int! = 500
countRespectsLimit: Boolean! = false
): GetRunsResult!
getSessionCoach(videoId: Int!): SessionCoachGQL!
videoPlayerClusters(videoId: Int!): [PlayerClusterGQL!]!
getShotAnnotationTypes(errorTypes: Boolean = false): [ShotAnnotationTypeGQL!]!
getTableState(b64Image: String!, tableSize: Float = 100, useHomography: HomographyInputGQL = null): TableStateGQL!
getTableState(
b64Image: String!
tableSize: Float = 100
useHomography: HomographyInputGQL = null
): TableStateGQL!
getLiveTableState(videoId: Int!): LiveTableStateGQL!
getShotTableState(shotId: Int!): ShotTableStateGQL!
simulateShot(simulationInput: SimulateShotInputGQL!): ShotProjectionGQL!
computePotAim(simulationInput: SimulateShotInputGQL!, targetBallId: Int!, pocket: PocketIdentifier!): PotAimGQL!
getOrderedShots(filterInput: FilterInput!, ids: [Int!] = null, shotsOrdering: GetShotsOrdering = null, limit: Int! = 500, countRespectsLimit: Boolean! = false): GetShotsResult!
getShotsWithMetadata(filterInput: FilterInput!, ids: [Int!] = null, shotsPagination: GetShotsPagination = null, limit: Int! = 500, countRespectsLimit: Boolean! = false): GetShotsResult!
getShots(filterInput: FilterInput!, shotsPagination: GetShotsPagination = null, limit: Int! = 500, countRespectsLimit: Boolean! = false): [ShotGQL!]!
computePotAim(
simulationInput: SimulateShotInputGQL!
targetBallId: Int!
pocket: PocketIdentifier!
): PotAimGQL!
getOrderedShots(
filterInput: FilterInput!
ids: [Int!] = null
shotsOrdering: GetShotsOrdering = null
limit: Int! = 500
countRespectsLimit: Boolean! = false
): GetShotsResult!
getShotsWithMetadata(
filterInput: FilterInput!
ids: [Int!] = null
shotsPagination: GetShotsPagination = null
limit: Int! = 500
countRespectsLimit: Boolean! = false
): GetShotsResult!
getShots(
filterInput: FilterInput!
shotsPagination: GetShotsPagination = null
limit: Int! = 500
countRespectsLimit: Boolean! = false
): [ShotGQL!]!
getShotsByIds(ids: [Int!]!): [ShotGQL!]!
getUsageStats(days: Int! = 30): UsageStatsGQL!
getUser(userId: Int!): UserGQL
doesUsernameExist(candidateUsername: String!): Boolean!
getLoggedInUser: UserGQL
getUsernames(matchString: String = null, limit: Int = null, after: String = null): [String!]!
getUsersMatching(matchString: String = null, limit: Int = null, after: String = null): [UserGQL!]!
getUserRelationshipsMatching(userId: Int!, matchString: String = null, limit: Int = 100, after: String = null): UserRelationshipsResult!
getUsernames(
matchString: String = null
limit: Int = null
after: String = null
): [String!]!
getUsersMatching(
matchString: String = null
limit: Int = null
after: String = null
): [UserGQL!]!
getUserRelationshipsMatching(
userId: Int!
matchString: String = null
limit: Int = 100
after: String = null
): UserRelationshipsResult!
getAvailableSubscriptionOptions: StripeSubscriptionOptionsGQL!
getUserSubscriptionStatus: UserSubscriptionStatusGQL!
getResolvedTier: ResolvedTierGQL!
@@ -52,7 +129,12 @@ type Query {
getQuotaStatus: QuotaStatusGQL!
getStorageStatus: StorageStatusGQL
getPlayTime(userId: Int!, filters: VideoFilterInput = null): UserPlayTimeGQL!
getUserVideos(userId: Int = null, limit: Int! = 5, after: String = null, filters: VideoFilterInput = null): VideoHistoryGQL!
getUserVideos(
userId: Int = null
limit: Int! = 5
after: String = null
filters: VideoFilterInput = null
): VideoHistoryGQL!
getUserTags(includeRetiredTags: Boolean = false): [TagGQL!]!
getGameTypeTagMetrics(input: GameTypeTagMetricsInput!): [GameTypeTagMetric!]!
videoExportJob(jobId: Int!): VideoExportJobGQL
@@ -124,7 +206,9 @@ input DatetimeRangeAggregationInput {
feature: String! = "created_at"
}
"""Date with time (isoformat)"""
"""
Date with time (isoformat)
"""
scalar DateTime
input TimeInterval @oneOf {
@@ -240,7 +324,9 @@ input DateRangeFilter {
greaterThanInclusive: Boolean! = true
}
"""Date (isoformat)"""
"""
Date (isoformat)
"""
scalar Date
type BucketSetGQL {
@@ -766,7 +852,8 @@ type MedalGQL {
input MedalScope @oneOf {
videoId: Int
interval: TimeInterval @deprecated(reason: "NO LONGER SUPPORTED, USE DATETIME_RANGE")
interval: TimeInterval
@deprecated(reason: "NO LONGER SUPPORTED, USE DATETIME_RANGE")
datetimeRange: DatetimeRangeAggregationInput
}
@@ -909,6 +996,81 @@ input DatetimeOrdering {
startingAt: DateTime = null
}
type SessionCoachGQL {
videoId: Int!
processingId: Int
analysisVersion: String!
state: SessionCoachStateEnum!
generatedAt: DateTime!
processingStatus: String
ineligibilityReason: SessionCoachIneligibilityReasonEnum
summary: String
shotCount: Int!
madeCount: Int!
missedCount: Int!
unknownOutcomeCount: Int!
analyzedShotCount: Int!
makePercentage: Float
primaryCandidate: SessionCoachCandidateGQL
candidates: [SessionCoachCandidateGQL!]!
}
enum SessionCoachStateEnum {
PROCESSING
READY
NO_TRACKED_SHOTS
INELIGIBLE
FAILED
}
enum SessionCoachIneligibilityReasonEnum {
MULTIPLAYER
}
type SessionCoachCandidateGQL {
id: ID!
rank: Int!
family: SessionCoachCandidateFamilyEnum!
kind: SessionCoachCandidateKindEnum!
title: String!
body: String!
action: String!
supportingShotIds: [Int!]!
drillId: String
evidence: SessionCoachEvidenceGQL!
limitations: [String!]!
}
enum SessionCoachCandidateFamilyEnum {
AIMING
CUT_ANGLE
SPIN
OBJECT_TO_POCKET_DISTANCE
CUE_TO_OBJECT_DISTANCE
BACK_CUT
}
enum SessionCoachCandidateKindEnum {
DIRECTIONAL_MISS
OUTCOME_PATTERN
USAGE_PATTERN
}
type SessionCoachEvidenceGQL {
bucketKey: String!
bucketLabel: String!
attemptCount: Int!
madeCount: Int!
missedCount: Int!
makePercentage: Float!
comparisonAttemptCount: Int!
comparisonMadeCount: Int!
comparisonMakePercentage: Float
patternShotCount: Int!
score: Float!
rankReason: String!
}
type PlayerClusterGQL {
videoId: Int!
clusterId: Int!
@@ -1285,7 +1447,9 @@ type StorageStatusGQL {
lastCalculatedAt: DateTime
}
"""Integer value that can exceed GraphQL Int's 32-bit range."""
"""
Integer value that can exceed GraphQL Int's 32-bit range.
"""
scalar BigInt
type UserPlayTimeGQL {
@@ -1356,14 +1520,34 @@ enum VideoExportStatusEnum {
"""
The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf).
"""
scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf")
scalar JSON
@specifiedBy(
url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf"
)
type Mutation {
createBucketSet(params: CreateBucketSetInput!): BucketSetGQL!
createRuleSet(name: String!, description: String = null): RuleSet!
createChallenge(name: String!, ruleSetId: ID!, minimumShots: Int!, startDate: DateTime!, endDate: DateTime!, description: String = null, requiredTableSize: Float = null, requiredPocketSize: Float = null, isPublic: Boolean! = false, maxAttempts: Int = null): Challenge!
inviteUsersToChallenge(challengeId: ID!, userIds: [ID!]!): [ChallengeInvitation!]!
respondToChallengeInvitation(invitationId: ID!, accept: Boolean!): ChallengeInvitation!
createChallenge(
name: String!
ruleSetId: ID!
minimumShots: Int!
startDate: DateTime!
endDate: DateTime!
description: String = null
requiredTableSize: Float = null
requiredPocketSize: Float = null
isPublic: Boolean! = false
maxAttempts: Int = null
): Challenge!
inviteUsersToChallenge(
challengeId: ID!
userIds: [ID!]!
): [ChallengeInvitation!]!
respondToChallengeInvitation(
invitationId: ID!
accept: Boolean!
): ChallengeInvitation!
startChallenge(challengeId: ID!): ChallengeEntry!
recalculateChallengeEntry(entryId: ID!): ChallengeEntry!
submitChallengeEntry(entryId: ID!, videoId: ID!): ChallengeEntry!
@@ -1371,30 +1555,61 @@ type Mutation {
undismissChallenge(challengeId: ID!): Boolean!
setLoggerLevel(path: String!, level: String!): Boolean!
reactToVideo(videoId: Int!, reaction: ReactionEnum): Boolean!
commentOnVideo(videoId: Int!, message: String!, parentCommentId: Int): Boolean!
commentOnVideo(
videoId: Int!
message: String!
parentCommentId: Int
): Boolean!
editComment(videoId: Int!, commentId: Int!, newMessage: String!): Boolean!
deleteComment(videoId: Int!, commentId: Int!): Boolean!
blockContent(videoId: Int!): Boolean!
blockUser(userId: Int!): Boolean!
reportContent(videoId: Int!, reason: ReportReasonEnum!, customReason: String = null): Boolean!
reportContent(
videoId: Int!
reason: ReportReasonEnum!
customReason: String = null
): Boolean!
markNotificationAsRead(notificationId: Int!): Boolean!
markAllNotificationsAsRead: Boolean!
markNotificationsAsRead(notificationIds: [Int!]!): Boolean!
deleteNotification(notificationId: Int!): Boolean!
createPoolHall(input: CreatePoolHallInput!): PoolHall!
updatePoolHall(input: UpdatePoolHallInput!): PoolHall!
createPoolHallCamera(input: CreatePoolHallCameraInput!): PoolHallCameraStreamCredentials!
createPoolHallCamera(
input: CreatePoolHallCameraInput!
): PoolHallCameraStreamCredentials!
updatePoolHallCamera(input: UpdatePoolHallCameraInput!): PoolHallCamera!
rotatePoolHallCameraStreamKey(cameraId: ID!): PoolHallCameraStreamCredentials!
createCameraClaimSession(cameraId: ID!, durationMinutes: Int = null, videoName: String = null, videoPrivate: Boolean = null, agreedTermsVersion: Int = null, tags: [VideoTagInput!] = null): CameraClaimSession!
createCameraClaimSession(
cameraId: ID!
durationMinutes: Int = null
videoName: String = null
videoPrivate: Boolean = null
agreedTermsVersion: Int = null
tags: [VideoTagInput!] = null
): CameraClaimSession!
cancelCameraClaimSession(claimSessionId: ID!): CameraClaimSession!
endCameraLease(leaseId: ID!): CameraLease!
extendCameraLease(leaseId: ID!, additionalMinutes: Int! = 60): CameraLease!
finalizePlayerAssignments(input: FinalizePlayerAssignmentsInput!): [PlayerClusterGQL!]!
addAnnotationToShot(shotId: Int!, annotationName: String!, notes: String = null): AddShotAnnotationReturn!
updateShotAnnotations(shotId: Int!, annotations: [UpdateAnnotationInputGQL!]!): UpdateShotAnnotationReturn!
editShot(shotId: Int!, fieldsToEdit: EditableShotFieldInputGQL!): EditShotReturn!
getProfileImageUploadLink(fileExt: String = ".png"): GetProfileUploadLinkReturn!
finalizePlayerAssignments(
input: FinalizePlayerAssignmentsInput!
): [PlayerClusterGQL!]!
addAnnotationToShot(
shotId: Int!
annotationName: String!
notes: String = null
): AddShotAnnotationReturn!
updateShotAnnotations(
shotId: Int!
annotations: [UpdateAnnotationInputGQL!]!
): UpdateShotAnnotationReturn!
editShot(
shotId: Int!
fieldsToEdit: EditableShotFieldInputGQL!
): EditShotReturn!
getProfileImageUploadLink(
fileExt: String = ".png"
): GetProfileUploadLinkReturn!
editProfileImageUri(profileImageUri: String!): UserGQL!
editUser(input: EditUserInputGQL!): UserGQL!
followUser(followedUserId: Int!): UserGQL!
@@ -1402,20 +1617,39 @@ type Mutation {
retireTags(tagIds: [Int!]!): Boolean!
ensureStripeCustomerExists: UserGQL!
deleteUser: Boolean!
syncAppleSubscription(input: SyncAppleSubscriptionInputGQL!): SyncAppleSubscriptionResultGQL!
syncAppleSubscription(
input: SyncAppleSubscriptionInputGQL!
): SyncAppleSubscriptionResultGQL!
createSubscription(priceId: String!): CreateSubscriptionResultGQL!
createCustomerPortalSession: CreateCustomerPortalSessionResultGQL!
cancelSubscription: UserSubscriptionStatusGQL!
grantManualEntitlement(userId: Int!, tierName: String! = "pro", startsAt: DateTime = null, endsAt: DateTime = null, reason: String = null): UserSubscriptionStatusGQL!
grantManualEntitlement(
userId: Int!
tierName: String! = "pro"
startsAt: DateTime = null
endsAt: DateTime = null
reason: String = null
): UserSubscriptionStatusGQL!
revokeManualEntitlement(userId: Int!): UserSubscriptionStatusGQL!
submitCancellationFeedback(reasons: [CancellationReasonEnum!] = null, feedback: String = null, metadata: CancellationFeedbackMetadataInput = null): Boolean!
submitCancellationFeedback(
reasons: [CancellationReasonEnum!] = null
feedback: String = null
metadata: CancellationFeedbackMetadataInput = null
): Boolean!
requestVideoExport(input: RequestVideoExportInput!): VideoExportJobGQL!
dismissVideoExport(jobId: Int!): Boolean!
findPrerecordTableLayout(b64Image: String!, videoId: Int!): HomographyInfoGQL
createUploadStream(videoMetadata: VideoMetadataInput!, expectedDurationSeconds: Float = null): CreateUploadStreamReturn!
createUploadStream(
videoMetadata: VideoMetadataInput!
expectedDurationSeconds: Float = null
): CreateUploadStreamReturn!
getUploadLink(videoId: Int!, segmentIndex: Int!): GetUploadLinkReturn!
getHlsInitUploadLink(videoId: Int!): GetUploadLinkReturn!
setSegmentDuration(videoId: Int!, segmentIndex: Int!, duration: Float!): Boolean!
setSegmentDuration(
videoId: Int!
segmentIndex: Int!
duration: Float!
): Boolean!
editUploadStream(videoId: Int!, videoMetadata: VideoMetadataInput!): Boolean!
deleteVideo(videoId: Int!): Boolean!
deleteTags(videoId: Int!, tagsToDelete: [VideoTagInput!]!): Boolean!
@@ -1507,7 +1741,9 @@ type AddShotAnnotationReturn {
value: SuccessfulAddAddShotAnnotationErrors!
}
union SuccessfulAddAddShotAnnotationErrors = SuccessfulAdd | AddShotAnnotationErrors
union SuccessfulAddAddShotAnnotationErrors =
SuccessfulAdd
| AddShotAnnotationErrors
type SuccessfulAdd {
value: Boolean!
@@ -1517,7 +1753,9 @@ type AddShotAnnotationErrors {
error: DoesNotOwnShotErrOtherErrorNeedsNote!
}
union DoesNotOwnShotErrOtherErrorNeedsNote = DoesNotOwnShotErr | OtherErrorNeedsNote
union DoesNotOwnShotErrOtherErrorNeedsNote =
DoesNotOwnShotErr
| OtherErrorNeedsNote
type DoesNotOwnShotErr {
shotId: Int!
@@ -1558,7 +1796,9 @@ type GetProfileUploadLinkReturn {
value: UploadLinkGetProfileUploadLinkErrors!
}
union UploadLinkGetProfileUploadLinkErrors = UploadLink | GetProfileUploadLinkErrors
union UploadLinkGetProfileUploadLinkErrors =
UploadLink
| GetProfileUploadLinkErrors
type UploadLink {
uploadUrl: String!
@@ -1645,9 +1885,12 @@ input VideoMetadataInput {
videoName: String = null
startTime: DateTime = null
endTime: DateTime = null
gameType: String = null @deprecated(reason: "`game_type` is deprecated. Use `tags` instead.")
gameType: String = null
@deprecated(reason: "`game_type` is deprecated. Use `tags` instead.")
"""A list of tags associated with the video. Replace `game_type`"""
"""
A list of tags associated with the video. Replace `game_type`
"""
tags: [VideoTagInput!] = null
tableSize: Float = null
pocketSize: Float = null
@@ -1676,7 +1919,15 @@ type GetUploadLinkErrors {
error: MustHaveSetForUploadLinkErrSegmentAlreadyUploadedErrProcessingFailedErrNoInitForChunkedUploadErrTooManyProfileImageUploadsErrInitUploadAlreadyCompletedErrTooManyInitUploadsErrStorageLimitExceededErr!
}
union MustHaveSetForUploadLinkErrSegmentAlreadyUploadedErrProcessingFailedErrNoInitForChunkedUploadErrTooManyProfileImageUploadsErrInitUploadAlreadyCompletedErrTooManyInitUploadsErrStorageLimitExceededErr = MustHaveSetForUploadLinkErr | SegmentAlreadyUploadedErr | ProcessingFailedErr | NoInitForChunkedUploadErr | TooManyProfileImageUploadsErr | InitUploadAlreadyCompletedErr | TooManyInitUploadsErr | StorageLimitExceededErr
union MustHaveSetForUploadLinkErrSegmentAlreadyUploadedErrProcessingFailedErrNoInitForChunkedUploadErrTooManyProfileImageUploadsErrInitUploadAlreadyCompletedErrTooManyInitUploadsErrStorageLimitExceededErr =
MustHaveSetForUploadLinkErr
| SegmentAlreadyUploadedErr
| ProcessingFailedErr
| NoInitForChunkedUploadErr
| TooManyProfileImageUploadsErr
| InitUploadAlreadyCompletedErr
| TooManyInitUploadsErr
| StorageLimitExceededErr
type MustHaveSetForUploadLinkErr {
resolution: Boolean