@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,188 @@
|
|
|
1
|
+
// Mail: draft outreach, read and edit a draft, then approve and send it as two separate, confirmed steps. Plus the sender identity.
|
|
2
|
+
// The browser talks only to this desk's own /api/mail routes. The vfrey token stays on the server.
|
|
3
|
+
import { html, icon, setHtml, $, read, api, toast, refusalToast } from "./core.js";
|
|
4
|
+
import { emptyState } from "./components.js";
|
|
5
|
+
|
|
6
|
+
const KINDS = [["creator_collab", "Creator collab"], ["sponsor", "Sponsor"], ["brand", "Brand"], ["client_update", "Client update"]];
|
|
7
|
+
const STATUSES = ["", "draft", "approved", "sent", "failed", "discarded"];
|
|
8
|
+
const TONE = { draft: "", approved: "warn", sent: "ok", failed: "bad", discarded: "" };
|
|
9
|
+
const offline = () => ({ ok: false, refusal: "The desk could not be reached.", fix: "Check that `npx vfrey dashboard` is still running.", code: "offline" });
|
|
10
|
+
const get = (path) => read(path).catch(offline);
|
|
11
|
+
const day = (iso) => (iso ? String(iso).slice(0, 16).replace("T", " ") + " UTC" : "—");
|
|
12
|
+
const who = (d) => (d.toName ? `${d.toName} <${d.to}>` : d.to || "(no recipient yet)");
|
|
13
|
+
const BLOCKING = ["not_signed_in", "unauthorized", "not_live", "offline", "plan_required"];
|
|
14
|
+
|
|
15
|
+
export function mailView() {
|
|
16
|
+
let root;
|
|
17
|
+
const m = {
|
|
18
|
+
drafts: null, status: "", open: null, edit: null, identity: undefined, identityOpen: false, error: null,
|
|
19
|
+
confirm: null, busy: false, composing: false, form: { kind: "creator_collab", need: "", to: "", platform: "tiktok", handle: "", tone: "" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const stateBox = (err) => {
|
|
23
|
+
const out = err.code === "not_signed_in" || err.code === "unauthorized" || err.code === "not_live";
|
|
24
|
+
const cmd = out ? html`<p><code data-testid="login-command">npx vfrey login</code></p>` : "";
|
|
25
|
+
return html`<div class="panel" data-testid="mail-state" data-state="${err.code || "error"}">${emptyState("alert", out ? "Sign in to use Mail" : err.code === "plan_required" ? "Mail needs the Mail add-on" : "Mail is not available", err.refusal || "Something went wrong.", html`${cmd}<p class="hint">${err.fix || ""}</p>`)}</div>`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const identityPanel = () => {
|
|
29
|
+
const i = m.identity;
|
|
30
|
+
if (i === undefined) return html`<section class="panel"><p class="muted" style="padding:20px">Loading identity…</p></section>`;
|
|
31
|
+
const f = i || { fromName: "", senderOrg: "", replyTo: "", postalAddress: "", verified: false };
|
|
32
|
+
const summary = i
|
|
33
|
+
? html`<span data-testid="identity-state" data-verified="${i.verified ? "true" : "false"}" class="chip ${i.verified ? "ok" : "warn"}">${i.verified ? "verified" : "not verified"}</span> <span class="sub">${i.fromName} · replies to ${i.replyTo}</span>`
|
|
34
|
+
: html`<span data-testid="identity-state" data-verified="none" class="chip warn">not set up</span> <span class="sub">Every message carries your name and postal address.</span>`;
|
|
35
|
+
return html`<section class="panel" data-testid="identity-panel"><div class="panel-head"><h2>Sender identity</h2><div>${summary} <button type="button" class="btn ghost sm" data-identity-toggle>${m.identityOpen ? "Hide" : i ? "Edit" : "Set up"}</button></div></div>
|
|
36
|
+
${i && !i.verified ? html`<p class="hint" style="padding:0 20px">Nothing can be sent until the link we emailed to ${i.replyTo} is opened. Saving again sends a new link.</p>` : ""}
|
|
37
|
+
${m.identityOpen ? html`<div class="panel-body"><form id="m-identity" class="stack" style="gap:12px"><div class="add-row" style="grid-template-columns:repeat(auto-fit,minmax(180px,1fr))">
|
|
38
|
+
<label class="field"><span class="muted">From name</span><input type="text" name="fromName" aria-label="From name" value="${f.fromName}"></label>
|
|
39
|
+
<label class="field"><span class="muted">Organization</span><input type="text" name="org" aria-label="Organization" value="${f.senderOrg || ""}"></label>
|
|
40
|
+
<label class="field"><span class="muted">Reply-to email</span><input type="text" name="replyTo" aria-label="Reply-to email" value="${f.replyTo}"></label>
|
|
41
|
+
<label class="field"><span class="muted">Postal address</span><input type="text" name="postalAddress" aria-label="Postal address" value="${f.postalAddress}"></label></div>
|
|
42
|
+
<div><button type="submit" class="btn" ${m.busy ? "disabled" : ""}>Save identity</button> <span class="hint">A new reply-to address is emailed a confirmation link.</span></div></form></div>` : ""}</section>`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const composeForm = () => html`<form id="m-compose" class="stack" style="gap:12px"><div class="add-row" style="grid-template-columns:repeat(auto-fit,minmax(160px,1fr))">
|
|
46
|
+
<label class="field"><span class="muted">Kind</span><select name="kind" aria-label="Kind">${KINDS.map(([id, n]) => html`<option value="${id}" ${id === m.form.kind ? "selected" : ""}>${n}</option>`)}</select></label>
|
|
47
|
+
<label class="field"><span class="muted">To (email)</span><input type="text" name="to" aria-label="To" placeholder="optional for a creator with a public contact" value="${m.form.to}"></label>
|
|
48
|
+
<label class="field"><span class="muted">Creator platform</span><select name="platform" aria-label="Creator platform">${["tiktok", "instagram", "youtube"].map((p) => html`<option value="${p}" ${p === m.form.platform ? "selected" : ""}>${p}</option>`)}</select></label>
|
|
49
|
+
<label class="field"><span class="muted">Creator handle</span><input type="text" name="handle" aria-label="Creator handle" placeholder="ana.cooks" value="${m.form.handle}"></label>
|
|
50
|
+
<label class="field"><span class="muted">Tone</span><input type="text" name="tone" aria-label="Tone" placeholder="friendly" value="${m.form.tone}"></label></div>
|
|
51
|
+
<label class="field"><span class="muted">What should it say</span><textarea name="need" aria-label="What should it say" rows="3">${m.form.need}</textarea></label>
|
|
52
|
+
<div><button type="submit" class="btn" ${m.busy ? "disabled" : ""}>${icon("plus")}Write draft</button> <span class="hint">Writing a draft is free and sends nothing.</span></div></form>`;
|
|
53
|
+
|
|
54
|
+
const list = () => {
|
|
55
|
+
if (!m.drafts) return html`<p class="muted" style="padding:20px">Loading drafts…</p>`;
|
|
56
|
+
const rows = m.drafts.length ? m.drafts.map((d) => html`<div class="list-row" data-draft="${d.id}"><div class="grow"><p class="t"><b>${d.subject}</b></p><p class="sub">${who(d)} · ${d.kind}</p></div><span class="chip ${TONE[d.status] || ""}" data-status>${d.status}</span><button type="button" class="btn secondary sm" data-open="${d.id}">Open</button></div>`)
|
|
57
|
+
: html`<p class="muted" style="padding:20px" data-testid="mail-empty">No drafts${m.status ? ` with status ${m.status}` : ""}. Press New draft to write one.</p>`;
|
|
58
|
+
return html`<section class="panel"><div class="panel-head"><h2>Drafts</h2><div><label class="sub">Show <select id="m-status" aria-label="Status">${STATUSES.map((s) => html`<option value="${s}" ${s === m.status ? "selected" : ""}>${s || "all"}</option>`)}</select></label> <button type="button" class="btn secondary sm" data-compose>${icon("plus")}New draft</button></div></div>${m.composing ? html`<div class="panel-body">${composeForm()}</div>` : ""}${rows}</section>`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const confirmBox = (d) => {
|
|
62
|
+
if (!m.confirm) return "";
|
|
63
|
+
const sending = m.confirm === "send";
|
|
64
|
+
return html`<div class="panel" role="alertdialog" aria-label="${sending ? "Confirm send" : "Confirm approve"}" data-testid="mail-confirm" data-action="${m.confirm}" style="border:2px solid currentColor;padding:16px"><p><b>${sending ? "Send this email now?" : "Approve this draft?"}</b></p>
|
|
65
|
+
<dl style="display:grid;grid-template-columns:max-content 1fr;gap:4px 16px;margin:8px 0"><dt class="muted">To</dt><dd data-testid="confirm-to">${d.to}</dd><dt class="muted">Subject</dt><dd data-testid="confirm-subject">${d.subject}</dd></dl>
|
|
66
|
+
<p class="sub">${sending ? "This sends a real email to that address and charges one send. It cannot be undone." : "Approving sends nothing. You still press Send as a separate step."}</p>
|
|
67
|
+
<button type="button" class="btn ${sending ? "danger" : ""}" data-confirm="${m.confirm}" ${m.busy ? "disabled" : ""}>${sending ? "Yes, send it" : "Yes, approve it"}</button> <button type="button" class="btn secondary" data-cancel ${m.busy ? "disabled" : ""}>Cancel</button></div>`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const detail = () => {
|
|
71
|
+
const d = m.open;
|
|
72
|
+
const e = m.edit;
|
|
73
|
+
const editable = ["draft", "approved", "failed"].includes(d.status);
|
|
74
|
+
const dirty = editable && (e.to !== d.to || e.subject !== d.subject || e.text !== d.text);
|
|
75
|
+
const canApprove = (d.status === "draft" || d.status === "failed") && !dirty;
|
|
76
|
+
const canSend = d.status === "approved" && !dirty && m.identity?.verified;
|
|
77
|
+
return html`<div class="stack" data-testid="mail-detail" data-status="${d.status}"><p><button type="button" class="btn secondary sm" data-back>Back to drafts</button></p>
|
|
78
|
+
<section class="panel"><div class="panel-head"><div><h2>${d.subject}</h2><span class="sub">${d.kind}${d.creatorRef ? ` · ${d.creatorRef}` : ""} · written ${day(d.createdAt)}</span></div><span class="chip ${TONE[d.status] || ""}" data-testid="detail-status">${d.status}</span></div>
|
|
79
|
+
<div class="panel-body stack" style="gap:12px">
|
|
80
|
+
${d.status === "approved" ? html`<p class="sub">Approved ${day(d.approvedAt)}. It has not been sent. Editing it takes the approval away.</p>` : ""}
|
|
81
|
+
${d.status === "sent" ? html`<p class="sub" data-testid="sent-note">Sent ${day(d.sentAt)}${d.deliveryStatus ? ` · ${d.deliveryStatus}` : ""}.</p>` : ""}
|
|
82
|
+
${d.status === "failed" ? html`<p class="sub">The provider did not take it${d.error ? `: ${d.error}` : ""}. Nothing was charged. Approve it again to retry.</p>` : ""}
|
|
83
|
+
<form id="m-edit" class="stack" style="gap:12px">
|
|
84
|
+
<label class="field"><span class="muted">To</span><input type="text" name="to" aria-label="Recipient" value="${e.to}" ${editable ? "" : "disabled"}></label>
|
|
85
|
+
<label class="field"><span class="muted">Subject</span><input type="text" name="subject" aria-label="Subject" value="${e.subject}" ${editable ? "" : "disabled"}></label>
|
|
86
|
+
<label class="field"><span class="muted">Message</span><textarea name="text" aria-label="Message" rows="14" ${editable ? "" : "disabled"}>${e.text}</textarea></label>
|
|
87
|
+
${editable ? html`<div><button type="submit" class="btn secondary" ${dirty && !m.busy ? "" : "disabled"}>Save changes</button> ${dirty ? html`<span class="hint">Save your changes before approving or sending.</span>` : ""}</div>` : ""}
|
|
88
|
+
</form>
|
|
89
|
+
${m.confirm ? "" : html`<div data-testid="mail-actions"><button type="button" class="btn" data-ask="approve" ${canApprove && !m.busy ? "" : "disabled"}>Approve</button> <button type="button" class="btn danger" data-ask="send" ${canSend && !m.busy ? "" : "disabled"}>Send</button> ${d.status !== "sent" && d.status !== "discarded" ? html`<button type="button" class="btn ghost" data-discard ${m.busy ? "disabled" : ""}>Discard</button>` : ""}
|
|
90
|
+
<p class="hint">Approve and Send are separate steps. ${d.status === "approved" && !m.identity?.verified ? "Sending waits until your reply-to address is confirmed." : "Send unlocks once the draft is approved."}</p></div>`}
|
|
91
|
+
${confirmBox(d)}
|
|
92
|
+
</div></section></div>`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const draw = () => {
|
|
96
|
+
if (!root) return;
|
|
97
|
+
if (m.error) { setHtml($(root, "#m-body"), stateBox(m.error)); return; }
|
|
98
|
+
const keep = document.activeElement?.id;
|
|
99
|
+
setHtml($(root, "#m-body"), html`${identityPanel()}${m.open ? detail() : list()}`);
|
|
100
|
+
if (keep) document.getElementById(keep)?.focus();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const fail = (r) => { if (BLOCKING.includes(r.code)) { m.error = r; draw(); } else refusalToast(r); };
|
|
104
|
+
|
|
105
|
+
const loadDrafts = async () => {
|
|
106
|
+
const r = await get(`/api/mail/drafts${m.status ? `?status=${m.status}` : ""}`);
|
|
107
|
+
if (!r.ok) { m.drafts = []; fail(r); return; }
|
|
108
|
+
m.drafts = r.drafts; draw();
|
|
109
|
+
};
|
|
110
|
+
const loadIdentity = async () => {
|
|
111
|
+
const r = await get("/api/mail/identity");
|
|
112
|
+
if (!r.ok) { m.identity = null; fail(r); return; }
|
|
113
|
+
m.identity = r.identity; if (!r.identity) m.identityOpen = true; draw();
|
|
114
|
+
};
|
|
115
|
+
const show = (d) => { m.open = d; m.edit = { to: d.to, subject: d.subject, text: d.text }; m.confirm = null; draw(); };
|
|
116
|
+
const refresh = (d) => { show(d); loadDrafts(); };
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
title: "Mail",
|
|
120
|
+
mount(el) {
|
|
121
|
+
root = el;
|
|
122
|
+
setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Mail</span><h1>Mail</h1><p class="sub">Write outreach, then approve and send each message yourself. Nothing is sent without you.</p></div></div><div id="m-body"></div>`);
|
|
123
|
+
el.addEventListener("input", (e) => {
|
|
124
|
+
const n = e.target.name; const form = e.target.form?.id;
|
|
125
|
+
if (form === "m-compose" && n in m.form) m.form[n] = e.target.value;
|
|
126
|
+
if (form === "m-edit" && m.edit && n in m.edit) { m.edit[n] = e.target.value; const d = m.open; const dirty = m.edit.to !== d.to || m.edit.subject !== d.subject || m.edit.text !== d.text; const f = e.target.form; for (const b of f.parentElement.querySelectorAll("[data-ask]")) b.disabled = true; const save = f.querySelector("button[type=submit]"); if (save) save.disabled = !dirty; }
|
|
127
|
+
});
|
|
128
|
+
el.addEventListener("change", (e) => {
|
|
129
|
+
if (e.target.id === "m-status") { m.status = e.target.value; m.drafts = null; draw(); loadDrafts(); return; }
|
|
130
|
+
if (e.target.form?.id === "m-compose" && e.target.name in m.form) m.form[e.target.name] = e.target.value;
|
|
131
|
+
});
|
|
132
|
+
el.addEventListener("submit", async (e) => {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
const id = e.target.id;
|
|
135
|
+
if (id === "m-compose") {
|
|
136
|
+
m.busy = true; draw();
|
|
137
|
+
const r = await api("/api/mail/draft", { kind: m.form.kind, need: m.form.need, to: m.form.to, tone: m.form.tone, platform: m.form.platform, handle: m.form.handle });
|
|
138
|
+
m.busy = false;
|
|
139
|
+
if (!r.ok) { refusalToast(r); draw(); return; }
|
|
140
|
+
m.composing = false; m.form.need = ""; m.form.handle = ""; m.form.to = "";
|
|
141
|
+
toast("Draft written", { detail: "Nothing was sent." }); refresh(r.draft);
|
|
142
|
+
} else if (id === "m-edit") {
|
|
143
|
+
m.busy = true; draw();
|
|
144
|
+
const r = await api("/api/mail/edit", { id: m.open.id, ...m.edit });
|
|
145
|
+
m.busy = false;
|
|
146
|
+
if (!r.ok) { refusalToast(r); draw(); return; }
|
|
147
|
+
toast("Saved", { detail: r.draft.status === "draft" ? "It needs approving again." : "" }); refresh(r.draft);
|
|
148
|
+
} else if (id === "m-identity") {
|
|
149
|
+
const data = Object.fromEntries(new FormData(e.target));
|
|
150
|
+
m.busy = true; draw();
|
|
151
|
+
const r = await api("/api/mail/identity", data);
|
|
152
|
+
m.busy = false;
|
|
153
|
+
if (!r.ok) { refusalToast(r); draw(); return; }
|
|
154
|
+
m.identity = r.identity; m.identityOpen = false;
|
|
155
|
+
toast("Identity saved", { detail: r.verificationEmailed ? `We emailed a confirmation link to ${r.identity.replyTo}.` : "" }); draw();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
el.addEventListener("click", async (e) => {
|
|
159
|
+
const t = e.target.closest("button"); if (!t) return;
|
|
160
|
+
if (t.hasAttribute("data-identity-toggle")) { m.identityOpen = !m.identityOpen; draw(); return; }
|
|
161
|
+
if (t.hasAttribute("data-compose")) { m.composing = !m.composing; draw(); return; }
|
|
162
|
+
if (t.dataset.open) { const r = await get(`/api/mail/draft?id=${encodeURIComponent(t.dataset.open)}`); if (!r.ok) { fail(r); return; } show(r.draft); return; }
|
|
163
|
+
if (t.hasAttribute("data-back")) { m.open = null; m.confirm = null; draw(); loadDrafts(); return; }
|
|
164
|
+
if (t.dataset.ask) { m.confirm = t.dataset.ask; draw(); return; }
|
|
165
|
+
if (t.hasAttribute("data-cancel")) { m.confirm = null; draw(); return; }
|
|
166
|
+
if (t.dataset.confirm && m.open) {
|
|
167
|
+
const action = t.dataset.confirm;
|
|
168
|
+
m.busy = true; draw();
|
|
169
|
+
const r = await api(`/api/mail/${action}`, { id: m.open.id, confirm: true });
|
|
170
|
+
m.busy = false; m.confirm = null;
|
|
171
|
+
if (!r.ok) { refusalToast(r); draw(); return; }
|
|
172
|
+
toast(action === "send" ? "Sent" : "Approved", { detail: action === "send" ? `To ${r.draft.to}` : "Nothing was sent yet." }); refresh(r.draft); return;
|
|
173
|
+
}
|
|
174
|
+
if (t.hasAttribute("data-discard") && m.open) {
|
|
175
|
+
m.busy = true; draw();
|
|
176
|
+
const r = await api("/api/mail/discard", { id: m.open.id });
|
|
177
|
+
m.busy = false;
|
|
178
|
+
if (!r.ok) { refusalToast(r); draw(); return; }
|
|
179
|
+
toast("Discarded", { detail: "It will not be sent." }); refresh(r.draft);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
draw();
|
|
183
|
+
loadIdentity();
|
|
184
|
+
loadDrafts();
|
|
185
|
+
},
|
|
186
|
+
update() {},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Metrics, Warnings, Settings, not found.
|
|
2
|
+
import { html, icon, setHtml, $, $$, api, toast, refusalToast, store, rel, abs, dur, num, copyText } from "./core.js";
|
|
3
|
+
import { emptyState, platformName, copyBtn } from "./components.js";
|
|
4
|
+
|
|
5
|
+
import { DASH, buildRows, summarize, describeSum, compact, percent, gapLabel, platformLabel } from "./metrics-math.js";
|
|
6
|
+
|
|
7
|
+
// A cell is the value, or a dash with a small muted label saying why there is no value. Never a zero.
|
|
8
|
+
function cell(m, field, text) {
|
|
9
|
+
if (!m) return html`<span class="dash">${DASH}</span>`;
|
|
10
|
+
const label = gapLabel(m, field);
|
|
11
|
+
if (m[field] != null && !label) return text;
|
|
12
|
+
return html`<span class="dash" title="${label}">${DASH}</span><span class="gap" data-gap="${field}">${label}</span>`;
|
|
13
|
+
}
|
|
14
|
+
function revenueCell(m) {
|
|
15
|
+
if (!m) return html`<span class="dash">${DASH}</span>`;
|
|
16
|
+
if (m.revenueStatus === "present" && m.estimatedRevenue != null) return html`$${Number(m.estimatedRevenue).toFixed(2)}`;
|
|
17
|
+
const label = gapLabel(m, "estimatedRevenue");
|
|
18
|
+
return html`<span class="dash" title="${label}">${DASH}</span><span class="chip ${m.revenueStatus === "forbidden" ? "bad" : "warn"}">${m.revenueStatus}</span><span class="gap" data-gap="estimatedRevenue">${label}</span>`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function metricsView() {
|
|
22
|
+
let root;
|
|
23
|
+
return {
|
|
24
|
+
title: "Metrics",
|
|
25
|
+
mount(el) { root = el; setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Metrics</span><h1>Metrics</h1><p class="sub">The latest reading for every published post. A dash means the platform did not return a value; nothing here is estimated.</p></div></div><div id="m-body"></div>`); },
|
|
26
|
+
update(s) {
|
|
27
|
+
const rows = buildRows(s.metrics || [], s.publications || []);
|
|
28
|
+
const sum = summarize(rows);
|
|
29
|
+
const asOf = sum.asOf ? html`<span class="gap" data-asof>as of ${abs(sum.asOf)}</span>` : "";
|
|
30
|
+
const stats = html`<div class="stats"><div class="panel stat"><span class="sub">Published posts</span><span class="n" id="pub-count">${sum.posts}</span><span class="gap">${sum.dryRuns ? `${sum.dryRuns} dry-run, not measured` : "none are dry-runs"}</span></div>
|
|
31
|
+
<div class="panel stat"><span class="sub">Posts measured</span><span class="n" id="measured-count">${sum.measured}</span><span class="gap">${sum.measured} of ${sum.posts} published posts have a reading</span></div>
|
|
32
|
+
<div class="panel stat"><span class="sub">Views, latest readings</span><span class="n" id="views-total">${sum.views.total === null ? DASH : compact(sum.views.total)}</span><span class="gap" id="views-note">${describeSum(sum.views, "views")}</span>${asOf}</div>
|
|
33
|
+
<div class="panel stat"><span class="sub">Average watch time</span><span class="n" id="watch-mean">${sum.watch.mean === null ? DASH : dur(sum.watch.mean * 1000)}</span><span class="gap" id="watch-note">${sum.watch.mean === null ? `No watch time reported by any of ${sum.watch.of} posts` : `Mean of ${sum.watch.known} of ${sum.watch.of} posts that report it`}</span></div>
|
|
34
|
+
<div class="panel stat"><span class="sub">Average retention</span><span class="n" id="retention-mean">${percent(sum.retention.mean)}</span><span class="gap" id="retention-note">${sum.retention.mean === null ? `No retention reported by any of ${sum.retention.of} posts` : `Mean of ${sum.retention.known} of ${sum.retention.of} posts that report it`}</span></div></div>`;
|
|
35
|
+
const row = (r) => {
|
|
36
|
+
const m = r.metric;
|
|
37
|
+
const post = html`<td>${platformName(r.platform)}</td><td class="mono">${r.externalId || DASH}</td>`;
|
|
38
|
+
if (!m) return html`<tr data-kind="${r.kind}">${post}<td class="num">${DASH}</td><td class="num">${DASH}</td><td class="num">${DASH}</td><td class="num">${DASH}</td><td><span class="gap" data-reason>${r.reason}</span></td></tr>`;
|
|
39
|
+
return html`<tr data-kind="measured">${post}<td class="num">${cell(m, "views", num(m.views))}</td><td class="num">${cell(m, "watchTimeSec", m.watchTimeSec == null ? DASH : dur(m.watchTimeSec * 1000))}</td><td class="num">${cell(m, "retention", percent(m.retention))}</td><td class="num">${revenueCell(m)}</td><td class="nowrap" title="${abs(m.capturedAt)}">as of ${rel(m.capturedAt)}<span class="gap">from ${platformLabel(m.platform)}</span></td></tr>`;
|
|
40
|
+
};
|
|
41
|
+
const table = rows.length ? html`<div class="table-wrap"><table><thead><tr><th>Platform</th><th>Post id</th><th class="num">Views</th><th class="num">Watch time</th><th class="num">Retention</th><th class="num">Revenue</th><th>As of</th></tr></thead><tbody>${rows.map(row)}</tbody></table></div>`
|
|
42
|
+
: html`<div class="panel">${emptyState("metrics", "No metrics yet", "Metrics appear after a published post has been measured. Run analytics once a post is live.")}</div>`;
|
|
43
|
+
const notes = html`<section class="panel" id="m-notes"><div class="panel-head"><h2>What the platforms do not tell us</h2></div><div class="panel-body"><ul class="hint" style="margin:0;padding-left:18px">
|
|
44
|
+
<li>Revenue: not available from the official API where the scope is forbidden or the report is empty. It is never shown as zero.</li>
|
|
45
|
+
<li>Strikes and demonetization: not exposed by the official API.</li>
|
|
46
|
+
<li>Watch time and retention: returned by YouTube; watch time also by Facebook. TikTok and Instagram do not return them.</li></ul></div></section>`;
|
|
47
|
+
setHtml($(root, "#m-body"), html`${stats}${table}${notes}`);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function warningsView() {
|
|
53
|
+
let root;
|
|
54
|
+
return {
|
|
55
|
+
title: "Warnings",
|
|
56
|
+
mount(el) {
|
|
57
|
+
root = el;
|
|
58
|
+
setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Warnings</span><h1>Warnings</h1><p class="sub">Things the pipeline wants a person to look at.</p></div></div><div id="w-body"></div>`);
|
|
59
|
+
el.addEventListener("click", async (e) => {
|
|
60
|
+
const b = e.target.closest("[data-ack]"); if (!b) return;
|
|
61
|
+
b.disabled = true;
|
|
62
|
+
const r = await api(`/api/warnings/${encodeURIComponent(b.dataset.ack)}/ack`, {});
|
|
63
|
+
if (!r.ok) { b.disabled = false; return refusalToast(r); }
|
|
64
|
+
toast("Warning acknowledged"); store.refresh?.();
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
update(s) {
|
|
68
|
+
const w = s.warnings || [];
|
|
69
|
+
const open = w.filter((x) => !x.ackedAt), done = w.filter((x) => x.ackedAt);
|
|
70
|
+
if (!w.length) { setHtml($(root, "#w-body"), html`<div class="panel">${emptyState("done", "All clear", "No warnings. The pipeline will list anything that needs you here.")}</div>`); return; }
|
|
71
|
+
const row = (x) => html`<div class="warn-row ${x.ackedAt ? "acked" : ""}" data-warning="${x.id}">${icon(x.ackedAt ? "done" : "alert", "ic")}
|
|
72
|
+
<div class="grow"><p class="code">${x.code}</p><p class="msg">${x.message}</p><p class="sub">${x.jobId ? html`<a href="/runs/${x.jobId}" data-link>Run ${String(x.jobId).slice(0, 8)}</a> · ` : ""}${x.ackedAt ? `Acknowledged ${rel(x.ackedAt)}` : rel(x.createdAt)}</p></div>
|
|
73
|
+
${x.ackedAt ? "" : html`<button type="button" class="btn secondary sm" data-ack="${x.id}">${icon("check")}Acknowledge</button>`}</div>`;
|
|
74
|
+
setHtml($(root, "#w-body"), html`<div class="stack">
|
|
75
|
+
<section class="panel"><div class="panel-head"><h2>Needs attention</h2><span class="sub">${open.length} open</span></div>${open.length ? open.map(row) : html`<p class="muted" style="padding:20px">Nothing open.</p>`}</section>
|
|
76
|
+
${done.length ? html`<section class="panel"><div class="panel-head"><h2>Acknowledged</h2><span class="sub">${done.length}</span></div>${done.map(row)}</section>` : ""}</div>`);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function settingsView() {
|
|
82
|
+
let root, notice = null, busy = false;
|
|
83
|
+
const draw = () => {
|
|
84
|
+
const s = store.state; if (!s) return;
|
|
85
|
+
const seg = ["agent", "api"].map((m) => html`<button type="button" data-mode="${m}" aria-pressed="${s.mode === m}" ${busy ? "disabled" : ""}>${m === "agent" ? "Agent" : "API"}</button>`);
|
|
86
|
+
const files = (s.insights || []);
|
|
87
|
+
setHtml($(root, "#set-body"), html`<div class="stack">
|
|
88
|
+
<section class="panel"><div class="setting"><div class="k">Orchestrator mode<small>Who drives the pipeline</small></div>
|
|
89
|
+
<div><div class="segmented" role="group" aria-label="Orchestrator mode">${seg}</div>
|
|
90
|
+
<p class="hint" style="margin-top:8px">${s.mode === "agent" ? "Agent: a coding agent runs each stage with its own skills." : "API: the worker calls model APIs directly."}</p>
|
|
91
|
+
${notice ? html`<div class="notice" role="alert"><b>${notice.refusal}</b> ${notice.fix || ""}</div>` : ""}</div></div>
|
|
92
|
+
<div class="setting"><div class="k">Environment<small>APP_ENV</small></div><div><span class="chip ${s.appEnv === "production" ? "bad" : "ok"}">${s.appEnv}</span></div></div>
|
|
93
|
+
<div class="setting"><div class="k">Data home<small>Where the database and renders live</small></div><div class="copy-row"><code>${s.home}</code>${copyBtn(s.home, "Copy data home path")}</div></div></section>
|
|
94
|
+
<section class="panel"><div class="panel-head"><h2>Engines</h2><span class="sub">Render and insight tools on this machine</span></div>
|
|
95
|
+
<div class="panel-body">${(s.engines || []).length ? html`<div class="chips" style="gap:10px">${s.engines.map((e) => html`<span class="chip ${e.availability === "ready" ? "ok" : "warn"}" style="height:28px;padding:0 12px" title="${e.reason || (e.availability === "ready" ? "Ready" : "Unavailable")}">${e.label}<span style="opacity:.75;font-weight:400">${e.availability === "ready" ? "Ready" : "Unavailable"}${e.id === s.defaultEngine ? " · default" : ""}</span></span>`)}</div>
|
|
96
|
+
<ul class="engine-reasons" style="margin:12px 0 0;padding:0;list-style:none">${s.engines.map((e) => html`<li class="hint" data-engine="${e.id}">${e.label}: ${e.availability === "ready" ? (e.note ? `ready (${e.note})` : "ready") : e.reason}${e.id === s.defaultEngine ? " (default)" : ""}</li>`)}</ul>
|
|
97
|
+
<p class="hint" style="margin-top:8px">Choose one per run with <code>--engine</code>, per series in its Basics, or for the machine with <code>npx vfrey engine use</code>.</p>` : html`<p class="muted">No engine reported.</p>`}</div></section>
|
|
98
|
+
<section class="panel"><div class="panel-head"><h2>Renders from insights</h2><span class="sub">${files.length} file${files.length === 1 ? "" : "s"}</span></div>
|
|
99
|
+
${files.length ? html`<div style="overflow-x:auto"><table><thead><tr><th>Engine</th><th>File</th><th class="num">Size</th><th>Made</th></tr></thead><tbody>${files.map((f) => html`<tr><td>${f.engineLabel}</td><td title="${f.outputPath}"><span class="t">${f.outputPath.split("/").pop()}</span>${f.subject ? html`<div class="sub">${f.subject}</div>` : ""}</td><td class="num">${f.bytes >= 1048576 ? (f.bytes / 1048576).toFixed(1) + " MB" : Math.max(1, Math.round(f.bytes / 1024)) + " KB"}</td><td class="nowrap" title="${abs(f.createdAt)}">${rel(f.createdAt)}</td></tr>`)}</tbody></table></div>`
|
|
100
|
+
: emptyState("film", "No insight render yet", "Renders made from insights show up here with their engine and size.")}</section></div>`);
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
title: "Settings",
|
|
104
|
+
mount(el) {
|
|
105
|
+
root = el;
|
|
106
|
+
setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Settings</span><h1>Settings</h1><p class="sub">How this install runs.</p></div></div><div id="set-body"></div>`);
|
|
107
|
+
el.addEventListener("click", async (e) => {
|
|
108
|
+
const b = e.target.closest("[data-mode]"); if (!b || busy || b.getAttribute("aria-pressed") === "true") return;
|
|
109
|
+
busy = true; notice = null; draw();
|
|
110
|
+
const r = await api("/api/mode", { mode: b.dataset.mode });
|
|
111
|
+
busy = false;
|
|
112
|
+
if (!r.ok) { notice = r; refusalToast(r); draw(); return; }
|
|
113
|
+
if (store.state) store.state.mode = r.mode || b.dataset.mode;
|
|
114
|
+
toast(`Mode set to ${r.mode || b.dataset.mode}`); draw(); store.refresh?.();
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
update() { draw(); },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function notFoundView() {
|
|
122
|
+
return { title: "Not found", mount(el) { setHtml(el, html`<div class="panel">${emptyState("alert", "Page not found", "That address is not part of the desk.", html`<a class="btn" href="/" data-link>Back to the Board</a>`)}</div>`); }, update() {} };
|
|
123
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Review: every job that waits for a person before it renders (a series with review on). Approve or reject here.
|
|
2
|
+
import { html, icon, setHtml, $, $$, api, read, toast, refusalToast, store, rel, abs, short } from "./core.js";
|
|
3
|
+
import { emptyState } from "./components.js";
|
|
4
|
+
import { startTick } from "./views-runs.js";
|
|
5
|
+
|
|
6
|
+
function previewOf(item) {
|
|
7
|
+
const sc = item.scene;
|
|
8
|
+
if (sc && sc.source === "agent" && sc.kind === "html" && sc.dir) {
|
|
9
|
+
return html`<div class="preview-box"><iframe title="Scene preview" data-preview src="/preview/runs/${item.id}/" sandbox="allow-scripts" allow="autoplay" loading="lazy"></iframe></div>`;
|
|
10
|
+
}
|
|
11
|
+
if (sc && sc.source === "agent" && sc.kind === "tsx") {
|
|
12
|
+
return html`<div class="preview-box"><img data-frame src="/preview/runs/${item.id}/frame.png" alt="Frame of the Remotion scene at 1 second" style="display:block;width:100%;max-width:270px"></div>
|
|
13
|
+
<p class="muted" data-frame-note>A Remotion scene cannot play in the browser. This is one frame (<span class="mono">render --frame 1</span>).</p>`;
|
|
14
|
+
}
|
|
15
|
+
return html`<p class="muted" data-preview-note>No scene was written for this run, so there is nothing to preview. It renders with the default template.</p>`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function checksOf(item) {
|
|
19
|
+
const checks = item.checks || [];
|
|
20
|
+
if (!checks.length) return html`<p class="muted">No checks ran.</p>`;
|
|
21
|
+
return html`<ul style="list-style:none;margin:0;padding:0;display:grid;gap:10px">${checks.map((c) => html`<li class="check" data-check="${c.id}" data-status="${c.status}">
|
|
22
|
+
<div class="row"><span class="mono">${c.id}</span><span class="pill tone-${c.status === "pass" ? "ok" : c.status === "skipped" ? "skip" : "rej"} plain">${c.status === "pass" ? "Pass" : c.status === "skipped" ? "Skipped" : "Fail"}</span></div>
|
|
23
|
+
<p class="detail">${c.message}</p>${c.fix ? html`<p class="detail"><b>Fix:</b> ${c.fix}</p>` : ""}</li>`)}</ul>`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function reviewView() {
|
|
27
|
+
let root, jobs = null, sig = "", rejecting = null, loading = false, busy = new Set();
|
|
28
|
+
|
|
29
|
+
async function load() {
|
|
30
|
+
if (loading) return;
|
|
31
|
+
loading = true;
|
|
32
|
+
try {
|
|
33
|
+
const r = await read("/api/review");
|
|
34
|
+
if (r.ok) jobs = r.jobs;
|
|
35
|
+
} catch { /* offline: keep what is shown */ }
|
|
36
|
+
loading = false;
|
|
37
|
+
draw();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function item(j) {
|
|
41
|
+
const c = j.content;
|
|
42
|
+
const asking = rejecting === j.id;
|
|
43
|
+
const off = busy.has(j.id) ? "disabled" : "";
|
|
44
|
+
return html`<section class="panel" data-review="${j.id}" aria-label="Review ${j.title || j.id}">
|
|
45
|
+
<div class="panel-head"><h2>${j.title || "Untitled run"}</h2><span class="sub"><a href="/series/${j.seriesId}" data-link>${j.seriesId}</a> · <a href="/runs/${j.id}" data-link class="mono">${short(j.id)}</a> · waiting <span title="${abs(j.waitingSince)}">${rel(j.waitingSince)}</span></span></div>
|
|
46
|
+
<div class="panel-body"><div class="run-grid">
|
|
47
|
+
<div class="stack">
|
|
48
|
+
<div><p class="label">Script</p>${c && c.script ? html`<pre data-script style="white-space:pre-wrap;margin:6px 0 0">${c.script}</pre>` : html`<p class="muted">No script was stored.</p>`}</div>
|
|
49
|
+
${c && c.hook ? html`<div><p class="label">Hook</p><p>${c.hook}</p></div>` : ""}
|
|
50
|
+
${c && c.hashtags && c.hashtags.length ? html`<div class="chips">${c.hashtags.map((h) => html`<span class="chip">${h}</span>`)}</div>` : ""}
|
|
51
|
+
<div><p class="label">Checks</p>${checksOf(j)}</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="stack"><div><p class="label">Scene</p>${previewOf(j)}</div></div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="actions" style="margin-top:20px;display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start">
|
|
56
|
+
${asking
|
|
57
|
+
? html`<div class="field" style="flex:1;min-width:240px"><label for="why-${j.id}">Reason</label><input type="text" id="why-${j.id}" data-reason maxlength="300" placeholder="Why is this rejected? (optional)" autocomplete="off"></div>
|
|
58
|
+
<button type="button" class="btn danger solid sm" data-act="reject-yes" data-id="${j.id}" ${off}>Reject run</button><button type="button" class="btn ghost sm" data-act="reject-no" data-id="${j.id}">Keep waiting</button>`
|
|
59
|
+
: html`<button type="button" class="btn" data-act="approve" data-id="${j.id}" ${off}>${icon("check")}Approve and render</button><button type="button" class="btn danger" data-act="reject" data-id="${j.id}" ${off}>${icon("x")}Reject</button>`}
|
|
60
|
+
</div></div></section>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function draw() {
|
|
64
|
+
const body = $(root, "#review-body");
|
|
65
|
+
if (jobs === null) return;
|
|
66
|
+
const n = jobs.length;
|
|
67
|
+
$(root, "#review-sub").textContent = n ? `${n} waiting` : "Nothing waiting";
|
|
68
|
+
if (!n) {
|
|
69
|
+
setHtml(body, html`<div class="panel">${emptyState("done", "Nothing to review", "A series with review on stops here after its script, scene and checks. Turn review on from the series page.")}</div>`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Keep what the person typed in the reason box while the poll redraws.
|
|
73
|
+
const typed = $(root, "[data-reason]")?.value;
|
|
74
|
+
setHtml(body, html`<div class="stack">${jobs.map(item)}</div>`);
|
|
75
|
+
const box = $(root, "[data-reason]");
|
|
76
|
+
if (box) { if (typed) box.value = typed; box.focus(); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function act(name, el) {
|
|
80
|
+
const id = el.dataset.id;
|
|
81
|
+
if (name === "reject") { rejecting = id; draw(); return; }
|
|
82
|
+
if (name === "reject-no") { rejecting = null; draw(); return; }
|
|
83
|
+
busy.add(id); draw();
|
|
84
|
+
let r;
|
|
85
|
+
if (name === "approve") r = await api(`/api/jobs/${encodeURIComponent(id)}/approve`, {});
|
|
86
|
+
else {
|
|
87
|
+
const reason = $(root, `#why-${CSS.escape(id)}`)?.value || "";
|
|
88
|
+
r = await api(`/api/jobs/${encodeURIComponent(id)}/reject`, { reason });
|
|
89
|
+
}
|
|
90
|
+
busy.delete(id); rejecting = null;
|
|
91
|
+
if (!r.ok) refusalToast(r);
|
|
92
|
+
else if (name === "approve") { toast("Approved", { detail: "Rendering now" }); startTick({}); }
|
|
93
|
+
else toast("Rejected");
|
|
94
|
+
await load(); store.refresh?.();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
title: "Review",
|
|
99
|
+
mount(el) {
|
|
100
|
+
root = el;
|
|
101
|
+
setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Review</span><h1>Review</h1><p class="sub" id="review-sub"></p></div></div><div id="review-body"></div>`);
|
|
102
|
+
el.addEventListener("click", (e) => { const a = e.target.closest("[data-act]"); if (a) act(a.dataset.act, a); });
|
|
103
|
+
// A frame that cannot be made says so instead of showing a broken image.
|
|
104
|
+
el.addEventListener("error", (e) => {
|
|
105
|
+
if (e.target.matches && e.target.matches("img[data-frame]")) {
|
|
106
|
+
const note = e.target.closest(".stack")?.querySelector("[data-frame-note]");
|
|
107
|
+
e.target.remove();
|
|
108
|
+
if (note) note.textContent = "The frame could not be rendered here. Approve or reject from the script and the checks.";
|
|
109
|
+
}
|
|
110
|
+
}, true);
|
|
111
|
+
load();
|
|
112
|
+
},
|
|
113
|
+
update(s) {
|
|
114
|
+
const waiting = (s.jobs || []).filter((j) => j.status === "awaiting_review");
|
|
115
|
+
const next = waiting.map((j) => j.id + j.updatedAt).join("|");
|
|
116
|
+
if (next !== sig) { sig = next; load(); }
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|