Compare commits

..

7 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
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
4 changed files with 317 additions and 51 deletions

View File

@@ -4319,7 +4319,7 @@ export type VideoExportJobGql = {
createdAt?: Maybe<Scalars["DateTime"]["output"]>;
downloadUrl?: Maybe<Scalars["String"]["output"]>;
expiresAt?: Maybe<Scalars["DateTime"]["output"]>;
fileSizeBytes?: Maybe<Scalars["Int"]["output"]>;
fileSizeBytes?: Maybe<Scalars["BigInt"]["output"]>;
id: Scalars["Int"]["output"];
mode: VideoExportModeEnum;
runId?: Maybe<Scalars["Int"]["output"]>;
@@ -6891,8 +6891,10 @@ export type ReactToVideoMutation = {
};
export type GetShotLabVideosQueryVariables = Exact<{
userId: Scalars["Int"]["input"];
feedInput: VideoFeedInputGql;
includePrivate: IncludePrivateEnum;
limit?: Scalars["Int"]["input"];
after?: InputMaybe<Scalars["String"]["input"]>;
}>;
export type GetShotLabVideosQuery = {
@@ -6920,6 +6922,11 @@ export type GetShotLabVideosQuery = {
lastSegmentUploadedAt?: any | null;
} | null;
}>;
pageInfo: {
__typename?: "PageInfoGQL";
hasNextPage: boolean;
endCursor?: string | null;
};
};
};
@@ -7474,6 +7481,76 @@ export type FinalizePlayerAssignmentsMutation = {
}>;
};
export type TableStateLayoutFieldsFragment = {
__typename?: "TableStateGQL";
identifierToPosition: Array<Array<number>>;
identifierToColor: Array<{ __typename?: "RGBColorGQL"; hex: string } | null>;
};
export type DetectionBoxFieldsFragment = {
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
};
export type DetectionPocketPointFieldsFragment = {
__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 };
};
export type TableStateDetectionFieldsFragment = {
__typename?: "TableStateGQL";
identifierToBox: Array<{
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
}>;
homography?: {
__typename?: "HomographyInfoGQL";
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 };
};
destPoints: {
__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;
};
export type GetTableStateQueryVariables = Exact<{
b64Image: Scalars["String"]["input"];
tableSize?: InputMaybe<Scalars["Float"]["input"]>;
@@ -7489,6 +7566,48 @@ export type GetTableStateQuery = {
__typename?: "RGBColorGQL";
hex: string;
} | null>;
identifierToBox: Array<{
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
}>;
homography?: {
__typename?: "HomographyInfoGQL";
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 };
};
destPoints: {
__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;
};
};
@@ -9275,7 +9394,7 @@ export type VideoExportJobFieldsFragment = {
shotIds?: Array<number> | null;
runId?: number | null;
downloadUrl?: string | null;
fileSizeBytes?: number | null;
fileSizeBytes?: any | null;
expiresAt?: any | null;
createdAt?: any | null;
};
@@ -9297,7 +9416,7 @@ export type RequestVideoExportMutation = {
shotIds?: Array<number> | null;
runId?: number | null;
downloadUrl?: string | null;
fileSizeBytes?: number | null;
fileSizeBytes?: any | null;
expiresAt?: any | null;
createdAt?: any | null;
};
@@ -9329,7 +9448,7 @@ export type VideoExportJobQuery = {
shotIds?: Array<number> | null;
runId?: number | null;
downloadUrl?: string | null;
fileSizeBytes?: number | null;
fileSizeBytes?: any | null;
expiresAt?: any | null;
createdAt?: any | null;
} | null;
@@ -9353,7 +9472,7 @@ export type MyVideoExportsQuery = {
shotIds?: Array<number> | null;
runId?: number | null;
downloadUrl?: string | null;
fileSizeBytes?: number | null;
fileSizeBytes?: any | null;
expiresAt?: any | null;
createdAt?: any | null;
}>;
@@ -9974,6 +10093,73 @@ export const PlayerClusterFieldsFragmentDoc = gql`
}
${PlayerClusterShotFieldsFragmentDoc}
`;
export const TableStateLayoutFieldsFragmentDoc = gql`
fragment TableStateLayoutFields on TableStateGQL {
identifierToPosition
identifierToColor {
hex
}
}
`;
export const DetectionBoxFieldsFragmentDoc = gql`
fragment DetectionBoxFields on BoundingBoxGQL {
left
top
width
height
}
`;
export const DetectionPocketPointFieldsFragmentDoc = gql`
fragment DetectionPocketPointFields on PocketPointsGQL {
topLeft {
x
y
}
topSide {
x
y
}
topRight {
x
y
}
bottomLeft {
x
y
}
bottomSide {
x
y
}
bottomRight {
x
y
}
}
`;
export const TableStateDetectionFieldsFragmentDoc = gql`
fragment TableStateDetectionFields on TableStateGQL {
identifierToBox {
...DetectionBoxFields
}
homography {
crop {
...DetectionBoxFields
}
pockets {
...DetectionBoxFields
}
sourcePoints {
...DetectionPocketPointFields
}
destPoints {
...DetectionPocketPointFields
}
}
}
${DetectionBoxFieldsFragmentDoc}
${DetectionPocketPointFieldsFragmentDoc}
`;
export const ShotClipRangeFragmentDoc = gql`
fragment ShotClipRange on ShotGQL {
id
@@ -15171,12 +15357,18 @@ export type ReactToVideoMutationOptions = Apollo.BaseMutationOptions<
ReactToVideoMutationVariables
>;
export const GetShotLabVideosDocument = gql`
query GetShotLabVideos($userId: Int!, $limit: Int! = 25) {
query GetShotLabVideos(
$feedInput: VideoFeedInputGQL!
$includePrivate: IncludePrivateEnum!
$limit: Int! = 25
$after: String = null
) {
getFeedVideos(
limit: $limit
after: $after
includeCallersVideos: false
includePrivate: MINE
feedInput: { userId: $userId }
includePrivate: $includePrivate
feedInput: $feedInput
) {
videos {
id
@@ -15196,6 +15388,10 @@ export const GetShotLabVideosDocument = gql`
lastSegmentUploadedAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
@@ -15212,8 +15408,10 @@ export const GetShotLabVideosDocument = gql`
* @example
* const { data, loading, error } = useGetShotLabVideosQuery({
* variables: {
* userId: // value for 'userId'
* feedInput: // value for 'feedInput'
* includePrivate: // value for 'includePrivate'
* limit: // value for 'limit'
* after: // value for 'after'
* },
* });
*/
@@ -15913,12 +16111,12 @@ export const GetTableStateDocument = gql`
tableSize: $tableSize
useHomography: $useHomography
) {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
...TableStateDetectionFields
}
}
${TableStateLayoutFieldsFragmentDoc}
${TableStateDetectionFieldsFragmentDoc}
`;
/**
@@ -15994,13 +16192,11 @@ export const GetLiveTableStateDocument = gql`
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
}
}
}
${TableStateLayoutFieldsFragmentDoc}
`;
/**
@@ -16075,13 +16271,11 @@ export const GetShotTableStateDocument = gql`
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
}
}
}
${TableStateLayoutFieldsFragmentDoc}
`;
/**

View File

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

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(
$b64Image: String!
$tableSize: Float
@@ -8,10 +77,8 @@ query GetTableState(
tableSize: $tableSize
useHomography: $useHomography
) {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
...TableStateDetectionFields
}
}
@@ -20,10 +87,7 @@ query GetLiveTableState($videoId: Int!) {
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
}
}
}
@@ -34,10 +98,7 @@ query GetShotTableState($shotId: Int!) {
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
...TableStateLayoutFields
}
}
}

View File

@@ -249,6 +249,11 @@ input CreatePoolHallCameraInput {
claimTermsText: String = null
}
input CreateShotShareLinkInput {
shotIds: [Int!]!
paddingSeconds: Float = null
}
input CreatePoolHallInput {
name: String!
address: String = null
@@ -259,11 +264,6 @@ input CreatePoolHallInput {
timezone: String = null
}
input CreateShotShareLinkInput {
shotIds: [Int!]!
paddingSeconds: Float = null
}
type CreateSubscriptionResultGQL {
checkoutUrl: String!
sessionId: String!
@@ -1459,11 +1459,6 @@ enum SessionCoachStateEnum {
FAILED
}
type ShareLinkGQL {
slug: String!
url: String!
}
type ShotAnnotationGQL {
shotId: Int!
type: ShotAnnotationTypeGQL!
@@ -1925,6 +1920,11 @@ type UserSubscriptionStatusGQL {
stripeSubscriptionId: String
}
type ShareLinkGQL {
slug: String!
url: String!
}
type VideoExportJobGQL {
id: Int!
videoId: Int!
@@ -1935,7 +1935,7 @@ type VideoExportJobGQL {
shotIds: [Int!]
runId: Int
downloadUrl: String
fileSizeBytes: Int
fileSizeBytes: BigInt
expiresAt: DateTime
createdAt: DateTime
}