@vfrey/vfrey 0.0.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +33 -0
- package/README.md +78 -3
- package/accounts.example.json +61 -0
- package/accounts.first-run.json +41 -0
- package/bin/vfrey.mjs +36 -0
- package/integration-tools/README.md +43 -0
- package/integration-tools/image/pillow.md +32 -0
- package/integration-tools/image/playwright.md +33 -0
- package/integration-tools/image/resvg.md +36 -0
- package/integration-tools/image/satori.md +36 -0
- package/integration-tools/image/sharp.md +38 -0
- package/integration-tools/video/blender.md +34 -0
- package/integration-tools/video/ffmpeg.md +41 -0
- package/integration-tools/video/manim.md +37 -0
- package/integration-tools/video/motion-canvas.md +37 -0
- package/integration-tools/video/remotion.md +50 -0
- package/integration-tools/video/revideo.md +32 -0
- package/package.json +74 -6
- package/skills/vfrey-orchestrator/SKILL.md +15 -0
- package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
- package/skills/vfrey-product/SKILL.md +63 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
- package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
- package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
- package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
- package/skills/vfrey-product/push/SKILL.md +15 -0
- package/skills/vfrey-product/scenes/NOTICE +5 -0
- package/skills/vfrey-product/scenes/blocks.md +57 -0
- package/skills/vfrey-product/scenes/craft.md +118 -0
- package/skills/vfrey-product/scenes/motion.md +88 -0
- package/src/analytics/dashboard.ts +109 -0
- package/src/analytics/index.ts +260 -0
- package/src/api/client.ts +361 -0
- package/src/api/cloud-files.ts +197 -0
- package/src/api/cloud-runs.ts +75 -0
- package/src/api/cloud.ts +413 -0
- package/src/api/commands.ts +330 -0
- package/src/api/config.ts +34 -0
- package/src/api/creators.ts +463 -0
- package/src/api/credentials.ts +152 -0
- package/src/api/mail.ts +333 -0
- package/src/api/topics.ts +207 -0
- package/src/api/types.ts +273 -0
- package/src/cli.ts +468 -0
- package/src/cloud/bundle.ts +70 -0
- package/src/cloud/cloudrun-executor.ts +237 -0
- package/src/cloud/executor.ts +281 -0
- package/src/cloud/gcs-sign.ts +76 -0
- package/src/cloud/job.ts +106 -0
- package/src/cloud/remote-job.ts +67 -0
- package/src/cloud/render-job.ts +191 -0
- package/src/config.ts +309 -0
- package/src/dashboard-link.ts +180 -0
- package/src/dashboard-start.ts +103 -0
- package/src/desk/channel.ts +64 -0
- package/src/desk/cli.ts +216 -0
- package/src/desk/creators-route.ts +129 -0
- package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
- package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
- package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
- package/src/desk/http.ts +460 -0
- package/src/desk/mail-route.ts +103 -0
- package/src/desk/overlay.ts +112 -0
- package/src/desk/preview.ts +422 -0
- package/src/desk/screens.ts +149 -0
- package/src/desk/secrets.ts +143 -0
- package/src/desk/series.ts +127 -0
- package/src/desk/state.ts +77 -0
- package/src/desk/studio-route.ts +119 -0
- package/src/desk/tick-runner.ts +66 -0
- package/src/desk/tick.ts +56 -0
- package/src/desk/topics-route.ts +41 -0
- package/src/desk/ui/app.css +505 -0
- package/src/desk/ui/app.js +185 -0
- package/src/desk/ui/apple-icon.png +0 -0
- package/src/desk/ui/components.js +79 -0
- package/src/desk/ui/core.js +156 -0
- package/src/desk/ui/favicon.ico +0 -0
- package/src/desk/ui/icon.svg +1 -0
- package/src/desk/ui/icons.js +49 -0
- package/src/desk/ui/index.html +41 -0
- package/src/desk/ui/metrics-math.d.ts +22 -0
- package/src/desk/ui/metrics-math.js +135 -0
- package/src/desk/ui/theme.js +5 -0
- package/src/desk/ui/views-creators.js +209 -0
- package/src/desk/ui/views-keys.js +114 -0
- package/src/desk/ui/views-mail.js +188 -0
- package/src/desk/ui/views-misc.js +123 -0
- package/src/desk/ui/views-review.js +119 -0
- package/src/desk/ui/views-runs.js +350 -0
- package/src/desk/ui/views-screens.js +163 -0
- package/src/desk/ui/views-series.js +318 -0
- package/src/desk/ui/views-topics.js +87 -0
- package/src/desk/verbs.ts +361 -0
- package/src/doctor.ts +212 -0
- package/src/drain.ts +14 -0
- package/src/engage/index.ts +217 -0
- package/src/generate/index.ts +110 -0
- package/src/generate/script.ts +152 -0
- package/src/generate/title.ts +78 -0
- package/src/help.ts +252 -0
- package/src/home.ts +45 -0
- package/src/llm/client.ts +177 -0
- package/src/log.ts +7 -0
- package/src/oauth/tokens.ts +189 -0
- package/src/oauth/vault.ts +72 -0
- package/src/pipeline/review.ts +642 -0
- package/src/pipeline/run.ts +443 -0
- package/src/pipeline/series-guard.ts +26 -0
- package/src/pipeline/source-fetch.ts +69 -0
- package/src/pipeline/stages.ts +728 -0
- package/src/pipeline/topics-source.ts +90 -0
- package/src/product/commands.ts +296 -0
- package/src/product/config-file.ts +38 -0
- package/src/product/engine-choice.ts +89 -0
- package/src/product/engines.ts +103 -0
- package/src/product/insights.ts +50 -0
- package/src/product/keys.ts +67 -0
- package/src/product/paths.ts +18 -0
- package/src/product/probes.ts +88 -0
- package/src/product/push-flow.ts +90 -0
- package/src/product/render-video.ts +21 -0
- package/src/product/short.ts +62 -0
- package/src/product/update.ts +32 -0
- package/src/project/canonical.ts +14 -0
- package/src/project/commands.ts +177 -0
- package/src/project/dashboard-status.ts +28 -0
- package/src/project/discover.ts +19 -0
- package/src/project/index-file.ts +73 -0
- package/src/project/marker.ts +92 -0
- package/src/project/migrate.ts +201 -0
- package/src/project/names.ts +27 -0
- package/src/project/notice.ts +16 -0
- package/src/project/open.ts +88 -0
- package/src/project/scaffold.ts +112 -0
- package/src/project/templates.ts +42 -0
- package/src/project/videos.ts +64 -0
- package/src/publish/facebook.ts +91 -0
- package/src/publish/index.ts +12 -0
- package/src/publish/instagram.ts +97 -0
- package/src/publish/rate.ts +62 -0
- package/src/publish/session.ts +242 -0
- package/src/publish/tiktok.ts +202 -0
- package/src/publish/youtube.ts +177 -0
- package/src/queue/desk-ops.ts +1170 -0
- package/src/queue/seal.ts +3 -0
- package/src/queue/store.ts +1006 -0
- package/src/remotion/AiLabel.tsx +26 -0
- package/src/remotion/Root.tsx +73 -0
- package/src/remotion/defaults.ts +26 -0
- package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
- package/src/remotion/films/engine/EngineMark.tsx +17 -0
- package/src/remotion/films/rea/ReaShort.tsx +552 -0
- package/src/remotion/films/rea/drawings.tsx +907 -0
- package/src/remotion/films/rea/timing.ts +144 -0
- package/src/remotion/index.ts +4 -0
- package/src/remotion/templates/EditorialSplit.tsx +39 -0
- package/src/remotion/templates/KineticType.tsx +39 -0
- package/src/remotion/templates/SignalBoard.tsx +50 -0
- package/src/remotion/timing.ts +22 -0
- package/src/render/chrome.ts +78 -0
- package/src/render/engine-mark.ts +29 -0
- package/src/render/framey-template.ts +130 -0
- package/src/render/html-engine.ts +149 -0
- package/src/render/node-crypto-stub.ts +11 -0
- package/src/render/probe.ts +29 -0
- package/src/render/props.ts +17 -0
- package/src/render/vertical-short.ts +35 -0
- package/src/render/webpack.ts +33 -0
- package/src/render/worker.ts +65 -0
- package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
- package/src/scene/assets/fonts/titan-one.woff2 +0 -0
- package/src/scene/assets/gsap/LICENSE.txt +8 -0
- package/src/scene/assets/gsap/gsap.min.js +11 -0
- package/src/scene/audio.ts +54 -0
- package/src/scene/blocks/LICENSE +190 -0
- package/src/scene/blocks/NOTICE +5 -0
- package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
- package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
- package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
- package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
- package/src/scene/blocks/code-diff/code-diff.html +817 -0
- package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
- package/src/scene/blocks/code-typing/code-typing.html +764 -0
- package/src/scene/blocks/data-chart/data-chart.html +452 -0
- package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
- package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
- package/src/scene/blocks/index.json +824 -0
- package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
- package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
- package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
- package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
- package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
- package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
- package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
- package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
- package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
- package/src/scene/blocks/mk-background/mk-background.html +236 -0
- package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
- package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
- package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
- package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
- package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
- package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
- package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
- package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
- package/src/scene/blocks/snow/snow.html +205 -0
- package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
- package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
- package/src/scene/blocks/starfield/starfield.html +188 -0
- package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
- package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
- package/src/scene/blocks/x-post/x-post.html +437 -0
- package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
- package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
- package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
- package/src/scene/blocks.ts +229 -0
- package/src/scene/command.ts +272 -0
- package/src/scene/figure.ts +181 -0
- package/src/scene/glyphs.ts +107 -0
- package/src/scene/init.ts +86 -0
- package/src/scene/inspect/css-tree.d.ts +6 -0
- package/src/scene/inspect/page-scripts.ts +272 -0
- package/src/scene/inspect/report.ts +405 -0
- package/src/scene/inspect/run.ts +200 -0
- package/src/scene/inspect/source.ts +236 -0
- package/src/scene/label.ts +131 -0
- package/src/scene/net-guard.ts +74 -0
- package/src/scene/render.ts +292 -0
- package/src/scene/runtime/browser.ts +199 -0
- package/src/scene/runtime/core.ts +181 -0
- package/src/scene/runtime/react.tsx +115 -0
- package/src/scene/runtime/transitions.ts +133 -0
- package/src/scene/sanitize.ts +181 -0
- package/src/scene/scene.ts +152 -0
- package/src/scene/studio-command.ts +48 -0
- package/src/scene/studio-skin.ts +59 -0
- package/src/scene/studio.ts +337 -0
- package/src/scene/supply.ts +111 -0
- package/src/scene/timing.ts +288 -0
- package/src/scene/transitions/LICENSE +190 -0
- package/src/scene/transitions/NOTICE +5 -0
- package/src/scene/transitions/SOURCE.json +24 -0
- package/src/scene/transitions/transitions.global.js +38 -0
- package/src/scene/transitions.ts +26 -0
- package/src/service/all-projects.ts +153 -0
- package/src/service/commands.ts +256 -0
- package/src/service/due.ts +82 -0
- package/src/service/lock.ts +101 -0
- package/src/service/log-file.ts +56 -0
- package/src/service/state.ts +41 -0
- package/src/service/units.ts +90 -0
- package/src/service/worker.ts +51 -0
- package/src/skill-agents.ts +74 -0
- package/src/skill-install.ts +334 -0
- package/src/sync-write.ts +21 -0
- package/src/tick-report.ts +205 -0
- package/src/tts.ts +124 -0
- package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
- package/vendor/@vfrey/contracts/dist/account.js +27 -0
- package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
- package/vendor/@vfrey/contracts/dist/checks.js +1 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
- package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
- package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
- package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/events.js +1 -0
- package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
- package/vendor/@vfrey/contracts/dist/http.js +37 -0
- package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
- package/vendor/@vfrey/contracts/dist/index.js +22 -0
- package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
- package/vendor/@vfrey/contracts/dist/log.js +10 -0
- package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
- package/vendor/@vfrey/contracts/dist/mail.js +1 -0
- package/vendor/@vfrey/contracts/package.json +18 -0
- package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
- package/vendor/@vfrey/ingest/dist/index.js +221 -0
- package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
- package/vendor/@vfrey/ingest/dist/parse.js +165 -0
- package/vendor/@vfrey/ingest/package.json +21 -0
- package/vendor/framey/LICENSE +190 -0
- package/vendor/framey/NOTICE +15 -0
- package/vendor/framey/SOURCE.json +24 -0
- package/vendor/framey/STUDIO-SOURCE.json +9 -0
- package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
- package/vendor/framey/bin/framey.mjs +16 -0
- package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
- package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
- package/vendor/framey/dist/api.d.ts +211 -0
- package/vendor/framey/dist/api.js +76 -0
- package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
- package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
- package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
- package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
- package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
- package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
- package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
- package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
- package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
- package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
- package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
- package/vendor/framey/dist/check-F5N544ML.js +111 -0
- package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
- package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
- package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
- package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
- package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
- package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
- package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
- package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
- package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
- package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
- package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
- package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
- package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
- package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
- package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
- package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
- package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
- package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
- package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
- package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
- package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
- package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
- package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
- package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
- package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
- package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
- package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
- package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
- package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
- package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
- package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
- package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
- package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
- package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
- package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
- package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
- package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
- package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
- package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
- package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
- package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
- package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
- package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
- package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
- package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
- package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
- package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
- package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
- package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
- package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
- package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
- package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
- package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
- package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
- package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
- package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
- package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
- package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
- package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
- package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
- package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
- package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
- package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
- package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
- package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
- package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
- package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
- package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
- package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
- package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
- package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
- package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
- package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
- package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
- package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
- package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
- package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
- package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
- package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
- package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
- package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
- package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
- package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
- package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
- package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
- package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
- package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
- package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
- package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
- package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
- package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
- package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
- package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
- package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
- package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
- package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
- package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
- package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
- package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
- package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
- package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
- package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
- package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
- package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
- package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
- package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
- package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
- package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
- package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
- package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
- package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
- package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
- package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
- package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
- package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
- package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
- package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
- package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
- package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
- package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
- package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
- package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
- package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
- package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
- package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
- package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
- package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
- package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
- package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
- package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
- package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
- package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
- package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
- package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
- package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
- package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
- package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
- package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
- package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
- package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
- package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
- package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
- package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
- package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
- package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
- package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
- package/vendor/framey/dist/cli.js +439 -0
- package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
- package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
- package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
- package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
- package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
- package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
- package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
- package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
- package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
- package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
- package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
- package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
- package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
- package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
- package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
- package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
- package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
- package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
- package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
- package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
- package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
- package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
- package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
- package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
- package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
- package/vendor/framey/dist/fontLocalizeCli.js +142 -0
- package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
- package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
- package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
- package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
- package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
- package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
- package/vendor/framey/dist/hyperframe.manifest.json +22 -0
- package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
- package/vendor/framey/dist/hyperframes-player.global.js +478 -0
- package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
- package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
- package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
- package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
- package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
- package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
- package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
- package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
- package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
- package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
- package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
- package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
- package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
- package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
- package/vendor/framey/dist/models-K363FYM4.js +195 -0
- package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
- package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
- package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
- package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
- package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
- package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
- package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
- package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
- package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
- package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
- package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
- package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
- package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
- package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
- package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
- package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
- package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
- package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
- package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
- package/vendor/framey/dist/renderSetupWorker.js +120 -0
- package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
- package/vendor/framey/dist/runtimeVersion.js +18 -0
- package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
- package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
- package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
- package/vendor/framey/dist/sherpaWorker.js +84 -0
- package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
- package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
- package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
- package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
- package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
- package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
- package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
- package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
- package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
- package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
- package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
- package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
- package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
- package/vendor/framey/dist/studio/favicon.svg +14 -0
- package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
- package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
- package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
- package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
- package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
- package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
- package/vendor/framey/dist/studio/index.html +23 -0
- package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
- package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
- package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
- package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
- package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
- package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
- package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
- package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
- package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
- package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
- package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
- package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
- package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
- package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
- package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
- package/vendor/framey/dist/warmCheck.js +220 -0
- package/vendor/framey/dist/warmCheckClient.js +139 -0
- package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
- package/vendor/framey-player/LICENSE +190 -0
- package/vendor/framey-player/NOTICE +16 -0
- package/vendor/framey-player/SOURCE.json +6 -0
- package/vendor/framey-player/hyperframes-player.global.js +478 -0
- package/vendor/framey.pin +1 -0
- package/bin/index.js +0 -3
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import type { JobKind, JobStage, JobStatus as ContractStatus, PipelineData } from "@vfrey/contracts";
|
|
7
|
+
import * as desk from "./desk-ops.js";
|
|
8
|
+
export type { DeskResult, JobEventRow, SeriesInput } from "./desk-ops.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The contract's statuses plus `awaiting_review` (spec 015 D): a job whose series has review on stops there after the
|
|
12
|
+
* script, scene and pre-render checks. It holds no lease; Approve makes it `pending` again, Reject ends it `rejected`
|
|
13
|
+
* with the code `review_rejected`.
|
|
14
|
+
*/
|
|
15
|
+
export type JobStatus = ContractStatus | "awaiting_review";
|
|
16
|
+
|
|
17
|
+
export interface JobRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
accountId: string;
|
|
20
|
+
kind: JobKind;
|
|
21
|
+
stage: JobStage;
|
|
22
|
+
status: JobStatus;
|
|
23
|
+
payload: PipelineData;
|
|
24
|
+
attempt: number;
|
|
25
|
+
maxAttempts: number;
|
|
26
|
+
leaseUntil: string | null;
|
|
27
|
+
/** Names the claim that holds this job. A write from a worker whose claim was cancelled or taken over is refused. */
|
|
28
|
+
leaseToken?: string | null;
|
|
29
|
+
runAfter: string;
|
|
30
|
+
lastError: string | null;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SCHEMA = `
|
|
36
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
account_id TEXT NOT NULL,
|
|
39
|
+
kind TEXT NOT NULL,
|
|
40
|
+
stage TEXT NOT NULL,
|
|
41
|
+
status TEXT NOT NULL,
|
|
42
|
+
payload TEXT NOT NULL,
|
|
43
|
+
attempt INTEGER NOT NULL,
|
|
44
|
+
max_attempts INTEGER NOT NULL,
|
|
45
|
+
lease_until TEXT,
|
|
46
|
+
run_after TEXT NOT NULL,
|
|
47
|
+
last_error TEXT,
|
|
48
|
+
created_at TEXT NOT NULL,
|
|
49
|
+
updated_at TEXT NOT NULL
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS jobs_claim ON jobs (status, run_after);
|
|
52
|
+
CREATE TABLE IF NOT EXISTS source_seen (
|
|
53
|
+
account_id TEXT NOT NULL,
|
|
54
|
+
fingerprint TEXT NOT NULL,
|
|
55
|
+
first_seen TEXT NOT NULL,
|
|
56
|
+
PRIMARY KEY (account_id, fingerprint)
|
|
57
|
+
);
|
|
58
|
+
CREATE TABLE IF NOT EXISTS publications (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
job_id TEXT NOT NULL,
|
|
61
|
+
account_id TEXT NOT NULL,
|
|
62
|
+
platform TEXT NOT NULL,
|
|
63
|
+
external_id TEXT,
|
|
64
|
+
disclosure TEXT NOT NULL,
|
|
65
|
+
variation TEXT NOT NULL,
|
|
66
|
+
hook TEXT NOT NULL,
|
|
67
|
+
title TEXT NOT NULL,
|
|
68
|
+
hashtags TEXT NOT NULL,
|
|
69
|
+
script TEXT NOT NULL,
|
|
70
|
+
published_at TEXT NOT NULL,
|
|
71
|
+
audit TEXT NOT NULL
|
|
72
|
+
);
|
|
73
|
+
CREATE TABLE IF NOT EXISTS comments_handled (
|
|
74
|
+
platform TEXT NOT NULL,
|
|
75
|
+
comment_id TEXT NOT NULL,
|
|
76
|
+
account_id TEXT NOT NULL,
|
|
77
|
+
action TEXT NOT NULL,
|
|
78
|
+
tone TEXT,
|
|
79
|
+
reply_text TEXT,
|
|
80
|
+
created_at TEXT NOT NULL,
|
|
81
|
+
PRIMARY KEY (platform, comment_id)
|
|
82
|
+
);
|
|
83
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
account_id TEXT NOT NULL,
|
|
86
|
+
platform TEXT NOT NULL,
|
|
87
|
+
external_id TEXT NOT NULL,
|
|
88
|
+
captured_at TEXT NOT NULL,
|
|
89
|
+
views INTEGER,
|
|
90
|
+
watch_time_sec REAL,
|
|
91
|
+
retention REAL,
|
|
92
|
+
estimated_revenue REAL,
|
|
93
|
+
revenue_status TEXT NOT NULL,
|
|
94
|
+
raw TEXT NOT NULL
|
|
95
|
+
);
|
|
96
|
+
CREATE TABLE IF NOT EXISTS rate_events (
|
|
97
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
bucket TEXT NOT NULL,
|
|
99
|
+
at TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS rate_bucket ON rate_events (bucket, at);
|
|
102
|
+
CREATE TABLE IF NOT EXISTS account_health (
|
|
103
|
+
account_id TEXT NOT NULL,
|
|
104
|
+
platform TEXT NOT NULL,
|
|
105
|
+
token_state TEXT NOT NULL,
|
|
106
|
+
last_error TEXT,
|
|
107
|
+
updated_at TEXT NOT NULL,
|
|
108
|
+
PRIMARY KEY (account_id, platform)
|
|
109
|
+
);
|
|
110
|
+
CREATE TABLE IF NOT EXISTS desk_accounts (
|
|
111
|
+
id TEXT PRIMARY KEY,
|
|
112
|
+
name TEXT NOT NULL,
|
|
113
|
+
owner_id TEXT,
|
|
114
|
+
paused INTEGER NOT NULL DEFAULT 0
|
|
115
|
+
);
|
|
116
|
+
CREATE TABLE IF NOT EXISTS desk_secrets (
|
|
117
|
+
owner_kind TEXT NOT NULL,
|
|
118
|
+
owner_id TEXT NOT NULL,
|
|
119
|
+
field TEXT NOT NULL,
|
|
120
|
+
ciphertext TEXT NOT NULL,
|
|
121
|
+
PRIMARY KEY (owner_kind, owner_id, field)
|
|
122
|
+
);
|
|
123
|
+
CREATE TABLE IF NOT EXISTS desk_series (
|
|
124
|
+
id TEXT PRIMARY KEY,
|
|
125
|
+
account_id TEXT NOT NULL,
|
|
126
|
+
niche TEXT NOT NULL,
|
|
127
|
+
language TEXT NOT NULL,
|
|
128
|
+
autopilot INTEGER NOT NULL,
|
|
129
|
+
timezone TEXT NOT NULL,
|
|
130
|
+
min_hours REAL NOT NULL,
|
|
131
|
+
max_posts INTEGER NOT NULL,
|
|
132
|
+
windows_json TEXT NOT NULL,
|
|
133
|
+
voices_json TEXT NOT NULL,
|
|
134
|
+
hashtags_json TEXT NOT NULL,
|
|
135
|
+
engage_enabled INTEGER NOT NULL,
|
|
136
|
+
allow_spicy INTEGER NOT NULL,
|
|
137
|
+
max_replies INTEGER NOT NULL,
|
|
138
|
+
min_seconds INTEGER NOT NULL,
|
|
139
|
+
upload_cap INTEGER NOT NULL,
|
|
140
|
+
style_path TEXT NOT NULL,
|
|
141
|
+
paused INTEGER NOT NULL DEFAULT 0
|
|
142
|
+
);
|
|
143
|
+
CREATE TABLE IF NOT EXISTS desk_platforms (
|
|
144
|
+
series_id TEXT NOT NULL,
|
|
145
|
+
platform TEXT NOT NULL,
|
|
146
|
+
enabled INTEGER NOT NULL,
|
|
147
|
+
config_json TEXT NOT NULL,
|
|
148
|
+
PRIMARY KEY (series_id, platform)
|
|
149
|
+
);
|
|
150
|
+
CREATE TABLE IF NOT EXISTS desk_sources (
|
|
151
|
+
id TEXT PRIMARY KEY,
|
|
152
|
+
series_id TEXT NOT NULL,
|
|
153
|
+
kind TEXT NOT NULL,
|
|
154
|
+
value TEXT NOT NULL,
|
|
155
|
+
last_status TEXT
|
|
156
|
+
);
|
|
157
|
+
CREATE TABLE IF NOT EXISTS desk_settings (
|
|
158
|
+
key TEXT PRIMARY KEY,
|
|
159
|
+
value TEXT NOT NULL
|
|
160
|
+
);
|
|
161
|
+
CREATE TABLE IF NOT EXISTS job_events (
|
|
162
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
163
|
+
job_id TEXT NOT NULL,
|
|
164
|
+
at TEXT NOT NULL,
|
|
165
|
+
stage TEXT NOT NULL,
|
|
166
|
+
status TEXT NOT NULL,
|
|
167
|
+
code TEXT,
|
|
168
|
+
message TEXT,
|
|
169
|
+
duration_ms INTEGER
|
|
170
|
+
);
|
|
171
|
+
CREATE TABLE IF NOT EXISTS series_events (
|
|
172
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
173
|
+
series_id TEXT NOT NULL,
|
|
174
|
+
at TEXT NOT NULL,
|
|
175
|
+
code TEXT NOT NULL,
|
|
176
|
+
message TEXT NOT NULL
|
|
177
|
+
);
|
|
178
|
+
CREATE TABLE IF NOT EXISTS desk_warnings (
|
|
179
|
+
id TEXT PRIMARY KEY,
|
|
180
|
+
account_id TEXT,
|
|
181
|
+
job_id TEXT,
|
|
182
|
+
code TEXT NOT NULL,
|
|
183
|
+
message TEXT NOT NULL,
|
|
184
|
+
created_at TEXT NOT NULL,
|
|
185
|
+
acked_at TEXT
|
|
186
|
+
);
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
export class Store {
|
|
190
|
+
readonly db: DatabaseSync;
|
|
191
|
+
/** Directory that holds the database file and `renders/`. Null for an in-memory store. */
|
|
192
|
+
readonly dataDir: string | null;
|
|
193
|
+
|
|
194
|
+
constructor(path: string) {
|
|
195
|
+
this.dataDir = path === ":memory:" ? null : dirname(resolve(path));
|
|
196
|
+
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
197
|
+
this.db = new DatabaseSync(path);
|
|
198
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
199
|
+
if (path !== ":memory:") this.db.exec("PRAGMA journal_mode = WAL");
|
|
200
|
+
this.db.exec(SCHEMA);
|
|
201
|
+
this.ensureColumn("desk_series", "base_json", "TEXT");
|
|
202
|
+
this.ensureColumn("desk_series", "engine", "TEXT");
|
|
203
|
+
this.ensureColumn("job_events", "attempt", "INTEGER");
|
|
204
|
+
this.ensureColumn("comments_handled", "code", "TEXT");
|
|
205
|
+
this.ensureColumn("jobs", "finished_at", "TEXT");
|
|
206
|
+
this.ensureColumn("jobs", "lease_owner", "TEXT");
|
|
207
|
+
this.ensureColumn("jobs", "lease_token", "TEXT");
|
|
208
|
+
this.ensureColumn("desk_secrets", "set_at", "TEXT");
|
|
209
|
+
// Dashboard screens (spec 015 B): what the last fetch of a source returned, and what a comment said.
|
|
210
|
+
this.ensureColumn("desk_sources", "last_fetch_at", "TEXT");
|
|
211
|
+
this.ensureColumn("desk_sources", "last_count", "INTEGER");
|
|
212
|
+
this.ensureColumn("desk_sources", "last_code", "TEXT");
|
|
213
|
+
this.ensureColumn("comments_handled", "comment_text", "TEXT");
|
|
214
|
+
this.ensureColumn("comments_handled", "dry_run", "INTEGER NOT NULL DEFAULT 0");
|
|
215
|
+
this.ensureColumn("desk_series", "publish_on", "INTEGER NOT NULL DEFAULT 1");
|
|
216
|
+
this.ensureColumn("desk_series", "review_on", "INTEGER NOT NULL DEFAULT 0");
|
|
217
|
+
const hadLive = (this.db.prepare(`PRAGMA table_info(desk_series)`).all() as Array<{ name: string }>).some((item) => item.name === "live");
|
|
218
|
+
this.ensureColumn("desk_series", "live", "INTEGER NOT NULL DEFAULT 0");
|
|
219
|
+
if (!hadLive) desk.backfillLive(this.db);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
ensureColumn(table: string, column: string, ddl: string): void {
|
|
223
|
+
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
224
|
+
if (columns.some((item) => item.name === column)) return;
|
|
225
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
close(): void {
|
|
229
|
+
this.db.close();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
enqueue(input: {
|
|
233
|
+
accountId: string;
|
|
234
|
+
kind: JobKind;
|
|
235
|
+
stage?: JobStage;
|
|
236
|
+
payload?: PipelineData;
|
|
237
|
+
runAfter?: string;
|
|
238
|
+
maxAttempts?: number;
|
|
239
|
+
/** A one-off job: it neither joins nor blocks the series' own in-progress job. */
|
|
240
|
+
standalone?: boolean;
|
|
241
|
+
}): JobRecord {
|
|
242
|
+
if (input.kind === "pipeline" && !input.standalone) {
|
|
243
|
+
const existing = this.db
|
|
244
|
+
.prepare(
|
|
245
|
+
`SELECT id FROM jobs WHERE account_id = ? AND kind = 'pipeline' AND status IN ('pending','leased') LIMIT 1`,
|
|
246
|
+
)
|
|
247
|
+
.get(input.accountId) as { id: string } | undefined;
|
|
248
|
+
if (existing) return this.mustGet(existing.id);
|
|
249
|
+
}
|
|
250
|
+
const now = new Date().toISOString();
|
|
251
|
+
const id = randomUUID();
|
|
252
|
+
const payload = input.payload ?? emptyPayload();
|
|
253
|
+
this.db
|
|
254
|
+
.prepare(
|
|
255
|
+
`INSERT INTO jobs (id, account_id, kind, stage, status, payload, attempt, max_attempts, lease_until, run_after, last_error, created_at, updated_at)
|
|
256
|
+
VALUES (?, ?, ?, ?, 'pending', ?, 0, ?, NULL, ?, NULL, ?, ?)`,
|
|
257
|
+
)
|
|
258
|
+
.run(
|
|
259
|
+
id,
|
|
260
|
+
input.accountId,
|
|
261
|
+
input.kind,
|
|
262
|
+
input.stage ?? "source",
|
|
263
|
+
JSON.stringify(payload),
|
|
264
|
+
input.maxAttempts ?? 5,
|
|
265
|
+
input.runAfter ?? now,
|
|
266
|
+
now,
|
|
267
|
+
now,
|
|
268
|
+
);
|
|
269
|
+
return this.mustGet(id);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Takes the next due job. A `leased` job whose lease has expired belonged to a worker that died,
|
|
274
|
+
* so it is taken over and a `reclaimed` event is recorded. A job that has no attempts left is
|
|
275
|
+
* ended `dead` instead of being taken again.
|
|
276
|
+
*/
|
|
277
|
+
claim(now: Date, leaseMs: number, onlyId?: string): JobRecord | null {
|
|
278
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
279
|
+
try {
|
|
280
|
+
const iso = now.toISOString();
|
|
281
|
+
const events: Array<{ id: string; stage: string; status: string; code: string; message: string; attempt: number }> = [];
|
|
282
|
+
let claimedId: string | null = null;
|
|
283
|
+
// A lease held by a process on this host that no longer exists is free now, not at expiry.
|
|
284
|
+
const gone = new Set<string>();
|
|
285
|
+
const held = this.db
|
|
286
|
+
.prepare(`SELECT id, lease_owner FROM jobs WHERE status = 'leased' AND lease_owner IS NOT NULL AND lease_until >= ?`)
|
|
287
|
+
.all(iso) as Array<{ id: string; lease_owner: string }>;
|
|
288
|
+
for (const item of held) {
|
|
289
|
+
const at = item.lease_owner.lastIndexOf(":");
|
|
290
|
+
const pid = Number(item.lease_owner.slice(at + 1));
|
|
291
|
+
if (item.lease_owner.slice(0, at) !== os.hostname() || !Number.isInteger(pid) || pid <= 0 || pidAlive(pid)) continue;
|
|
292
|
+
this.db.prepare(`UPDATE jobs SET lease_until = ? WHERE id = ?`).run(new Date(now.getTime() - 1000).toISOString(), item.id);
|
|
293
|
+
gone.add(item.id);
|
|
294
|
+
}
|
|
295
|
+
for (;;) {
|
|
296
|
+
const row = this.db
|
|
297
|
+
.prepare(
|
|
298
|
+
`SELECT id, status, stage, attempt, max_attempts FROM jobs
|
|
299
|
+
WHERE ((status = 'pending' AND run_after <= ?)
|
|
300
|
+
OR (status = 'leased' AND lease_until IS NOT NULL AND lease_until < ?))
|
|
301
|
+
AND (? IS NULL OR id = ?)
|
|
302
|
+
ORDER BY run_after ASC LIMIT 1`,
|
|
303
|
+
)
|
|
304
|
+
.get(iso, iso, onlyId ?? null, onlyId ?? null) as { id: string; status: string; stage: string; attempt: number; max_attempts: number } | undefined;
|
|
305
|
+
if (!row) break;
|
|
306
|
+
const reclaimed = row.status === "leased";
|
|
307
|
+
if (reclaimed && Number(row.attempt) >= Number(row.max_attempts)) {
|
|
308
|
+
const message = "lease_expired: the worker stopped and no attempts are left";
|
|
309
|
+
this.db
|
|
310
|
+
.prepare(`UPDATE jobs SET status = 'dead', lease_until = NULL, last_error = ?, updated_at = ? WHERE id = ?`)
|
|
311
|
+
.run(message, iso, row.id);
|
|
312
|
+
events.push({ id: row.id, stage: row.stage, status: "dead", code: "lease_expired", message, attempt: Number(row.attempt) });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const leaseUntil = new Date(now.getTime() + leaseMs).toISOString();
|
|
316
|
+
this.db
|
|
317
|
+
.prepare(
|
|
318
|
+
`UPDATE jobs SET status = 'leased', lease_until = ?, lease_owner = ?, lease_token = ?, attempt = attempt + 1, updated_at = ? WHERE id = ?`,
|
|
319
|
+
)
|
|
320
|
+
.run(leaseUntil, `${os.hostname()}:${process.pid}`, randomUUID(), iso, row.id);
|
|
321
|
+
if (reclaimed) {
|
|
322
|
+
events.push({
|
|
323
|
+
id: row.id,
|
|
324
|
+
stage: row.stage,
|
|
325
|
+
status: "reclaimed",
|
|
326
|
+
code: gone.has(row.id) ? "owner_gone" : "lease_expired",
|
|
327
|
+
message: gone.has(row.id)
|
|
328
|
+
? "The process holding this job is gone. A new worker took it over now and resumes from the stored session."
|
|
329
|
+
: "The worker holding this job stopped. A new worker took it over and resumes from the stored session.",
|
|
330
|
+
attempt: Number(row.attempt) + 1,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
claimedId = row.id;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
for (const event of events) {
|
|
337
|
+
desk.recordEvent(this.db, event.id, { stage: event.stage, status: event.status, code: event.code, message: event.message, durationMs: null, attempt: event.attempt });
|
|
338
|
+
}
|
|
339
|
+
this.db.exec("COMMIT");
|
|
340
|
+
return claimedId ? this.mustGet(claimedId) : null;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this.db.exec("ROLLBACK");
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Gives back every job this process holds, so an interrupted run does not leave a lease behind. */
|
|
348
|
+
releaseOwned(now: Date = new Date()): string[] {
|
|
349
|
+
const owner = `${os.hostname()}:${process.pid}`;
|
|
350
|
+
const rows = this.db.prepare(`SELECT id, stage, attempt FROM jobs WHERE status = 'leased' AND lease_owner = ?`).all(owner) as Array<{ id: string; stage: string; attempt: number }>;
|
|
351
|
+
for (const row of rows) {
|
|
352
|
+
this.db
|
|
353
|
+
.prepare(`UPDATE jobs SET status = 'pending', lease_until = NULL, lease_owner = NULL, run_after = ?, attempt = MAX(0, attempt - 1), updated_at = ? WHERE id = ?`)
|
|
354
|
+
.run(now.toISOString(), now.toISOString(), row.id);
|
|
355
|
+
this.recordTerminal(row.id, { stage: row.stage, status: "retrying", code: "shutdown", message: "Interrupted. The job resumes from this stage on the next tick.", attempt: Number(row.attempt) });
|
|
356
|
+
}
|
|
357
|
+
return rows.map((row) => row.id);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Extends the lease of a job this worker still holds. Returns false when the job is no longer leased. */
|
|
361
|
+
renewLease(job: JobRecord, leaseMs: number, now: Date = new Date()): boolean {
|
|
362
|
+
const leaseUntil = new Date(now.getTime() + leaseMs).toISOString();
|
|
363
|
+
const result = this.db
|
|
364
|
+
.prepare(`UPDATE jobs SET lease_until = ? WHERE id = ? AND status = 'leased' AND (? IS NULL OR lease_token = ?)`)
|
|
365
|
+
.run(leaseUntil, job.id, job.leaseToken ?? null, job.leaseToken ?? null);
|
|
366
|
+
if (Number(result.changes) === 0) return false;
|
|
367
|
+
job.leaseUntil = leaseUntil;
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Puts a job back to `pending` without spending the attempt that claimed it.
|
|
373
|
+
* Used for a platform pause, a processing poll, and a shutdown.
|
|
374
|
+
*/
|
|
375
|
+
releaseWithoutAttempt(job: JobRecord, runAfter: Date, note: string | null): void {
|
|
376
|
+
job.status = "pending";
|
|
377
|
+
job.leaseUntil = null;
|
|
378
|
+
job.runAfter = runAfter.toISOString();
|
|
379
|
+
job.attempt = Math.max(0, job.attempt - 1);
|
|
380
|
+
job.lastError = note;
|
|
381
|
+
this.save(job);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Parks the job for review: the claim is given up (no lease, no owner, no token), the attempt that claimed it is
|
|
386
|
+
* not spent, and nothing holds a worker while the person looks. A worker whose claim was lost writes nothing.
|
|
387
|
+
*/
|
|
388
|
+
holdForReview(job: JobRecord): void {
|
|
389
|
+
const result = this.db
|
|
390
|
+
.prepare(
|
|
391
|
+
`UPDATE jobs SET stage = ?, status = 'awaiting_review', payload = ?, attempt = MAX(0, attempt - 1), lease_until = NULL,
|
|
392
|
+
lease_owner = NULL, lease_token = NULL, last_error = NULL, updated_at = ?
|
|
393
|
+
WHERE id = ? AND (? IS NULL OR lease_token = ?)`,
|
|
394
|
+
)
|
|
395
|
+
.run(job.stage, JSON.stringify(job.payload), new Date().toISOString(), job.id, job.leaseToken ?? null, job.leaseToken ?? null);
|
|
396
|
+
if (Number(result.changes) === 0 && job.leaseToken) throw new LeaseLost(job.id);
|
|
397
|
+
job.status = "awaiting_review";
|
|
398
|
+
job.leaseUntil = null;
|
|
399
|
+
job.attempt = Math.max(0, job.attempt - 1);
|
|
400
|
+
job.lastError = null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
save(job: JobRecord): void {
|
|
404
|
+
// A worker whose claim was cancelled or taken over must not write the job back. Its own view is stale.
|
|
405
|
+
const result = this.db
|
|
406
|
+
.prepare(
|
|
407
|
+
`UPDATE jobs SET stage = ?, status = ?, payload = ?, attempt = ?, lease_until = ?, run_after = ?, last_error = ?, updated_at = ?
|
|
408
|
+
WHERE id = ? AND (? IS NULL OR lease_token = ?)`,
|
|
409
|
+
)
|
|
410
|
+
.run(
|
|
411
|
+
job.stage,
|
|
412
|
+
job.status,
|
|
413
|
+
JSON.stringify(job.payload),
|
|
414
|
+
job.attempt,
|
|
415
|
+
job.leaseUntil,
|
|
416
|
+
job.runAfter,
|
|
417
|
+
job.lastError,
|
|
418
|
+
new Date().toISOString(),
|
|
419
|
+
job.id,
|
|
420
|
+
job.leaseToken ?? null,
|
|
421
|
+
job.leaseToken ?? null,
|
|
422
|
+
);
|
|
423
|
+
if (Number(result.changes) === 0 && job.leaseToken) throw new LeaseLost(job.id);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** True while this record's claim is the one the database holds. */
|
|
427
|
+
holdsLease(job: JobRecord): boolean {
|
|
428
|
+
if (!job.leaseToken) return true;
|
|
429
|
+
const row = this.db.prepare(`SELECT lease_token FROM jobs WHERE id = ?`).get(job.id) as { lease_token: string | null } | undefined;
|
|
430
|
+
return row?.lease_token === job.leaseToken;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** A worker that learned it lost its claim says it has stopped, so a retry no longer has to wait for it. */
|
|
434
|
+
releaseLeaseOwner(job: JobRecord): void {
|
|
435
|
+
this.db
|
|
436
|
+
.prepare(`UPDATE jobs SET lease_owner = NULL WHERE id = ? AND lease_owner = ? AND (lease_token IS NULL OR lease_token != ?)`)
|
|
437
|
+
.run(job.id, `${os.hostname()}:${process.pid}`, job.leaseToken ?? "");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
releaseRetry(job: JobRecord, runAfter: Date, error: string): void {
|
|
441
|
+
const status: JobStatus = job.attempt >= job.maxAttempts ? "dead" : "pending";
|
|
442
|
+
job.status = status;
|
|
443
|
+
job.leaseUntil = null;
|
|
444
|
+
job.runAfter = runAfter.toISOString();
|
|
445
|
+
job.lastError = error;
|
|
446
|
+
this.save(job);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
finish(job: JobRecord, status: JobStatus, error: string | null, at: Date = new Date()): void {
|
|
450
|
+
job.status = status;
|
|
451
|
+
job.leaseUntil = null;
|
|
452
|
+
job.lastError = error;
|
|
453
|
+
if (status === "succeeded") job.stage = "done";
|
|
454
|
+
this.save(job);
|
|
455
|
+
this.db.prepare(`UPDATE jobs SET finished_at = ? WHERE id = ?`).run(at.toISOString(), job.id);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
seen(accountId: string, fingerprint: string): boolean {
|
|
459
|
+
const row = this.db
|
|
460
|
+
.prepare(`SELECT fingerprint FROM source_seen WHERE account_id = ? AND fingerprint = ?`)
|
|
461
|
+
.get(accountId, fingerprint);
|
|
462
|
+
return Boolean(row);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
markSeen(accountId: string, fingerprint: string, at: Date): void {
|
|
466
|
+
this.db
|
|
467
|
+
.prepare(`INSERT OR IGNORE INTO source_seen (account_id, fingerprint, first_seen) VALUES (?, ?, ?)`)
|
|
468
|
+
.run(accountId, fingerprint, at.toISOString());
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
recentPublications(accountId: string, limit: number): Array<Record<string, unknown>> {
|
|
472
|
+
return this.db
|
|
473
|
+
.prepare(
|
|
474
|
+
`SELECT * FROM publications WHERE account_id = ? ORDER BY published_at DESC LIMIT ?`,
|
|
475
|
+
)
|
|
476
|
+
.all(accountId, limit) as Array<Record<string, unknown>>;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** One row per video (job), newest first. A video on three platforms is one entry. */
|
|
480
|
+
recentVideos(accountId: string, limit: number, excludeJobId: string | null): Array<Record<string, unknown>> {
|
|
481
|
+
return this.db
|
|
482
|
+
.prepare(
|
|
483
|
+
`SELECT p.* FROM publications p
|
|
484
|
+
WHERE p.account_id = ? AND (? IS NULL OR p.job_id != ?)
|
|
485
|
+
AND p.id = (SELECT q.id FROM publications q WHERE q.job_id = p.job_id ORDER BY q.published_at ASC, q.id ASC LIMIT 1)
|
|
486
|
+
ORDER BY p.published_at DESC LIMIT ?`,
|
|
487
|
+
)
|
|
488
|
+
.all(accountId, excludeJobId, excludeJobId, limit) as Array<Record<string, unknown>>;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
publicationExists(jobId: string, platform: string): boolean {
|
|
492
|
+
return Boolean(
|
|
493
|
+
this.db.prepare(`SELECT id FROM publications WHERE job_id = ? AND platform = ?`).get(jobId, platform),
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
insertPublication(row: {
|
|
498
|
+
id: string;
|
|
499
|
+
jobId: string;
|
|
500
|
+
accountId: string;
|
|
501
|
+
platform: string;
|
|
502
|
+
externalId: string | null;
|
|
503
|
+
disclosure: unknown;
|
|
504
|
+
variation: unknown;
|
|
505
|
+
hook: string;
|
|
506
|
+
title: string;
|
|
507
|
+
hashtags: string[];
|
|
508
|
+
script: string;
|
|
509
|
+
publishedAt: string;
|
|
510
|
+
audit: unknown;
|
|
511
|
+
}): void {
|
|
512
|
+
this.db
|
|
513
|
+
.prepare(
|
|
514
|
+
`INSERT INTO publications (id, job_id, account_id, platform, external_id, disclosure, variation, hook, title, hashtags, script, published_at, audit)
|
|
515
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
516
|
+
)
|
|
517
|
+
.run(
|
|
518
|
+
row.id,
|
|
519
|
+
row.jobId,
|
|
520
|
+
row.accountId,
|
|
521
|
+
row.platform,
|
|
522
|
+
row.externalId,
|
|
523
|
+
JSON.stringify(row.disclosure),
|
|
524
|
+
JSON.stringify(row.variation),
|
|
525
|
+
row.hook,
|
|
526
|
+
row.title,
|
|
527
|
+
JSON.stringify(row.hashtags),
|
|
528
|
+
row.script,
|
|
529
|
+
row.publishedAt,
|
|
530
|
+
JSON.stringify(row.audit),
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
lastPublishAt(accountId: string): Date | null {
|
|
535
|
+
const row = this.db
|
|
536
|
+
.prepare(`SELECT published_at FROM publications WHERE account_id = ? ORDER BY published_at DESC LIMIT 1`)
|
|
537
|
+
.get(accountId) as { published_at: string } | undefined;
|
|
538
|
+
return row ? new Date(row.published_at) : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
publicationTimes(accountId: string, limit = 50): Date[] {
|
|
542
|
+
const rows = this.db
|
|
543
|
+
.prepare(
|
|
544
|
+
`SELECT MIN(published_at) AS published_at FROM publications WHERE account_id = ?
|
|
545
|
+
GROUP BY job_id ORDER BY MIN(published_at) DESC LIMIT ?`,
|
|
546
|
+
)
|
|
547
|
+
.all(accountId, limit) as Array<{ published_at: string }>;
|
|
548
|
+
return rows.map((row) => new Date(row.published_at));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** True when any platform already has this job's video. */
|
|
552
|
+
jobHasPublication(jobId: string): boolean {
|
|
553
|
+
return Boolean(this.db.prepare(`SELECT id FROM publications WHERE job_id = ? LIMIT 1`).get(jobId));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* When each video of the series was made, newest first. A video is one pipeline job that succeeded
|
|
558
|
+
* (at its finish time) or has a publication row (at its first publication when it did not succeed).
|
|
559
|
+
* A video on three platforms is one entry; a render nobody could publish still counts. A job waiting for review
|
|
560
|
+
* counts from the moment it began waiting, so the cadence goes on and the series does not pile up waiting jobs.
|
|
561
|
+
*/
|
|
562
|
+
videoTimes(accountId: string, limit = 50): Date[] {
|
|
563
|
+
const rows = this.db
|
|
564
|
+
.prepare(
|
|
565
|
+
`SELECT CASE WHEN j.status = 'succeeded' THEN COALESCE(j.finished_at, j.updated_at)
|
|
566
|
+
WHEN j.status = 'awaiting_review' THEN j.updated_at
|
|
567
|
+
ELSE (SELECT MIN(p.published_at) FROM publications p WHERE p.job_id = j.id) END AS at
|
|
568
|
+
FROM jobs j
|
|
569
|
+
WHERE j.account_id = ? AND j.kind = 'pipeline'
|
|
570
|
+
AND (j.status IN ('succeeded', 'awaiting_review') OR EXISTS (SELECT 1 FROM publications p WHERE p.job_id = j.id))
|
|
571
|
+
ORDER BY at DESC LIMIT ?`,
|
|
572
|
+
)
|
|
573
|
+
.all(accountId, limit) as Array<{ at: string }>;
|
|
574
|
+
return rows.map((row) => new Date(row.at));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
commentHandled(platform: string, commentId: string): boolean {
|
|
578
|
+
return Boolean(
|
|
579
|
+
this.db.prepare(`SELECT comment_id FROM comments_handled WHERE platform = ? AND comment_id = ?`).get(platform, commentId),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
recordComment(row: {
|
|
584
|
+
platform: string;
|
|
585
|
+
commentId: string;
|
|
586
|
+
accountId: string;
|
|
587
|
+
action: string;
|
|
588
|
+
tone: string | null;
|
|
589
|
+
replyText: string | null;
|
|
590
|
+
at: Date;
|
|
591
|
+
/** Why a comment was skipped, for example `youtube_reply_403`. */
|
|
592
|
+
code?: string | null;
|
|
593
|
+
/** The comment itself, shown on the Engage screen. */
|
|
594
|
+
commentText?: string | null;
|
|
595
|
+
/** True when the reply was drafted but not sent (the series is not live). */
|
|
596
|
+
dryRun?: boolean;
|
|
597
|
+
}): void {
|
|
598
|
+
this.db
|
|
599
|
+
.prepare(
|
|
600
|
+
`INSERT OR IGNORE INTO comments_handled (platform, comment_id, account_id, action, tone, reply_text, created_at, code, comment_text, dry_run)
|
|
601
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
602
|
+
)
|
|
603
|
+
.run(row.platform, row.commentId, row.accountId, row.action, row.tone, row.replyText, row.at.toISOString(), row.code ?? null, row.commentText ? row.commentText.slice(0, 600) : null, row.dryRun ? 1 : 0);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Records what the last fetch of a source returned: n candidates, or an error code. */
|
|
607
|
+
recordSourceFetch(id: string, input: { at: Date; count: number | null; code: string | null; status: string }): void {
|
|
608
|
+
this.db
|
|
609
|
+
.prepare(`UPDATE desk_sources SET last_fetch_at = ?, last_count = ?, last_code = ?, last_status = ? WHERE id = ?`)
|
|
610
|
+
.run(input.at.toISOString(), input.count, input.code, input.status, id);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
repliesSince(accountId: string, sinceIso: string): number {
|
|
614
|
+
const row = this.db
|
|
615
|
+
.prepare(
|
|
616
|
+
`SELECT COUNT(*) AS n FROM comments_handled WHERE account_id = ? AND action = 'replied' AND created_at >= ?`,
|
|
617
|
+
)
|
|
618
|
+
.get(accountId, sinceIso) as { n: number };
|
|
619
|
+
return Number(row.n);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
lastReplyAt(accountId: string): Date | null {
|
|
623
|
+
const row = this.db
|
|
624
|
+
.prepare(
|
|
625
|
+
`SELECT created_at FROM comments_handled WHERE account_id = ? AND action = 'replied' ORDER BY created_at DESC LIMIT 1`,
|
|
626
|
+
)
|
|
627
|
+
.get(accountId) as { created_at: string } | undefined;
|
|
628
|
+
return row ? new Date(row.created_at) : null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
recordRate(bucket: string, at: Date): void {
|
|
632
|
+
this.db.prepare(`INSERT INTO rate_events (bucket, at) VALUES (?, ?)`).run(bucket, at.toISOString());
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
countRate(bucket: string, sinceIso: string): number {
|
|
636
|
+
const row = this.db
|
|
637
|
+
.prepare(`SELECT COUNT(*) AS n FROM rate_events WHERE bucket = ? AND at >= ?`)
|
|
638
|
+
.get(bucket, sinceIso) as { n: number };
|
|
639
|
+
return Number(row.n);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
insertMetric(row: {
|
|
643
|
+
accountId: string;
|
|
644
|
+
platform: string;
|
|
645
|
+
externalId: string;
|
|
646
|
+
capturedAt: string;
|
|
647
|
+
views: number | null;
|
|
648
|
+
watchTimeSec: number | null;
|
|
649
|
+
retention: number | null;
|
|
650
|
+
estimatedRevenue: number | null;
|
|
651
|
+
revenueStatus: string;
|
|
652
|
+
raw: unknown;
|
|
653
|
+
}): void {
|
|
654
|
+
if ((row.revenueStatus === "forbidden" || row.revenueStatus === "unavailable") && row.estimatedRevenue != null) {
|
|
655
|
+
throw new Error(`A ${row.revenueStatus} metric cannot store a revenue number.`);
|
|
656
|
+
}
|
|
657
|
+
this.db
|
|
658
|
+
.prepare(
|
|
659
|
+
`INSERT INTO metrics (id, account_id, platform, external_id, captured_at, views, watch_time_sec, retention, estimated_revenue, revenue_status, raw)
|
|
660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
661
|
+
)
|
|
662
|
+
.run(
|
|
663
|
+
randomUUID(),
|
|
664
|
+
row.accountId,
|
|
665
|
+
row.platform,
|
|
666
|
+
row.externalId,
|
|
667
|
+
row.capturedAt,
|
|
668
|
+
row.views,
|
|
669
|
+
row.watchTimeSec,
|
|
670
|
+
row.retention,
|
|
671
|
+
row.estimatedRevenue,
|
|
672
|
+
row.revenueStatus,
|
|
673
|
+
JSON.stringify(row.raw),
|
|
674
|
+
);
|
|
675
|
+
if (row.revenueStatus === "forbidden") {
|
|
676
|
+
desk.insertWarningOnce(this.db, {
|
|
677
|
+
accountId: row.accountId,
|
|
678
|
+
jobId: null,
|
|
679
|
+
code: "revenue_forbidden",
|
|
680
|
+
message: `Revenue for ${row.platform} ${row.externalId} is forbidden.`,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
recordEvent(jobId: string, event: desk.EventInput): void {
|
|
686
|
+
desk.recordEvent(this.db, jobId, event);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
recordTerminal(jobId: string, event: Omit<desk.EventInput, "durationMs">): void {
|
|
690
|
+
desk.recordTerminal(this.db, jobId, event);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
eventsFor(jobId: string): desk.JobEventRow[] {
|
|
694
|
+
return desk.eventsFor(this.db, jobId);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
createAccount(input: { id: string; name: string; ownerId?: string | null }): void {
|
|
698
|
+
desk.createAccount(this.db, input);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
renameAccount(id: string, name: string): desk.DeskResult {
|
|
702
|
+
return desk.renameAccount(this.db, id, name);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
deleteAccount(id: string): desk.DeskResult {
|
|
706
|
+
return desk.deleteAccount(this.db, id);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
setSecret(input: { ownerKind: "account" | "series"; ownerId: string; field: string; plaintext: string; key: string }) {
|
|
710
|
+
return desk.setSecret(this.db, input);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
clearSecret(input: { ownerKind: "account" | "series"; ownerId: string; field: string }) {
|
|
714
|
+
return desk.clearSecret(this.db, input);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
secretPresence(ownerKind: "account" | "series", ownerId: string): Record<string, boolean> {
|
|
718
|
+
return desk.secretPresence(this.db, ownerKind, ownerId);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
createSeries(input: desk.SeriesInput): void {
|
|
722
|
+
desk.createSeries(this.db, input);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
updateSeries(id: string, patch: Partial<desk.SeriesInput>): desk.DeskResult {
|
|
726
|
+
return desk.updateSeries(this.db, id, patch);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
deleteSeries(id: string): desk.DeskResult {
|
|
730
|
+
return desk.deleteSeries(this.db, id);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
pauseSeries(id: string): desk.DeskResult {
|
|
734
|
+
return desk.pauseSeries(this.db, id);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
resumeSeries(id: string): desk.DeskResult {
|
|
738
|
+
return desk.resumeSeries(this.db, id);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
seriesPaused(accountId: string): boolean {
|
|
742
|
+
return desk.seriesPaused(this.db, accountId);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** The publish gates of a series: live, publish, review. A series the desk does not know is dry-run with publishing on. */
|
|
746
|
+
seriesGates(seriesId: string): desk.SeriesGates {
|
|
747
|
+
return desk.seriesGates(this.db, seriesId);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
setLive(seriesId: string, live: boolean, message: string): desk.DeskResult {
|
|
751
|
+
return desk.setLive(this.db, seriesId, live, message);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
seriesEvents(seriesId: string, limit = 20): Array<{ id: number; at: string; code: string; message: string }> {
|
|
755
|
+
return desk.seriesEvents(this.db, seriesId, limit);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
recordSeriesEvent(seriesId: string, code: string, message: string): void {
|
|
759
|
+
desk.recordSeriesEvent(this.db, seriesId, code, message);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
setPlatform(input: { seriesId: string; platform: string; enabled: boolean; config?: Record<string, unknown> }): desk.DeskResult {
|
|
763
|
+
return desk.setPlatform(this.db, input);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
disablePlatform(seriesId: string, platform: string): desk.DeskResult {
|
|
767
|
+
return desk.disablePlatform(this.db, seriesId, platform);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
removePlatform(seriesId: string, platform: string): desk.DeskResult {
|
|
771
|
+
return desk.removePlatform(this.db, seriesId, platform);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
addSource(input: { seriesId: string; kind: string; value: string }): { id: string } {
|
|
775
|
+
return desk.addSource(this.db, input);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
updateSource(id: string, value: string): desk.DeskResult {
|
|
779
|
+
return desk.updateSource(this.db, id, value);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
removeSource(id: string): desk.DeskResult {
|
|
783
|
+
return desk.removeSource(this.db, id);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
setMode(userId: string, mode: string, llmKeyPresent: boolean): desk.DeskResult {
|
|
787
|
+
return desk.setMode(this.db, userId, mode, llmKeyPresent);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
clearMode(userId: string): desk.DeskResult {
|
|
791
|
+
return desk.clearMode(this.db, userId);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
readMode(userId: string): "agent" | "api" {
|
|
795
|
+
return desk.readMode(this.db, userId);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
setCadence(
|
|
799
|
+
seriesId: string,
|
|
800
|
+
input: { timezone: string; minHoursBetweenPosts: number; maxPostsPerDay: number; windows: string[] },
|
|
801
|
+
): desk.DeskResult {
|
|
802
|
+
return desk.setCadence(this.db, seriesId, input);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
clearCadence(): desk.DeskResult {
|
|
806
|
+
return desk.clearCadence();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
setQuota(
|
|
810
|
+
seriesId: string,
|
|
811
|
+
input: { youtubeDailyUploadCap: number; maxRepliesPerDay: number; minSecondsBetween: number },
|
|
812
|
+
): desk.DeskResult {
|
|
813
|
+
return desk.setQuota(this.db, seriesId, input);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
clearQuota(): desk.DeskResult {
|
|
817
|
+
return desk.clearQuota();
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
insertWarning(input: { accountId: string | null; jobId: string | null; code: string; message: string }): { id: string } {
|
|
821
|
+
return desk.insertWarning(this.db, input);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
ackWarning(id: string): desk.DeskResult {
|
|
825
|
+
return desk.ackWarning(this.db, id);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
deleteWarning(): desk.DeskResult {
|
|
829
|
+
return desk.deleteWarning();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
deleteJob(): desk.DeskResult {
|
|
833
|
+
return desk.deleteJob();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
deletePublication(): desk.DeskResult {
|
|
837
|
+
return desk.deletePublication();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
deleteMetric(): desk.DeskResult {
|
|
841
|
+
return desk.deleteMetric();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
deleteComment(): desk.DeskResult {
|
|
845
|
+
return desk.deleteComment();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
retryJob(id: string, now = new Date()): desk.DeskResult {
|
|
849
|
+
return desk.retryJob(this.db, id, now);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Releases a job that waits for review. Exactly one caller wins; the next tick renders it. */
|
|
853
|
+
approveJob(id: string, now = new Date()): desk.DeskResult {
|
|
854
|
+
return desk.approveJob(this.db, id, now);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Ends a job that waits for review as `rejected` with the code `review_rejected`. */
|
|
858
|
+
rejectJob(id: string, reason: string | null, now = new Date()): desk.DeskResult {
|
|
859
|
+
return desk.rejectJob(this.db, id, reason, now);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** How many jobs wait for review, overall or for one series. */
|
|
863
|
+
awaitingReviewCount(seriesId?: string): number {
|
|
864
|
+
return desk.awaitingReviewCount(this.db, seriesId);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** The ids of a series' jobs that wait for review, oldest first. */
|
|
868
|
+
awaitingReviewJobs(seriesId: string): string[] {
|
|
869
|
+
return (this.db.prepare(`SELECT id FROM jobs WHERE account_id = ? AND status = 'awaiting_review' ORDER BY updated_at ASC, rowid ASC`).all(seriesId) as Array<{ id: string }>).map((row) => row.id);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** The Review screen: every job waiting for review, oldest first. */
|
|
873
|
+
reviewQueue(dataDir: string | null = null) {
|
|
874
|
+
return desk.reviewQueue(this.db, dataDir);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
cancelJob(id: string, now = new Date()): desk.DeskResult {
|
|
878
|
+
return desk.cancelJob(this.db, id, now);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
engineDefault(): string | null {
|
|
882
|
+
return desk.readEngineDefault(this.db);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
setEngineDefault(engine: string): void {
|
|
886
|
+
desk.writeEngineDefault(this.db, engine);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
seriesEngine(seriesId: string): string | null {
|
|
890
|
+
return desk.readSeriesEngine(this.db, seriesId);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
snapshot(userId: string): Record<string, unknown> {
|
|
894
|
+
return desk.buildSnapshot(this.db, userId, this.dataDir);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
importLegacy(fromDir: string): { imported: Record<string, number>; files: number } {
|
|
898
|
+
return desk.importLegacy(this.db, this.dataDir, fromDir);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
runDetail(id: string): ReturnType<typeof desk.buildRunDetail> {
|
|
902
|
+
return desk.buildRunDetail(this.db, id, this.dataDir);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
latestMetrics(): Array<Record<string, unknown>> {
|
|
906
|
+
return this.db
|
|
907
|
+
.prepare(
|
|
908
|
+
`SELECT m.* FROM metrics m
|
|
909
|
+
JOIN (
|
|
910
|
+
SELECT external_id, platform, MAX(captured_at) AS captured_at
|
|
911
|
+
FROM metrics GROUP BY external_id, platform
|
|
912
|
+
) latest
|
|
913
|
+
ON m.external_id = latest.external_id AND m.platform = latest.platform AND m.captured_at = latest.captured_at
|
|
914
|
+
ORDER BY m.views IS NULL, m.views DESC, m.watch_time_sec IS NULL, m.watch_time_sec DESC`,
|
|
915
|
+
)
|
|
916
|
+
.all() as Array<Record<string, unknown>>;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
setHealth(accountId: string, platform: string, tokenState: string, lastError: string | null, at: Date): void {
|
|
920
|
+
this.db
|
|
921
|
+
.prepare(
|
|
922
|
+
`INSERT INTO account_health (account_id, platform, token_state, last_error, updated_at)
|
|
923
|
+
VALUES (?, ?, ?, ?, ?)
|
|
924
|
+
ON CONFLICT (account_id, platform) DO UPDATE SET token_state = excluded.token_state, last_error = excluded.last_error, updated_at = excluded.updated_at`,
|
|
925
|
+
)
|
|
926
|
+
.run(accountId, platform, tokenState, lastError, at.toISOString());
|
|
927
|
+
if (lastError) {
|
|
928
|
+
desk.insertWarningOnce(this.db, {
|
|
929
|
+
accountId,
|
|
930
|
+
jobId: null,
|
|
931
|
+
code: `health:${platform}`,
|
|
932
|
+
message: lastError,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
deskOverlay(accountId: string): desk.DeskOverlay | null {
|
|
938
|
+
return desk.deskOverlay(this.db, accountId);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
warnOnce(input: { accountId: string | null; jobId: string | null; code: string; message: string }): { id: string } {
|
|
942
|
+
return desk.insertWarningOnce(this.db, input);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
healthRows(): Array<Record<string, unknown>> {
|
|
946
|
+
return this.db.prepare(`SELECT * FROM account_health`).all() as Array<Record<string, unknown>>;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** One job by id, or null. */
|
|
950
|
+
job(id: string): JobRecord | null {
|
|
951
|
+
try {
|
|
952
|
+
return this.mustGet(id);
|
|
953
|
+
} catch {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private mustGet(id: string): JobRecord {
|
|
959
|
+
const row = this.db.prepare(`SELECT * FROM jobs WHERE id = ?`).get(id) as Record<string, unknown> | undefined;
|
|
960
|
+
if (!row) throw new Error(`job ${id} missing`);
|
|
961
|
+
return {
|
|
962
|
+
id: String(row.id),
|
|
963
|
+
accountId: String(row.account_id),
|
|
964
|
+
kind: row.kind as JobKind,
|
|
965
|
+
stage: row.stage as JobStage,
|
|
966
|
+
status: row.status as JobStatus,
|
|
967
|
+
payload: JSON.parse(String(row.payload)) as PipelineData,
|
|
968
|
+
attempt: Number(row.attempt),
|
|
969
|
+
maxAttempts: Number(row.max_attempts),
|
|
970
|
+
leaseUntil: row.lease_until ? String(row.lease_until) : null,
|
|
971
|
+
leaseToken: row.lease_token ? String(row.lease_token) : null,
|
|
972
|
+
runAfter: String(row.run_after),
|
|
973
|
+
lastError: row.last_error ? String(row.last_error) : null,
|
|
974
|
+
createdAt: String(row.created_at),
|
|
975
|
+
updatedAt: String(row.updated_at),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** The job's claim was cancelled or taken over while this worker still held it. The worker stops without writing. */
|
|
981
|
+
export class LeaseLost extends Error {
|
|
982
|
+
constructor(readonly jobId: string) {
|
|
983
|
+
super(`The claim on job ${jobId} is gone.`);
|
|
984
|
+
this.name = "LeaseLost";
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export function pidAlive(pid: number): boolean {
|
|
989
|
+
try {
|
|
990
|
+
process.kill(pid, 0);
|
|
991
|
+
return true;
|
|
992
|
+
} catch (error) {
|
|
993
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export function emptyPayload(): PipelineData {
|
|
998
|
+
return {
|
|
999
|
+
candidates: [],
|
|
1000
|
+
rejectedFingerprints: [],
|
|
1001
|
+
content: null,
|
|
1002
|
+
platforms: {},
|
|
1003
|
+
engageReplied: 0,
|
|
1004
|
+
dryRunPayloads: {},
|
|
1005
|
+
};
|
|
1006
|
+
}
|