@vfrey/vfrey 0.0.1 → 1.0.0-beta.0
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 +282 -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 +205 -0
- package/src/cli.ts +467 -0
- package/src/cloud/executor.ts +281 -0
- package/src/cloud/job.ts +106 -0
- package/src/cloud/render-job.ts +187 -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 +233 -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 +87 -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,350 @@
|
|
|
1
|
+
// Board, Runs list, Run detail.
|
|
2
|
+
import {
|
|
3
|
+
html, icon, setHtml, $, $$, api, read, toast, refusalToast, store, dur, rel, abs, clock, ms, short, esc,
|
|
4
|
+
STATUS, STAGES, GROUPS, TERMINAL, groupOf, jobTimes, stageInfo, seriesFor, cap, copyText, num,
|
|
5
|
+
} from "./core.js";
|
|
6
|
+
import { statusPill, stageRail, stageTone, emptyState, when, copyBtn, platformName } from "./components.js";
|
|
7
|
+
|
|
8
|
+
export async function startTick(body) {
|
|
9
|
+
const r = await api("/api/tick", body);
|
|
10
|
+
if (!r.ok) { refusalToast(r); return r; }
|
|
11
|
+
if (r.tick && store.state) store.state.tick = r.tick;
|
|
12
|
+
store.refresh?.();
|
|
13
|
+
return r;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const jobSeries = (state, job) => seriesFor(state, job);
|
|
17
|
+
const sortKey = (j) => ms(j.updatedAt);
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------- Board
|
|
20
|
+
export function boardView() {
|
|
21
|
+
let root, state;
|
|
22
|
+
const view = {
|
|
23
|
+
title: "Board",
|
|
24
|
+
mount(el) {
|
|
25
|
+
root = el;
|
|
26
|
+
setHtml(el, html`<div class="page-head">
|
|
27
|
+
<div><span class="eyebrow">Desk / Board</span><h1>Board</h1><p class="sub">Every run, from waiting to finished. Updates by itself.</p></div>
|
|
28
|
+
<div class="actions" id="tick-actions">
|
|
29
|
+
<span id="tick-chip" hidden></span>
|
|
30
|
+
<button type="button" class="btn secondary" data-tick="dry" id="btn-dry">${icon("play")}Dry-run</button>
|
|
31
|
+
<button type="button" class="btn" data-tick="now" id="btn-tick">${icon("refresh")}Run tick now</button>
|
|
32
|
+
</div></div>
|
|
33
|
+
<div id="review-banner" hidden></div>
|
|
34
|
+
<div class="board" id="board"></div>`);
|
|
35
|
+
el.addEventListener("click", (e) => {
|
|
36
|
+
const t = e.target.closest("[data-tick]");
|
|
37
|
+
if (t) startTick(t.dataset.tick === "dry" ? { dryRun: true } : { now: true });
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
update(s) {
|
|
41
|
+
state = s;
|
|
42
|
+
const tick = s.tick || {};
|
|
43
|
+
const chip = $(root, "#tick-chip");
|
|
44
|
+
chip.hidden = !tick.running;
|
|
45
|
+
if (tick.running) {
|
|
46
|
+
chip.className = "tick-chip";
|
|
47
|
+
const label = `${tick.dryRun ? "Dry-run" : "Ticking"} · `;
|
|
48
|
+
setHtml(chip, html`${icon("spinner", "spin")}<span>${label}<span data-tick-since="${tick.startedAt}">${clock(Date.now() - ms(tick.startedAt))}</span></span>`);
|
|
49
|
+
}
|
|
50
|
+
for (const b of $$(root, "[data-tick]")) b.disabled = !!tick.running;
|
|
51
|
+
const jobs = s.jobs || [];
|
|
52
|
+
const toReview = jobs.filter((j) => j.status === "awaiting_review").length;
|
|
53
|
+
const banner = $(root, "#review-banner");
|
|
54
|
+
banner.hidden = !toReview;
|
|
55
|
+
if (toReview) setHtml(banner, html`<a class="callout" href="/review" data-link data-review-banner>${icon("alert")}<div><p class="t">${toReview} ${toReview === 1 ? "run waits" : "runs wait"} for your review</p><p>Nothing renders or publishes until you approve it. Open the Review screen.</p></div></a>`);
|
|
56
|
+
const cols = Object.entries(GROUPS).map(([key, g]) => {
|
|
57
|
+
const list = jobs.filter((j) => g.statuses.includes(j.status)).sort((a, b) => sortKey(b) - sortKey(a));
|
|
58
|
+
const empty = { waiting: "Nothing waiting. Run a tick to queue work.", running: "No run in progress.", finished: "No finished run yet.", failed: "No failures. Good." }[key];
|
|
59
|
+
return html`<section class="col" data-col="${key}" aria-label="${g.label}">
|
|
60
|
+
<div class="col-head"><span class="dot" style="background:var(--${{ waiting: "wait", running: "run", finished: "ok", failed: "block" }[key]}-fg)"></span><h2 style="font-size:14px;font-weight:560;margin:0">${g.label}</h2><span class="n" data-count>${list.length}</span></div>
|
|
61
|
+
<div class="col-cards">${list.length ? list.map((j) => card(s, j)) : html`<div class="empty-col">${empty}</div>`}</div></section>`;
|
|
62
|
+
});
|
|
63
|
+
setHtml($(root, "#board"), html`${cols}`);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
return view;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Board badges are short so every card keeps one header line at the narrowest column; the full label is the tooltip
|
|
70
|
+
// and stays in the accessible name through the screen-reader tail.
|
|
71
|
+
function cardBadge(status) {
|
|
72
|
+
const s = STATUS[status];
|
|
73
|
+
const [head, ...rest] = s.label.split(" · ");
|
|
74
|
+
const tail = rest.length ? html`<span class="sr"> · ${rest.join(" · ")}</span>` : "";
|
|
75
|
+
return html`<span class="pill tone-${s.tone}${status === "leased" ? " live" : ""}" title="${s.label}">${head}${tail}</span>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function card(state, job) {
|
|
79
|
+
const t = jobTimes(job);
|
|
80
|
+
const last = (job.events || [])[(job.events || []).length - 1];
|
|
81
|
+
let badge = "";
|
|
82
|
+
if (["rejected", "blocked", "dead", "leased", "awaiting_review"].includes(job.status)) badge = cardBadge(job.status);
|
|
83
|
+
else if (job.status === "pending" && last?.status === "retrying") {
|
|
84
|
+
const full = `Retrying · attempt ${job.attempt}/${job.maxAttempts}`;
|
|
85
|
+
badge = html`<span class="pill tone-retry" title="${full}">Retry ${job.attempt}/${job.maxAttempts}<span class="sr"> · attempt ${job.attempt} of ${job.maxAttempts}</span></span>`;
|
|
86
|
+
}
|
|
87
|
+
const foot = job.status === "pending" || job.status === "awaiting_review"
|
|
88
|
+
? html`<span>Queued ${rel(job.createdAt)}</span><span>${job.status === "awaiting_review" ? "needs review" : ms(job.runAfter) > Date.now() ? "runs " + rel(job.runAfter) : "ready"}</span>`
|
|
89
|
+
: html`<span title="${abs(new Date(t.start).toISOString())}">Started ${rel(new Date(t.start).toISOString())}</span><span ${t.running ? html`data-dur-since="${new Date(t.start).toISOString()}"` : ""}>${dur(t.total)}</span>`;
|
|
90
|
+
return html`<a class="card ${job.status === "leased" ? "running" : ""}" href="/runs/${job.id}" data-link data-job="${job.id}">
|
|
91
|
+
<div class="top"><span class="series-id">${jobSeries(state, job)}</span>${badge}</div>
|
|
92
|
+
<div class="title ${job.title ? "" : "none"}" title="${job.title || "Untitled run"}">${job.title || "Untitled run"}</div>
|
|
93
|
+
${stageRail(job)}
|
|
94
|
+
<div class="foot">${foot}</div></a>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------- Runs
|
|
98
|
+
const filterMemo = { f: "all", q: "" };
|
|
99
|
+
export function runsView() {
|
|
100
|
+
let root;
|
|
101
|
+
const FILTERS = [["all", "All"], ["running", "Running"], ["waiting", "Waiting"], ["finished", "Finished"], ["failed", "Failed"]];
|
|
102
|
+
const view = {
|
|
103
|
+
title: "Runs",
|
|
104
|
+
mount(el, params, ctx) {
|
|
105
|
+
root = el;
|
|
106
|
+
setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Runs</span><h1>Runs</h1><p class="sub">Every job the pipeline has picked up.</p></div></div>
|
|
107
|
+
<div class="toolbar"><div class="filters" id="filters" role="group" aria-label="Filter by status"></div>
|
|
108
|
+
<label class="search"><span class="sr">Search runs</span>${icon("search")}<input type="search" id="q" placeholder="Search id, series, or title" autocomplete="off" value="${filterMemo.q}"></label></div>
|
|
109
|
+
<div id="runs-body"></div>`);
|
|
110
|
+
$(el, "#q").addEventListener("input", (e) => { filterMemo.q = e.target.value; view.update(store.state); });
|
|
111
|
+
el.addEventListener("click", (e) => {
|
|
112
|
+
const f = e.target.closest("[data-filter]");
|
|
113
|
+
if (f) { filterMemo.f = f.dataset.filter; view.update(store.state); return; }
|
|
114
|
+
if (e.target.closest("[data-tick]")) { startTick({ dryRun: true }); return; }
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
update(s) {
|
|
118
|
+
const jobs = s.jobs || [];
|
|
119
|
+
const counts = { all: jobs.length };
|
|
120
|
+
for (const g of Object.keys(GROUPS)) counts[g] = jobs.filter((j) => GROUPS[g].statuses.includes(j.status)).length;
|
|
121
|
+
setHtml($(root, "#filters"), html`${FILTERS.map(([k, l]) => html`<button type="button" class="filter" data-filter="${k}" aria-pressed="${filterMemo.f === k}">${l}<span class="n">${counts[k]}</span></button>`)}`);
|
|
122
|
+
const q = filterMemo.q.trim().toLowerCase();
|
|
123
|
+
const rows = jobs.filter((j) => (filterMemo.f === "all" || groupOf(j.status) === filterMemo.f)
|
|
124
|
+
&& (!q || [j.id, jobSeries(s, j), j.title || ""].some((x) => String(x).toLowerCase().includes(q))));
|
|
125
|
+
const body = $(root, "#runs-body");
|
|
126
|
+
if (!jobs.length) {
|
|
127
|
+
setHtml(body, html`<div class="panel">${emptyState("film", "No runs yet", "Nothing has run on this machine. A dry-run shows the whole pipeline without publishing.", html`<button type="button" class="btn" data-tick="dry">${icon("play")}Dry-run</button>`)}</div>`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!rows.length) { setHtml(body, html`<div class="panel">${emptyState("clock", "No run matches", "Try another filter or clear the search.")}</div>`); return; }
|
|
131
|
+
setHtml(body, html`<div class="table-wrap"><table><thead><tr><th>Status</th><th>Run</th><th>Series</th><th>Stage</th><th>Started</th><th class="num">Duration</th></tr></thead><tbody>
|
|
132
|
+
${rows.map((j) => {
|
|
133
|
+
const t = jobTimes(j);
|
|
134
|
+
const iso = isFinite(t.start) ? new Date(t.start).toISOString() : j.createdAt;
|
|
135
|
+
const open = html`href="/runs/${j.id}" data-link tabindex="-1"`;
|
|
136
|
+
return html`<tr class="row-link" data-job="${j.id}">
|
|
137
|
+
<td><a class="cell-link" ${open}>${statusPill(j.status, { iconic: true })}</a></td>
|
|
138
|
+
<td><a class="cover cell-link" href="/runs/${j.id}" data-link><span class="t">${j.title || "Untitled run"}</span><div class="sub mono">${short(j.id)}</div></a></td>
|
|
139
|
+
<td><a class="cell-fill" ${open} aria-hidden="true"></a><a class="series-link" href="/series/${jobSeries(s, j)}" data-link>${jobSeries(s, j)}</a></td>
|
|
140
|
+
<td><a class="cell-link" ${open}>${cap(j.status === "succeeded" ? "done" : j.stage)}</a></td>
|
|
141
|
+
<td class="nowrap" title="${abs(iso)}"><a class="cell-link" ${open}>${rel(iso)}</a></td>
|
|
142
|
+
<td class="num"><a class="cell-link" ${open} ${t.running ? html`data-dur-since="${iso}"` : ""}>${dur(t.total)}</a></td></tr>`;
|
|
143
|
+
})}</tbody></table></div>`);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
return view;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------- Run detail
|
|
150
|
+
const PUB_PHASE = { missing_key: "No key, upload skipped" };
|
|
151
|
+
function disclosureText(d) {
|
|
152
|
+
if (d == null) return "none recorded";
|
|
153
|
+
if (typeof d === "string") return d;
|
|
154
|
+
if (typeof d === "object") return Object.entries(d).map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`).join(" · ") || "none recorded";
|
|
155
|
+
return String(d);
|
|
156
|
+
}
|
|
157
|
+
function publishText(p) {
|
|
158
|
+
if (p.errorCode && PUB_PHASE[p.errorCode]) return PUB_PHASE[p.errorCode];
|
|
159
|
+
if (p.externalId) return `Published · ${p.externalId}`;
|
|
160
|
+
const base = cap(String(p.phase || "pending").replace(/_/g, " "));
|
|
161
|
+
return [base, p.errorCode, p.errorMessage].filter(Boolean).join(" · ");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function runView() {
|
|
165
|
+
let root, ctx, id, run = null, missing = false, confirming = false, notice = null, outKey = null, lastSig = "";
|
|
166
|
+
const open = new Set();
|
|
167
|
+
let loading = false;
|
|
168
|
+
|
|
169
|
+
async function load() {
|
|
170
|
+
if (loading) return; loading = true;
|
|
171
|
+
try {
|
|
172
|
+
const r = await fetch(`/api/runs/${encodeURIComponent(id)}`).then(async (res) => ({ status: res.status, body: await res.json().catch(() => null) }));
|
|
173
|
+
if (r.status === 404 || !r.body?.run) { if (r.status === 404) { missing = true; run = null; } }
|
|
174
|
+
else { run = r.body.run; missing = false; }
|
|
175
|
+
} catch { /* offline: keep what we have */ }
|
|
176
|
+
loading = false;
|
|
177
|
+
render();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function render() {
|
|
181
|
+
if (missing) {
|
|
182
|
+
setHtml(root, html`<a class="crumb" href="/runs" data-link>${icon("chevronRight")}Runs</a><div class="panel">${emptyState("alert", "Run not found", "This run id is not in the local database.", html`<a class="btn" href="/runs" data-link>Back to runs</a>`)}</div>`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!run) return;
|
|
186
|
+
renderHead(); renderTimeline(); renderSide();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderHead() {
|
|
190
|
+
const s = store.state;
|
|
191
|
+
const t = jobTimes(run);
|
|
192
|
+
const iso = (n) => (isFinite(n) ? new Date(n).toISOString() : null);
|
|
193
|
+
const canRetry = ["pending", "rejected", "blocked", "dead"].includes(run.status);
|
|
194
|
+
const canCancel = run.status === "leased";
|
|
195
|
+
const cancelUi = confirming
|
|
196
|
+
? html`<span class="confirm">Cancel this run?<button type="button" class="btn danger solid sm" data-act="cancel-yes">Yes, cancel</button><button type="button" class="btn ghost sm" data-act="cancel-no">Keep running</button></span>`
|
|
197
|
+
: html`<button type="button" class="btn danger" data-act="cancel" ${canCancel ? "" : "disabled"}>${icon("cancel")}Cancel</button>`;
|
|
198
|
+
const seriesId = seriesFor(s, run);
|
|
199
|
+
setHtml($(root, "#head"), html`
|
|
200
|
+
<a class="crumb" href="/runs" data-link>${icon("chevronRight")}Runs</a>
|
|
201
|
+
<div class="run-head">
|
|
202
|
+
<div class="run-title">${statusPill(run.status, { iconic: true })}<h1>${run.title || "Untitled run"}</h1></div>
|
|
203
|
+
<dl class="meta">
|
|
204
|
+
<div><dt>Run id</dt><dd><span class="mono">${short(run.id)}</span>${copyBtn(run.id, "Copy run id")}</dd></div>
|
|
205
|
+
<div><dt>Series</dt><dd><a href="/series/${seriesId}" data-link>${seriesId}</a></dd></div>
|
|
206
|
+
<div><dt>Engine</dt><dd data-engine="${run.engine ? run.engine.id : ""}">${run.engine ? run.engine.label : "—"}</dd></div>
|
|
207
|
+
<div><dt>Scene</dt><dd data-scene="${run.scene ? run.scene.source : ""}" title="${run.scene && run.scene.dir ? run.scene.dir : ""}">${run.scene ? (run.scene.source === "agent" ? "Agent-written" : "Default template") : "—"}</dd></div>
|
|
208
|
+
<div><dt>Attempt</dt><dd>${run.attempt} / ${run.maxAttempts}</dd></div>
|
|
209
|
+
<div><dt>Started</dt><dd title="${isFinite(t.start) ? abs(iso(t.start)) : ""}">${isFinite(t.start) ? rel(iso(t.start)) : "Not started"}</dd></div>
|
|
210
|
+
<div><dt>Finished</dt><dd title="${TERMINAL.has(run.status) ? abs(iso(t.end)) : ""}">${TERMINAL.has(run.status) ? rel(iso(t.end)) : "—"}</dd></div>
|
|
211
|
+
<div><dt>Total duration</dt><dd ${t.running ? html`data-dur-since="${iso(t.start)}"` : ""}>${dur(t.total)}</dd></div>
|
|
212
|
+
</dl>
|
|
213
|
+
<div class="actions">${run.status === "awaiting_review" ? html`<a class="btn" href="/review" data-link>${icon("check")}Review it</a>` : ""}${run.scene && run.scene.source === "agent" && run.scene.dir ? html`<button type="button" class="btn" data-act="studio" title="Opens a copy of this scene in the visual editor">${icon("external")}Edit in studio</button>` : ""}<button type="button" class="btn" data-act="retry" ${canRetry ? "" : "disabled"}>${icon("retry")}Retry</button>${cancelUi}</div>
|
|
214
|
+
${notice ? html`<div class="notice" role="alert"><b>${notice.refusal}</b> ${notice.fix}</div>` : ""}
|
|
215
|
+
</div>
|
|
216
|
+
${run.lastError ? html`<div class="callout" role="alert">${icon("alert")}<div><p class="t">Last error</p><p>${run.lastError}</p></div></div>` : ""}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderTimeline() {
|
|
220
|
+
const infos = STAGES.map((s) => stageInfo(run, s)).filter(Boolean);
|
|
221
|
+
const body = $(root, "#timeline");
|
|
222
|
+
if (!infos.length) {
|
|
223
|
+
setHtml(body, emptyState("clock", "No stage has started", "Events show up here as soon as the first stage begins."));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const allEv = run.events || [];
|
|
227
|
+
const t0 = Math.min(...infos.map((i) => i.startAt));
|
|
228
|
+
const running = run.status === "leased" || infos.some((i) => i.open);
|
|
229
|
+
const t1 = running ? Date.now() : Math.max(...allEv.filter((e) => e.status !== "running").map((e) => ms(e.at)), t0);
|
|
230
|
+
const span = Math.max(1, t1 - t0);
|
|
231
|
+
const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => dur(span * f));
|
|
232
|
+
setHtml(body, html`<div class="axis"><span></span><div class="scale">${ticks.map((x) => html`<span>${x}</span>`)}</div><span></span><span></span></div>
|
|
233
|
+
${infos.map((i) => {
|
|
234
|
+
const left = ((i.startAt - t0) / span) * 100;
|
|
235
|
+
const end = i.open ? t1 : i.endAt;
|
|
236
|
+
const width = Math.max(0, ((end - i.startAt) / span) * 100);
|
|
237
|
+
const isOpen = open.has(i.stage);
|
|
238
|
+
const label = i.open ? dur(Date.now() - i.startAt) : dur(i.durationMs);
|
|
239
|
+
return html`<div class="tl-row" data-stage="${i.stage}">
|
|
240
|
+
<button type="button" class="tl-main" data-act="toggle" data-stage="${i.stage}" aria-expanded="${isOpen}" aria-label="${cap(i.stage)}, ${i.status}, ${label}">
|
|
241
|
+
<span class="tl-name">${i.stage}</span>
|
|
242
|
+
<span class="track"><span class="bar ${i.status}" data-bar="${i.stage}" style="left:${left.toFixed(2)}%;width:max(2px,${width.toFixed(2)}%)"></span></span>
|
|
243
|
+
<span class="tl-dur">${label}</span><span class="tl-chev">${icon("chevronRight")}</span></button>
|
|
244
|
+
${isOpen ? html`<div class="tl-events"><table><thead><tr><th>Time</th><th>Status</th><th>Attempt</th><th>Code</th><th>Message</th></tr></thead><tbody>
|
|
245
|
+
${i.events.map((e) => html`<tr><td class="nowrap mono" title="${abs(e.at)}">${new Date(e.at).toLocaleTimeString([], { hour12: false })}</td><td><span class="pill tone-${stageTone(e.status)}">${e.status}</span></td><td>${e.attempt}</td><td class="mono">${e.code || "—"}</td><td class="msg">${e.message || "—"}${e.durationMs != null ? html` <span class="muted">(${dur(e.durationMs)})</span>` : ""}</td></tr>`)}
|
|
246
|
+
</tbody></table></div>` : ""}</div>`;
|
|
247
|
+
})}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renderSide() {
|
|
251
|
+
const key = JSON.stringify([run.output, run.content, run.publish, run.publications, run.checks, run.id, run.scene]);
|
|
252
|
+
if (key === outKey) return;
|
|
253
|
+
outKey = key;
|
|
254
|
+
const c = run.content;
|
|
255
|
+
const out = run.output || {};
|
|
256
|
+
const video = out.mp4
|
|
257
|
+
? html`<div class="video-box"><video controls preload="metadata" src="/media/runs/${run.id}.mp4" playsinline></video></div>`
|
|
258
|
+
: html`<div class="video-ph">${icon("film")}<p>The render has not produced a file yet.</p></div>`;
|
|
259
|
+
const transcript = out.transcript ? html`<details class="fold" id="transcript"><summary>Transcript</summary><pre id="transcript-body">Loading…</pre></details>` : "";
|
|
260
|
+
const content = c ? html`<div class="kv">
|
|
261
|
+
<div><p class="k">Title</p><p class="v">${c.title}</p></div>
|
|
262
|
+
<div><p class="k">Hook</p><p class="v">${c.hook}</p></div>
|
|
263
|
+
<div><p class="k">Hashtags</p><div class="chips">${(c.hashtags || []).map((h) => html`<span class="chip">${h}</span>`)}</div></div></div>`
|
|
264
|
+
: html`<p class="muted">No script yet.</p>`;
|
|
265
|
+
const pubs = (run.publish || []).map((p) => {
|
|
266
|
+
const pub = (run.publications || []).find((x) => x.platform === p.platform);
|
|
267
|
+
const tone = p.errorCode === "missing_key" ? "skip" : p.externalId ? "ok" : p.errorCode ? "block" : "wait";
|
|
268
|
+
return html`<div class="pub"><div class="row"><span>${platformName(p.platform)}</span><span class="pill tone-${tone} plain">${cap(String(p.phase).replace(/_/g, " "))}</span></div>
|
|
269
|
+
<p class="detail">${publishText(p)}${p.errorMessage && p.errorCode === "missing_key" ? html` <span class="muted">· ${p.errorMessage}</span>` : ""}</p>
|
|
270
|
+
${pub ? html`<p class="detail">Disclosure: ${disclosureText(pub.disclosure)}</p>` : ""}</div>`;
|
|
271
|
+
});
|
|
272
|
+
const orphan = (run.publications || []).filter((x) => !(run.publish || []).some((p) => p.platform === x.platform))
|
|
273
|
+
.map((x) => html`<div class="pub"><div class="row"><span>${platformName(x.platform)}</span><span class="pill tone-ok plain">Published</span></div><p class="detail">Published · ${x.externalId || "no id"}</p><p class="detail">Disclosure: ${disclosureText(x.disclosure)}</p></div>`);
|
|
274
|
+
const sc = run.scene;
|
|
275
|
+
const preview = sc && sc.source === "agent" && sc.kind === "html" && sc.dir
|
|
276
|
+
? html`<section class="panel" id="preview" aria-label="Live preview"><div class="panel-head"><h2>Preview</h2><span class="sub">Plays the scene with its voice. Captions and the AI label are as they will be rendered.</span></div><div class="panel-body"><div class="preview-box"><iframe title="Scene preview" data-preview src="/preview/runs/${run.id}/" sandbox="allow-scripts" allow="autoplay" loading="lazy"></iframe></div></div></section>`
|
|
277
|
+
: sc && sc.source === "agent" && sc.kind === "tsx"
|
|
278
|
+
? html`<section class="panel" id="preview"><div class="panel-head"><h2>Preview</h2></div><div class="panel-body"><p class="muted" data-preview-note>Live preview is for HTML scenes. This is a Remotion scene, so it keeps the frame preview: the rendered video below.</p></div></section>`
|
|
279
|
+
: "";
|
|
280
|
+
setHtml($(root, "#side"), html`${preview}<section class="panel"><div class="panel-head"><h2>Output</h2><span class="sub">${[out.durationSec != null ? `${out.durationSec} s` : "", out.width && out.height ? `${out.width}×${out.height}` : "", out.bytes ? `${(out.bytes / 1048576).toFixed(1)} MB` : ""].filter(Boolean).join(" · ")}</span></div><div class="panel-body stack" style="gap:14px">${video}${transcript}</div></section>`);
|
|
281
|
+
const checks = run.checks || [];
|
|
282
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
283
|
+
const checkRows = checks.map((c) => html`<li class="check" data-check="${c.id}" data-status="${c.status}">
|
|
284
|
+
<div class="row"><span class="mono">${c.id}</span><span class="pill tone-${c.status === "pass" ? "ok" : c.status === "skipped" ? "skip" : c.severity === "compliance" ? "block" : "rej"} plain">${c.status === "pass" ? "Pass" : c.status === "skipped" ? "Skipped" : c.severity === "compliance" ? "Fail · compliance" : "Fail · quality"}</span></div>
|
|
285
|
+
<p class="detail">${c.message}</p>
|
|
286
|
+
${c.fix ? html`<p class="detail" data-fix><b>Fix:</b> ${c.fix}</p>` : ""}</li>`);
|
|
287
|
+
setHtml($(root, "#below"), html`
|
|
288
|
+
<section class="panel" id="checks"><div class="panel-head"><h2>Checks</h2><span class="sub">${checks.length ? `${checks.length - failed} of ${checks.length} passed` : ""}</span></div><div class="panel-body">${checks.length ? html`<ul class="checks" style="list-style:none;margin:0;padding:0;display:grid;gap:12px">${checkRows}</ul>` : html`<p class="muted">No checks ran for this run yet.</p>`}</div></section>
|
|
289
|
+
<section class="panel"><div class="panel-head"><h2>Content</h2></div><div class="panel-body">${content}</div></section>
|
|
290
|
+
<section class="panel"><div class="panel-head"><h2>Publish</h2></div><div class="panel-body">${pubs.length || orphan.length ? html`${pubs}${orphan}` : html`<p class="muted">Nothing has reached the publish stage.</p>`}</div></section>`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function act(name, el) {
|
|
294
|
+
if (name === "toggle") {
|
|
295
|
+
const st = el.dataset.stage; open.has(st) ? open.delete(st) : open.add(st); renderTimeline();
|
|
296
|
+
$(root, `[data-act="toggle"][data-stage="${st}"]`)?.focus();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (name === "studio") {
|
|
300
|
+
// The tab opens at once (a popup after an await is blocked) and is pointed at the studio when it is up.
|
|
301
|
+
const tab = window.open("about:blank", "_blank");
|
|
302
|
+
if (tab) tab.opener = null;
|
|
303
|
+
notice = null; el.disabled = true;
|
|
304
|
+
const r = await api("/api/studio", { run: id });
|
|
305
|
+
el.disabled = false;
|
|
306
|
+
if (!r.ok) { tab?.close(); notice = r; refusalToast(r); renderHead(); return; }
|
|
307
|
+
if (tab) tab.location.href = r.url;
|
|
308
|
+
notice = { refusal: "Editing a copy.", fix: `${r.message}${tab ? "" : ` Open ${r.url}`}` };
|
|
309
|
+
toast("Studio opened", { detail: r.dir });
|
|
310
|
+
renderHead();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (name === "cancel") { confirming = true; renderHead(); return; }
|
|
314
|
+
if (name === "cancel-no") { confirming = false; renderHead(); return; }
|
|
315
|
+
const verb = name === "retry" ? "retry" : "cancel";
|
|
316
|
+
confirming = false; notice = null;
|
|
317
|
+
el.disabled = true;
|
|
318
|
+
const r = await api(`/api/jobs/${encodeURIComponent(id)}/${verb}`, {});
|
|
319
|
+
if (!r.ok) { notice = r; refusalToast(r); } else toast(verb === "retry" ? "Run queued again" : "Run cancelled");
|
|
320
|
+
await load(); store.refresh?.();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
title: "Run",
|
|
325
|
+
mount(el, params, c) {
|
|
326
|
+
root = el; ctx = c; id = params.id;
|
|
327
|
+
setHtml(el, html`<div id="head"></div><div class="run-grid"><div class="stack"><section class="panel" aria-label="Timeline"><div class="panel-head"><h2>Timeline</h2><span class="sub">Stages on one time axis</span></div><div id="timeline"></div></section><div id="below" class="stack"></div></div><div id="side"></div></div>`);
|
|
328
|
+
el.addEventListener("click", (e) => {
|
|
329
|
+
const a = e.target.closest("[data-act]");
|
|
330
|
+
if (a) act(a.dataset.act, a);
|
|
331
|
+
});
|
|
332
|
+
el.addEventListener("toggle", async (e) => {
|
|
333
|
+
if (e.target.id === "transcript" && e.target.open && !e.target.dataset.loaded) {
|
|
334
|
+
e.target.dataset.loaded = "1";
|
|
335
|
+
try {
|
|
336
|
+
const res = await fetch(`/media/runs/${encodeURIComponent(id)}.txt`);
|
|
337
|
+
$(el, "#transcript-body").textContent = res.ok ? await res.text() : "No transcript file.";
|
|
338
|
+
} catch { $(el, "#transcript-body").textContent = "Transcript unavailable."; }
|
|
339
|
+
}
|
|
340
|
+
}, true);
|
|
341
|
+
load();
|
|
342
|
+
},
|
|
343
|
+
update(s) {
|
|
344
|
+
const job = (s.jobs || []).find((j) => j.id === id);
|
|
345
|
+
if (!run) return;
|
|
346
|
+
const sig = job ? `${job.status}|${job.updatedAt}|${(job.events || []).length}` : "";
|
|
347
|
+
if (!TERMINAL.has(run.status) || sig !== lastSig) { lastSig = sig; load(); } else renderHead();
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Autopilot screens (spec 015 B): Publications, Sources, Engage, Trends.
|
|
2
|
+
// Each reads its own /api route. Unknown values stay "—" with a label; nothing is estimated.
|
|
3
|
+
import { html, icon, setHtml, $, read, api, toast, refusalToast, rel, abs } from "./core.js";
|
|
4
|
+
import { emptyState, platformName, when } from "./components.js";
|
|
5
|
+
import { DASH, compact, TREND_REASONS } from "./metrics-math.js";
|
|
6
|
+
|
|
7
|
+
const OFFLINE = { ok: false, refusal: "The desk could not be reached.", fix: "Check that `npx vfrey dashboard` is still running." };
|
|
8
|
+
const load = (path) => read(path).catch(() => OFFLINE);
|
|
9
|
+
const safeUrl = (u) => (/^https:\/\//i.test(String(u || "")) ? u : "");
|
|
10
|
+
const head = (eyebrow, title, sub) => html`<div class="page-head"><div><span class="eyebrow">Desk / ${eyebrow}</span><h1>${title}</h1><p class="sub">${sub}</p></div></div>`;
|
|
11
|
+
const failed = (r) => html`<div class="panel" data-testid="screen-error">${emptyState("alert", "Could not load this screen", r.refusal || "Something went wrong.", html`<p class="hint">${r.fix || ""}</p>`)}</div>`;
|
|
12
|
+
|
|
13
|
+
// A view that refetches on every state poll and on demand.
|
|
14
|
+
function screen({ title, eyebrow, heading, sub, path, draw, onMount }) {
|
|
15
|
+
let root; let data = null; let busy = false; const ui = {};
|
|
16
|
+
const body = () => $(root, "#sc-body");
|
|
17
|
+
const refresh = async () => {
|
|
18
|
+
if (busy || !root) return;
|
|
19
|
+
busy = true;
|
|
20
|
+
const r = await load(typeof path === "function" ? path(ui) : path);
|
|
21
|
+
busy = false;
|
|
22
|
+
if (!root) return;
|
|
23
|
+
data = r;
|
|
24
|
+
paint();
|
|
25
|
+
};
|
|
26
|
+
const paint = () => { if (!root || !data) return; setHtml(body(), data.ok ? draw(data, ui) : failed(data)); };
|
|
27
|
+
return {
|
|
28
|
+
title,
|
|
29
|
+
mount(el) {
|
|
30
|
+
root = el;
|
|
31
|
+
setHtml(el, html`${head(eyebrow, heading, sub)}<div id="sc-body"><div class="panel"><p class="muted" style="padding:20px">Loading…</p></div></div>`);
|
|
32
|
+
onMount?.(el, ui, { refresh, paint, get: () => data });
|
|
33
|
+
refresh();
|
|
34
|
+
},
|
|
35
|
+
update() { refresh(); },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------- Publications ----------
|
|
40
|
+
export function publicationsView() {
|
|
41
|
+
const select = (id, label, options, value) => html`<label class="field"><span class="muted">${label}</span><select id="${id}" aria-label="${label}"><option value="">All</option>${options.map(([v, l]) => html`<option value="${v}" ${v === value ? "selected" : ""}>${l}</option>`)}</select></label>`;
|
|
42
|
+
return screen({
|
|
43
|
+
title: "Publications",
|
|
44
|
+
eyebrow: "Publications",
|
|
45
|
+
heading: "Publications",
|
|
46
|
+
sub: "Every video sent to a platform, and every dry run that was not. The disclosure column is the request field and value, not a promise.",
|
|
47
|
+
path: (ui) => `/api/publications?${new URLSearchParams(Object.entries({ series: ui.series || "", platform: ui.platform || "" }).filter(([, v]) => v))}`,
|
|
48
|
+
onMount(el, ui, { refresh }) {
|
|
49
|
+
ui.series = ""; ui.platform = "";
|
|
50
|
+
el.addEventListener("change", (e) => {
|
|
51
|
+
if (e.target.id === "f-series") ui.series = e.target.value;
|
|
52
|
+
else if (e.target.id === "f-platform") ui.platform = e.target.value;
|
|
53
|
+
else return;
|
|
54
|
+
refresh();
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
draw(d, ui) {
|
|
58
|
+
const filters = html`<div class="add-row screen-filters">${select("f-series", "Series", d.series.map((s) => [s, s]), ui.series)}${select("f-platform", "Platform", d.platforms.map((p) => [p, platformName(p)]), ui.platform)}</div>`;
|
|
59
|
+
if (!d.publications.length) return html`${filters}<div class="panel">${emptyState("film", "No publications", ui.series || ui.platform ? "Nothing matches these filters." : "Nothing has been published yet. A series in dry-run records a dry-run publication here.")}</div>`;
|
|
60
|
+
const row = (p) => html`<tr data-publication="${p.id}" data-status="${p.status}">
|
|
61
|
+
<td><span class="t">${p.title || "(untitled)"}</span><span class="gap">${p.seriesId} · <a href="/runs/${p.jobId}" data-link>run ${String(p.jobId).slice(0, 8)}</a></span></td>
|
|
62
|
+
<td>${platformName(p.platform)}</td>
|
|
63
|
+
<td>${p.dryRun ? html`<span class="chip warn" data-badge="dry-run">Dry run</span>` : html`<span class="chip ok">Published</span>`}</td>
|
|
64
|
+
<td data-cell="external">${p.dryRun ? html`<span class="muted">${DASH}</span><span class="gap">nothing was uploaded</span>`
|
|
65
|
+
: safeUrl(p.externalUrl) ? html`<a href="${safeUrl(p.externalUrl)}" target="_blank" rel="noopener noreferrer">${icon("external")} ${p.externalId}</a>`
|
|
66
|
+
: html`<span class="mono">${p.externalId || DASH}</span><span class="gap">${platformName(p.platform)} gives no public link for this id</span>`}</td>
|
|
67
|
+
<td data-cell="disclosure">${p.disclosure.field ? html`<code>${p.disclosure.field} = ${String(p.disclosure.value)}</code><span class="gap" data-sent="${p.disclosureSent}">${p.disclosureSent ? "sent with the upload" : "would be sent (dry run, not sent)"}</span>` : html`<span class="muted">${DASH}</span><span class="gap">no proof stored</span>`}</td>
|
|
68
|
+
<td class="nowrap">${when(p.publishedAt)}</td></tr>`;
|
|
69
|
+
return html`${filters}<div class="table-wrap"><table><thead><tr><th>Video</th><th>Platform</th><th>Status</th><th>External link</th><th>Disclosure proof</th><th>Time</th></tr></thead><tbody>${d.publications.map(row)}</tbody></table></div>`;
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------- Sources ----------
|
|
75
|
+
const KINDS = { rss: "RSS feed", subreddit: "Subreddit", youtubeChannel: "YouTube channel", youtubeTrending: "YouTube trending", "vfrey-topics": "vfrey Topics (niche id)" };
|
|
76
|
+
export function sourcesView() {
|
|
77
|
+
return screen({
|
|
78
|
+
title: "Sources",
|
|
79
|
+
eyebrow: "Sources",
|
|
80
|
+
heading: "Sources",
|
|
81
|
+
sub: "What each series reads for ideas, and what the last read returned.",
|
|
82
|
+
path: "/api/sources",
|
|
83
|
+
onMount(el, ui, { refresh, paint }) {
|
|
84
|
+
ui.notice = null;
|
|
85
|
+
el.addEventListener("submit", async (e) => {
|
|
86
|
+
if (e.target.id !== "src-form") return;
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const f = new FormData(e.target);
|
|
89
|
+
const r = await api(`/api/series/${encodeURIComponent(f.get("series"))}/sources`, { kind: f.get("kind"), value: String(f.get("value") || "").trim() });
|
|
90
|
+
if (!r.ok) { ui.notice = r; paint(); return refusalToast(r); }
|
|
91
|
+
ui.notice = null; toast("Source added"); await refresh();
|
|
92
|
+
});
|
|
93
|
+
el.addEventListener("click", async (e) => {
|
|
94
|
+
const b = e.target.closest("[data-remove]"); if (!b) return;
|
|
95
|
+
b.disabled = true;
|
|
96
|
+
const r = await api(`/api/sources/${encodeURIComponent(b.dataset.remove)}/delete`, {});
|
|
97
|
+
if (!r.ok) { b.disabled = false; return refusalToast(r); }
|
|
98
|
+
toast("Source removed"); await refresh();
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
draw(d, ui) {
|
|
102
|
+
const result = (x) => x.lastCode ? html`<span class="chip bad" data-result="error" data-code="${x.lastCode}">${x.lastCode}</span>`
|
|
103
|
+
: x.lastCount != null ? html`<span class="chip ok" data-result="ok">${x.lastCount} candidate${x.lastCount === 1 ? "" : "s"}</span>`
|
|
104
|
+
: html`<span class="muted" data-result="none">Not fetched yet</span>`;
|
|
105
|
+
const warn = d.warnings.length ? html`<div class="notice" role="alert" data-testid="topics-warning"><b>topics_unavailable</b> ${d.warnings[0].message}${d.warnings.length > 1 ? ` (${d.warnings.length - 1} more)` : ""} Other sources still ran. See <a href="/warnings" data-link>Warnings</a>.</div>` : "";
|
|
106
|
+
const form = d.series.length ? html`<form class="add-row src-form" id="src-form"><select name="series" aria-label="Series">${d.series.map((s) => html`<option value="${s}">${s}</option>`)}</select>
|
|
107
|
+
<select name="kind" aria-label="Source kind">${Object.entries(KINDS).map(([k, l]) => html`<option value="${k}">${l}</option>`)}</select>
|
|
108
|
+
<input type="text" name="value" required aria-label="Source value" placeholder="https://… or r/subreddit or channel id or niche id" autocomplete="off"><button type="submit" class="btn">${icon("plus")}Add source</button></form>
|
|
109
|
+
${ui.notice ? html`<div class="notice" role="alert" data-testid="source-refusal" data-code="${ui.notice.code || ""}"><b>${ui.notice.refusal}</b> ${ui.notice.fix || ""}</div>` : ""}` : "";
|
|
110
|
+
const groups = d.series.length ? d.series.map((s) => {
|
|
111
|
+
const list = d.sources.filter((x) => x.seriesId === s);
|
|
112
|
+
return html`<section class="panel" data-series="${s}"><div class="panel-head"><h2>${s}</h2><span class="sub">${list.length} source${list.length === 1 ? "" : "s"}</span></div>
|
|
113
|
+
${list.length ? html`<div class="table-wrap" style="border:0"><table><thead><tr><th>Kind</th><th>Value</th><th>Last fetch</th><th>Last result</th><th></th></tr></thead><tbody>${list.map((x) => html`<tr data-source="${x.id}"><td><span class="chip">${KINDS[x.kind] || x.kind}</span></td><td class="mono" style="word-break:break-all">${x.value}</td>
|
|
114
|
+
<td class="nowrap">${x.lastFetchAt ? when(x.lastFetchAt) : html`<span class="muted">${DASH}</span>`}</td><td>${result(x)}</td>
|
|
115
|
+
<td class="nowrap"><button type="button" class="btn ghost sm" data-remove="${x.id}" aria-label="Remove ${x.value}">${icon("trash")}Remove</button></td></tr>`)}</tbody></table></div>`
|
|
116
|
+
: html`<p class="muted" style="padding:20px">No source yet. Add one so the series has something to write about.</p>`}</section>`;
|
|
117
|
+
}) : emptyState("series", "No series yet", "Create a series first, then add its sources here.");
|
|
118
|
+
return html`<div class="stack">${warn}<section class="panel"><div class="panel-head"><h2>Add a source</h2><span class="sub">Feed addresses must start with http:// or https://</span></div><div class="panel-body">${form}</div></section>${groups}</div>`;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------- Engage ----------
|
|
124
|
+
const STATUS = { replied: ["Replied", "ok"], drafted: ["Drafted, not sent", "warn"], skipped: ["Skipped", ""], blocked: ["Blocked", "bad"] };
|
|
125
|
+
export function engageView() {
|
|
126
|
+
return screen({
|
|
127
|
+
title: "Engage",
|
|
128
|
+
eyebrow: "Engage",
|
|
129
|
+
heading: "Engage",
|
|
130
|
+
sub: "Comment replies the engage pass handled, newest first.",
|
|
131
|
+
path: "/api/engage",
|
|
132
|
+
draw(d) {
|
|
133
|
+
const note = html`<p class="hint" data-testid="tiktok-note" style="margin:0 0 16px">${d.note}</p>`;
|
|
134
|
+
if (!d.replies.length) return html`${note}<div class="panel">${emptyState("clock", "No replies yet", "Replies appear after the engage pass has handled comments on a published video. A series that is not live drafts replies without sending them.")}</div>`;
|
|
135
|
+
const row = (r) => { const [label, tone] = STATUS[r.status] || [r.status, ""]; return html`<tr data-reply="${r.commentId}" data-status="${r.status}"><td>${platformName(r.platform)}<span class="gap">${r.seriesId}</span></td>
|
|
136
|
+
<td>${r.comment ? r.comment : html`<span class="muted">${DASH}</span><span class="gap">comment text was not stored</span>`}</td>
|
|
137
|
+
<td>${r.reply ? r.reply : html`<span class="muted">${DASH}</span>`}</td>
|
|
138
|
+
<td><span class="chip ${tone}">${label}</span>${r.code ? html`<span class="gap">${r.code}</span>` : ""}</td>
|
|
139
|
+
<td class="nowrap">${when(r.at)}</td></tr>`; };
|
|
140
|
+
return html`${note}<div class="table-wrap"><table><thead><tr><th>Platform</th><th>Comment</th><th>Reply</th><th>Status</th><th>Time</th></tr></thead><tbody>${d.replies.map(row)}</tbody></table></div>`;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------- Trends ----------
|
|
146
|
+
const signed = (n) => (n === null ? DASH : `${n > 0 ? "+" : n < 0 ? "−" : ""}${compact(Math.abs(n))}`);
|
|
147
|
+
export function trendsView() {
|
|
148
|
+
return screen({
|
|
149
|
+
title: "Trends",
|
|
150
|
+
eyebrow: "Trends",
|
|
151
|
+
heading: "Trends",
|
|
152
|
+
sub: "Views gained between the two latest readings of each post. A dash means it cannot be known; nothing is estimated.",
|
|
153
|
+
path: "/api/trends",
|
|
154
|
+
draw(d) {
|
|
155
|
+
if (!d.trends.length) return html`<div class="panel">${emptyState("metrics", "No trends yet", "Trends appear once a published post has been measured twice. Run analytics again after it has been live for a while.")}</div>`;
|
|
156
|
+
const row = (t) => html`<tr data-trend="${t.seriesId}:${t.platform}"><td>${t.seriesId}</td><td>${platformName(t.platform)}</td>
|
|
157
|
+
<td class="num"><span data-gained>${signed(t.gained)}</span>${t.gained === null ? html`<span class="gap" data-reason="${t.reason}">${TREND_REASONS[t.reason] || ""}</span>` : ""}</td>
|
|
158
|
+
<td class="num" data-posts>${t.known} of ${t.of} post${t.of === 1 ? "" : "s"}</td>
|
|
159
|
+
<td class="nowrap">${t.asOf ? html`<span title="${abs(t.asOf)}" data-asof>as of ${rel(t.asOf)}</span>${t.since ? html`<span class="gap">since ${rel(t.since)}</span>` : ""}` : html`<span class="muted">${DASH}</span>`}</td></tr>`;
|
|
160
|
+
return html`<div class="table-wrap"><table><thead><tr><th>Series</th><th>Platform</th><th class="num">Views gained</th><th class="num">Posts</th><th>As of</th></tr></thead><tbody>${d.trends.map(row)}</tbody></table></div>`;
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|