Compare commits

..

5 Commits

Author SHA1 Message Date
19bdf69830 Add suggestPositionShot schema types and operation
Mirrors the backend's position-play search: SuggestPositionShotInputGQL
(pot lock + target-area polygon), PositionSuggestionGQL /
PositionShotCandidateGQL / CueStrikeGQL, the Query field, and a
SuggestPositionShot operation with a shared candidate fragment. The
regenerated hooks also provide the result types the mobile app uses to
shape local (billsim) physics results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:28:45 -07:00
303261f2d7 Add recorded Shot Lab queries 2026-07-11 20:56:47 -07:00
Dean Wenstrand
6f356f9535 Add freemium epoch + era conversion to funnel
All checks were successful
Tests / Tests (pull_request) Successful in 12s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:38:20 -07:00
Dean Wenstrand
5c0ff8c31b Add freemium funnel to getUsageStats
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:48:25 -07:00
Dean Wenstrand
582c20170d Add getUsageStats admin query + operation (schema regen on shot-lab tip)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:36:47 -07:00
5 changed files with 5946 additions and 14295 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
# 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
}
}
}

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
}
}
}
@@ -74,3 +85,53 @@ query ComputePotAim(
occludingBallIds
}
}
fragment PositionShotCandidate on PositionShotCandidateGQL {
strike {
v0
phi
theta
a
b
}
cueFinalPosition
inTargetArea
distanceToTargetArea
robustness
potted
scratched
projection {
trajectories {
ballId
points {
time
position
}
}
events {
eventType
time
ballIds
position
}
finalState {
ballId
position
}
pottedBallIds
}
}
query SuggestPositionShot($suggestionInput: SuggestPositionShotInputGQL!) {
suggestPositionShot(suggestionInput: $suggestionInput) {
achievable
evaluatedCount
successfulCount
best {
...PositionShotCandidate
}
alternates {
...PositionShotCandidate
}
}
}

View File

@@ -0,0 +1,38 @@
query getUsageStats($days: Int! = 30) {
getUsageStats(days: $days) {
funnel {
freemiumEpoch
eraSignups
eraPaying
activationCohort
activationActivated
payingUsers
newPaid7d
newPaid30d
retentionPrevActive
retentionReturned
}
totalUsers
totalVideos
totalShots
totalRuns
windows {
label
newUsers
activeUsers
sessions
shots
}
daily {
day
newUsers
activeUsers
sessions
shots
}
processingLast24h {
status
count
}
}
}

View File

@@ -73,12 +73,16 @@ type Query {
useHomography: HomographyInputGQL = null
): TableStateGQL!
getLiveTableState(videoId: Int!): LiveTableStateGQL!
getShotTableState(shotId: Int!): ShotTableStateGQL!
simulateShot(simulationInput: SimulateShotInputGQL!): ShotProjectionGQL!
computePotAim(
simulationInput: SimulateShotInputGQL!
targetBallId: Int!
pocket: PocketIdentifier!
): PotAimGQL!
suggestPositionShot(
suggestionInput: SuggestPositionShotInputGQL!
): PositionSuggestionGQL!
getOrderedShots(
filterInput: FilterInput!
ids: [Int!] = null
@@ -100,6 +104,7 @@ type Query {
countRespectsLimit: Boolean! = false
): [ShotGQL!]!
getShotsByIds(ids: [Int!]!): [ShotGQL!]!
getUsageStats(days: Int! = 30): UsageStatsGQL!
getUser(userId: Int!): UserGQL
doesUsernameExist(candidateUsername: String!): Boolean!
getLoggedInUser: UserGQL
@@ -608,7 +613,7 @@ type VideoTagClass {
}
type HomographyInfoGQL {
id: Int!
id: Int
frameIndex: Int!
crop: BoundingBoxGQL!
pockets: [BoundingBoxGQL!]!
@@ -1064,6 +1069,13 @@ type LiveTableStateGQL {
tableState: TableStateGQL!
}
type ShotTableStateGQL {
shotId: Int!
videoId: Int!
frameIndex: Int!
tableState: TableStateGQL!
}
type ShotProjectionGQL {
trajectories: [BallTrajectoryGQL!]!
events: [SimulationEventGQL!]!
@@ -1134,6 +1146,44 @@ type PotAimGQL {
occludingBallIds: [Int!]!
}
type PositionSuggestionGQL {
achievable: Boolean!
best: PositionShotCandidateGQL
alternates: [PositionShotCandidateGQL!]!
evaluatedCount: Int!
successfulCount: Int!
}
type PositionShotCandidateGQL {
strike: CueStrikeGQL!
cueFinalPosition: [Float!]!
inTargetArea: Boolean!
distanceToTargetArea: Float!
robustness: Int!
potted: Boolean!
scratched: Boolean!
projection: ShotProjectionGQL
}
type CueStrikeGQL {
v0: Float!
phi: Float!
theta: Float!
a: Float!
b: Float!
}
input SuggestPositionShotInputGQL {
cueBallId: Int!
targetBallId: Int!
pocket: PocketIdentifier!
targetArea: [[Float!]!]!
balls: [SimulationBallStateInputGQL!] = null
b64Image: String = null
useHomography: HomographyInputGQL = null
tableSize: Float = null
}
type GetShotsResult {
shots: [ShotGQL!]!
count: Int
@@ -1168,6 +1218,51 @@ input CreatedAfter @oneOf {
createdAt: DateTime
}
type UsageStatsGQL {
funnel: FunnelStatsGQL!
totalUsers: Int!
totalVideos: Int!
totalShots: Int!
totalRuns: Int!
windows: [UsageStatsWindowGQL!]!
daily: [UsageStatsDailyGQL!]!
processingLast24h: [ProcessingStatusCountGQL!]!
}
type FunnelStatsGQL {
freemiumEpoch: Date!
eraSignups: Int!
eraPaying: Int!
activationCohort: Int!
activationActivated: Int!
payingUsers: Int!
newPaid7d: Int!
newPaid30d: Int!
retentionPrevActive: Int!
retentionReturned: Int!
}
type UsageStatsWindowGQL {
label: String!
newUsers: Int!
activeUsers: Int!
sessions: Int!
shots: Int!
}
type UsageStatsDailyGQL {
day: Date!
newUsers: Int!
activeUsers: Int!
sessions: Int!
shots: Int!
}
type ProcessingStatusCountGQL {
status: String!
count: Int!
}
type UserRelationshipsResult {
inquiringUser: UserGQL!
relationships: [UserRelationship!]!