Compare commits

...
Author SHA1 Message Date
Brandon Thomas c65a66655a performance 2026-07-02 16:38:11 -04:00
Brandon Thomas 23cbb03b39 codegen db 2026-07-02 16:36:17 -04:00
Brandon Thomas d3cb23753a format page 2026-07-02 16:32:26 -04:00
Brandon Thomas 47c0cd23f8 frontend updates 2026-07-02 16:27:32 -04:00
Brandon Thomas f398b3af99 sticky footers 2026-07-02 16:15:42 -04:00
Brandon Thomas 8bd0e9219b links 2026-07-02 16:11:56 -04:00
Brandon Thomas 298aff708e frontend update 2026-07-02 16:06:23 -04:00
Brandon Thomas 197ec95012 debug logs: new field, new queries, new APIs 2026-07-02 15:54:59 -04:00
Brandon Thomas cbd3710f4f seedance text prompt max length (#1721) 2026-07-02 11:26:02 -04:00
BFlat 044c6d8a88 Model list cleanup: shared omni-gen hooks, dead code removal (#1719)
### New shared lib: `@storyteller/omni-gen`
- The `omni-gen-hooks.ts` file was copy-pasted between `artcraft-webapp`
and
`artcraft-website` and had drifted (website copy was missing outage
toasts,
carried a stale hardcoded display-name map, and had weaker icon
resolution).
- Extracted the newer (webapp) version into a new lib
`frontend/libs/omni-gen`
  (generated via `nx g @nx/react:library`): singleton session cache,
`useOmniGenImageModels()` / `useOmniGenVideoModels()`, outage message
constants.
- App-local toasts are injected via `setOmniGenErrorNotifier()`, called
once in
each app's `main.tsx`, since the hooks are singleton-cache based and
each app
  has its own toast component.

### Consolidated model metadata into `@storyteller/model-list`
- `getModelDescription()` / `getModelInfo()` (tagline + info-blurb
fallbacks)
  moved to `ModelDescriptions.ts`; API values still always win.
- `getModelDisplayName()` gained an optional `fullName` param (API name
wins);
  the app-local wrapper is gone.
- Both apps' local `getModelCreatorIconPath()` replaced with the
existing,
stronger `getCreatorIconPathForModelId()` (checks `ALL_MODELS_LIST`
first,
then prefix match). The loader's duplicate `CREATOR_BY_PREFIX` map now
reuses
  the same shared `MODEL_ID_PREFIX_CREATORS` table.

### Dead code removed
- `modelReconciliation.ts`: deprecated `reconcile*()` helpers (never
called) and
the empty, retired `BACKEND_TO_TAURI_*` alias maps.
`buildModelsFromListing()`
dropped its alias-map indirection accordingly (backend ids == tauriIds
since
the 2026-07 id migration). `modelCreatorFromBackend()` moved inline into
the
  loader, its only consumer.
- Six unused static `*_PAGE_MODEL_LIST` constants in
`model-selectors-for-pages.tsx`
(superseded by the `use*PageModelList` hooks in #1710). The two
`IMAGE_TO_3D_*`
  constants stay: 3D models are not served by the omni backend.
- Untracked the accidentally committed
`apps/artcraft/tsconfig.tsbuildinfo` and
  gitignored `*.tsbuildinfo`.

### Website-visible improvements (from adopting the newer hooks)
- Model outage toasts now appear on the website (previously
webapp-only).
- Current display names from the canonical map (previously-unmapped ids
like
  `gpt_image_2` and Midjourney models rendered as raw ids).
- Model taglines/descriptions now render in the website model picker.
- Midjourney and SwitchX creator icons now resolve.
2026-07-02 15:06:15 +07:00
Brandon Thomas 353c49c55f bump artcraft to 0.36 (#1718) 2026-07-02 03:45:07 -04:00
Brandon Thomas 98ea5cdb62 shink assets that are way too large (#1717) 2026-07-02 03:42:41 -04:00
Brandon Thomas 8fdc5100af vidu (#1716) 2026-07-02 03:12:15 -04:00
Brandon Thomas b782cb0c67 veo (#1715) 2026-07-02 02:18:16 -04:00
Brandon Thomas f01e81b66a artcraft 0.35 (#1714) 2026-07-02 02:02:16 -04:00
Brandon Thomas 3c0d1415e5 update pathing 2026-07-02 01:30:07 -04:00
Brandon Thomas 5e4b7f271d artcraft 0.34 (#1713) 2026-07-02 01:16:31 -04:00
Brandon Thomas 2371dfd49e move assets out of build to decrease binary size (#1712) 2026-07-02 01:15:30 -04:00
Brandon Thomas 2f20e7b3a3 fix midjourney (#1711) 2026-07-02 00:06:27 -04:00
Brandon Thomas 9de8fb8979 tauri frontend - read from omni api (#1710) 2026-07-01 23:20:11 -04:00
Brandon Thomas a61fcd50ed tauri omni client (#1709) 2026-07-01 16:31:23 -04:00
Brandon Thomas c16db8793c dashboard - change icon; group sidebar (#1708) 2026-06-30 11:05:18 -04:00
Brandon Thomas b3311d1574 payment informatics 4 (#1707) 2026-06-30 10:40:28 -04:00
Brandon Thomas 35add74498 payment informatics 3 (#1706) 2026-06-30 08:19:52 -04:00
Brandon Thomas d99da56df0 payment informatics #2 (#1705) 2026-06-29 21:21:26 -04:00
Brandon Thomas 3e4f998599 payments informatics (part 1) (#1704) 2026-06-29 17:49:28 -04:00
Brandon Thomas 3bccac5b2e block usernames that look like tokens (#1703) 2026-06-27 19:47:56 -04:00
Brandon Thomas aa9e862ff2 grok fixes (#1702) 2026-06-27 13:52:51 -04:00
BFlat db871126df Webapp/website (promptbox): full editing controls in fullscreen focus mode (#1701)
Add a footer to the fullscreen prompt modal (desktop lib + webapp) so
focus mode is a complete editing surface, not just the textarea:

- Model selector, always-on reference-image row, and a Done button
  (closes the modal) instead of Generate.
- Video: also surfaces the input-mode (keyframe/reference) picker and
  the @Characters button (seedance_2p0). Shared image-row / character /
  input-mode elements are reused inline and in the modal (DRY).
- Webapp: model selector + leftToolbar + image row in the footer.

Layout: editor area is flex-1/min-h-0/overflow-hidden with the image row
and footer shrink-0, so the editor takes only the leftover space and
scrolls internally — resizing the window shorter no longer overflows.
MentionTextarea gains a `style` prop so the modal can cap resize/height.
2026-06-26 23:16:35 +07:00
BFlat aaff33955e Webapp/website: Promptbox fullscreen "focus mode" + 3D & tooltip fixes (#1700)
### Fullscreen focus mode (desktop + webapp)
- Added a top-right expand icon on every prompt editor that opens a
"Prompt" focus-mode overlay (blurred backdrop, `max-w-4xl` centered
card) for more prompting space.
- Same editor state — overlay binds to the same value + onChange, so
text/@-mentions persist. Auto-focuses with the caret at the end on open.
- Editor fills full height; @-mention dropdown no longer clipped; closes
on Esc / backdrop click.
- Reused `@storyteller/ui-modal`; shared `PromptFullscreenModal` +
button + `useFullscreenPrompt` hook across all 5 desktop promptboxes,
mirrored in the webapp's own PromptBox.

### 3D editor
- Model selector now renders inside the promptbox (was standalone on the
canvas).
- Pose button hides on deselect; pressing "Done" keeps it (flips back to
"Enter Pose Mode") while the character is still selected.
- Drag and drop into folders fix within gallery modal - doesn't close
immediately unless on 3D editor page.

### Tooltips & expand
- Fixed top-left menu tooltips getting stuck open after a tab change.
- Reworked image promptbox auto-grow/expand into a shared
`useAutoGrowEditorHeight` hook (no per-render style writes) with a
smooth height transition.
2026-06-26 21:54:29 +07:00
Tanish Manku ad5ae7968f Fix login modal: stop Reveal motion wrappers from stealing input focus (#1698)
Reveal/RevealGroup called `motion(as)` inside the render body. Unlike
the cached `motion.div` accessor, calling `motion()` as a function
returns a brand-new component type on every invocation. So each
re-render produced a different element type at that position, React
unmounted and remounted the whole subtree, and any focused <input>
inside (the login/signup/forgot-password forms) lost focus on every
keystroke — you could only type one character at a time.

Cache one motion component per element type via a module-level map so
the component reference stays stable across renders. React now
reconciles in place and focus is preserved.

Verified with a Playwright A/B test on the login form: before the fix
typing "test@example.com" left only "t" with focus on <body> (1 blur
mid-type); after the fix the full value is retained, focus stays on the
input, zero blurs.
2026-06-25 13:48:53 -07:00
Brandon Thomas fef83ad271 mini models in api gateway (#1697) 2026-06-25 15:33:51 -04:00
BFlatandClaude 864cfb5faf Fix image refs not attaching when selected from a folder in gallery modal (#1696)
handleUseSelected only searched groupedItems (root library), so picks
made
while browsing inside a folder — which live in folderGroupedItems —
resolved
to nothing and an empty array was passed to onUseSelected. Search both
sources, deduping by id, so folder selections attach correctly.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-25 20:38:53 +07:00
Brandon Thomas 2aed062790 seedance mini client (#1695) 2026-06-25 07:45:25 -04:00
Brandon Thomas 4d5f8e9c21 router 4k (#1693) 2026-06-24 17:18:37 -04:00
Brandon Thomas 98f9504bdd video models list refactor (#1692) 2026-06-24 13:56:56 -04:00
Brandon Thomas 7ff72d2ecc seedance 4k (#1691) 2026-06-24 12:45:11 -04:00
bflatastic 10eb062b6f seedance 2.1 -> 2.5 text + uncomment sidebar edit page 2026-06-24 22:52:32 +07:00
Brandon Thomas 3b225a7c6d omni api: status endpoint, api docs, etc (#1690) 2026-06-24 00:24:26 -04:00
Tanish MankuandBFlat 61a0880c80 Moodboard: Extensive feature support (#1661)
Co-authored-by: BFlat <bombayapirat@gmail.com>
2026-06-23 20:15:52 -07:00
Tanish Manku c5c68d301a Webapp: Premium motion pass across app (#1662)
Motion foundation + expressive entrance/interaction polish for the
webapp, following a "balanced" direction (restrained chrome, expressive
canvas).

Foundation
- Motion tokens: --ease-*/--dur-* CSS vars (styles.css) mirrored as JS
constants/presets in lib/motion.ts; prefers-reduced-motion respected.
- Shared Reveal/RevealGroup primitive (components/motion) — fade-up +
stagger.

Chrome & navigation
- Global route fade (components/motion/page-transition) keyed by
section.
- Sidebar active-indicator slide via framer layoutId + icon hover.

Pages
- Home: hero/eyebrow reveal, staggered launcher cards, spring lift/press
hover, double-bezel cards, nested arrow chip.
- Auth: AuthHeader staggers in (login+signup); form blocks cascade
after.
- Pricing: wire the orphaned data-reveal hero through the real Reveal.
- Library: sliding active indicator on tab/filter switchers (layoutId),
shimmer-sweep loading skeleton.

Pagescene entrance animations (user-drop only)
- EntranceAnimator: meshes get a transform-based pop/rise/fall drop with
a rubbery squash overshoot; splats get a per-gaussian radial settle
(Spark dyno) with center-out fade + height drop. Dev-only speed slider.

Gallery + asset drag-under UX
- On drag-start the gallery/asset modal becomes pointer-transparent so
the drag passes under onto the canvas; steps back to translucent (reopen
on) or eases fully out (reopen off), all interpolated. Shared DragGhost
(lift/tilt/follow) feeds both DnD systems. Media fades to placeholders
so thumbnails don't block the canvas. Asset modal always reopens fully
visible.

PageDraw: entrance fade on add + workspace vignette.

Dev-only (local testing): drop setDevelopment() API host pin; add /__cdn
proxy + maybeProxyCdnUrl so CDN splat/GLB assets dodge CORS.
2026-06-23 20:14:06 -07:00
Brandon Thomas 81de7838d3 omni api url support (#1689) 2026-06-23 23:10:14 -04:00
Brandon Thomas 9653e9082e minor change to test build works 2026-06-23 19:40:13 -04:00
Brandon ThomasandClaude Opus 4.8 ee6c064f4f dashboard netlify build script (#1687)
The netlify_build.sh referenced by the Netlify build command was never
committed, so the build failed with "No such file or directory".

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:36:19 -04:00
Brandon Thomas 630b132ae7 modify netlify configs for dashboard 2026-06-23 19:16:06 -04:00
Brandon Thomas 5742e638ed attempt to re-add the dashboard (#1686) 2026-06-23 19:04:53 -04:00
Tanish Mankuandbflatastic dc33a99d75 pagedraw: replace hardcoded chrome colors with semantic tokens (#1651)
Implements the P0 color item from the PageDraw design review. Swap raw
hex/palette chrome colors for the semantic tokens shared by both lib
hosts (artcraft desktop + artcraft-webapp):

- ContextMenu: #1A1A1A/#333333/#ECECEC -> bg-ui-panel, white/10
hairlines, text-base-fg/90
- SideToolbar: zinc swatch + #404040/#303030 popovers -> ui-controls /
bg-ui-panel + white/10 borders
- SliderWithIndicator: #5F5F68 tooltip -> bg-ui-controls
- SplitPane: gray-500/600 -> white/20-30
- HistoryStack: text-gray-400 -> text-base-fg/50
- PageDraw + base-image-selector: blue-500/600 accents -> primary token
(so the editor accent matches the product's brand-primary)
- pagedraw.css pegboard: #1b1b1b -> var(--st-canvas, #1b1b1b)

Only tokens defined in both host themes are used. Konva canvas-paint
colors (node strokes, brush defaults, selection fills) are intentionally
left as literals — they're canvas content, not CSS chrome. Oversized
modules deferred per scope.

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-06-23 15:15:28 -07:00
Brandon Thomas f6c5ea94eb fix staff logs null column (#1685) 2026-06-23 18:15:15 -04:00
Brandon Thomas 123fe83a3d better logging (#1684) 2026-06-23 17:21:33 -04:00
Brandon Thomas 654f9164d3 typo 2026-06-23 16:57:27 -04:00
Brandon Thomas 539824564b better job logging per poll iter (#1683) 2026-06-23 16:31:20 -04:00
Brandon Thomas fef420129d fix seedance client again - nullable width/height (#1682) 2026-06-23 16:13:54 -04:00
BFlat 39dd4dee9a revert dashboard merge (#1681) 2026-06-23 23:31:29 +07:00
bflatastic 02ac2319a1 temporarily commented out dashboard in tsconfig 2026-06-23 21:14:59 +07:00
bflatastic 6af1493c1c build fix react version nx 2026-06-23 21:05:49 +07:00
bflatastic 7c7e050d02 revert package json/lock file 2026-06-23 20:51:19 +07:00
Brandon Thomas fb93d297d3 development ports (#1680) 2026-06-23 09:22:25 -04:00
BFlat 38d0642904 Dashboard: ported over dashboard to monorepo (#1679) 2026-06-23 20:18:49 +07:00
bflatastic 2f9ffd4575 webapp: hide edit image for now 2026-06-23 18:03:48 +07:00
Brandon Thomas e8b19148fa seedance2pro better polling (#1678) 2026-06-22 21:01:08 -04:00
BFlat 31e912c4be Webapp: omni gen added bitrate field (#1677)
Add bitrate selector to video create page

- Add bitrate / bitrate_options / bitrate_default to OmniGenApi TS types
- Add bitrate to video UI store (sticky + persisted)
- Send bitrate in generate and cost-estimate requests
- Render Bitrate popover (desktop) and drawer section (mobile), shown
  only when the model returns bitrate_options
2026-06-22 21:56:34 +07:00
Brandon Thomas ceb9d402cf alter prompts add bitrate (#1676) 2026-06-22 10:18:43 -04:00
Brandon Thomas ac2e728000 seedance bitrate (#1675) 2026-06-22 08:24:29 -04:00
BFlat ef255f4ee6 Website: seedance 2.5 page (#1674) 2026-06-22 13:09:09 +07:00
Brandon Thomas 0c11a95669 resolution measure (#1673) 2026-06-20 10:13:30 -04:00
Brandon Thomas 9191fe337a handler filename (#1672) 2026-06-20 09:09:49 -04:00
Brandon Thomas eb3b733852 seedance cors (#1671) 2026-06-20 00:00:18 -04:00
Brandon Thomas 7d8d9462d1 rename legacy bucket client (#1670) 2026-06-19 23:52:30 -04:00
Brandon Thomas 660cf9f0ac bucket client (#1669) 2026-06-19 23:35:22 -04:00
Brandon Thomas bf373b1d5c netlify configs 2026-06-19 21:27:36 -04:00
Brandon Thomas 914c2532ec uploaded videos - tables, endpoints, frontend (#1668) 2026-06-19 21:17:45 -04:00
Brandon Thomas c7341d8f63 video info website (#1667) 2026-06-19 13:42:34 -04:00
Brandon Thomas 92e99cfcb6 video info handler (#1666) 2026-06-19 10:44:22 -04:00
Brandon Thomas 754cdb8a6c parse video info (#1665) 2026-06-19 10:24:01 -04:00
Brandon Thomas 7b9db101fe fix float/int credits parsing (#1664) 2026-06-18 19:37:13 -04:00
BFlat e94987e5a4 Restyle manifesto "control" emphasis as a tilted marker highlight
Swap the underline for a shared MarkerHighlight SVG stroke behind the word. Desktop reveal wipes left-to-right via clip-path instead of scaleX so the stroke is uncovered, not stretched.
2026-06-18 23:05:09 +07:00
BFlat 47d83be3fb Landing page polish: manifesto, feature deck, model marquee, communit… (#1663)
…y CTA

- Manifesto: drop "3D" and animate a blue underline on "control"
- Feature cards: scroll-driven stacking deck (scale back + fade once
covered)
- Use Every Model: fill chips from canonical model list with correct
brand icons
- Use Every Model: alternating-direction marquees, highlight on hover
- Stop renting: italicize "renting"; make the good/bad comparison read
clearly
- Community CTA: "Join our community" banner restyled to fit site + not
steal focus from final CTA
2026-06-18 11:25:24 +07:00
bflatastic fa4b57980c Webapp: rounded corner ui updates for library page 2026-06-17 22:15:57 +07:00
Brandon Thomas 87d060e68c fixes for frontend build (#1660) 2026-06-16 03:03:38 -04:00
Brandon Thomas fc7651071c more frontend build fixes (#1659) 2026-06-16 02:53:06 -04:00
Brandon Thomas 7a0bfa35e6 fixes for frontend build failures (#1658) 2026-06-16 02:41:36 -04:00
Brandon Thomas 0b4e288a3e refactor another module location 2026-06-16 01:53:13 -04:00
Brandon Thomas a0b6047a78 refactor module location 2026-06-16 01:42:01 -04:00
Brandon Thomas ebe1287182 misc cleanup, update claude docs (#1657) 2026-06-16 01:34:50 -04:00
Brandon Thomas de538ba94c package import 2026-06-16 00:37:23 -04:00
Brandon Thomas 29a127aac0 jsonify actix handlers (#1656) 2026-06-16 00:30:06 -04:00
Brandon Thomas 126f874c90 api key feature flag (#1655) 2026-06-15 23:25:58 -04:00
Brandon Thomas 65e72f8c24 omni api fixes (#1654) 2026-06-15 23:06:05 -04:00
Brandon Thomas fafb485a60 omni upload APIs (#1653) 2026-06-15 22:37:11 -04:00
Brandon Thomas 1e20f2bfff omni api image and video (#1652) 2026-06-15 22:08:31 -04:00
BFlat e30b4366ce Update SettingsModal.tsx to flip api feature flag 2026-06-16 09:06:38 +07:00
Tanish Manku d27d5cec75 PageDraw: Port and mount to web app (#1650) 2026-06-15 17:53:15 -07:00
Brandon Thomas 749124f834 api groundwork (#1648) 2026-06-15 20:11:36 -04:00
bflatastic 65b18b9b51 Webapp: API section styling updates 2026-06-16 00:10:04 +07:00
BFlat 6eb85010d6 Webapp: API keys section in settings modal (#1647)
- create, update, delete api keys
- feature flagged (api_key)
2026-06-15 22:38:21 +07:00
Brandon Thomas 855e169b44 codegen database 2026-06-15 11:10:11 -04:00
Brandon Thomas ea2f5106df api keys (#1646) 2026-06-15 11:09:31 -04:00
Brandon Thomas 0de867ad13 upgrade tauri 2.8 -> 2.11 (#1645) 2026-06-14 22:20:29 -04:00
Brandon Thomas a1cb1b2629 enhance database connection pooling (#1644) 2026-06-14 18:09:42 -04:00
BFlat 8da68b009c Desktop create pages redesign: shared generation feed, embedded model selector, gallery folders sidebar (#1635)
## Summary

Reworks the desktop (Tauri) app's Create Image / Create Video pages to
match the webapp,
extracts the webapp's generation gallery into a shared library used by
both apps, embeds the
model selector into every prompt box, adds a folders sidebar + marquee
selection to the gallery
modal, restyles the login/pricing/credits modals to the webapp look, and
turns the Apps page
into the default landing tab.

### Shared generation-list lib

- Extracted the webapp's generation feed into
`@storyteller/ui-generation-list`: list + masonry
grid views, pending/failed/completed rows and cards, thumbnail handling,
gallery/prompt data
hooks, and the shared grid/list view store + toggle. Host-specific
actions (recreate,
make-video, share, download, toasts) inject via render props — webapp
markup unchanged, its
  old files are now thin wrappers.

### Desktop create pages

- Prompt box fixed at the bottom, `max-w-5xl` like the webapp; pages
renamed to
  "Create Image" / "Create Video".
- Generation feed: in-progress/failed from the Tauri task queue, history
from the library API,
rendered as grid or list (toggle in the TopBar next to credits). Typed
Tauri completion events
  push finished media in instantly; the poll reconciles and dedupes.
- Row/card quick actions (recreate, make video, copy link, download)
backed by a shared
  `desktopMediaActions` module extracted from the TopBar gallery wiring.
- Hero + background act as a splash that fades when the feed loads;
bottom gradient behind the
prompt box. Batch generations show "Generating N images" on the single
pending chip.
- Video prompt box now expands vertically as far as the image prompt box
(viewport-relative).

### Embedded model selector

- `ClassyModelSelector` gained `variant="embedded"`: a compact pill that
opens the webapp-style
rich-list popover while keeping the desktop's provider selection.
Embedded on all five
prompt-input pages; floating selectors removed; legacy "10 sec." badges
dropped.

### Gallery modal

- Left sidebar lists the user's folders (drag-and-drop, right-click
management, new-folder
button) and shows on the Folders tab too, highlighting the active
folder's root branch.
- Marquee (drag-rectangle) selection in view mode, with edge
auto-scroll; starts only on blank
  background so it doesn't interfere with drag-to-folder/canvas.

### Restyled modals (desktop)

- Login/signup: webapp 2-column layout (form left, Vimeo showcase
right), no Google button
(Google auth isn't wired for Tauri), `artcraft-icon` logo, eye toggles.
The dismiss button is
removed so login is required; the post-auth Discord + Success onboarding
steps are kept, as is
  the existing Tauri auth flow.
- Pricing + credits: match the webapp visually (unified monochrome plan
cards with
Most Popular / Best Value highlight frames; `#161618` credit-pack cards
with Popular badge).
All billing stays on the existing Tauri `invoke` calls — no billing
logic changed.

### Apps page / navigation

- The Apps page is now the first tab and the default landing tab, with a
Home icon. Restyled to
the webapp home: "What will you craft today?" hero, Create / Edit
sections, gradient-hover
cards sized to their content, full-width scrollbar. The TopBar
quick-menu header reads
  "Create" instead of "Generate".

### Fixes

- Video Editor no longer crashes on the desktop app (`Tooltip must be
used within
TooltipProvider`): the editor shell now mounts its own Radix
`TooltipProvider`. The webapp got
one app-wide via `SidebarProvider`; the Tauri app had none, so panel
tooltips threw.

### Misc

- `getCreatorIconPathForModelId()` added to `@storyteller/model-list`
(model id → creator icon
  path, desktop/webapp aware).
2026-06-13 17:25:40 -04:00
Brandon Thomas e4bc031fbb modernize require_user_session_extended() (#1643) 2026-06-13 17:03:21 -04:00
Brandon Thomas d937a7f78f cleanup require_user_session (#1642) 2026-06-13 05:54:01 -04:00
Brandon Thomas fef96c3835 cleanup usage of require_moderator further (#1641) 2026-06-13 05:26:36 -04:00
Brandon Thomas b9e793098c cleanup session and require_moderator (#1640) 2026-06-13 04:52:35 -04:00
Brandon Thomas 222f3e958d actix crates upgrades (#1638) 2026-06-12 21:47:36 -04:00
Brandon Thomas 4ab06030dc fix cors with PUT methods (#1637) 2026-06-12 20:02:44 -04:00
Brandon Thomas 189c5afc80 updating pricing tests (#1636) 2026-06-12 19:52:58 -04:00
BFlat 2818831a66 Webapp: seed new folders with selection + marquee selection fixes (#1634)
- **New folder keeps selection:** "Add to folder > Create new folder"
now drops
the selected items into the folder on create instead of leaving it empty
  (webapp library + desktop gallery modal)
- **Marquee dead below the fold (main bug):** flex-column scroll parent
shrank
the page root to one viewport; presses lower down missed the handler.
Fixed
  with `shrink-0` so the root spans the full scroll height
- **Edge auto-scroll:** holding a drag near the top/bottom edge scrolls
the list
so you can keep selecting past the viewport in one drag (anchor
compensated for
  scroll; virtualized tiles stay selected)
- **No blue highlight:** `select-none` on the page (inputs opted back
in) blocks
accidental text/image highlighting and stops it competing with the drag
- **Rectangle no longer covers the top bar:** visible marquee box
clamped to the
  gallery viewport; selection hit-testing stays unclamped
2026-06-12 10:48:25 +07:00
Brandon Thomas 8497a8c83a artcraft 0.33 (#1633) 2026-06-11 23:02:35 -04:00
Brandon Thomas 5fff16ee22 cost accounting; credits updates (#1632) 2026-06-11 23:01:30 -04:00
bflatastic 1c3810e5a5 webapp: rounder corners and styling updates for mobile 2026-06-12 05:50:58 +07:00
bflatastic 5206a1d3e5 webapp: less blurry bg more visible promptbox 2026-06-12 05:19:15 +07:00
Tanish Manku 10f8297c1f Fix desktop video generation ignoring selected resolution (#1631)
The desktop video promptbox built the GenerateVideoRequest without ever
setting `resolution`, so generation always omitted it and the backend
fell back to the model's default (720p) — and billed for it. The cost
preview reads the same store value and sends it, so the preview was
correct (e.g. 117 for 480p) while the charge stayed at the 720p price
(240). Populate request.resolution in buildRequest, mapping the store's
legacy display string ("480p"/"720p"/"1080p") to CommonResolution,
guarded on the model exposing resolutionOptions.

Also fix useImageCostEstimate (webapp) omitting `quality` from its
effect deps, which meant the image cost preview didn't refresh when only
the quality option changed.
2026-06-11 14:48:48 -07:00
Brandon Thomas edb771ccfb uncommitted comet api module 2026-06-11 16:47:38 -04:00
Brandon Thomas 5979002f7e new cost tracking fields (#1630) 2026-06-11 16:37:43 -04:00
Brandon Thomas 74e30c4ffb comet api (#1629) 2026-06-11 10:43:32 -04:00
bflatastic 4061f00874 webapp: tailwind config colors update 2026-06-11 21:19:07 +07:00
bflatastic 633003c43b webapp: styling updates and package versions 2026-06-11 21:18:49 +07:00
Brandon Thomas 957db2d07f folders - add video previews / thumbnails (#1628) 2026-06-11 08:50:23 -04:00
Brandon Thomas b82a4e1599 change sorting for media files in folders (#1627) 2026-06-10 23:32:52 -04:00
BFlat bee790b952 feat(library): marquee selection, multi-select UX, mobile polish and … (#1626)
…perf

- Add desktop-style marquee drag selection on the webapp library: drag
on blank space to select covered tiles, click blank space to deselect
all
- Multi-select: clicking anywhere on a tile toggles it (fixes a
double-fire that cancelled the toggle so only the checkbox worked);
selection persists across tabs and folder navigation
- Gallery item menus show "View image" / "View video" / "View 3D" above
Add to Folder; menu panels size to content so labels no longer wrap
- Mobile/touch: no tile dragging or marquee (finger drags scroll),
checkbox and menu buttons always visible (no hover on touch), and the
Add to Folder submenu expands inline as an accordion instead of
overflowing the screen
- Select-mode gallery modal: folder cards hide the options menu and
ignore right-click (picker is browse-only)
- Perf: bulk selection moved to a module store with per-tile
subscriptions so selection changes re-render only flipped tiles, never
the page; marquee rectangle is positioned imperatively with cached tile
rects and commits only when coverage changes; webapp date groups
virtualized via the shared LazyDateGroup for smooth scrolling on large
libraries
- Bulk selection bar extracted to its own component, fixed to the
viewport bottom and centered within the content area
2026-06-11 00:47:42 +07:00
BFlat a2d57e3685 Folders for the library modal (#1309)
- Redesigned the gallery modal with a sidebar replacing the old filter
dropdown - filters now show as a list with item counts
- Added a Folders system - create, rename, and delete folders to
organize assets
- Users can drag items onto folders in the sidebar, including
multi-selected items
- Added "Add to Folder" option in the item right-click menu with a
folder picker submenu
- Added "Add to folder" button in the bulk selection bar with a folder
picker popup
- Clicking a folder in the sidebar opens a folder view with its own
content area and empty state
- Folder names are editable - click the title in the header or
right-click a folder in the sidebar to rename
- Videos are now draggable - previously only images and 3D objects could
be dragged
- Folder data is mocked locally for now - ready for API integration
2026-06-10 22:04:26 +07:00
Brandon Thomas 9104faf806 fix frontend dev scripts (#1625) 2026-06-10 09:28:25 -04:00
1819 changed files with 110095 additions and 13897 deletions
+12 -3
View File
@@ -19,9 +19,18 @@ Handlers in `storyteller_web` follow this structure:
### Session/Auth
- `require_user_session_using_connection()` reuses an open DB connection (preferred)
- `require_user_session()` acquires its own connection (deprecated)
- `require_moderator()` for admin-only endpoints
Helpers live in `storyteller_web` under `http_server/web_utils/user_session/`. Each takes a
generic `mysql_executor: E` (any `sqlx::Executor`), so whether it reuses a connection or acquires
its own depends on the **argument you pass**:
- `require_user_session(http_request, session_checker, mysql_executor)` — require a logged-in user.
- `require_user_session_extended(...)` — same, with extra session detail.
- `require_moderator(http_request, session_checker, mysql_executor)` — admin-only endpoints.
PREFER passing an already-open connection (`&mut *conn`) so the helper reuses it. Passing
`&server_state.mysql_pool` makes it acquire a fresh connection — avoid that when the handler
already holds one (the double-acquire adds pool pressure; this crate has had pool-timeout
incidents). There is no `*_using_connection` variant — the reuse/acquire choice is the argument.
### API Type Definitions
+5 -4
View File
@@ -7,10 +7,11 @@
- `#[macro_use] extern crate serde_derive;` is used in binary crates; library crates use `use serde_derive::{Serialize, Deserialize};`
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros (`info!`, `warn!`, `error!`, `debug!`, `trace!`) instead
- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code; new database queries should use `Result<T, sqlx::Error>` instead
- SQLx queries: prefer `sqlx::query!` and `sqlx::query_as!` compile-time checked macros over
runtime `sqlx::query()` / `sqlx::query_as()`. The macros catch SQL typos, column mismatches,
and type errors at build time. Use runtime queries only when assembling complicated
dynamic/branching queries that cannot be expressed as a compile-time macro.
- SQLx: MySQL queries belong in the `mysql_queries` crate. Do NOT write `sqlx::query!` /
`sqlx::query()` in application/service crates (e.g. `storyteller-web`, which has zero direct
SQLx queries) — add or reuse a function in `mysql_queries` and call it. The compile-time
`query!` / `query_as!` macro conventions for that crate are documented in
`crates/schema/database/mysql_queries/CLAUDE.md`.
- Enum Display/Debug: use `impl_enum_display_and_debug_using_to_str!` macro, not `derive_more::Display` (CLion doesn't understand it)
- Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`, `maybe_prompt_token`
- When implementing `fmt::Display` for error types, use the pattern: `write!(f, "{:?}", self)`
+2
View File
@@ -38,6 +38,7 @@ script/socat_dev_database_connect.sh
venv/
# LLM Testing
/external/
/prompts/
/reports/
/requests/
@@ -102,3 +103,4 @@ windows/sqlite/sqlite3.lib
# ArtCraft Windows DLLs
crates/desktop/src-tauri/*.dll
windows_dlls/
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: ApiKeyToken`,\n LEFT(api_key, 20) as `api_key_prefix!`,\n name,\n maybe_description,\n owner_user_token as `owner_user_token: UserToken`,\n ip_address_creation,\n ip_address_update,\n created_at,\n updated_at,\n maybe_deleted_at\nFROM api_keys\nWHERE owner_user_token = ?\nORDER BY id DESC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "api_key_prefix!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "maybe_description",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 4,
"name": "owner_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "ip_address_update",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_deleted_at",
"type_info": {
"type": "Timestamp",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
true,
false,
true,
false,
false,
false,
false,
false,
true
]
},
"hash": "097b8a8d02734c8c8e31dbb8a3bf4ace970693a2a0df2f05a31caf86b4d3d0ab"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT token AS `token: UserToken`\nFROM users\nWHERE email_address = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "0d2eadc64240b9ce5348b3b238b0ae6229df21697c952b08e930d03f97414cf4"
}
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n d.id as `id: u64`,\n d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,\n d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n d.message,\n d.created_at,\n u.display_name as `maybe_user_display_name?`,\n u.username as `maybe_user_username?`,\n u.email_gravatar_hash as `maybe_user_gravatar_hash?`\nFROM debug_logs d\nLEFT OUTER JOIN users u\n ON u.token = d.maybe_creator_user_token\nWHERE d.maybe_creator_user_token = ?\n AND d.id < ?\nORDER BY d.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 96
}
},
{
"ordinal": 3,
"name": "maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 6,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 7,
"name": "maybe_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 8,
"name": "maybe_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 9,
"name": "maybe_user_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
true,
true,
false,
false,
true,
true,
true
]
},
"hash": "1197c08c8b390fb8bffbe10ad9f7d073a58b6c9124ee11573c6723d6d5471881"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO uploaded_videos\nSET\n token = ?,\n sha1_checksum = ?,\n filesize_bytes = ?,\n maybe_filename = ?,\n maybe_width = ?,\n maybe_height = ?,\n maybe_resolution = ?,\n maybe_detected_model_family = ?,\n maybe_detected_model_type = ?,\n maybe_report = ?,\n upload_ip_address = ?,\n created_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
},
"hash": "14e63e59f7d550181126151029bcd17d08278196ba0ee4d32450c2567b1da26e"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n jobs.created_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -92,6 +92,16 @@
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 9,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
@@ -106,8 +116,9 @@
false,
true,
true,
true
true,
false
]
},
"hash": "ddbbd3886c00268edd3993508f635549fc8ddc877bb059fd58f14b8c07586fc9"
"hash": "1703bc63ce65d98cbcad81d515cbcf211a943c533e38e887d2c12713c78b687d"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::generation::common_model_type::CommonModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n\n p.maybe_generation_mode as `maybe_generation_mode: enums::common::generation::common_generation_mode::CommonGenerationMode`,\n p.maybe_aspect_ratio as `maybe_aspect_ratio: enums::common::generation::common_aspect_ratio::CommonAspectRatio`,\n p.maybe_resolution as `maybe_resolution: enums::common::generation::common_resolution::CommonResolution`,\n p.maybe_batch_count as `maybe_batch_count: u8`,\n p.maybe_generate_audio as `maybe_generate_audio: bool`,\n p.maybe_duration_seconds as `maybe_duration_seconds: u32`,\n\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::generation::common_model_type::CommonModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n\n p.maybe_generation_mode as `maybe_generation_mode: enums::common::generation::common_generation_mode::CommonGenerationMode`,\n p.maybe_aspect_ratio as `maybe_aspect_ratio: enums::common::generation::common_aspect_ratio::CommonAspectRatio`,\n p.maybe_resolution as `maybe_resolution: enums::common::generation::common_resolution::CommonResolution`,\n p.maybe_bitrate as `maybe_bitrate: enums::common::generation::common_bitrate::CommonBitrate`,\n p.maybe_batch_count as `maybe_batch_count: u8`,\n p.maybe_generate_audio as `maybe_generate_audio: bool`,\n p.maybe_duration_seconds as `maybe_duration_seconds: u32`,\n\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
"describe": {
"columns": [
{
@@ -105,6 +105,16 @@
},
{
"ordinal": 10,
"name": "maybe_bitrate: enums::common::generation::common_bitrate::CommonBitrate",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "maybe_batch_count: u8",
"type_info": {
"type": "Tiny",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_generate_audio: bool",
"type_info": {
"type": "Tiny",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_duration_seconds: u32",
"type_info": {
"type": "Long",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_other_args",
"type_info": {
"type": "Blob",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "creator_ip_address",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -182,9 +192,10 @@
true,
true,
true,
true,
false,
false
]
},
"hash": "4a50b7bf06061f05e769a41e547cb927bd2b3a6f67c8ceb37883b744237ba34c"
"hash": "1b69f192c3bb51bd249f97d37f209ae271d498bb46c978351b2c1d8759338bab"
}
@@ -0,0 +1,58 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payment_occurred_at AS `payment_occurred_at!: DateTime<Utc>`,\n amount_usd_cents,\n event_type AS `event_type!: PaymentEventType`,\n maybe_credits_granted AS `maybe_credits_granted: u32`\nFROM user_spend_events\nWHERE maybe_user_token = ?\n AND payments_namespace = ?\n AND is_production = TRUE\nORDER BY payment_occurred_at ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payment_occurred_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 1,
"name": "amount_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 2,
"name": "event_type!: PaymentEventType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_credits_granted: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "1c997019b7ed18059bc08d857918c3ce3f5e6ed56d358894cbc496d345566519"
}
@@ -0,0 +1,36 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n stripe_customer_id AS `stripe_customer_id!`,\n user_token AS `user_token!: UserToken`\nFROM user_stripe_customer_links\nWHERE payments_namespace = 'artcraft'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "stripe_customer_id!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false
]
},
"hash": "2d49cfbafc29bce35f3e03135054c7521fb0182663aefa971245f91f33a6fc49"
}
@@ -48,7 +48,7 @@
"name": "maybe_entity_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
@@ -132,7 +132,7 @@
false,
false,
true,
false,
true,
true,
true,
true,
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_spend_events\nSET\n token = ?,\n payments_namespace = ?,\n maybe_user_token = ?,\n event_type = ?,\n amount_usd_cents = ?,\n maybe_credits_granted = ?,\n maybe_user_subscription_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n payment_source = ?,\n source_object_id = ?,\n maybe_stripe_customer_id = ?,\n maybe_stripe_invoice_id = ?,\n maybe_stripe_payment_intent_id = ?,\n maybe_stripe_charge_id = ?,\n maybe_stripe_event_id = ?,\n is_production = ?,\n payment_occurred_at = ?\nON DUPLICATE KEY UPDATE token = token\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 17
},
"nullable": []
},
"hash": "3e5a71cedc31cc656177c8331c2de1d1d2d2eb310aabeb7e6238943b073f6998"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE uploaded_video_notes\nSET\n maybe_filename = ?,\n maybe_reported_model_type = ?,\n maybe_reported_model_name = ?,\n maybe_website = ?,\n maybe_other_website = ?,\n maybe_comments = ?,\n maybe_email_address = ?,\n can_share_report = ?,\n was_scammed = ?,\n maybe_comment_update_ip_address = ?,\n version = version + 1\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
},
"hash": "4036dfd9d4e9e77e557776468c14b75f49170de1e55a1b9b9e83446a0c356e74"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: UploadedVideoToken`,\n sha1_checksum,\n filesize_bytes as `filesize_bytes: u32`,\n maybe_width as `maybe_width: u32`,\n maybe_height as `maybe_height: u32`,\n maybe_resolution,\n maybe_detected_model_family as `maybe_detected_model_family: UploadedVideoDetectedModelFamily`,\n maybe_detected_model_type as `maybe_detected_model_type: UploadedVideoDetectedModelType`,\n maybe_report,\n upload_ip_address,\n maybe_updated_ip_address,\n created_at,\n updated_at\nFROM uploaded_videos\nWHERE sha1_checksum = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: UploadedVideoToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "sha1_checksum",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 2,
"name": "filesize_bytes: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 3,
"name": "maybe_width: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 4,
"name": "maybe_height: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "maybe_resolution",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 48
}
},
{
"ordinal": 6,
"name": "maybe_detected_model_family: UploadedVideoDetectedModelFamily",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_detected_model_type: UploadedVideoDetectedModelType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_report",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 9,
"name": "upload_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 10,
"name": "maybe_updated_ip_address",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 12,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "408c893a0ced805de4c3c6cfdf0c6a4bcc93dd1eaf98382522eae46e72eb3aa5"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_spend_summaries\nSET\n payments_namespace = ?,\n user_token = ?,\n lifetime_gross_spend_usd_cents = ?,\n lifetime_subscription_spend_usd_cents = ?,\n lifetime_credits_spend_usd_cents = ?,\n lifetime_refund_usd_cents = ?,\n lifetime_net_spend_usd_cents = ?,\n lifetime_payment_count = ?,\n lifetime_refund_count = ?,\n first_payment_at = ?,\n first_spend_usd_cents = ?,\n last_payment_at = ?,\n last_spend_usd_cents = ?,\n days_since_first_payment = ?,\n days_since_last_payment = ?,\n net_spend_7d_usd_cents = ?,\n net_spend_prev_7d_usd_cents = ?,\n net_spend_14d_usd_cents = ?,\n net_spend_prev_14d_usd_cents = ?,\n net_spend_30d_usd_cents = ?,\n net_spend_prev_30d_usd_cents = ?,\n net_spend_60d_usd_cents = ?,\n net_spend_90d_usd_cents = ?,\n net_spend_this_year_usd_cents = ?,\n avg_weekly_net_spend_4w_usd_cents = ?,\n avg_weekly_net_spend_12w_usd_cents = ?,\n active_weeks_in_last_4 = ?,\n active_weeks_in_last_8 = ?,\n active_weeks_in_last_12 = ?,\n active_weeks_in_last_24 = ?,\n active_weeks_in_last_52 = ?,\n consecutive_active_weeks = ?,\n consecutive_inactive_weeks = ?,\n weeks_since_last_spend = ?,\n is_active_subscriber = ?,\n maybe_subscription_interval = ?,\n maybe_reengagement_score = ?\nON DUPLICATE KEY UPDATE\n lifetime_gross_spend_usd_cents = VALUES(lifetime_gross_spend_usd_cents),\n lifetime_subscription_spend_usd_cents = VALUES(lifetime_subscription_spend_usd_cents),\n lifetime_credits_spend_usd_cents = VALUES(lifetime_credits_spend_usd_cents),\n lifetime_refund_usd_cents = VALUES(lifetime_refund_usd_cents),\n lifetime_net_spend_usd_cents = VALUES(lifetime_net_spend_usd_cents),\n lifetime_payment_count = VALUES(lifetime_payment_count),\n lifetime_refund_count = VALUES(lifetime_refund_count),\n first_payment_at = VALUES(first_payment_at),\n first_spend_usd_cents = VALUES(first_spend_usd_cents),\n last_payment_at = VALUES(last_payment_at),\n last_spend_usd_cents = VALUES(last_spend_usd_cents),\n days_since_first_payment = VALUES(days_since_first_payment),\n days_since_last_payment = VALUES(days_since_last_payment),\n net_spend_7d_usd_cents = VALUES(net_spend_7d_usd_cents),\n net_spend_prev_7d_usd_cents = VALUES(net_spend_prev_7d_usd_cents),\n net_spend_14d_usd_cents = VALUES(net_spend_14d_usd_cents),\n net_spend_prev_14d_usd_cents = VALUES(net_spend_prev_14d_usd_cents),\n net_spend_30d_usd_cents = VALUES(net_spend_30d_usd_cents),\n net_spend_prev_30d_usd_cents = VALUES(net_spend_prev_30d_usd_cents),\n net_spend_60d_usd_cents = VALUES(net_spend_60d_usd_cents),\n net_spend_90d_usd_cents = VALUES(net_spend_90d_usd_cents),\n net_spend_this_year_usd_cents = VALUES(net_spend_this_year_usd_cents),\n avg_weekly_net_spend_4w_usd_cents = VALUES(avg_weekly_net_spend_4w_usd_cents),\n avg_weekly_net_spend_12w_usd_cents = VALUES(avg_weekly_net_spend_12w_usd_cents),\n active_weeks_in_last_4 = VALUES(active_weeks_in_last_4),\n active_weeks_in_last_8 = VALUES(active_weeks_in_last_8),\n active_weeks_in_last_12 = VALUES(active_weeks_in_last_12),\n active_weeks_in_last_24 = VALUES(active_weeks_in_last_24),\n active_weeks_in_last_52 = VALUES(active_weeks_in_last_52),\n consecutive_active_weeks = VALUES(consecutive_active_weeks),\n consecutive_inactive_weeks = VALUES(consecutive_inactive_weeks),\n weeks_since_last_spend = VALUES(weeks_since_last_spend),\n is_active_subscriber = VALUES(is_active_subscriber),\n maybe_subscription_interval = VALUES(maybe_subscription_interval),\n maybe_reengagement_score = VALUES(maybe_reengagement_score)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 37
},
"nullable": []
},
"hash": "41cf2f8d64aa36fb930cc1bf8a46d37e0987b743438b44e1c55859fc6b6ee6a0"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT status as `status: enums::common::job_status_plus::JobStatusPlus`\nFROM generic_inference_jobs\nWHERE token = ?\nFOR UPDATE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "status: enums::common::job_status_plus::JobStatusPlus",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 76
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "489c6ef00507330fedbaf2b6e024ecf409a920efd85f75456829a8ed412d9bc3"
}
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n s.user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n s.maybe_reengagement_score AS `reengagement_score!: u32`,\n s.lifetime_net_spend_usd_cents,\n s.last_payment_at AS `maybe_last_payment_at: DateTime<Utc>`,\n s.days_since_last_payment AS `maybe_days_since_last_payment: u32`,\n s.weeks_since_last_spend AS `maybe_weeks_since_last_spend: u32`,\n s.is_active_subscriber AS `is_active_subscriber: bool`\nFROM user_spend_summaries s\nJOIN users u ON u.token = s.user_token\nWHERE s.payments_namespace = ?\n AND s.maybe_reengagement_score > 0\nORDER BY s.maybe_reengagement_score DESC, s.id ASC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_gravatar_hash!",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "reengagement_score!: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "maybe_last_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 7,
"name": "maybe_days_since_last_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 8,
"name": "maybe_weeks_since_last_spend: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
true,
false,
true,
true,
true,
false
]
},
"hash": "5b1954301fc16703a04e2f63fd85c28bf2283bc1ecb5624107686dad0dfe364f"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n platform_type = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n platform_type = ?,\n\n maybe_external_third_party_cost_credits = ?,\n maybe_external_third_party_cost_usd_cents = ?,\n maybe_system_cost_credits = ?,\n maybe_system_cost_usd_cents = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 31
"Right": 35
},
"nullable": []
},
"hash": "108731dc51d915ebb8b6ed391c5bae4c41ae6f2c813be92c07d8a10ca401db53"
"hash": "5c67c3dd7f8bb6771c6b058a8cf6ffcc66b2be2bea41bd988070713173982dab"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_log_level = ?,\n maybe_creator_user_token = ?,\n message = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "62995374005dc0553d007fb69e642d89a95cd5aaeeababd110c92c565023cfcf"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: UploadedVideoToken`,\n sha1_checksum,\n filesize_bytes as `filesize_bytes: u32`,\n maybe_width as `maybe_width: u32`,\n maybe_height as `maybe_height: u32`,\n maybe_resolution,\n maybe_detected_model_family as `maybe_detected_model_family: UploadedVideoDetectedModelFamily`,\n maybe_detected_model_type as `maybe_detected_model_type: UploadedVideoDetectedModelType`,\n maybe_report,\n upload_ip_address,\n maybe_updated_ip_address,\n created_at,\n updated_at\nFROM uploaded_videos\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: UploadedVideoToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "sha1_checksum",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 2,
"name": "filesize_bytes: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 3,
"name": "maybe_width: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 4,
"name": "maybe_height: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "maybe_resolution",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 48
}
},
{
"ordinal": 6,
"name": "maybe_detected_model_family: UploadedVideoDetectedModelFamily",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_detected_model_type: UploadedVideoDetectedModelType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_report",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 9,
"name": "upload_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 10,
"name": "maybe_updated_ip_address",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 12,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "73d70c5ee77f5059801d324610a80b3f52da3ddec2e89f63e8f41fc68ff0aaec"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n \n maybe_model_type = ?,\n maybe_generation_provider = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n\n maybe_generation_mode = ?,\n maybe_aspect_ratio = ?,\n maybe_resolution = ?,\n maybe_batch_count = ?,\n maybe_generate_audio = ?,\n maybe_duration_seconds = ?,\n\n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n \n maybe_model_type = ?,\n maybe_generation_provider = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n\n maybe_generation_mode = ?,\n maybe_aspect_ratio = ?,\n maybe_resolution = ?,\n maybe_bitrate = ?,\n maybe_batch_count = ?,\n maybe_generate_audio = ?,\n maybe_duration_seconds = ?,\n\n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 15
"Right": 16
},
"nullable": []
},
"hash": "ccf92ebe0328d3019ac6b01be4b442df706a183362fa7e5c331f5a4bf1a74a00"
"hash": "7519c1dcc2946f4c31a8aad995bbb66a5519f6ee571797a6c0f597c9e57986a7"
}
@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n user_token AS `user_token!: UserToken`,\n spend_date AS `spend_date!: NaiveDate`,\n subscription_spend_usd_cents,\n credits_spend_usd_cents,\n gross_spend_usd_cents,\n refund_usd_cents,\n net_spend_usd_cents,\n payment_count,\n credits_granted,\n created_at AS `created_at: DateTime<Utc>`,\n updated_at AS `updated_at: DateTime<Utc>`\nFROM user_daily_spends\nWHERE user_token = ?\n AND payments_namespace = ?\nORDER BY spend_date DESC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "spend_date!: NaiveDate",
"type_info": {
"type": "Date",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 3,
"name": "subscription_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 4,
"name": "credits_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "gross_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "refund_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "payment_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "credits_granted",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 10,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 11,
"name": "updated_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "77dca63cffdd18957cbc4be6b7d982a2599a2c06df07f8e34501a032cd688ead"
}
@@ -0,0 +1,113 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_user_token AS `user_token!: UserToken`,\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0\n AND event_type IN ('subscription_initial', 'subscription_renewal', 'subscription_proration_upgrade')\n THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `subscription_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 AND event_type = 'credit_pack_purchase'\n THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `credits_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `gross_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents < 0 THEN -amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `refund_usd_cents!: u64`,\n CAST(SUM(amount_usd_cents) AS SIGNED) AS `net_spend_usd_cents!: i64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 THEN 1 ELSE 0 END) AS UNSIGNED) AS `payment_count!: u64`,\n CAST(COALESCE(SUM(maybe_credits_granted), 0) AS UNSIGNED) AS `credits_granted!: u64`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n AND payment_occurred_at >= ?\n AND payment_occurred_at < ?\nGROUP BY maybe_user_token, payments_namespace\nHAVING SUM(CASE WHEN amount_usd_cents <> 0 THEN 1 ELSE 0 END) > 0\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "subscription_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 3,
"name": "credits_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 4,
"name": "gross_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 5,
"name": "refund_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 6,
"name": "net_spend_usd_cents!: i64",
"type_info": {
"type": "LongLong",
"flags": "BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 7,
"name": "payment_count!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 8,
"name": "credits_granted!: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
false,
true,
true,
true,
true,
true,
true,
false
]
},
"hash": "83fc05a3594b379482745f851cd5a22786adde9968dd70490c91100cf21f8023"
}
@@ -1,14 +1,14 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY fmf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "membership_id: u64",
"name": "media_file_id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
@@ -263,5 +263,5 @@
false
]
},
"hash": "88673127b54626dc38d28693e709dfaeced9a05300a26b3f3c52a17f28171c9a"
"hash": "8aca9bc976b980ea1a3b3052bc8a45dd4d8db55c20fd1aede2a4415cc1395cf3"
}
@@ -48,7 +48,7 @@
"name": "maybe_entity_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
@@ -132,7 +132,7 @@
false,
false,
true,
false,
true,
true,
true,
true,
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT status as `status: enums::common::job_status_plus::JobStatusPlus`\nFROM generic_inference_jobs\nWHERE token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "status: enums::common::job_status_plus::JobStatusPlus",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 76
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "9312694d1651235b06d1744d904b59f64d66449b8927c1606bb08bd5fdc15c7e"
}
@@ -0,0 +1,443 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n user_token AS `user_token!: UserToken`,\n lifetime_gross_spend_usd_cents,\n lifetime_subscription_spend_usd_cents,\n lifetime_credits_spend_usd_cents,\n lifetime_refund_usd_cents,\n lifetime_net_spend_usd_cents,\n lifetime_payment_count,\n lifetime_refund_count,\n first_payment_at AS `maybe_first_payment_at: DateTime<Utc>`,\n first_spend_usd_cents,\n last_payment_at AS `maybe_last_payment_at: DateTime<Utc>`,\n last_spend_usd_cents,\n days_since_first_payment AS `maybe_days_since_first_payment: u32`,\n days_since_last_payment AS `maybe_days_since_last_payment: u32`,\n net_spend_7d_usd_cents,\n net_spend_prev_7d_usd_cents,\n net_spend_14d_usd_cents,\n net_spend_prev_14d_usd_cents,\n net_spend_30d_usd_cents,\n net_spend_prev_30d_usd_cents,\n net_spend_60d_usd_cents,\n net_spend_90d_usd_cents,\n net_spend_this_year_usd_cents,\n avg_weekly_net_spend_4w_usd_cents,\n avg_weekly_net_spend_12w_usd_cents,\n active_weeks_in_last_4,\n active_weeks_in_last_8,\n active_weeks_in_last_12,\n active_weeks_in_last_24,\n active_weeks_in_last_52,\n consecutive_active_weeks,\n consecutive_inactive_weeks,\n weeks_since_last_spend AS `maybe_weeks_since_last_spend: u32`,\n is_active_subscriber AS `is_active_subscriber: bool`,\n maybe_subscription_interval,\n maybe_reengagement_score AS `maybe_reengagement_score: u32`,\n created_at AS `created_at: DateTime<Utc>`,\n updated_at AS `updated_at: DateTime<Utc>`\nFROM user_spend_summaries\nWHERE payments_namespace = ?\n AND user_token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "lifetime_gross_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 3,
"name": "lifetime_subscription_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 4,
"name": "lifetime_credits_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "lifetime_refund_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "lifetime_payment_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 8,
"name": "lifetime_refund_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "maybe_first_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 10,
"name": "first_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 11,
"name": "maybe_last_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 12,
"name": "last_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 13,
"name": "maybe_days_since_first_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 14,
"name": "maybe_days_since_last_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 15,
"name": "net_spend_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 16,
"name": "net_spend_prev_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 17,
"name": "net_spend_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 18,
"name": "net_spend_prev_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 19,
"name": "net_spend_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 20,
"name": "net_spend_prev_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 21,
"name": "net_spend_60d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 22,
"name": "net_spend_90d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 23,
"name": "net_spend_this_year_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 24,
"name": "avg_weekly_net_spend_4w_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 25,
"name": "avg_weekly_net_spend_12w_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 26,
"name": "active_weeks_in_last_4",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 27,
"name": "active_weeks_in_last_8",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 28,
"name": "active_weeks_in_last_12",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 29,
"name": "active_weeks_in_last_24",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 30,
"name": "active_weeks_in_last_52",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 31,
"name": "consecutive_active_weeks",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 32,
"name": "consecutive_inactive_weeks",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 33,
"name": "maybe_weeks_since_last_spend: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 34,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 35,
"name": "maybe_subscription_interval",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 36,
"name": "maybe_reengagement_score: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 37,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 38,
"name": "updated_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
true,
false,
false
]
},
"hash": "9715e4dc2ec6e596fdb91ecc50a2b80f63494bbc0c9df6c0a57ede2360716869"
}
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: ApiKeyToken`,\n LEFT(api_key, 20) as `api_key_prefix!`,\n name,\n maybe_description,\n owner_user_token as `owner_user_token: UserToken`,\n ip_address_creation,\n ip_address_update,\n created_at,\n updated_at,\n maybe_deleted_at\nFROM api_keys\nWHERE api_key = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "api_key_prefix!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "maybe_description",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 4,
"name": "owner_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "ip_address_update",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_deleted_at",
"type_info": {
"type": "Timestamp",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
true,
false,
false,
false,
false,
false,
true
]
},
"hash": "9b30c67842e4c408c9002fc116dc86705484268efcadb3100ef13244bcd8f922"
}
@@ -1,80 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n id as `id: u64`,\n event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n message,\n created_at\nFROM debug_logs\nWHERE event_token = ?\nORDER BY id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 96
}
},
{
"ordinal": 3,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 5,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
false,
false
]
},
"hash": "a62b88fec40a11952cc199383090ea338f97091e8bfc6b68aad67416685889af"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_creator_user_token = ?,\n message = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "a9745ecf60b564ab096ee1dbe124221b5f288a4ae38e788e19bbd359db0b580f"
}
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n d.id as `id: u64`,\n d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,\n d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n d.message,\n d.created_at,\n u.display_name as `maybe_user_display_name?`,\n u.username as `maybe_user_username?`,\n u.email_gravatar_hash as `maybe_user_gravatar_hash?`\nFROM debug_logs d\nLEFT OUTER JOIN users u\n ON u.token = d.maybe_creator_user_token\nWHERE d.event_token = ?\nORDER BY d.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 96
}
},
{
"ordinal": 3,
"name": "maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 6,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 7,
"name": "maybe_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 8,
"name": "maybe_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 9,
"name": "maybe_user_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
true,
false,
false,
true,
true,
true
]
},
"hash": "ae311f7fde7c73f166cd8c0a5c9965850e314936a401b24a3900585ab53a7db6"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO uploaded_video_notes\nSET\n token = ?,\n uploaded_video_token = ?,\n maybe_filename = ?,\n maybe_reported_model_type = ?,\n maybe_reported_model_name = ?,\n maybe_website = ?,\n maybe_other_website = ?,\n maybe_comments = ?,\n maybe_email_address = ?,\n can_share_report = ?,\n was_scammed = ?,\n comment_create_ip_address = ?,\n created_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "b1f30a79864351a28a58cae43ee38ff12a8d64d9393b8f27590fe03b0ae3ab05"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE api_keys\nSET\n maybe_description = ?,\n ip_address_update = ?\nWHERE token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "b72d951fc8b60e3622bb981dd5cd66ce86c6c2d788c5606d2df80a3d07e588fe"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_daily_spends\nSET\n user_token = ?,\n payments_namespace = ?,\n spend_date = ?,\n subscription_spend_usd_cents = ?,\n credits_spend_usd_cents = ?,\n gross_spend_usd_cents = ?,\n refund_usd_cents = ?,\n net_spend_usd_cents = ?,\n payment_count = ?,\n credits_granted = ?\nON DUPLICATE KEY UPDATE\n subscription_spend_usd_cents = VALUES(subscription_spend_usd_cents),\n credits_spend_usd_cents = VALUES(credits_spend_usd_cents),\n gross_spend_usd_cents = VALUES(gross_spend_usd_cents),\n refund_usd_cents = VALUES(refund_usd_cents),\n net_spend_usd_cents = VALUES(net_spend_usd_cents),\n payment_count = VALUES(payment_count),\n credits_granted = VALUES(credits_granted)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "b7a5fea76da70c03b8744c985940a5bcdabdb3316aa407c5ca883bca863ee633"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO api_keys\nSET\n token = ?,\n api_key = ?,\n name = ?,\n maybe_description = ?,\n owner_user_token = ?,\n ip_address_creation = ?,\n ip_address_update = ?,\n created_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "bb39045ffcc9d3a9e150b32e7ff884dccbae60101deeaaad23ccd430b25bb5a4"
}
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: ApiKeyToken`,\n LEFT(api_key, 20) as `api_key_prefix!`,\n name,\n maybe_description,\n owner_user_token as `owner_user_token: UserToken`,\n ip_address_creation,\n ip_address_update,\n created_at,\n updated_at,\n maybe_deleted_at\nFROM api_keys\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "api_key_prefix!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "maybe_description",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 4,
"name": "owner_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "ip_address_update",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_deleted_at",
"type_info": {
"type": "Timestamp",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
true,
false,
false,
false,
false,
false,
true
]
},
"hash": "bb542b8a86cf2bf770daf29e09a92be981b307810241dd3cc1a7225a83f6824c"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT DISTINCT maybe_user_token AS `user_token!: UserToken`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n AND payments_namespace = ?\n AND maybe_user_token > ?\nORDER BY maybe_user_token\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
true
]
},
"hash": "bcd07050d5557bf59d6b2691478763b5b067447d295b002f6e766209ea077f42"
}
@@ -1,14 +1,14 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND fmf.id < ?\nORDER BY fmf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "membership_id: u64",
"name": "media_file_id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
@@ -263,5 +263,5 @@
false
]
},
"hash": "d2a406bcf08f656fd602aa6ba1124a77d04ad874b15abafca300e8b381c7b211"
"hash": "c0ba9f9aa3184b1cb3d714173b31e530e201a6c24afd29ad534cf7f3954c7316"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE api_keys\nSET maybe_deleted_at = NOW()\nWHERE token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "c28c716aa08601b208c727102680bfa91025f0e3958390ed9e27e521d38b5805"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE uploaded_videos\nSET\n maybe_filename = ?,\n maybe_width = ?,\n maybe_height = ?,\n maybe_resolution = ?,\n maybe_detected_model_family = ?,\n maybe_detected_model_type = ?,\n maybe_report = ?,\n maybe_updated_ip_address = ?,\n version = version + 1\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "c6bbdc963e0d49ac55973a728d04b371967c20bfb9dd4bd75e4693dc7a114c99"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT DISTINCT payments_namespace AS `payments_namespace!: PaymentsNamespace`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false
]
},
"hash": "cf8c702126bba73cd3abc2ed7202962b6da773460505222a75408dab1d26b70f"
}
@@ -0,0 +1,212 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n e.token AS `token!: UserSpendEventToken`,\n e.payments_namespace AS `payments_namespace!`,\n e.maybe_user_token AS `maybe_user_token: UserToken`,\n u.username AS `maybe_username?`,\n u.display_name AS `maybe_display_name?`,\n u.email_gravatar_hash AS `maybe_email_gravatar_hash?`,\n e.event_type AS `event_type!`,\n e.amount_usd_cents,\n e.maybe_credits_granted AS `maybe_credits_granted: u32`,\n e.payment_source AS `payment_source!`,\n e.source_object_id AS `maybe_source_object_id`,\n e.maybe_stripe_invoice_id,\n e.maybe_stripe_payment_intent_id,\n e.maybe_stripe_charge_id,\n e.maybe_stripe_customer_id,\n e.is_production AS `is_production: bool`,\n e.payment_occurred_at AS `payment_occurred_at!: DateTime<Utc>`,\n e.created_at AS `created_at!: DateTime<Utc>`\nFROM user_spend_events e\nLEFT JOIN users u ON u.token = e.maybe_user_token\nWHERE (? IS NULL OR e.payments_namespace = ?)\nORDER BY e.payment_occurred_at DESC, e.id DESC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token!: UserSpendEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "payments_namespace!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "maybe_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 4,
"name": "maybe_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 5,
"name": "maybe_email_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "event_type!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "amount_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "maybe_credits_granted: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "payment_source!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 10,
"name": "maybe_source_object_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 11,
"name": "maybe_stripe_invoice_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 12,
"name": "maybe_stripe_payment_intent_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 13,
"name": "maybe_stripe_charge_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 14,
"name": "maybe_stripe_customer_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 15,
"name": "is_production: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 16,
"name": "payment_occurred_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 17,
"name": "created_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
true,
true,
true,
true,
false,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "d5d3a59ea4885868b26168cd305f6168a1e62fe9f8fa663063591335c645b927"
}
@@ -0,0 +1,80 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n wle.maybe_entity_ref AS `stripe_object_id!`,\n wle.entry_type AS `entry_type!`,\n wle.token AS `ledger_token!: WalletLedgerEntryToken`,\n w.owner_user_token AS `owner_user_token!: UserToken`,\n wle.credits_delta AS `credits_delta!`,\n wle.created_at AS `created_at!: DateTime<Utc>`\nFROM wallet_ledger_entries wle\nJOIN wallets w ON w.token = wle.wallet_token\nWHERE w.wallet_namespace = 'artcraft'\n AND wle.entry_type IN ('credit_banked', 'credit_monthly')\n AND wle.created_at >= ?\n AND (wle.maybe_entity_ref LIKE 'pi\\_%' OR wle.maybe_entity_ref LIKE 'in\\_%')\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "stripe_object_id!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 1,
"name": "entry_type!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "ledger_token!: WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "owner_user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "credits_delta!",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 5,
"name": "created_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
false,
false,
false
]
},
"hash": "de69f3d564830f8fff90f754d13982de623fcda50118d261466eeaf034de5ecb"
}
@@ -0,0 +1,47 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n api_keys.token as `api_key_token: ApiKeyToken`,\n users.token as `user_token: UserToken`,\n users.is_banned\nFROM api_keys\nINNER JOIN users\n ON users.token = api_keys.owner_user_token\nWHERE api_keys.api_key = ?\n AND api_keys.maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "api_key_token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "is_banned",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "f52c495a7ff5cafb357f79c4cdb503bbbef045fd193a5a37a66180fcac77f363"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n s.user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n s.lifetime_net_spend_usd_cents,\n s.net_spend_7d_usd_cents,\n s.net_spend_14d_usd_cents,\n s.net_spend_30d_usd_cents,\n s.net_spend_60d_usd_cents,\n s.net_spend_90d_usd_cents,\n s.net_spend_this_year_usd_cents,\n s.is_active_subscriber AS `is_active_subscriber: bool`,\n s.maybe_reengagement_score AS `maybe_reengagement_score: u32`\nFROM user_spend_summaries s\nJOIN users u ON u.token = s.user_token\nWHERE s.payments_namespace = ?\nORDER BY\n (CASE ?\n WHEN '7d' THEN s.net_spend_7d_usd_cents\n WHEN '14d' THEN s.net_spend_14d_usd_cents\n WHEN '30d' THEN s.net_spend_30d_usd_cents\n WHEN '60d' THEN s.net_spend_60d_usd_cents\n WHEN '90d' THEN s.net_spend_90d_usd_cents\n WHEN '1y' THEN s.net_spend_this_year_usd_cents\n ELSE CAST(s.lifetime_net_spend_usd_cents AS SIGNED)\n END) DESC,\n s.id ASC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_gravatar_hash!",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "net_spend_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "net_spend_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "net_spend_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "net_spend_60d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 9,
"name": "net_spend_90d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 10,
"name": "net_spend_this_year_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 11,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "maybe_reengagement_score: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "f6db2f3bf8695fa8431db209576328154b8ac20aa6e899739095b6206bb38944"
}
@@ -0,0 +1,47 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_stripe_subscription_status,\n maybe_stripe_recurring_interval,\n subscription_expires_at AS `subscription_expires_at!: DateTime<Utc>`\nFROM user_subscriptions\nWHERE user_token = ?\n AND subscription_namespace = ?\nORDER BY subscription_expires_at DESC\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "maybe_stripe_subscription_status",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_stripe_recurring_interval",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "subscription_expires_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
true,
false
]
},
"hash": "f74615faa4321ec4d583829997e12cb8ae152afefd2574385c120c63278893d2"
}
+1 -1
View File
@@ -58,7 +58,7 @@ artcraft/
- Rust with no minimum supported version
- Actix-web for HTTP services
- SQLx for MySQL and SQLite; prefer `sqlx::query!` / `sqlx::query_as!` compile-time checked macros over runtime `sqlx::query()` whenever possible
- SQLx for MySQL and SQLite. MySQL queries belong in the `mysql_queries` crate, not in application/service crates like `storyteller-web` — call those query functions instead of embedding `sqlx::query!` / `sqlx::query()` in handlers. (Query-writing conventions live in `crates/schema/database/mysql_queries/CLAUDE.md`.)
- A mix of wreq and reqwest for Rust HTTP clients
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead
- When two crates export the same type name, alias imports with a suffix: `use foo::Bar as BarFoo;`
Generated
+704 -721
View File
File diff suppressed because it is too large Load Diff
+21 -13
View File
@@ -7,9 +7,12 @@ members = [
# API clients for external services
# These might be custom implementations or wrappers over other client libraries.
"crates/api_clients/artcraft/artcraft_api_defs",
"crates/api_clients/artcraft/artcraft_api_keys",
"crates/api_clients/artcraft/artcraft_client",
"crates/api_clients/artcraft/artcraft_router",
"crates/api_clients/bucket_client",
"crates/api_clients/cloud_storage",
"crates/api_clients/comet_api_client",
"crates/api_clients/datadog_client",
"crates/api_clients/fal_client",
"crates/api_clients/google_drive_downloader",
@@ -85,6 +88,7 @@ members = [
"crates/lib/pager",
"crates/lib/password",
"crates/lib/server_environment",
"crates/lib/sha1_hash",
"crates/lib/sqlx_mysql_helpers",
"crates/lib/storyteller_root",
"crates/lib/url_config",
@@ -92,6 +96,7 @@ members = [
"crates/lib/user_input_common",
"crates/lib/users",
"crates/lib/uuid_utils",
"crates/lib/video_info",
#"crates/lib/api_clients/fakeyou_client",
#"crates/lib/deprecated/machine_learning/ml_models",
@@ -131,6 +136,7 @@ members = [
"crates/service/job/grok_api_job",
"crates/service/job/seedance2_pro_job",
"crates/service/job/video_thumbnail_job",
"crates/service/job/user_spend_analytics_job",
"crates/service/job/worldlabs_job",
"crates/service/web/dummy_service",
"crates/service/web/storyteller_web",
@@ -231,9 +237,11 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
actix_artcraft = { path = "crates/lib/actix_artcraft" }
actix_helpers = { path = "crates/lib/actix_helpers" }
artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
artcraft_api_keys = { path = "crates/api_clients/artcraft/artcraft_api_keys" }
artcraft_client = { path = "crates/api_clients/artcraft/artcraft_client" }
artcraft_router = { path = "crates/api_clients/artcraft/artcraft_router" }
bootstrap = { path = "crates/lib/bootstrap" }
bucket_client = { path = "crates/api_clients/bucket_client" }
bucket_paths = { path = "crates/schema/buckets/bucket_paths" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
@@ -278,6 +286,7 @@ openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
seedance2pro_client = { path = "crates/api_clients/seedance2pro_client" }
server_environment = { path = "crates/lib/server_environment" }
sha1_hash = { path = "crates/lib/sha1_hash" }
shared_service_components = { path = "crates/lib/shared_service_components" }
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
subprocess_common = { path = "crates/lib/deprecated/subprocess_common" }
@@ -287,6 +296,7 @@ tokens = { path = "crates/schema/public/tokens" }
url_utils = { path = "crates/lib/url_utils" }
users = { path = "crates/lib/users" }
uuid_utils = { path = "crates/lib/uuid_utils" }
video_info = { path = "crates/lib/video_info" }
web_base64 = { path = "crates/lib/data/web_base64" }
worldlabs_api_client = { path = "crates/api_clients/worldlabs_api_client" }
worldlabs_consumer_client = { path = "crates/api_clients/worldlabs_consumer_client" }
@@ -294,16 +304,14 @@ worldlabs_consumer_client = { path = "crates/api_clients/worldlabs_consumer_clie
# External libs
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
actix = "0.13.5"
actix-cors = "0.6.4" # TODO(bt): Carefully upgrade this. CORS broke last time.
actix-http = "3.7.0"
actix-multipart = "0.6.2"
actix-rt = "2.10.0"
actix-cors = "0.7.1"
actix-http = "3.12.1"
actix-multipart = "0.7.2"
actix-rt = "2.11.0"
actix-service = "2.0.3"
actix-utils = "3.0.1"
actix-web = "4.7.0" # TODO(bt): We're behind and need to bump versions.
actix-web-actors = "4.3.1+deprecated"
actix-web-lab = "0.20.2"
actix-web = "4.13.0"
actix-web-lab = "0.26.0"
anyhow = "1.0.102"
assertor = "0.0.3"
base64 = "0.22.1"
@@ -343,11 +351,11 @@ sha2 = "0.11.0-rc.5"
sha3 = "0.10.8"
speculoos = "0.11.0"
strum = { version = "0.28.0", features = ["derive"] }
tauri = { version = "2.8.3", features = ["unstable", "devtools", "protocol-asset"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
tauri-plugin-dialog = "2.3.3"
tauri-plugin-fs = "2.4.2"
tauri-plugin-log = "2.6.0"
tauri-plugin-upload = "2.3.1"
tauri = { version = "2.11.2", features = ["unstable", "devtools", "protocol-asset"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
tauri-plugin-dialog = "2.7.1"
tauri-plugin-fs = "2.5.1"
tauri-plugin-log = "2.8.0"
tauri-plugin-upload = "2.4.0"
tempdir = "0.3.7"
tempfile = "3.27.0"
thiserror = "2.0.18"
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
DROP COLUMN maybe_external_third_party_cost_credits;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN maybe_external_third_party_cost_credits INT(10) UNSIGNED DEFAULT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
DROP COLUMN maybe_external_third_party_cost_usd_cents;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN maybe_external_third_party_cost_usd_cents INT(10) UNSIGNED DEFAULT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
DROP COLUMN maybe_system_cost_credits;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN maybe_system_cost_credits INT(10) UNSIGNED DEFAULT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
DROP COLUMN maybe_system_cost_usd_cents;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN maybe_system_cost_usd_cents INT(10) UNSIGNED DEFAULT NULL;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS api_keys;
@@ -0,0 +1,50 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE api_keys (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- The actual API key value presented by clients to authenticate.
api_key VARCHAR(255) NOT NULL,
-- ========== KEY INFO ==========
-- User-supplied display name for the key.
name VARCHAR(255) NOT NULL,
-- Optional user-supplied description.
maybe_description VARCHAR(512) DEFAULT NULL,
-- ========== OWNER ==========
-- The user that owns this API key.
owner_user_token VARCHAR(32) NOT NULL,
-- ========== IP ADDRESSES ==========
-- IP address that created the key. Wide enough for IPv4/IPv6.
ip_address_creation VARCHAR(40) NOT NULL,
-- IP address that last updated the key. Wide enough for IPv4/IPv6.
ip_address_update VARCHAR(40) NOT NULL,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Soft-delete timestamp. NULL means the key is live.
maybe_deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (api_key),
KEY fk_owner_user_token (owner_user_token),
KEY index_maybe_deleted_at (maybe_deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS uploaded_videos;
@@ -0,0 +1,62 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE uploaded_videos (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- ========== FILE ==========
-- SHA-1 checksum of the uploaded bytes (40 hex chars). De-dupes uploads.
sha1_checksum VARCHAR(40) NOT NULL,
-- Size of the uploaded file in bytes.
filesize_bytes INT(10) UNSIGNED NOT NULL,
-- Pixel dimensions, when they could be determined.
maybe_width INT(10) UNSIGNED DEFAULT NULL,
maybe_height INT(10) UNSIGNED DEFAULT NULL,
-- Resolution label (e.g. "1080p", "1920x1080"), when known.
maybe_resolution VARCHAR(12) DEFAULT NULL,
-- ========== DETECTION ==========
-- Detected provenance family (e.g. "seedance", "veo", "sora", "dreamina", "kling").
maybe_detected_model_family VARCHAR(32) DEFAULT NULL,
-- Detected model type / variant (e.g. "full", "fast", "mini").
maybe_detected_model_type VARCHAR(32) DEFAULT NULL,
-- Free-form report / analysis blob for this upload.
maybe_report MEDIUMTEXT DEFAULT NULL,
-- ========== IP ADDRESSES ==========
-- IP address that uploaded the video. Wide enough for IPv4/IPv6.
upload_ip_address VARCHAR(40) NOT NULL,
-- IP address that last updated the row. Wide enough for IPv4/IPv6.
maybe_updated_ip_address VARCHAR(40) DEFAULT NULL,
-- ========== VERSION ==========
-- Optimistic-concurrency "vector clock". Bump by 1 on every update — set in
-- the UPDATE statement (MySQL can't auto-increment a plain column via schema).
version INT(8) UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (sha1_checksum),
KEY index_maybe_detected_model_family (maybe_detected_model_family)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS uploaded_video_notes;
@@ -0,0 +1,69 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE uploaded_video_notes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- The uploaded video this note is about (FK to uploaded_videos.token).
uploaded_video_token VARCHAR(32) NOT NULL,
-- ========== SUBMISSION ==========
-- Original filename of the submitted clip, as provided by the user.
maybe_filename VARCHAR(512) DEFAULT NULL,
-- The model type the submitter claims this video is.
maybe_reported_model_type VARCHAR(32) DEFAULT NULL,
-- Free-form custom model name when the submitter's model isn't a known type.
maybe_reported_model_name VARCHAR(64) DEFAULT NULL,
-- The website / platform the video was obtained from.
maybe_website VARCHAR(32) DEFAULT NULL,
-- Free-form website when it isn't a known option.
maybe_other_website VARCHAR(128) DEFAULT NULL,
-- Free-form comments from the submitter.
maybe_comments VARCHAR(1024) DEFAULT NULL,
-- ========== CONTACT & FLAGS ==========
-- Optional email address for follow-up.
maybe_email_address VARCHAR(255) DEFAULT NULL,
-- Whether the submitter consents to us sharing the report.
can_share_report BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the submitter believes they were scammed.
was_scammed BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== IP ADDRESSES ==========
-- IP address that submitted the note. Wide enough for IPv4/IPv6.
comment_create_ip_address VARCHAR(40) NOT NULL,
-- IP address that last updated the note. Wide enough for IPv4/IPv6.
maybe_comment_update_ip_address VARCHAR(40) DEFAULT NULL,
-- ========== VERSION ==========
-- Optimistic-concurrency "vector clock". Bump by 1 on every update — set in
-- the UPDATE statement (MySQL can't auto-increment a plain column via schema).
version INT(8) UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY fk_uploaded_video_token (uploaded_video_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE uploaded_videos
DROP COLUMN maybe_filename;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE uploaded_videos
ADD COLUMN maybe_filename VARCHAR(512) DEFAULT NULL AFTER filesize_bytes;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE prompts
DROP COLUMN maybe_bitrate;
@@ -0,0 +1,8 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Add bitrate parameter to prompts table.
-- Represented by the enum `CommonBitrate` (normal, high).
ALTER TABLE prompts
ADD COLUMN maybe_bitrate VARCHAR(16) DEFAULT NULL;
@@ -0,0 +1,8 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Revert `maybe_entity_token` back to NOT NULL.
-- NB: This will fail if any rows have a null `maybe_entity_token`.
ALTER TABLE staff_audit_logs
MODIFY COLUMN maybe_entity_token VARCHAR(32) NOT NULL;
@@ -0,0 +1,9 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- `maybe_entity_token` is optional (many audited actions have no entity), but it
-- was mistakenly created as NOT NULL, so inserts with a null entity token fail
-- with "Column 'maybe_entity_token' cannot be null". Make it nullable.
ALTER TABLE staff_audit_logs
MODIFY COLUMN maybe_entity_token VARCHAR(32) DEFAULT NULL;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_spend_events;
@@ -0,0 +1,103 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Append-only ledger of real money movements (USD cents), one row per settled
-- event. Derived from settled Stripe webhooks (invoice.paid, payment_intent.
-- succeeded / checkout.session.completed, charge.refunded, charge.dispute.*),
-- NOT from every webhook. Idempotent via uq_source_object so retries never
-- double-count. We always work in USD via Stripe, so there are no currency or
-- fee columns.
CREATE TABLE user_spend_events (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective public primary key. Token prefix "spend_".
token VARCHAR(32) NOT NULL,
-- Multi-site segregation. Reuses the existing PaymentsNamespace enum.
payments_namespace VARCHAR(32) NOT NULL,
-- Who paid. Nullable so we can record money we can't yet attribute and
-- backfill -- but expect this set ~always.
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- PaymentEventType enum. Splits subscription vs credit-pack revenue:
-- 'subscription_initial' -- first paid invoice of a new subscription
-- 'subscription_renewal' -- recurring paid invoice (monthly, or annual once/yr)
-- 'subscription_proration_upgrade' -- mid-cycle upgrade proration charge (positive)
-- 'subscription_proration_downgrade' -- mid-cycle downgrade proration credit (rare as a settled event)
-- 'credit_pack_purchase' -- one-time credit pack (the ~10x-volume case)
-- 'refund' -- refund (amount negative)
-- 'chargeback' -- dispute/chargeback (amount negative)
-- 'manual_adjustment' -- staff/console correction (payment_source = 'manual')
-- 'subscription_monthly_refill' -- OPTIONAL non-revenue event for annual-plan
-- monthly credit refills (amount 0)
event_type VARCHAR(32) NOT NULL,
-- ========== MONEY ==========
-- USD cents. SIGNED: refunds/chargebacks negative; refills are 0. BIGINT so
-- downstream lifetime sums never overflow.
amount_usd_cents BIGINT NOT NULL,
-- For credit-pack purchases / subscription grants/refills: how many of OUR
-- credits this granted. Lets us correlate dollars-in vs credits-granted vs
-- credits-used. NULL for refunds/chargebacks.
maybe_credits_granted INTEGER UNSIGNED DEFAULT NULL,
-- ========== INTERNAL LINKAGE ==========
-- The subscription this payment belongs to, if any (-> user_subscriptions.token).
maybe_user_subscription_token VARCHAR(32) DEFAULT NULL,
-- The credit-grant ledger entry this produced, if any
-- (-> wallet_ledger_entries.token). Ties a dollar event to its credit effect.
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL,
-- ========== SOURCE / PROVIDER ==========
-- PaymentSource enum: 'stripe' or 'manual'. (USD/Stripe only for the
-- foreseeable future; 'manual' covers staff/console adjustments.)
payment_source VARCHAR(16) NOT NULL,
-- The authoritative object this row represents -- the dedup anchor. NULLABLE.
-- For Stripe: invoice id (subscriptions), payment_intent id (one-time), or
-- refund/dispute id (refunds). For payment_source='manual' (no provider
-- object) leave it NULL. MySQL allows multiple NULLs in a unique index, so
-- manual rows freely duplicate, while any present id is still deduped.
source_object_id VARCHAR(255) DEFAULT NULL,
-- Convenience copies for joins/forensics (255 + utf8mb4_bin for Stripe IDs).
maybe_stripe_customer_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_invoice_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_payment_intent_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_charge_id VARCHAR(255) DEFAULT NULL,
-- The webhook event that caused this row (provenance only).
maybe_stripe_event_id VARCHAR(255) DEFAULT NULL,
-- Stripe livemode. Keep test-mode money out of dashboards.
is_production BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
-- When the money actually moved (Stripe settlement time). Bin/aggregate on
-- THIS, not created_at (which is when we ingested the webhook).
payment_occurred_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
-- Idempotency: one row per settled object. NULL source_object_id rows (manual)
-- are exempt -- MySQL treats each NULL in a unique index as distinct.
UNIQUE KEY uq_source_object (payment_source, source_object_id),
KEY idx_user_occurred (maybe_user_token, payment_occurred_at),
KEY idx_occurred (payment_occurred_at),
KEY idx_namespace_occurred (payments_namespace, payment_occurred_at),
KEY idx_event_type (event_type),
KEY idx_stripe_customer (maybe_stripe_customer_id),
KEY idx_stripe_invoice (maybe_stripe_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_daily_spends;
@@ -0,0 +1,44 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Per-user, per-day spend rollup (L1). Net = gross - refunds. Subscription vs
-- credit-pack splits expose the revenue mix per day without touching the event
-- table. Sparsely populated -- no row for days with zero spend and zero refunds.
-- Derived from (and fully rebuildable from) user_spend_events.
CREATE TABLE user_daily_spends (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_token VARCHAR(32) NOT NULL,
payments_namespace VARCHAR(32) NOT NULL,
-- Reporting day. Bins user_spend_events.payment_occurred_at in a FIXED tz
-- (UTC). DATE, not TIMESTAMP.
spend_date DATE NOT NULL,
-- Revenue mix (subset of gross). Category totals can never go negative, so
-- they're UNSIGNED.
subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- sum of positive amounts (UNSIGNED)
refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- refund/chargeback magnitude (UNSIGNED)
-- NET is the ONE field here that must be SIGNED: a single day can net negative
-- when refunds (often of an earlier day's purchase) exceed that day's purchases.
net_spend_usd_cents BIGINT NOT NULL DEFAULT 0, -- gross - refund (SIGNED)
payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # positive money events
credits_granted BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY uq_user_day (user_token, payments_namespace, spend_date),
-- "Top spenders / spend in a date range": filter by date, then group by user.
KEY idx_day_user_net (user_token, payments_namespace, spend_date, net_spend_usd_cents),
KEY idx_user_day (user_token, spend_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_spend_summaries;
@@ -0,0 +1,95 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Per-user spend summary (L2). Lifetime/recency facts are event-maintained (they
-- only grow); sliding windows and weekly-rhythm metrics are job-maintained (they
-- shift even on days a user doesn't pay). Powers churn / dip / re-engagement
-- dashboards in O(1). Derived from (and fully rebuildable from) user_spend_events
-- and user_daily_spends.
CREATE TABLE user_spend_summaries (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
payments_namespace VARCHAR(32) NOT NULL,
user_token VARCHAR(32) NOT NULL,
-- ===== Lifetime (event-maintained) =====
-- Gross / category / refund totals are UNSIGNED. Lifetime net is UNSIGNED too:
-- over a lifetime you can never refund more than was paid.
lifetime_gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_net_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- gross - refund
-- Event COUNTS (not money): number of payment events vs refund/chargeback
-- events over the user's lifetime. Both event-maintained.
lifetime_payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of positive money events
lifetime_refund_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of refund/chargeback events
first_payment_at TIMESTAMP NULL DEFAULT NULL,
first_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the first payment
last_payment_at TIMESTAMP NULL DEFAULT NULL,
last_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the most recent payment
days_since_first_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
days_since_last_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
-- ===== Sliding NET spend windows -- SIGNED, a window can net negative
-- (job-refreshed from user_daily_spends) =====
-- "prev" = the equally-sized window immediately before, for WoW/MoM deltas.
net_spend_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_14d_usd_cents BIGINT NOT NULL DEFAULT 0, -- bi-weekly
net_spend_prev_14d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_60d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_90d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_this_year_usd_cents BIGINT NOT NULL DEFAULT 0,
-- ===== Weekly cadence / rhythm (job-refreshed) -- for the sales team =====
-- A "week" is Mon-Sun UTC. These let sales spot users breaking their habit.
-- Per-user weekly baselines at two horizons: 4w is responsive (recent norm),
-- 12w is stable (established norm). 4w well below 12w is itself a cooling-off
-- signal. Mean weekly NET spend over the trailing window (calendar weeks,
-- zero-spend weeks included). SIGNED, like the window nets.
avg_weekly_net_spend_4w_usd_cents BIGINT NOT NULL DEFAULT 0,
avg_weekly_net_spend_12w_usd_cents BIGINT NOT NULL DEFAULT 0,
-- How many of the last N calendar weeks had any net spend -- a regularity
-- measure at several horizons.
active_weeks_in_last_4 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..4
active_weeks_in_last_8 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..8
active_weeks_in_last_12 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..12
active_weeks_in_last_24 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..24
active_weeks_in_last_52 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..52
consecutive_active_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "streak"
consecutive_inactive_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "dip" length
-- Recency in weeks. Subsumes the entire "is_active_spender_last_N_weeks"
-- family: "active in the last N weeks" == (weeks_since_last_spend < N). So we
-- intentionally store NO is_active_* booleans (redundant + poor index
-- selectivity); we index this field instead and range-query any N. NULL = never spent.
weeks_since_last_spend INT UNSIGNED DEFAULT NULL,
-- ===== Flags =====
is_active_subscriber BOOLEAN NOT NULL DEFAULT FALSE, -- from user_subscriptions
maybe_subscription_interval VARCHAR(16) DEFAULT NULL, -- 'month' / 'year' (annual plans behave differently)
-- Optional precomputed re-engagement priority (job-set).
maybe_reengagement_score INT UNSIGNED DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY uq_user (payments_namespace, user_token),
KEY idx_lifetime (payments_namespace, lifetime_net_spend_usd_cents),
KEY idx_last_payment_at (payments_namespace, last_payment_at),
KEY idx_first_payment_at (payments_namespace, first_payment_at),
KEY idx_spend_7d (payments_namespace, net_spend_7d_usd_cents),
KEY idx_spend_30d (payments_namespace, net_spend_30d_usd_cents),
KEY idx_reengagement (payments_namespace, maybe_reengagement_score),
-- Sort / range-filter by recency. idx_weeks_since_last_spend also powers every
-- "active in the last N weeks" query (weeks_since_last_spend < N).
KEY idx_weeks_since_last_spend (payments_namespace, weeks_since_last_spend),
KEY idx_days_since_last_payment (payments_namespace, days_since_last_payment)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP COLUMN maybe_log_level;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD COLUMN maybe_log_level VARCHAR(16) DEFAULT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP INDEX index_maybe_log_level;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD INDEX index_maybe_log_level (maybe_log_level);
@@ -0,0 +1,50 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE api_keys (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- The actual API key value presented by clients to authenticate.
api_key VARCHAR(255) NOT NULL,
-- ========== KEY INFO ==========
-- User-supplied display name for the key.
name VARCHAR(255) NOT NULL,
-- Optional user-supplied description.
maybe_description VARCHAR(512) DEFAULT NULL,
-- ========== OWNER ==========
-- The user that owns this API key.
owner_user_token VARCHAR(32) NOT NULL,
-- ========== IP ADDRESSES ==========
-- IP address that created the key. Wide enough for IPv4/IPv6.
ip_address_creation VARCHAR(40) NOT NULL,
-- IP address that last updated the key. Wide enough for IPv4/IPv6.
ip_address_update VARCHAR(40) NOT NULL,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Soft-delete timestamp. NULL means the key is live.
maybe_deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (api_key),
KEY fk_owner_user_token (owner_user_token),
KEY index_maybe_deleted_at (maybe_deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -13,6 +13,9 @@ CREATE TABLE debug_logs (
-- The type of debug log entry.
debug_log_type VARCHAR(24) NOT NULL,
-- The severity level of the log entry (info, warn, error, debug, trace).
maybe_log_level VARCHAR(16) DEFAULT NULL,
-- The user who created the log entry (if any).
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
@@ -25,6 +28,7 @@ CREATE TABLE debug_logs (
PRIMARY KEY (id),
KEY index_event_token (event_token),
KEY index_debug_log_type (debug_log_type),
KEY index_maybe_log_level (maybe_log_level),
KEY index_maybe_creator_user_token (maybe_creator_user_token),
KEY index_created_at (created_at)
@@ -56,6 +56,19 @@ CREATE TABLE generic_inference_jobs (
-- service uses.
maybe_external_third_party_id VARCHAR(64) DEFAULT NULL,
-- What the third-party service charges us for the job, in the service's
-- own credit denomination (if it bills in credits).
maybe_external_third_party_cost_credits INT(10) UNSIGNED DEFAULT NULL,
-- What the third-party service charges us for the job, in USD cents.
maybe_external_third_party_cost_usd_cents INT(10) UNSIGNED DEFAULT NULL,
-- What we charge the user for the job, in our own credits.
maybe_system_cost_credits INT(10) UNSIGNED DEFAULT NULL,
-- What we charge the user for the job, in USD cents.
maybe_system_cost_usd_cents INT(10) UNSIGNED DEFAULT NULL,
-- There is an index on this column.
-- NB: See notes on inference_category. This is becoming a problematic field. We're using
-- this to load the container spin-up arguments and dependencies, but the former is being
@@ -72,6 +72,10 @@ CREATE TABLE prompts (
-- The quality setting (eg. high, medium, low)
maybe_quality VARCHAR(16) DEFAULT NULL,
-- Represented by the enum `CommonBitrate`.
-- The output bitrate setting (eg. normal, high)
maybe_bitrate VARCHAR(16) DEFAULT NULL,
-- The duration in seconds
maybe_duration_seconds INT UNSIGNED DEFAULT NULL,
@@ -0,0 +1,47 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Per-user, per-day spend rollup (L1). Net = gross - refunds. Subscription vs
-- credit-pack splits expose the revenue mix per day without touching the event
-- table. Sparsely populated -- no row for days with zero spend and zero refunds.
-- Derived from (and fully rebuildable from) user_spend_events.
CREATE TABLE user_daily_spends (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_token VARCHAR(32) NOT NULL,
payments_namespace VARCHAR(32) NOT NULL,
-- Reporting day. Bins user_spend_events.payment_occurred_at in a FIXED tz
-- (UTC). DATE, not TIMESTAMP.
spend_date DATE NOT NULL,
-- Revenue mix (subset of gross). Category totals can never go negative, so
-- they're UNSIGNED.
subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- sum of positive amounts (UNSIGNED)
refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- refund/chargeback magnitude (UNSIGNED)
-- NET is the ONE field here that must be SIGNED: a single day can net negative
-- when refunds (often of an earlier day's purchase) exceed that day's purchases.
net_spend_usd_cents BIGINT NOT NULL DEFAULT 0, -- gross - refund (SIGNED)
payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # positive money events
credits_granted BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY uq_user_day (user_token, payments_namespace, spend_date),
-- "Top spenders / spend in a date range": filter by date, then group by user.
KEY idx_day_user_net (user_token, payments_namespace, spend_date, net_spend_usd_cents),
KEY idx_user_day (user_token, spend_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,106 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Append-only ledger of real money movements (USD cents), one row per settled
-- event. Derived from settled Stripe webhooks (invoice.paid, payment_intent.
-- succeeded / checkout.session.completed, charge.refunded, charge.dispute.*),
-- NOT from every webhook. Idempotent via uq_source_object so retries never
-- double-count. We always work in USD via Stripe, so there are no currency or
-- fee columns.
CREATE TABLE user_spend_events (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective public primary key. Token prefix "spend_".
token VARCHAR(32) NOT NULL,
-- Multi-site segregation. Reuses the existing PaymentsNamespace enum.
payments_namespace VARCHAR(32) NOT NULL,
-- Who paid. Nullable so we can record money we can't yet attribute and
-- backfill -- but expect this set ~always.
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- PaymentEventType enum. Splits subscription vs credit-pack revenue:
-- 'subscription_initial' -- first paid invoice of a new subscription
-- 'subscription_renewal' -- recurring paid invoice (monthly, or annual once/yr)
-- 'subscription_proration_upgrade' -- mid-cycle upgrade proration charge (positive)
-- 'subscription_proration_downgrade' -- mid-cycle downgrade proration credit (rare as a settled event)
-- 'credit_pack_purchase' -- one-time credit pack (the ~10x-volume case)
-- 'refund' -- refund (amount negative)
-- 'chargeback' -- dispute/chargeback (amount negative)
-- 'manual_adjustment' -- staff/console correction (payment_source = 'manual')
-- 'subscription_monthly_refill' -- OPTIONAL non-revenue event for annual-plan
-- monthly credit refills (amount 0)
event_type VARCHAR(32) NOT NULL,
-- ========== MONEY ==========
-- USD cents. SIGNED: refunds/chargebacks negative; refills are 0. BIGINT so
-- downstream lifetime sums never overflow.
amount_usd_cents BIGINT NOT NULL,
-- For credit-pack purchases / subscription grants/refills: how many of OUR
-- credits this granted. Lets us correlate dollars-in vs credits-granted vs
-- credits-used. NULL for refunds/chargebacks.
maybe_credits_granted INTEGER UNSIGNED DEFAULT NULL,
-- ========== INTERNAL LINKAGE ==========
-- The subscription this payment belongs to, if any (-> user_subscriptions.token).
maybe_user_subscription_token VARCHAR(32) DEFAULT NULL,
-- The credit-grant ledger entry this produced, if any
-- (-> wallet_ledger_entries.token). Ties a dollar event to its credit effect.
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL,
-- ========== SOURCE / PROVIDER ==========
-- PaymentSource enum: 'stripe' or 'manual'. (USD/Stripe only for the
-- foreseeable future; 'manual' covers staff/console adjustments.)
payment_source VARCHAR(16) NOT NULL,
-- The authoritative object this row represents -- the dedup anchor. NULLABLE.
-- For Stripe: invoice id (subscriptions), payment_intent id (one-time), or
-- refund/dispute id (refunds). For payment_source='manual' (no provider
-- object) leave it NULL. MySQL allows multiple NULLs in a unique index, so
-- manual rows freely duplicate, while any present id is still deduped.
source_object_id VARCHAR(255) DEFAULT NULL,
-- Convenience copies for joins/forensics (255 + utf8mb4_bin for Stripe IDs).
maybe_stripe_customer_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_invoice_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_payment_intent_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_charge_id VARCHAR(255) DEFAULT NULL,
-- The webhook event that caused this row (provenance only).
maybe_stripe_event_id VARCHAR(255) DEFAULT NULL,
-- Stripe livemode. Keep test-mode money out of dashboards.
is_production BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
-- When the money actually moved (Stripe settlement time). Bin/aggregate on
-- THIS, not created_at (which is when we ingested the webhook).
payment_occurred_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
-- Idempotency: one row per settled object. NULL source_object_id rows (manual)
-- are exempt -- MySQL treats each NULL in a unique index as distinct.
UNIQUE KEY uq_source_object (payment_source, source_object_id),
KEY idx_user_occurred (maybe_user_token, payment_occurred_at),
KEY idx_occurred (payment_occurred_at),
KEY idx_namespace_occurred (payments_namespace, payment_occurred_at),
KEY idx_event_type (event_type),
KEY idx_stripe_customer (maybe_stripe_customer_id),
KEY idx_stripe_invoice (maybe_stripe_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,98 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Per-user spend summary (L2). Lifetime/recency facts are event-maintained (they
-- only grow); sliding windows and weekly-rhythm metrics are job-maintained (they
-- shift even on days a user doesn't pay). Powers churn / dip / re-engagement
-- dashboards in O(1). Derived from (and fully rebuildable from) user_spend_events
-- and user_daily_spends.
CREATE TABLE user_spend_summaries (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
payments_namespace VARCHAR(32) NOT NULL,
user_token VARCHAR(32) NOT NULL,
-- ===== Lifetime (event-maintained) =====
-- Gross / category / refund totals are UNSIGNED. Lifetime net is UNSIGNED too:
-- over a lifetime you can never refund more than was paid.
lifetime_gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_net_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- gross - refund
-- Event COUNTS (not money): number of payment events vs refund/chargeback
-- events over the user's lifetime. Both event-maintained.
lifetime_payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of positive money events
lifetime_refund_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of refund/chargeback events
first_payment_at TIMESTAMP NULL DEFAULT NULL,
first_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the first payment
last_payment_at TIMESTAMP NULL DEFAULT NULL,
last_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the most recent payment
days_since_first_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
days_since_last_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
-- ===== Sliding NET spend windows -- SIGNED, a window can net negative
-- (job-refreshed from user_daily_spends) =====
-- "prev" = the equally-sized window immediately before, for WoW/MoM deltas.
net_spend_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_14d_usd_cents BIGINT NOT NULL DEFAULT 0, -- bi-weekly
net_spend_prev_14d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_60d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_90d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_this_year_usd_cents BIGINT NOT NULL DEFAULT 0,
-- ===== Weekly cadence / rhythm (job-refreshed) -- for the sales team =====
-- A "week" is Mon-Sun UTC. These let sales spot users breaking their habit.
-- Per-user weekly baselines at two horizons: 4w is responsive (recent norm),
-- 12w is stable (established norm). 4w well below 12w is itself a cooling-off
-- signal. Mean weekly NET spend over the trailing window (calendar weeks,
-- zero-spend weeks included). SIGNED, like the window nets.
avg_weekly_net_spend_4w_usd_cents BIGINT NOT NULL DEFAULT 0,
avg_weekly_net_spend_12w_usd_cents BIGINT NOT NULL DEFAULT 0,
-- How many of the last N calendar weeks had any net spend -- a regularity
-- measure at several horizons.
active_weeks_in_last_4 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..4
active_weeks_in_last_8 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..8
active_weeks_in_last_12 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..12
active_weeks_in_last_24 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..24
active_weeks_in_last_52 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..52
consecutive_active_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "streak"
consecutive_inactive_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "dip" length
-- Recency in weeks. Subsumes the entire "is_active_spender_last_N_weeks"
-- family: "active in the last N weeks" == (weeks_since_last_spend < N). So we
-- intentionally store NO is_active_* booleans (redundant + poor index
-- selectivity); we index this field instead and range-query any N. NULL = never spent.
weeks_since_last_spend INT UNSIGNED DEFAULT NULL,
-- ===== Flags =====
is_active_subscriber BOOLEAN NOT NULL DEFAULT FALSE, -- from user_subscriptions
maybe_subscription_interval VARCHAR(16) DEFAULT NULL, -- 'month' / 'year' (annual plans behave differently)
-- Optional precomputed re-engagement priority (job-set).
maybe_reengagement_score INT UNSIGNED DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY uq_user (payments_namespace, user_token),
KEY idx_lifetime (payments_namespace, lifetime_net_spend_usd_cents),
KEY idx_last_payment_at (payments_namespace, last_payment_at),
KEY idx_first_payment_at (payments_namespace, first_payment_at),
KEY idx_spend_7d (payments_namespace, net_spend_7d_usd_cents),
KEY idx_spend_30d (payments_namespace, net_spend_30d_usd_cents),
KEY idx_reengagement (payments_namespace, maybe_reengagement_score),
-- Sort / range-filter by recency. idx_weeks_since_last_spend also powers every
-- "active in the last N weeks" query (weeks_since_last_spend < N).
KEY idx_weeks_since_last_spend (payments_namespace, weeks_since_last_spend),
KEY idx_days_since_last_payment (payments_namespace, days_since_last_payment)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -17,7 +17,7 @@ CREATE TABLE staff_audit_logs (
maybe_entity_type VARCHAR(16) DEFAULT NULL,
-- The token of the entity mutated (if any)
maybe_entity_token VARCHAR(32) NOT NULL,
maybe_entity_token VARCHAR(32) DEFAULT NULL,
-- The staff user who performed the action.
staff_user_token VARCHAR(32) DEFAULT NULL,
@@ -0,0 +1,69 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE uploaded_video_notes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- The uploaded video this note is about (FK to uploaded_videos.token).
uploaded_video_token VARCHAR(32) NOT NULL,
-- ========== SUBMISSION ==========
-- Original filename of the submitted clip, as provided by the user.
maybe_filename VARCHAR(512) DEFAULT NULL,
-- The model type the submitter claims this video is.
maybe_reported_model_type VARCHAR(32) DEFAULT NULL,
-- Free-form custom model name when the submitter's model isn't a known type.
maybe_reported_model_name VARCHAR(64) DEFAULT NULL,
-- The website / platform the video was obtained from.
maybe_website VARCHAR(32) DEFAULT NULL,
-- Free-form website when it isn't a known option.
maybe_other_website VARCHAR(128) DEFAULT NULL,
-- Free-form comments from the submitter.
maybe_comments VARCHAR(1024) DEFAULT NULL,
-- ========== CONTACT & FLAGS ==========
-- Optional email address for follow-up.
maybe_email_address VARCHAR(255) DEFAULT NULL,
-- Whether the submitter consents to us sharing the report.
can_share_report BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the submitter believes they were scammed.
was_scammed BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== IP ADDRESSES ==========
-- IP address that submitted the note. Wide enough for IPv4/IPv6.
comment_create_ip_address VARCHAR(40) NOT NULL,
-- IP address that last updated the note. Wide enough for IPv4/IPv6.
maybe_comment_update_ip_address VARCHAR(40) DEFAULT NULL,
-- ========== VERSION ==========
-- Optimistic-concurrency "vector clock". Bump by 1 on every update — set in
-- the UPDATE statement (MySQL can't auto-increment a plain column via schema).
version INT(8) UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY fk_uploaded_video_token (uploaded_video_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,65 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE uploaded_videos (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- ========== FILE ==========
-- SHA-1 checksum of the uploaded bytes (40 hex chars). De-dupes uploads.
sha1_checksum VARCHAR(40) NOT NULL,
-- Size of the uploaded file in bytes.
filesize_bytes INT(10) UNSIGNED NOT NULL,
-- Original filename of the uploaded clip, as provided by the user.
maybe_filename VARCHAR(512) DEFAULT NULL,
-- Pixel dimensions, when they could be determined.
maybe_width INT(10) UNSIGNED DEFAULT NULL,
maybe_height INT(10) UNSIGNED DEFAULT NULL,
-- Resolution label (e.g. "1080p", "1920x1080"), when known.
maybe_resolution VARCHAR(12) DEFAULT NULL,
-- ========== DETECTION ==========
-- Detected provenance family (e.g. "seedance", "veo", "sora", "dreamina", "kling").
maybe_detected_model_family VARCHAR(32) DEFAULT NULL,
-- Detected model type / variant (e.g. "full", "fast", "mini").
maybe_detected_model_type VARCHAR(32) DEFAULT NULL,
-- Free-form report / analysis blob for this upload.
maybe_report MEDIUMTEXT DEFAULT NULL,
-- ========== IP ADDRESSES ==========
-- IP address that uploaded the video. Wide enough for IPv4/IPv6.
upload_ip_address VARCHAR(40) NOT NULL,
-- IP address that last updated the row. Wide enough for IPv4/IPv6.
maybe_updated_ip_address VARCHAR(40) DEFAULT NULL,
-- ========== VERSION ==========
-- Optimistic-concurrency "vector clock". Bump by 1 on every update — set in
-- the UPDATE statement (MySQL can't auto-increment a plain column via schema).
version INT(8) UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (sha1_checksum),
KEY index_maybe_detected_model_family (maybe_detected_model_family)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+625
View File
@@ -0,0 +1,625 @@
# ArtCraft Omni API
> # 📚 Full API reference: **<https://storyteller-docs.netlify.app/>**
>
> **Every ArtCraft API endpoint is documented there.** This guide covers only the Omni API video
> endpoints — for everything else, start at the link above.
The **Omni API** is the API-key–authenticated surface for ArtCraft generation. It mirrors the
in-app "Omni Gen" endpoints but is designed for programmatic use: you authenticate with an API key
in the `Authorization` header instead of a browser session cookie.
This guide covers **video generation from URL inputs** — passing image, video, and audio reference
URLs directly. The server downloads each URL, stores it as a media file on your account, and then
runs generation against it. You never have to upload files yourself.
> All examples send **no cookies**. The Omni API is authenticated purely by your API key. If you
> send session cookies they are ignored.
---
## Base URLs
| Environment | Base URL |
| ------------- | ------------------------------ |
| Development | `http://localhost:12345` |
| Production | `https://api.storyteller.ai` |
All paths below are relative to the base URL (e.g. `POST /v1/omni_api/generate/video`).
The examples in this guide use the **development** base URL. To run against production, swap
`http://localhost:12345` for `https://api.storyteller.ai`.
Note: Our APIs are hosted on "`api.storyteller.ai`", not "`api.getartcraft.com`". This is not a
typo. Our Cloudflare CDN URLs are `cdn-2.fakeyou.com`.
---
## Getting an API key
Create and manage API keys from the ArtCraft web app:
1. Go to **<https://app.getartcraft.com/>**
2. Click **Account → Settings → API Keys** (your account must have API keys enabled from our staff)
3. Create a key and copy it. The secret is shown **once** — store it somewhere safe.
> **Don't see the API Keys section?** API access is gated per account. Contact the Artcraft team
> to enable it.
### What an API key looks like
- Prefix: `artcraft_api_`
- Followed by 40 random lowercase Crockford-base32 characters (`0-9 a-z`, excluding `i l o u`)
- Total length: **53 characters**, e.g. `artcraft_api_3k7q9w0xv2hs5n8m4d6r1t8y0p2s4f6h8j0k2m4n6`
Treat the key like a password: never commit it to source control or paste it into logs.
---
## Authentication
Send the secret key in the `Authorization` header. Three forms are accepted:
```
Authorization: Bearer artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: Key artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
`Bearer` is recommended. **Do not send cookies.**
---
## Video generation — `POST /v1/omni_api/generate/video`
Generates a video and returns an inference job token you can poll for results.
### Request body
| Field | Type | Notes |
| ---------------------------- | --------------- | ----- |
| `model` | string | Generation model, e.g. `seedance_2p0`. **Required.** |
| `prompt` | string | Text prompt. |
| `duration_seconds` | integer | Length of the output video, e.g. `6`. |
| `aspect_ratio` | string | e.g. `wide_sixteen_by_nine`, `tall_nine_by_sixteen`, `square`. |
| `idempotency_token` | string (UUID) | **Required.** A fresh UUID per request; prevents accidental duplicates. |
| `negative_prompt` | string | Optional. |
| `resolution` | string | Optional, e.g. `seven_twenty_p`, `ten_eighty_p`. |
| `generate_audio` | boolean | Optional. |
| **URL inputs** (this guide) | | |
| `start_frame_image_url` | string (URL) | Starting keyframe image. |
| `end_frame_image_url` | string (URL) | Ending keyframe image. |
| `reference_image_urls` | array of URL | Reference images. |
| `reference_video_urls` | array of URL | Reference videos (`mp4`). |
| `reference_audio_urls` | array of URL | Reference audio (`wav`, `mp3`, `ogg`, …). |
| **Media-token inputs** | | |
| `start_frame_image_media_token`, `end_frame_image_media_token`, `reference_image_media_tokens`, `reference_video_media_tokens`, `reference_audio_media_tokens` | token(s) | Use these instead if you've already uploaded media. |
#### Rules for URL inputs
- Each URL field and its media-token counterpart are **mutually exclusive**. Sending both
`reference_image_urls` and `reference_image_media_tokens`, for example, returns:
`Either reference_image_media_tokens or reference_image_urls must be set, not both`.
- Every URL must start with `http://` or `https://`, otherwise:
`URL must start with http:// or https://, bad URL: <url>`.
- Redirects are followed (up to 10), so CDN/redirecting URLs work.
- The file type is detected from the downloaded bytes. Images must be a real image
(`jpeg`/`png`/`gif`/`webp`), reference videos must be `mp4`, reference audio must be a
supported audio type (`wav`, `mp3`, `aac`, `ogg`, `flac`, …).
### Response
```json
{
"success": true,
"inference_job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
"all_job_tokens": ["jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"]
}
```
Use `inference_job_token` to poll for the finished video. `all_job_tokens` lists every job created
(batch requests can create more than one).
---
## Examples
The examples reuse these public sample URLs:
- Images (Wikimedia Commons): an Atlantic puffin, the Matterhorn, a toco toucan
- Video: a short sample `mp4`
- Audio: a short sample `wav`
Replace `YOUR_API_KEY` with your `artcraft_api_…` secret, and generate a fresh `idempotency_token`
(UUID) for every request.
---
### Example 1 — Starting-frame Seedance (`start_frame_image_url`)
Animate outward from a single starting image.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900"
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
start_frame_image_url:
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
}),
});
console.log(resp.status, await resp.json());
```
---
### Example 2 — Image-reference Seedance (`reference_image_urls`)
Guide the generation with one or more reference images.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
]
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
reference_image_urls: [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
}),
});
console.log(resp.status, await resp.json());
```
---
### Example 3 — Image + video reference Seedance (`reference_image_urls` + `reference_video_urls`)
Combine reference images with a reference video (and, optionally, reference audio). This is the
fullest URL-driven request.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
],
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"]
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"],
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
reference_image_urls: [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
reference_video_urls: ["https://download.samplelib.com/mp4/sample-5s.mp4"],
reference_audio_urls: ["https://download.samplelib.com/wav/sample-6s.wav"],
}),
});
console.log(resp.status, await resp.json());
```
---
## Checking job status
Generation is asynchronous. The generate call returns an `inference_job_token`; poll the job-status
endpoint until the job reaches a terminal state, then read the result URL.
### Endpoints
- **Single job:** `GET /v1/omni_api/job_status/job/{token}`
- **Batch (many jobs):** `GET /v1/omni_api/job_status/batch?tokens=jinf_aaa&tokens=jinf_bbb`
Both require the same `Authorization: Bearer …` header (no cookies).
### Response shape
```json
{
"success": true,
"state": {
"job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
"request": {
"inference_category": "video_generation",
"maybe_prompt_token": "prompt_xxxxxxxxxxxxxxxxxxxxxxxxx",
"maybe_model_type": "seedance_2p0",
"maybe_model_token": null
},
"status": {
"status": "pending",
"maybe_first_started_at": null,
"maybe_failure_category": null,
"progress_percentage": 0
},
"maybe_result": null,
"created_at": "2026-06-24T04:06:14Z",
"updated_at": "2026-06-24T04:06:14Z"
}
}
```
- `state.status.status` — the job state. Keep polling while it's `pending`, `started`, or
`attempt_failed`. **Terminal** states are `complete_success`, `complete_failure`, `dead`,
`cancelled_by_user`, and `cancelled_by_system`.
- `state.status.progress_percentage` — an integer 0–100.
- `state.maybe_result` — `null` until the job succeeds. On `complete_success` it contains the
finished media; the playable/downloadable URL is **`state.maybe_result.media_links.cdn_url`**.
For video, `media_links.maybe_video_previews` also carries still/animated preview URLs.
The batch endpoint returns `{ "success": true, "job_states": [ … ] }` where each entry is the same
`state` payload as above.
### Where the result video / image lives
When `state.status.status` is `complete_success`, the finished media is under `state.maybe_result`:
- **`state.maybe_result.media_links.cdn_url`** — the primary, playable/downloadable URL of the
finished asset. **This is the field you want.** For a video job it's the `.mp4`; for an image job
it's the image file (`.jpg` / `.png` / etc.).
- `state.maybe_result.entity_token` — the `m_…` media-file token for the result.
- `state.maybe_result.media_links.maybe_video_previews` — **video jobs only**. Contains a `still`
(poster `.jpg`) and an `animated` (`.gif`) preview URL, plus `{WIDTH}` thumbnail templates. This
is `null` for image jobs.
- `state.maybe_result.media_links.maybe_thumbnail_template` — **image jobs only**. A URL template;
replace `{WIDTH}` with a pixel width to fetch a resized thumbnail. `null` for video jobs.
**Example — completed video job:**
```json
{
"success": true,
"state": {
"job_token": "jinf_9fcn2qv4dv3vvbrrxbnmfgen06v",
"request": {
"inference_category": "video_generation",
"maybe_prompt_token": "prompt_q53tp4rh97qh8q159p21tazzt",
"maybe_model_type": "seedance_2p0",
"maybe_model_token": null
},
"status": {
"status": "complete_success",
"maybe_first_started_at": "2026-06-24T04:07:11Z",
"maybe_failure_category": null,
"progress_percentage": 100
},
"maybe_result": {
"entity_type": "media_file",
"entity_token": "m_ss682134tx19yhqs5y0nbnd0bd59a6",
"media_links": {
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4",
"maybe_thumbnail_template": null,
"maybe_video_previews": {
"still": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.jpg",
"animated": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.gif",
"still_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.jpg",
"animated_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.gif"
}
},
"maybe_successfully_completed_at": "2026-06-24T04:17:30Z"
},
"created_at": "2026-06-24T04:06:14Z",
"updated_at": "2026-06-24T04:17:30Z"
}
}
```
→ The video is at
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4`
(example, not a live URL).
**Example — completed image job** (from `POST /v1/omni_api/generate/image`):
```json
{
"maybe_result": {
"entity_type": "media_file",
"entity_token": "m_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6",
"media_links": {
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg",
"maybe_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg-thumb-{WIDTH}.jpg",
"maybe_video_previews": null
},
"maybe_successfully_completed_at": "2026-06-24T04:09:02Z"
}
}
```
→ The image is at
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg`
(example, not a live URL).
### Poll until complete
#### bash
```bash
JOB_TOKEN="jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
while :; do
resp=$(curl -s "http://localhost:12345/v1/omni_api/job_status/job/$JOB_TOKEN" \
-H "Authorization: Bearer YOUR_API_KEY")
status=$(echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['status']['status'])")
echo "status=$status"
case "$status" in
complete_success)
echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['maybe_result']['media_links']['cdn_url'])"
break ;;
complete_failure|dead|cancelled_by_user|cancelled_by_system)
echo "job ended without a result: $status"; break ;;
esac
sleep 5
done
```
#### python
```python
import time
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
TERMINAL_FAILURES = {"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system"}
while True:
resp = requests.get(
f"{BASE_URL}/v1/omni_api/job_status/job/{JOB_TOKEN}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
resp.raise_for_status()
state = resp.json()["state"]
status = state["status"]["status"]
print("status:", status, state["status"]["progress_percentage"], "%")
if status == "complete_success":
print("result URL:", state["maybe_result"]["media_links"]["cdn_url"])
break
if status in TERMINAL_FAILURES:
print("job ended without a result:", status)
break
time.sleep(5)
```
#### javascript
```javascript
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx";
const TERMINAL_FAILURES = new Set([
"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system",
]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
while (true) {
const resp = await fetch(`${BASE_URL}/v1/omni_api/job_status/job/${JOB_TOKEN}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { state } = await resp.json();
const status = state.status.status;
console.log("status:", status, state.status.progress_percentage, "%");
if (status === "complete_success") {
console.log("result URL:", state.maybe_result.media_links.cdn_url);
break;
}
if (TERMINAL_FAILURES.has(status)) {
console.log("job ended without a result:", status);
break;
}
await sleep(5000);
}
```
---
## Notes & troubleshooting
- **Always send a fresh `idempotency_token`** (UUID). Reusing one is rejected as a duplicate.
- **No cookies.** The endpoint authenticates only via the `Authorization` header.
- **URL must be `http(s)`** and reachable by the server; redirects are followed.
- **Type mismatches** (e.g. a `.webm` passed as a reference video, which requires `mp4`) are
rejected with a `400` describing the unpermitted type.
- **`401 Unauthorized`** — missing/invalid API key, or the key's owner is banned.
- **`402 Payment required`** — insufficient credits/balance.
- There is a companion image endpoint, `POST /v1/omni_api/generate/image`, which accepts
`image_urls` (mutually exclusive with `image_media_tokens`) following the same rules.
+203
View File
@@ -0,0 +1,203 @@
# PageDraw — Visual Design & Architecture Review
**Surface:** `@storyteller/ui-pagedraw` image editor + its `artcraft-webapp` integration (`/edit-image`).
**Lens:** Awwwards / Linear-tier "premium app chrome" rubric, adapted for an in-product creative tool.
**Date:** 2026-06-15 · **Branch:** `worktree-pagedraw-webapp`
---
## 0. How to read this
PageDraw is a **professional canvas tool**, not a marketing page. So the elite-design rubric is
applied in its *app-chrome* register — the north star is **Figma / Linear / Photoshop-web**, not an
Awwwards landing page. That means:
- **Applies:** token discipline, concentric radii, hairline + inset-highlight surfaces ("double-bezel"),
island/pill controls, custom-eased micro-motion, z-index hygiene, premium type ramp.
- **Does _not_ apply:** `py-40` macro-whitespace, hero typography, editorial-split layouts, scroll-reveal
entrances. A tool surface is dense and persistent by design; forcing landing-page whitespace would hurt it.
Citations are `file:line` against the current tree.
---
## 1. Verdict
| Axis | Score | One-liner |
|---|---|---|
| Color system | ⚠️ 5/10 | Good token foundation, but hardcoded greys (`#1b1b1b`, `#303030`, `#404040`) leak through a *themeable lib*. |
| Typography | ❌ 3/10 | No type ramp, no font-family ownership; rides whatever the host sets. Banned-by-rubric default stack risk. |
| Radii | ⚠️ 4/10 | Five radius scales in play (`sm`→`2xl`→`full`) with no concentric nesting. |
| Surfaces (border/shadow) | ⚠️ 5/10 | Flat `shadow-lg` everywhere; no inset highlights; no double-bezel. Reads "functional", not "machined". |
| Motion | ❌ 3/10 | Only `transition-colors`/`ease-in-out`, durations 75–200ms; **zero custom easing**, no spring, no entrance choreography. |
| Layout / z-index | ⚠️ 6/10 | Clean fixed-panel composition, but a `z-[9999999]` escape hatch and a 6-tier ad-hoc z-stack. |
| Icons | ✅ 7/10 | FontAwesome Pro (solid + regular) — acceptable, but solid weight is heavier than the rubric's "ultra-light line" ideal. |
| Architecture | ⚠️ 5/10 | Clean adapter boundary; but `PaintSurface.tsx` (2005 LOC), `SceneState.ts` (1539), `PageDraw.tsx` (1292) are oversized, and the lib hardcodes host-theme colors. |
**Bottom line:** the editor is *structurally competent and functional* but visually reads as
"capable internal tool," not "$150k product surface." The single highest-leverage fix is a **shared
design-token layer** (color + radius + motion + type), because today the lib both (a) reaches for
host tokens (`bg-ui-panel`, `text-base-fg`) *and* (b) hardcodes its own greys — so it can never be
fully reskinned by the webapp vs. the desktop app.
---
## 2. What's already good (keep)
- **Token-aware base.** Most surfaces use semantic tokens (`bg-ui-panel`, `bg-ui-controls`,
`text-base-fg`, `border-ui-panel-border`) — the right instinct (SideToolbar, PromptEditor, HistoryStack).
- **Clean platform boundary.** The lib has zero REST/Tauri imports; everything I/O flows through
`PageDrawAdapter` (`adapter.ts`). The webapp adapter (`web-adapter.tsx`) and desktop adapter stay
swappable. This is genuinely good architecture and should be protected.
- **Performance-minded canvas.** Composite pre-baking on idle (`PageDraw.tsx` ~661–710) and worker
encoding (`generatePipeline.worker.ts`) keep the generate click off the main thread. Don't regress this.
- **CORS already handled** for browser canvas export (`crossOrigin="anonymous"` in `Node.ts:134`,
`SceneState.ts:1473`, `imageHelpers.ts:16`) — the web port works because of this.
---
## 3. Findings & recommendations
### 3.1 Color — stop hardcoding greys in a themeable lib ⚠️ P0
**Current:** semantic tokens *and* raw hex coexist:
- `#1b1b1b` pegboard (`pagedraw.css:6`), `#1A1A1A`/`#333333` context menu (`ContextMenu.tsx:44`),
`#303030`/`#404040` dividers + popovers (`SideToolbar.tsx:381,550`), `bg-zinc-700/600` swatch
(`SideToolbar.tsx:173`), plus ad-hoc `bg-blue-600/500` accents (`PageDraw.tsx:157`, base selector).
- Accent color is inconsistent: the lib's primary action uses raw **blue-600**, while the webapp's
design system uses `--primary` / `brand-primary`. So the editor's blue doesn't match the product.
**Why it fails the bar:** a premium component library exposes *one* surface ramp and *one* accent, both
driven by tokens, so it reskins per host. Hardcoded greys mean the desktop and web themes can drift, and
the raw `blue-600` clashes with `brand-primary` used elsewhere in the webapp.
**Do:**
- Replace every raw grey with the existing ramp: `#1b1b1b`→`bg-ui-background`, `#1A1A1A`→`bg-ui-panel`,
`#303030`/`#404040`→`border-ui-panel-border` / `bg-ui-controls`. For the pegboard, add a token
`--st-canvas` so hosts can theme the canvas backdrop.
- Replace `blue-600/500` accents with `bg-primary` / `text-primary` (the token the rest of the webapp uses),
so the Generate/Edit-3D buttons match `brand-primary`.
- Move the react-colorful overrides off pure `rgb(255,255,255)` borders (`pagedraw.css:15,19`) onto
`white/15` hairlines for consistency with the rest of the chrome.
### 3.2 Typography — own a type ramp ❌ P0
**Current:** no `font-family` anywhere in the lib (grep: 0 hits); only `text-xs/sm/base/lg/xl` +
`font-medium/semibold/bold`. The editor inherits whatever the host's body font is.
**Why it fails the bar:** the rubric explicitly bans the default web stack (Inter/Roboto/Arial/Helvetica).
Inheriting an unspecified stack means the tool can render in a banned font with no recourse, and there's
no deliberate hierarchy (numerics in sliders, labels, modal titles all share one undifferentiated ramp).
**Do:**
- Adopt the product's display/grotesk token (e.g. a `font-sans` mapped to Geist / Plus Jakarta Sans at
the app level) and reference it in the lib's root container, not per-component.
- Define a small semantic ramp: `text-[11px] tracking-[0.16em] uppercase` for toolbar/section labels,
`text-sm` for controls, `text-base font-semibold` for modal titles. Apply `tabular-nums` to slider
read-outs (`SliderWithIndicator.tsx`) and dimension fields (`BlankCanvasModal`) so numbers don't jitter.
### 3.3 Radii — collapse to a concentric scale ⚠️ P1
**Current:** `rounded-sm / md / lg / xl / 2xl / full` all appear, with no parent→child relationship
(e.g. `rounded-2xl` card at `PageDraw.tsx:1123`, `rounded-xl` panels in SideToolbar, `rounded-lg`
buttons in InpaintToolBar).
**Do:** pick a 3-step system — **`rounded-2xl` shells, `rounded-xl` inner cards, `rounded-lg` controls,
`rounded-full` pills** — and apply concentrically (the "double-bezel" curve math: inner radius =
outer − padding). Drop `rounded-sm`/`rounded-md` from the editor entirely.
### 3.4 Surfaces — add the "machined" double-bezel ⚠️ P1
**Current:** panels are a single `bg-ui-panel` + `border` + `shadow-lg`. No nested enclosure, no inset
top-highlight. Reads flat.
**Do (for the four floating panels — SideToolbar, HistoryStack, PromptEditor, InpaintToolBar):**
- Outer shell: `bg-ui-panel/80 ring-1 ring-white/10 backdrop-blur-xl p-1.5 rounded-2xl`.
- Inner core: own bg + `shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] rounded-xl`.
- This is the single change that most moves the surface from "tool" to "premium hardware." The panels are
already `fixed` (`PageDraw.tsx:1144,1175,1195`), so `backdrop-blur` here is GPU-safe per the perf rules.
### 3.5 Motion — replace linear/ease with spring-eased micro-motion ❌ P0 (cheap, high impact)
**Current:** `transition-colors`/`transition-all` with `ease-in-out` and `duration-75/150/200`
(`SideToolbar.tsx:381,550`, `SliderWithIndicator.tsx:65`, `ContextMenu.tsx:53`). No custom easing,
no entrance/exit on popovers, color pickers, or the context menu (they pop instantly).
**Why it fails the bar:** instant state changes and `ease-in-out` are explicitly banned. A pro tool still
deserves *interpolated* motion — it's what separates Figma from a jQuery plugin.
**Do:**
- Standardize on one curve: `transition-[transform,opacity,background-color] duration-300
ease-[cubic-bezier(0.32,0.72,0,1)]`.
- Tool buttons: `active:scale-[0.97]` press physics; selected tool gets a spring-eased indicator slide
rather than a hard background swap.
- Popovers / color picker / context menu: animate in with `opacity-0 translate-y-1 scale-[0.98]` →
resolved, ~180ms. Transform+opacity only (perf rule).
- History tiles: stagger their `opacity` reveal when a generation lands (they already use
`transition-opacity`, just add per-index delay).
### 3.6 Layout & z-index — tame the stack ⚠️ P1
**Current:** clean fixed-panel composition, but z-index is ad-hoc across 6 tiers and includes a
`z-[9999999]` on the context menu (`ContextMenu.tsx:44`) and a one-off `z-[60]` (`PageDraw.tsx:237`).
**Do:** define a named scale in one place — `canvas:0, chrome:10, floating-controls:20, overlay-button:40,
menu:50` — and replace the magic numbers. The portal menu only needs to clear the chrome, not `9999999`.
### 3.7 Responsive — declare intent ⚠️ P2
**Current:** essentially desktop-only; fixed sidebars assume a wide viewport; only LightboxDetails has
`sm:` variants. The webapp wrapper scopes the lib correctly (`pagedraw.tsx` `translateZ(0)` containing
block) but there's no mobile story.
**Do:** mirror the Edit-3D page's explicit **mobile gate** (`pagescene.tsx` `MobileGate`) on `/edit-image`
rather than letting fixed toolbars overlap on touch. A precision canvas tool is legitimately desktop-first;
just say so deliberately instead of degrading silently.
---
## 4. Architecture notes
- **Oversized modules.** `PaintSurface.tsx` (2005 LOC), `SceneState.ts` (1539), `PageDraw.tsx` (1292).
These are review/maintenance hotspots. Suggest extracting from `SceneState.ts`: history/undo slice,
base-image-load slice, and generation/pending slice into composable Zustand slices. `PageDraw.tsx`'s
generate-flow (cases 1–3, ~780–868) could move to a `useGenerate()` hook.
- **Theme ownership.** Per §3.1, the lib should *only* consume tokens. Today it mixes tokens + hex, which
defeats the adapter pattern's promise (one component, many themed hosts).
- **Adapter surface is healthy.** `PageDrawAdapter` is small and intentional; the web vs. Tauri split is
clean. The webapp's `enqueueInpaint` stub and 402→credits-modal handling are explicit and documented.
- **Icon weight.** FontAwesome **solid** is the heaviest option; if a line/light set is available
(Phosphor/Remix or FA light), the toolbar would read more refined. Low priority.
---
## 5. Prioritized roadmap
| P | Item | Effort | Payoff |
|---|---|---|---|
| **P0** | Token sweep: kill hardcoded greys; unify accent on `--primary` (§3.1) | M | Highest — enables true theming + product-consistent accent |
| **P0** | Own a font-family + semantic type ramp at the lib root (§3.2) | S | High — removes banned-font risk, adds hierarchy |
| **P0** | One custom easing curve + press/popover micro-motion (§3.5) | S | High — cheapest "feels expensive" win |
| **P1** | Double-bezel + inset highlights on the 4 floating panels (§3.4) | M | High — "machined" surface upgrade |
| **P1** | Concentric 3-step radius system (§3.3) | S | Medium — visual coherence |
| **P1** | Named z-index scale; remove `z-[9999999]` (§3.6) | S | Medium — maintainability |
| **P2** | Explicit mobile gate on `/edit-image` (§3.7) | S | Medium — correctness on touch |
| **P2** | Split `SceneState.ts` / `PaintSurface.tsx` into slices (§4) | L | Medium — maintainability |
---
## 6. Appendix — proposed token additions
```css
/* canvas + chrome surface ramp (lib should consume only these) */
--st-canvas: <theme>; /* replaces #1b1b1b pegboard */
--st-accent: var(--primary); /* replaces blue-600/500 */
--st-hairline: rgb(255 255 255 / 0.10);
--st-inset-hi: inset 0 1px 0 rgb(255 255 255 / 0.08);
/* motion */
--st-ease: cubic-bezier(0.32, 0.72, 0, 1);
--st-dur-fast: 180ms;
--st-dur: 300ms;
/* radius scale (concentric) */
--st-r-shell: 1.25rem; /* rounded-2xl */
--st-r-card: 0.875rem; /* rounded-xl */
--st-r-control: 0.5rem; /* rounded-lg */
```
> Scope note: every recommendation here is **styling/architecture only** — none change the generate,
> polling, upload, bg-removal, or adapter contracts validated during the functional debug pass.
+2
View File
@@ -138,6 +138,7 @@ RUN SQLX_OFFLINE=true \
$HOME/.cargo/bin/cargo build \
--release \
--bin storyteller-web \
--bin user-spend-analytics-job \
--bin seedance2-pro-job \
--bin worldlabs-job \
--bin video-thumbnail-job \
@@ -190,6 +191,7 @@ COPY --from=builder /tmp/target/release/analytics-job /
COPY --from=builder /tmp/target/release/email-sender-job /
COPY --from=builder /tmp/target/release/es-update-job /
COPY --from=builder /tmp/target/release/grok-api-job /
COPY --from=builder /tmp/target/release/user-spend-analytics-job /
# Legacy apps:
# COPY --from=builder /tmp/target/release/tts-download-job /
@@ -0,0 +1,42 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::api_keys::ApiKeyToken;
use tokens::tokens::users::UserToken;
/// Canonical wire shape for an API key, used by the list rows and the
/// single-key GET.
///
/// NB: the full secret `api_key` is NEVER included here — only
/// `truncated_api_key` (its first 20 characters), enough to disambiguate keys
/// in a UI. The full value is returned exactly once, at creation time.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct ApiKeyInfo {
pub token: ApiKeyToken,
/// First 20 characters of the API key. The full secret is never returned
/// after creation.
pub truncated_api_key: String,
pub name: String,
pub maybe_description: Option<String>,
pub owner_user_token: UserToken,
pub ip_address_creation: String,
pub ip_address_update: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
/// Soft-delete timestamp. `None` for live keys.
pub maybe_deleted_at: Option<DateTime<Utc>>,
}
/// Path info for endpoints addressed by the API key's `token` (NOT the secret
/// `api_key` value): `GET` / `DELETE` / `PUT` `/v1/api_keys/{api_key_token}`.
#[derive(Deserialize, ToSchema)]
pub struct ApiKeyPathInfo {
pub api_key_token: ApiKeyToken,
}
@@ -0,0 +1,28 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::api_keys::ApiKeyToken;
// ── POST /v1/api_keys/create ──
#[derive(Deserialize, ToSchema)]
pub struct CreateApiKeyRequest {
/// Display name (title) for the key.
pub name: String,
/// Optional description.
pub maybe_description: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct CreateApiKeySuccessResponse {
pub success: bool,
/// The key's `token` — used for all subsequent management
/// (get / update / delete). Safe to store and display.
pub api_key_token: ApiKeyToken,
/// The full secret API key value. This is the ONLY time it is ever
/// returned — the caller must store it now, as it cannot be retrieved again.
pub api_key: String,
}
@@ -0,0 +1,9 @@
use serde_derive::Serialize;
use utoipa::ToSchema;
// ── DELETE /v1/api_keys/{api_key_token} ──
#[derive(Serialize, ToSchema)]
pub struct DeleteApiKeySuccessResponse {
pub success: bool,
}
@@ -0,0 +1,12 @@
use serde_derive::Serialize;
use utoipa::ToSchema;
use crate::api_keys::common::ApiKeyInfo;
// ── GET /v1/api_keys/{api_key_token} ──
#[derive(Serialize, ToSchema)]
pub struct GetApiKeySuccessResponse {
pub success: bool,
pub api_key: ApiKeyInfo,
}
@@ -0,0 +1,24 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use crate::api_keys::common::ApiKeyInfo;
// ── GET /v1/api_keys/list ──
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListApiKeysQueryParams {
/// Page size. Defaults server-side if omitted.
pub limit: Option<u32>,
/// Number of rows to skip. Defaults to 0.
pub offset: Option<u32>,
}
#[derive(Serialize, ToSchema)]
pub struct ListApiKeysSuccessResponse {
pub success: bool,
/// The user's API keys (including soft-deleted ones), newest first. Each
/// entry's key value is truncated — the full secret is never listed.
pub api_keys: Vec<ApiKeyInfo>,
}
@@ -0,0 +1,6 @@
pub mod common;
pub mod create_api_key;
pub mod delete_api_key;
pub mod get_api_key;
pub mod list_api_keys;
pub mod update_api_key;
@@ -0,0 +1,15 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
// ── PUT /v1/api_keys/{api_key_token} ──
#[derive(Deserialize, ToSchema)]
pub struct UpdateApiKeyRequest {
/// New description for the key. `None` clears it.
pub maybe_description: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct UpdateApiKeySuccessResponse {
pub success: bool,
}

Some files were not shown because too many files have changed in this diff Show More