@vfrey/vfrey 0.0.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +33 -0
- package/README.md +78 -3
- package/accounts.example.json +61 -0
- package/accounts.first-run.json +41 -0
- package/bin/vfrey.mjs +36 -0
- package/integration-tools/README.md +43 -0
- package/integration-tools/image/pillow.md +32 -0
- package/integration-tools/image/playwright.md +33 -0
- package/integration-tools/image/resvg.md +36 -0
- package/integration-tools/image/satori.md +36 -0
- package/integration-tools/image/sharp.md +38 -0
- package/integration-tools/video/blender.md +34 -0
- package/integration-tools/video/ffmpeg.md +41 -0
- package/integration-tools/video/manim.md +37 -0
- package/integration-tools/video/motion-canvas.md +37 -0
- package/integration-tools/video/remotion.md +50 -0
- package/integration-tools/video/revideo.md +32 -0
- package/package.json +74 -6
- package/skills/vfrey-orchestrator/SKILL.md +15 -0
- package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
- package/skills/vfrey-product/SKILL.md +63 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
- package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
- package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
- package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
- package/skills/vfrey-product/push/SKILL.md +15 -0
- package/skills/vfrey-product/scenes/NOTICE +5 -0
- package/skills/vfrey-product/scenes/blocks.md +57 -0
- package/skills/vfrey-product/scenes/craft.md +118 -0
- package/skills/vfrey-product/scenes/motion.md +88 -0
- package/src/analytics/dashboard.ts +109 -0
- package/src/analytics/index.ts +260 -0
- package/src/api/client.ts +361 -0
- package/src/api/cloud-files.ts +197 -0
- package/src/api/cloud-runs.ts +75 -0
- package/src/api/cloud.ts +413 -0
- package/src/api/commands.ts +330 -0
- package/src/api/config.ts +34 -0
- package/src/api/creators.ts +463 -0
- package/src/api/credentials.ts +152 -0
- package/src/api/mail.ts +333 -0
- package/src/api/topics.ts +207 -0
- package/src/api/types.ts +273 -0
- package/src/cli.ts +468 -0
- package/src/cloud/bundle.ts +70 -0
- package/src/cloud/cloudrun-executor.ts +237 -0
- package/src/cloud/executor.ts +281 -0
- package/src/cloud/gcs-sign.ts +76 -0
- package/src/cloud/job.ts +106 -0
- package/src/cloud/remote-job.ts +67 -0
- package/src/cloud/render-job.ts +191 -0
- package/src/config.ts +309 -0
- package/src/dashboard-link.ts +180 -0
- package/src/dashboard-start.ts +103 -0
- package/src/desk/channel.ts +64 -0
- package/src/desk/cli.ts +216 -0
- package/src/desk/creators-route.ts +129 -0
- package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
- package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
- package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
- package/src/desk/http.ts +460 -0
- package/src/desk/mail-route.ts +103 -0
- package/src/desk/overlay.ts +112 -0
- package/src/desk/preview.ts +422 -0
- package/src/desk/screens.ts +149 -0
- package/src/desk/secrets.ts +143 -0
- package/src/desk/series.ts +127 -0
- package/src/desk/state.ts +77 -0
- package/src/desk/studio-route.ts +119 -0
- package/src/desk/tick-runner.ts +66 -0
- package/src/desk/tick.ts +56 -0
- package/src/desk/topics-route.ts +41 -0
- package/src/desk/ui/app.css +505 -0
- package/src/desk/ui/app.js +185 -0
- package/src/desk/ui/apple-icon.png +0 -0
- package/src/desk/ui/components.js +79 -0
- package/src/desk/ui/core.js +156 -0
- package/src/desk/ui/favicon.ico +0 -0
- package/src/desk/ui/icon.svg +1 -0
- package/src/desk/ui/icons.js +49 -0
- package/src/desk/ui/index.html +41 -0
- package/src/desk/ui/metrics-math.d.ts +22 -0
- package/src/desk/ui/metrics-math.js +135 -0
- package/src/desk/ui/theme.js +5 -0
- package/src/desk/ui/views-creators.js +209 -0
- package/src/desk/ui/views-keys.js +114 -0
- package/src/desk/ui/views-mail.js +188 -0
- package/src/desk/ui/views-misc.js +123 -0
- package/src/desk/ui/views-review.js +119 -0
- package/src/desk/ui/views-runs.js +350 -0
- package/src/desk/ui/views-screens.js +163 -0
- package/src/desk/ui/views-series.js +318 -0
- package/src/desk/ui/views-topics.js +87 -0
- package/src/desk/verbs.ts +361 -0
- package/src/doctor.ts +212 -0
- package/src/drain.ts +14 -0
- package/src/engage/index.ts +217 -0
- package/src/generate/index.ts +110 -0
- package/src/generate/script.ts +152 -0
- package/src/generate/title.ts +78 -0
- package/src/help.ts +252 -0
- package/src/home.ts +45 -0
- package/src/llm/client.ts +177 -0
- package/src/log.ts +7 -0
- package/src/oauth/tokens.ts +189 -0
- package/src/oauth/vault.ts +72 -0
- package/src/pipeline/review.ts +642 -0
- package/src/pipeline/run.ts +443 -0
- package/src/pipeline/series-guard.ts +26 -0
- package/src/pipeline/source-fetch.ts +69 -0
- package/src/pipeline/stages.ts +728 -0
- package/src/pipeline/topics-source.ts +90 -0
- package/src/product/commands.ts +296 -0
- package/src/product/config-file.ts +38 -0
- package/src/product/engine-choice.ts +89 -0
- package/src/product/engines.ts +103 -0
- package/src/product/insights.ts +50 -0
- package/src/product/keys.ts +67 -0
- package/src/product/paths.ts +18 -0
- package/src/product/probes.ts +88 -0
- package/src/product/push-flow.ts +90 -0
- package/src/product/render-video.ts +21 -0
- package/src/product/short.ts +62 -0
- package/src/product/update.ts +32 -0
- package/src/project/canonical.ts +14 -0
- package/src/project/commands.ts +177 -0
- package/src/project/dashboard-status.ts +28 -0
- package/src/project/discover.ts +19 -0
- package/src/project/index-file.ts +73 -0
- package/src/project/marker.ts +92 -0
- package/src/project/migrate.ts +201 -0
- package/src/project/names.ts +27 -0
- package/src/project/notice.ts +16 -0
- package/src/project/open.ts +88 -0
- package/src/project/scaffold.ts +112 -0
- package/src/project/templates.ts +42 -0
- package/src/project/videos.ts +64 -0
- package/src/publish/facebook.ts +91 -0
- package/src/publish/index.ts +12 -0
- package/src/publish/instagram.ts +97 -0
- package/src/publish/rate.ts +62 -0
- package/src/publish/session.ts +242 -0
- package/src/publish/tiktok.ts +202 -0
- package/src/publish/youtube.ts +177 -0
- package/src/queue/desk-ops.ts +1170 -0
- package/src/queue/seal.ts +3 -0
- package/src/queue/store.ts +1006 -0
- package/src/remotion/AiLabel.tsx +26 -0
- package/src/remotion/Root.tsx +73 -0
- package/src/remotion/defaults.ts +26 -0
- package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
- package/src/remotion/films/engine/EngineMark.tsx +17 -0
- package/src/remotion/films/rea/ReaShort.tsx +552 -0
- package/src/remotion/films/rea/drawings.tsx +907 -0
- package/src/remotion/films/rea/timing.ts +144 -0
- package/src/remotion/index.ts +4 -0
- package/src/remotion/templates/EditorialSplit.tsx +39 -0
- package/src/remotion/templates/KineticType.tsx +39 -0
- package/src/remotion/templates/SignalBoard.tsx +50 -0
- package/src/remotion/timing.ts +22 -0
- package/src/render/chrome.ts +78 -0
- package/src/render/engine-mark.ts +29 -0
- package/src/render/framey-template.ts +130 -0
- package/src/render/html-engine.ts +149 -0
- package/src/render/node-crypto-stub.ts +11 -0
- package/src/render/probe.ts +29 -0
- package/src/render/props.ts +17 -0
- package/src/render/vertical-short.ts +35 -0
- package/src/render/webpack.ts +33 -0
- package/src/render/worker.ts +65 -0
- package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
- package/src/scene/assets/fonts/titan-one.woff2 +0 -0
- package/src/scene/assets/gsap/LICENSE.txt +8 -0
- package/src/scene/assets/gsap/gsap.min.js +11 -0
- package/src/scene/audio.ts +54 -0
- package/src/scene/blocks/LICENSE +190 -0
- package/src/scene/blocks/NOTICE +5 -0
- package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
- package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
- package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
- package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
- package/src/scene/blocks/code-diff/code-diff.html +817 -0
- package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
- package/src/scene/blocks/code-typing/code-typing.html +764 -0
- package/src/scene/blocks/data-chart/data-chart.html +452 -0
- package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
- package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
- package/src/scene/blocks/index.json +824 -0
- package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
- package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
- package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
- package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
- package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
- package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
- package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
- package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
- package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
- package/src/scene/blocks/mk-background/mk-background.html +236 -0
- package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
- package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
- package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
- package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
- package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
- package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
- package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
- package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
- package/src/scene/blocks/snow/snow.html +205 -0
- package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
- package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
- package/src/scene/blocks/starfield/starfield.html +188 -0
- package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
- package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
- package/src/scene/blocks/x-post/x-post.html +437 -0
- package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
- package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
- package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
- package/src/scene/blocks.ts +229 -0
- package/src/scene/command.ts +272 -0
- package/src/scene/figure.ts +181 -0
- package/src/scene/glyphs.ts +107 -0
- package/src/scene/init.ts +86 -0
- package/src/scene/inspect/css-tree.d.ts +6 -0
- package/src/scene/inspect/page-scripts.ts +272 -0
- package/src/scene/inspect/report.ts +405 -0
- package/src/scene/inspect/run.ts +200 -0
- package/src/scene/inspect/source.ts +236 -0
- package/src/scene/label.ts +131 -0
- package/src/scene/net-guard.ts +74 -0
- package/src/scene/render.ts +292 -0
- package/src/scene/runtime/browser.ts +199 -0
- package/src/scene/runtime/core.ts +181 -0
- package/src/scene/runtime/react.tsx +115 -0
- package/src/scene/runtime/transitions.ts +133 -0
- package/src/scene/sanitize.ts +181 -0
- package/src/scene/scene.ts +152 -0
- package/src/scene/studio-command.ts +48 -0
- package/src/scene/studio-skin.ts +59 -0
- package/src/scene/studio.ts +337 -0
- package/src/scene/supply.ts +111 -0
- package/src/scene/timing.ts +288 -0
- package/src/scene/transitions/LICENSE +190 -0
- package/src/scene/transitions/NOTICE +5 -0
- package/src/scene/transitions/SOURCE.json +24 -0
- package/src/scene/transitions/transitions.global.js +38 -0
- package/src/scene/transitions.ts +26 -0
- package/src/service/all-projects.ts +153 -0
- package/src/service/commands.ts +256 -0
- package/src/service/due.ts +82 -0
- package/src/service/lock.ts +101 -0
- package/src/service/log-file.ts +56 -0
- package/src/service/state.ts +41 -0
- package/src/service/units.ts +90 -0
- package/src/service/worker.ts +51 -0
- package/src/skill-agents.ts +74 -0
- package/src/skill-install.ts +334 -0
- package/src/sync-write.ts +21 -0
- package/src/tick-report.ts +205 -0
- package/src/tts.ts +124 -0
- package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
- package/vendor/@vfrey/contracts/dist/account.js +27 -0
- package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
- package/vendor/@vfrey/contracts/dist/checks.js +1 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
- package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
- package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
- package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/events.js +1 -0
- package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
- package/vendor/@vfrey/contracts/dist/http.js +37 -0
- package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
- package/vendor/@vfrey/contracts/dist/index.js +22 -0
- package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
- package/vendor/@vfrey/contracts/dist/log.js +10 -0
- package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
- package/vendor/@vfrey/contracts/dist/mail.js +1 -0
- package/vendor/@vfrey/contracts/package.json +18 -0
- package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
- package/vendor/@vfrey/ingest/dist/index.js +221 -0
- package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
- package/vendor/@vfrey/ingest/dist/parse.js +165 -0
- package/vendor/@vfrey/ingest/package.json +21 -0
- package/vendor/framey/LICENSE +190 -0
- package/vendor/framey/NOTICE +15 -0
- package/vendor/framey/SOURCE.json +24 -0
- package/vendor/framey/STUDIO-SOURCE.json +9 -0
- package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
- package/vendor/framey/bin/framey.mjs +16 -0
- package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
- package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
- package/vendor/framey/dist/api.d.ts +211 -0
- package/vendor/framey/dist/api.js +76 -0
- package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
- package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
- package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
- package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
- package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
- package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
- package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
- package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
- package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
- package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
- package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
- package/vendor/framey/dist/check-F5N544ML.js +111 -0
- package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
- package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
- package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
- package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
- package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
- package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
- package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
- package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
- package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
- package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
- package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
- package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
- package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
- package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
- package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
- package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
- package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
- package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
- package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
- package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
- package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
- package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
- package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
- package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
- package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
- package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
- package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
- package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
- package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
- package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
- package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
- package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
- package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
- package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
- package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
- package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
- package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
- package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
- package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
- package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
- package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
- package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
- package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
- package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
- package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
- package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
- package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
- package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
- package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
- package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
- package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
- package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
- package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
- package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
- package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
- package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
- package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
- package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
- package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
- package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
- package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
- package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
- package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
- package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
- package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
- package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
- package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
- package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
- package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
- package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
- package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
- package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
- package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
- package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
- package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
- package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
- package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
- package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
- package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
- package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
- package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
- package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
- package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
- package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
- package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
- package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
- package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
- package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
- package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
- package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
- package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
- package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
- package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
- package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
- package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
- package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
- package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
- package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
- package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
- package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
- package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
- package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
- package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
- package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
- package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
- package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
- package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
- package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
- package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
- package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
- package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
- package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
- package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
- package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
- package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
- package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
- package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
- package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
- package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
- package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
- package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
- package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
- package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
- package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
- package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
- package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
- package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
- package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
- package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
- package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
- package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
- package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
- package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
- package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
- package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
- package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
- package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
- package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
- package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
- package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
- package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
- package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
- package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
- package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
- package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
- package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
- package/vendor/framey/dist/cli.js +439 -0
- package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
- package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
- package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
- package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
- package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
- package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
- package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
- package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
- package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
- package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
- package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
- package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
- package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
- package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
- package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
- package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
- package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
- package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
- package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
- package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
- package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
- package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
- package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
- package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
- package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
- package/vendor/framey/dist/fontLocalizeCli.js +142 -0
- package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
- package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
- package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
- package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
- package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
- package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
- package/vendor/framey/dist/hyperframe.manifest.json +22 -0
- package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
- package/vendor/framey/dist/hyperframes-player.global.js +478 -0
- package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
- package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
- package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
- package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
- package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
- package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
- package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
- package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
- package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
- package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
- package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
- package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
- package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
- package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
- package/vendor/framey/dist/models-K363FYM4.js +195 -0
- package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
- package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
- package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
- package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
- package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
- package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
- package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
- package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
- package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
- package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
- package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
- package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
- package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
- package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
- package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
- package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
- package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
- package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
- package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
- package/vendor/framey/dist/renderSetupWorker.js +120 -0
- package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
- package/vendor/framey/dist/runtimeVersion.js +18 -0
- package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
- package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
- package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
- package/vendor/framey/dist/sherpaWorker.js +84 -0
- package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
- package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
- package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
- package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
- package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
- package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
- package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
- package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
- package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
- package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
- package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
- package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
- package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
- package/vendor/framey/dist/studio/favicon.svg +14 -0
- package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
- package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
- package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
- package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
- package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
- package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
- package/vendor/framey/dist/studio/index.html +23 -0
- package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
- package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
- package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
- package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
- package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
- package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
- package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
- package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
- package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
- package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
- package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
- package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
- package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
- package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
- package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
- package/vendor/framey/dist/warmCheck.js +220 -0
- package/vendor/framey/dist/warmCheckClient.js +139 -0
- package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
- package/vendor/framey-player/LICENSE +190 -0
- package/vendor/framey-player/NOTICE +16 -0
- package/vendor/framey-player/SOURCE.json +6 -0
- package/vendor/framey-player/hyperframes-player.global.js +478 -0
- package/vendor/framey.pin +1 -0
- package/bin/index.js +0 -3
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Pure rules for the Metrics page. No DOM. The page never invents a number:
|
|
2
|
+
// - a value the platform did not return is null and stays null (rendered as an em dash),
|
|
3
|
+
// - totals and averages run over known values only and always carry "N of M",
|
|
4
|
+
// - a field the official API does not expose is labelled, never guessed.
|
|
5
|
+
// Retention is a percent from 0 to 100, as the platform returns it. It is never rescaled.
|
|
6
|
+
|
|
7
|
+
export const DASH = "—";
|
|
8
|
+
export const DRY_RUN_ID = "dry-run";
|
|
9
|
+
|
|
10
|
+
const NAMES = { youtube: "YouTube", tiktok: "TikTok", instagram: "Instagram", facebook: "Facebook" };
|
|
11
|
+
export const platformLabel = (p) => NAMES[p] || (p ? p[0].toUpperCase() + p.slice(1) : "the platform");
|
|
12
|
+
|
|
13
|
+
// What each platform's official API returns for a published post (docs/STAGE-SPECS.md, Analytics).
|
|
14
|
+
// "returned": the collector asks for it; a null means the read gave nothing back.
|
|
15
|
+
// "not_exposed": the official API never returns it, so null always means "cannot be known".
|
|
16
|
+
const EXPOSURE = {
|
|
17
|
+
youtube: { views: "returned", watchTimeSec: "returned", retention: "returned", estimatedRevenue: "returned" },
|
|
18
|
+
tiktok: { views: "returned", watchTimeSec: "not_exposed", retention: "not_exposed", estimatedRevenue: "not_exposed" },
|
|
19
|
+
instagram: { views: "returned", watchTimeSec: "not_exposed", retention: "not_exposed", estimatedRevenue: "not_exposed" },
|
|
20
|
+
facebook: { views: "returned", watchTimeSec: "returned", retention: "not_exposed", estimatedRevenue: "not_exposed" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function exposure(platform, field) {
|
|
24
|
+
return (EXPOSURE[platform] || {})[field] || "returned";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The muted label under a "—", or "" when the platform simply returned nothing this time. */
|
|
28
|
+
export function gapLabel(m, field) {
|
|
29
|
+
if (field === "estimatedRevenue") {
|
|
30
|
+
if (m.estimatedRevenue != null && m.revenueStatus === "present") return "";
|
|
31
|
+
if (m.revenueStatus === "forbidden") return "forbidden: not available from the official API";
|
|
32
|
+
return "not available from the official API";
|
|
33
|
+
}
|
|
34
|
+
if (m[field] != null) return "";
|
|
35
|
+
if (exposure(m.platform, field) === "not_exposed") return `not exposed by the ${platformLabel(m.platform)} API`;
|
|
36
|
+
return `${platformLabel(m.platform)} returned no value`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Sum over known values only. total is null when nothing is known. */
|
|
40
|
+
export function sumKnown(values) {
|
|
41
|
+
const known = values.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
42
|
+
return { total: known.length ? known.reduce((a, b) => a + b, 0) : null, known: known.length, of: values.length };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Mean over known values only. mean is null when nothing is known. */
|
|
46
|
+
export function meanKnown(values) {
|
|
47
|
+
const s = sumKnown(values);
|
|
48
|
+
return { mean: s.total === null ? null : s.total / s.known, known: s.known, of: s.of };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function compact(n) {
|
|
52
|
+
if (n == null || !Number.isFinite(n)) return DASH;
|
|
53
|
+
const abs = Math.abs(n);
|
|
54
|
+
const f = (x, unit) => `${(Math.round(x * 10) / 10).toString()}${unit}`;
|
|
55
|
+
if (abs >= 1e9) return f(n / 1e9, "B");
|
|
56
|
+
if (abs >= 1e6) return f(n / 1e6, "M");
|
|
57
|
+
if (abs >= 1e4) return f(n / 1e3, "K");
|
|
58
|
+
return Number(n).toLocaleString("en-US");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const percent = (r) => (r == null || !Number.isFinite(r) ? DASH : `${Math.round(r * 10) / 10}%`);
|
|
62
|
+
|
|
63
|
+
export const isDryRun = (externalId) => !externalId || externalId === DRY_RUN_ID;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One row per post: every measured post, plus every published post that has no reading yet,
|
|
67
|
+
* plus dry-run publications (which are never measured, and say why).
|
|
68
|
+
*/
|
|
69
|
+
export function buildRows(metrics, publications) {
|
|
70
|
+
const rows = new Map();
|
|
71
|
+
for (const m of metrics || []) rows.set(`${m.platform}:${m.externalId}`, { kind: "measured", platform: m.platform, externalId: m.externalId, metric: m, reason: "" });
|
|
72
|
+
for (const p of publications || []) {
|
|
73
|
+
if (isDryRun(p.externalId)) {
|
|
74
|
+
rows.set(`dry:${p.id}`, { kind: "dry_run", platform: p.platform, externalId: p.externalId, metric: null, reason: "Dry run: nothing was published, so there is nothing to measure." });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const key = `${p.platform}:${p.externalId}`;
|
|
78
|
+
if (!rows.has(key)) rows.set(key, { kind: "unmeasured", platform: p.platform, externalId: p.externalId, metric: null, reason: "Not measured yet. Run analytics once the post is live." });
|
|
79
|
+
}
|
|
80
|
+
return [...rows.values()];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Aggregates over live (non dry-run) posts, each with its known/total count. */
|
|
84
|
+
export function summarize(rows) {
|
|
85
|
+
const live = rows.filter((r) => r.kind !== "dry_run");
|
|
86
|
+
const metrics = live.map((r) => r.metric);
|
|
87
|
+
const pick = (field) => metrics.map((m) => (m ? m[field] : null));
|
|
88
|
+
const captured = metrics.filter(Boolean).map((m) => m.capturedAt).filter(Boolean).sort();
|
|
89
|
+
return {
|
|
90
|
+
posts: live.length,
|
|
91
|
+
dryRuns: rows.length - live.length,
|
|
92
|
+
measured: metrics.filter(Boolean).length,
|
|
93
|
+
views: sumKnown(pick("views")),
|
|
94
|
+
watch: meanKnown(pick("watchTimeSec")),
|
|
95
|
+
retention: meanKnown(pick("retention")),
|
|
96
|
+
asOf: captured.length ? captured[captured.length - 1] : null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** "12.4K views across 3 of 5 posts", or an honest "no views reported". */
|
|
101
|
+
export function describeSum(s, noun, posts = "posts") {
|
|
102
|
+
if (s.total === null) return `No ${noun} reported by any of ${s.of} ${posts}`;
|
|
103
|
+
return `${compact(s.total)} ${noun} across ${s.known} of ${s.of} ${posts}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- Trends: views gained between a post's two latest snapshots ----
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Views gained by one post between its two latest snapshots (any input order).
|
|
110
|
+
* gained is null, never 0, when it cannot be known: fewer than two snapshots, or either reading has no view count.
|
|
111
|
+
*/
|
|
112
|
+
export function viewsGained(snapshots) {
|
|
113
|
+
const rows = (snapshots || []).filter((s) => s && s.capturedAt).sort((a, b) => (a.capturedAt < b.capturedAt ? 1 : a.capturedAt > b.capturedAt ? -1 : 0));
|
|
114
|
+
if (rows.length < 2) return { gained: null, from: null, to: rows[0] ? rows[0].capturedAt : null, reason: rows.length ? "one_snapshot" : "no_snapshot" };
|
|
115
|
+
const [latest, previous] = rows;
|
|
116
|
+
const known = typeof latest.views === "number" && typeof previous.views === "number" && Number.isFinite(latest.views) && Number.isFinite(previous.views);
|
|
117
|
+
return { gained: known ? latest.views - previous.views : null, from: previous.capturedAt, to: latest.capturedAt, reason: known ? "ok" : "unknown_views" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Combines per-post results for one series and platform. Totals run over known gains only and carry "N of M". */
|
|
121
|
+
export function summarizeGains(results) {
|
|
122
|
+
const s = sumKnown(results.map((r) => r.gained));
|
|
123
|
+
const known = results.filter((r) => r.gained !== null);
|
|
124
|
+
const times = (key) => known.map((r) => r[key]).filter(Boolean).sort();
|
|
125
|
+
let reason = "ok";
|
|
126
|
+
if (s.total === null) reason = results.length === 0 || results.every((r) => r.reason === "no_snapshot") ? "not_measured" : results.some((r) => r.reason === "unknown_views") ? "unknown_views" : "one_snapshot";
|
|
127
|
+
return { gained: s.total, known: s.known, of: s.of, asOf: times("to").slice(-1)[0] || null, since: times("from")[0] || null, reason };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const TREND_REASONS = {
|
|
131
|
+
ok: "",
|
|
132
|
+
not_measured: "Not measured yet. Run analytics once the post is live.",
|
|
133
|
+
one_snapshot: "Only one reading so far. Gains need two.",
|
|
134
|
+
unknown_views: "The platform returned no view count for one of the two readings.",
|
|
135
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Runs before first paint: apply the saved theme (if any) so the page never flashes the wrong one.
|
|
2
|
+
try {
|
|
3
|
+
const t = localStorage.getItem("vfrey-desk-theme");
|
|
4
|
+
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
|
|
5
|
+
} catch { /* storage unavailable: follow the system */ }
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Creators: find creators in a niche, read a profile (stats, formats, hooks, top posts), keep watchlists, see breakouts,
|
|
2
|
+
// and "Study formats" for the agent. The browser talks only to this desk's own /api/creators routes. The vfrey token stays on the server.
|
|
3
|
+
import { html, icon, setHtml, $, $$, read, api, toast, refusalToast } from "./core.js";
|
|
4
|
+
import { emptyState, platformName } from "./components.js";
|
|
5
|
+
|
|
6
|
+
const DASH = "—";
|
|
7
|
+
const short = (n) => (typeof n === "number" && Number.isFinite(n) ? (n >= 1e6 ? `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}K` : String(Math.round(n))) : DASH);
|
|
8
|
+
const pct = (f) => (typeof f === "number" && Number.isFinite(f) ? `${(f * 100).toFixed(1).replace(/\.0$/, "")}%` : DASH);
|
|
9
|
+
const signed = (f) => (typeof f === "number" && Number.isFinite(f) ? `${f >= 0 ? "+" : ""}${pct(f)}` : DASH);
|
|
10
|
+
const safeUrl = (u) => (/^https?:\/\//i.test(String(u || "")) ? u : "");
|
|
11
|
+
const basis = (b) => (b === "views" ? "of views" : b === "followers" ? "of followers" : "(basis unknown)");
|
|
12
|
+
const offline = () => ({ ok: false, refusal: "The desk could not be reached.", fix: "Check that `npx vfrey dashboard` is still running.", code: "offline" });
|
|
13
|
+
const get = (path) => read(path).catch(offline);
|
|
14
|
+
const SORTS = ["engagement", "growth", "views", "followers"];
|
|
15
|
+
const TABS = [["search", "Find"], ["watchlists", "Watchlists"], ["alerts", "Alerts"]];
|
|
16
|
+
|
|
17
|
+
export function creatorsView() {
|
|
18
|
+
let root;
|
|
19
|
+
const m = {
|
|
20
|
+
tab: "search", niches: [], filters: { niche: "", platform: "", followers: "", postsPerWeek: "", engagement: "", sort: "engagement" },
|
|
21
|
+
results: null, searching: false, error: null, profile: null, profileBusy: false,
|
|
22
|
+
watchlists: null, openList: null, items: null, alerts: null, alertsBusy: false, study: null, watchTo: "",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const stateBox = (err) => {
|
|
26
|
+
const out = err.code === "not_signed_in" || err.code === "unauthorized" || err.code === "not_live";
|
|
27
|
+
const cmd = out ? html`<p><code data-testid="login-command">npx vfrey login</code></p>` : "";
|
|
28
|
+
return html`<div class="panel" data-testid="creators-state" data-state="${err.code || "error"}">${emptyState("alert", out ? "Sign in to find creators" : err.code === "plan_required" ? "Creators needs the Research plan" : "Creators are not available", err.refusal || "Something went wrong.", html`${cmd}<p class="hint">${err.fix || ""}</p>`)}</div>`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const filterForm = () => html`<section class="panel"><div class="panel-body"><form id="c-form" class="stack" style="gap:12px">
|
|
32
|
+
<div class="add-row" style="grid-template-columns:repeat(auto-fit,minmax(150px,1fr))">
|
|
33
|
+
<label class="field"><span class="muted">Niche</span><select id="f-niche" name="niche" aria-label="Niche"><option value="">Any niche</option>${m.niches.map((n) => html`<option value="${n.id}" ${n.id === m.filters.niche ? "selected" : ""}>${n.name}</option>`)}</select></label>
|
|
34
|
+
<label class="field"><span class="muted">Platform</span><select name="platform" aria-label="Platform"><option value="">All platforms</option>${["tiktok", "instagram", "youtube"].map((p) => html`<option value="${p}" ${p === m.filters.platform ? "selected" : ""}>${platformName(p)}</option>`)}</select></label>
|
|
35
|
+
<label class="field"><span class="muted">Followers</span><input type="text" name="followers" aria-label="Followers" placeholder="10k-200k" value="${m.filters.followers}"></label>
|
|
36
|
+
<label class="field"><span class="muted">Posts per week</span><input type="text" name="postsPerWeek" aria-label="Posts per week" placeholder="3" value="${m.filters.postsPerWeek}"></label>
|
|
37
|
+
<label class="field"><span class="muted">Engagement</span><input type="text" name="engagement" aria-label="Engagement" placeholder="4%" value="${m.filters.engagement}"></label>
|
|
38
|
+
<label class="field"><span class="muted">Sort by</span><select name="sort" aria-label="Sort by">${SORTS.map((s) => html`<option value="${s}" ${s === m.filters.sort ? "selected" : ""}>${s}</option>`)}</select></label>
|
|
39
|
+
</div>
|
|
40
|
+
<div><button type="submit" class="btn" ${m.searching ? "disabled" : ""}>${icon("search")}Search</button> <span class="hint">A search uses credits; see your balance with <code>npx vfrey whoami</code>.</span></div>
|
|
41
|
+
</form></div></section>`;
|
|
42
|
+
|
|
43
|
+
const resultsTable = () => {
|
|
44
|
+
if (m.searching) return html`<p class="muted" style="padding:20px">Searching…</p>`;
|
|
45
|
+
if (!m.results) return html`<section class="panel"><p class="muted" style="padding:20px">Set filters and press Search to find creators.</p></section>`;
|
|
46
|
+
if (!m.results.length) return html`<section class="panel"><p class="muted" style="padding:20px" data-testid="creators-empty">No creators match. Loosen a filter, or try another niche.</p></section>`;
|
|
47
|
+
return html`<section class="panel"><div class="panel-head"><h2>Creators</h2><span class="sub">${DASH} means not known yet. Engagement is a share of views or of followers; do not compare across the two.</span></div>
|
|
48
|
+
<div class="table-wrap"><table data-testid="creators-table"><thead><tr><th>Creator</th><th>Platform</th><th>Followers</th><th>Engagement</th><th>Median views</th><th>Posts / week</th><th>30d growth</th></tr></thead><tbody>${m.results.map((c) => html`<tr data-creator="${c.platform}/${c.handle}">
|
|
49
|
+
<td><button type="button" class="btn ghost sm" data-open="${c.platform}/${c.handle}">@${c.handle}</button>${c.verified ? html` <span class="chip ok">verified</span>` : ""}</td>
|
|
50
|
+
<td>${platformName(c.platform)}</td><td>${short(c.followers)}</td>
|
|
51
|
+
<td>${typeof c.engagementRate === "number" ? html`${pct(c.engagementRate)} <span class="sub">${basis(c.engagementBasis)}</span>` : DASH}</td>
|
|
52
|
+
<td>${short(c.medianViews)}</td><td>${typeof c.postsPerWeek === "number" ? Number(c.postsPerWeek.toFixed(1)) : DASH}</td><td>${signed(c.growth30d)}</td></tr>`)}</tbody></table></div></section>`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const profileView = () => {
|
|
56
|
+
const p = m.profile;
|
|
57
|
+
const s = p.stats || {};
|
|
58
|
+
const g = (x, fb) => (typeof x?.pct === "number" ? signed(x.pct) : typeof x?.abs === "number" ? `${x.abs >= 0 ? "+" : ""}${x.abs} followers` : signed(fb));
|
|
59
|
+
const lists = m.watchlists || [];
|
|
60
|
+
const watched = (p.watchlists || []).length > 0;
|
|
61
|
+
return html`<div class="stack" data-testid="creator-profile">
|
|
62
|
+
<p><button type="button" class="btn secondary sm" data-back>Back to results</button></p>
|
|
63
|
+
<section class="panel"><div class="panel-head"><div><h2>@${p.handle} <span class="sub">${platformName(p.platform)}${p.displayName ? ` · ${p.displayName}` : ""}${p.verified ? " · verified" : ""}</span></h2>
|
|
64
|
+
<span class="sub">Followers ${short(p.followers)} · Following ${short(p.following)} · Posts ${short(p.postCount)} · Likes ${short(p.totalLikes)}</span></div></div>
|
|
65
|
+
<div class="panel-body stack" style="gap:12px">
|
|
66
|
+
${p.bio ? html`<p class="sub">${p.bio}</p>` : ""}
|
|
67
|
+
<div class="add-row" style="grid-template-columns:1fr auto auto">
|
|
68
|
+
<label class="field"><span class="muted">Watchlist</span><select id="c-watchlist" aria-label="Watchlist">${lists.map((l) => html`<option value="${l.name}" ${l.name === m.watchTo ? "selected" : ""}>${l.name}</option>`)}<option value="Watching" ${lists.some((l) => l.name === "Watching") ? "hidden" : ""}>Watching (new)</option></select></label>
|
|
69
|
+
<button type="button" class="btn secondary" data-watch="add" ${m.profileBusy ? "disabled" : ""}>${icon("plus")}Watch</button>
|
|
70
|
+
<button type="button" class="btn" data-study ${m.profileBusy ? "disabled" : ""}>${icon("film")}Study formats</button>
|
|
71
|
+
</div>
|
|
72
|
+
${watched ? html`<p class="hint">On ${p.watchlists.length} of your watchlists.</p>` : ""}
|
|
73
|
+
${m.study ? html`<p data-testid="study-done" class="sub">Wrote <code>${m.study.file}</code> (${m.study.formats} formats, ${m.study.hooks} hooks). Your agent reads it before designing a scene.</p>` : ""}
|
|
74
|
+
</div></section>
|
|
75
|
+
<section class="panel"><div class="panel-head"><h2>Stats</h2><span class="sub">${typeof s.sampleSize === "number" ? `from ${s.sampleSize} recent posts` : "sample size not known"}</span></div>
|
|
76
|
+
<div class="panel-body"><dl data-testid="creator-stats" style="display:grid;grid-template-columns:max-content 1fr;gap:6px 24px;margin:0">
|
|
77
|
+
<dt class="muted">Median views</dt><dd>${short(p.medianViews)}</dd>
|
|
78
|
+
<dt class="muted">Engagement</dt><dd>${typeof p.engagementRate === "number" ? html`${pct(p.engagementRate)} <span class="sub">${basis(p.engagementBasis)}</span>` : DASH}</dd>
|
|
79
|
+
<dt class="muted">Posts per week</dt><dd>${typeof p.postsPerWeek === "number" ? html`${Number(p.postsPerWeek.toFixed(1))} <span class="sub">${s.postsPerWeekBasis === "30d" ? "last 30 days" : s.postsPerWeekBasis === "sample" ? "over the recent posts only" : ""}</span>` : DASH}</dd>
|
|
80
|
+
<dt class="muted">Follower growth</dt><dd>7d ${g(s.growth7d, p.growth7d)} · 30d ${g(s.growth30d, p.growth30d)}</dd>
|
|
81
|
+
<dt class="muted">Median length</dt><dd>${typeof s.medianLengthSec === "number" ? `${Math.round(s.medianLengthSec)}s` : DASH}</dd>
|
|
82
|
+
<dt class="muted">Business contact</dt><dd>${p.businessContact ? html`${p.businessContact.value} <span class="sub">from their public bio</span>` : "none in their public bio"}</dd>
|
|
83
|
+
</dl></div></section>
|
|
84
|
+
<section class="panel"><div class="panel-head"><h2>Formats</h2></div>${(p.formats || []).length ? html`<ul class="evidence" style="list-style:none;padding:0 20px 16px;margin:0" data-testid="creator-formats">${p.formats.map((f) => html`<li class="sub"><b>${f.name}</b> · ${f.postCount} posts · ${(f.terms || []).join(", ")}</li>`)}</ul>` : html`<p class="muted" style="padding:0 20px 16px">None found yet.</p>`}</section>
|
|
85
|
+
<section class="panel"><div class="panel-head"><h2>Hooks</h2><span class="sub">The first sentence of their captions.</span></div>${(p.hooks || []).length ? html`<ul style="list-style:none;padding:0 20px 16px;margin:0" data-testid="creator-hooks">${p.hooks.map((h) => html`<li class="sub">“${h.text}”</li>`)}</ul>` : html`<p class="muted" style="padding:0 20px 16px">None found yet.</p>`}</section>
|
|
86
|
+
<section class="panel"><div class="panel-head"><h2>Top posts</h2></div>${(p.topPosts || []).length ? html`<ul style="list-style:none;padding:0 20px 16px;margin:0" data-testid="creator-posts">${p.topPosts.map((t) => html`<li class="sub">${safeUrl(t.url) ? html`<a href="${safeUrl(t.url)}" target="_blank" rel="noopener noreferrer">${t.url}</a>` : "(no link)"} · ${short(t.metrics?.views)} views · ${short(t.metrics?.likes)} likes${t.caption ? ` · ${String(t.caption).slice(0, 80)}` : ""}</li>`)}</ul>` : html`<p class="muted" style="padding:0 20px 16px">None found yet.</p>`}</section>
|
|
87
|
+
</div>`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const watchlistsTab = () => {
|
|
91
|
+
if (!m.watchlists) return html`<p class="muted" style="padding:20px">Loading watchlists…</p>`;
|
|
92
|
+
const list = m.watchlists.length ? m.watchlists.map((l) => html`<div class="list-row" data-watchlist="${l.id}"><div class="grow"><p class="t"><b>${l.name}</b></p><p class="sub">${l.items} creator${l.items === 1 ? "" : "s"}</p></div><button type="button" class="btn secondary sm" data-list="${l.id}">Show</button></div>`) : html`<p class="muted" style="padding:20px" data-testid="watchlists-empty">No watchlists yet. Open a creator and press Watch.</p>`;
|
|
93
|
+
const items = m.openList && m.items ? html`<section class="panel" data-testid="watchlist-items"><div class="panel-head"><h2>${m.openList.name}</h2><span class="sub">Export it with <code>npx vfrey creators export --watchlist ${m.openList.id} > watchlist.csv</code></span></div>${m.items.length ? m.items.map((i) => html`<div class="list-row" data-item="${i.platform}/${i.handle}"><div class="grow"><p class="t">@${i.handle} <span class="sub">${platformName(i.platform)} · ${short(i.followers)} followers</span></p></div><button type="button" class="btn ghost sm" data-open="${i.platform}/${i.handle}">Profile</button><button type="button" class="btn danger sm" data-unwatch="${i.platform}/${i.handle}">Remove</button></div>`) : html`<p class="muted" style="padding:20px">Nobody yet.</p>`}</section>` : "";
|
|
94
|
+
return html`<div class="stack"><section class="panel"><div class="panel-head"><h2>Watchlists</h2><span class="sub">Watching is free.</span></div>${list}</section>${items}</div>`;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const alertsTab = () => {
|
|
98
|
+
const body = m.alertsBusy ? html`<p class="muted" style="padding:20px">Checking…</p>` : !m.alerts ? html`<p class="muted" style="padding:20px">Checking alerts uses credits, so it runs when you ask.</p>`
|
|
99
|
+
: !m.alerts.alerts.length ? html`<p class="muted" style="padding:20px" data-testid="alerts-empty">No breakouts right now.</p>`
|
|
100
|
+
: m.alerts.alerts.map((b) => html`<div class="list-row" data-alert="${b.postId}"><div class="grow"><p class="t"><b>@${b.creator.handle}</b> <span class="sub">${platformName(b.creator.platform)} · ${b.multiple}x their median</span></p><p class="sub">${safeUrl(b.url) ? html`<a href="${safeUrl(b.url)}" target="_blank" rel="noopener noreferrer">${b.url}</a>` : "(no link)"} · ${short(b.views)} views (median ${short(b.medianViews)})</p></div></div>`);
|
|
101
|
+
return html`<section class="panel"><div class="panel-head"><h2>Breakouts</h2><span class="sub">${m.alerts ? `A post counts at ${m.alerts.rule?.multiple ?? 5}x the creator's median views within ${m.alerts.rule?.withinHours ?? 48} hours.` : "On creators you watch."}</span></div><div class="panel-body"><button type="button" class="btn secondary" data-check-alerts ${m.alertsBusy ? "disabled" : ""}>${icon("refresh")}Check alerts</button></div>${body}</section>`;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const draw = () => {
|
|
105
|
+
if (!root) return;
|
|
106
|
+
if (m.error) { setHtml($(root, "#c-body"), stateBox(m.error)); return; }
|
|
107
|
+
const tabs = html`<div class="filters" role="group" aria-label="Creators sections" style="margin-bottom:16px">${TABS.map(([id, label]) => html`<button type="button" class="filter" data-tab="${id}" aria-pressed="${m.tab === id}">${label}</button>`)}</div>`;
|
|
108
|
+
let main;
|
|
109
|
+
if (m.profile) main = profileView();
|
|
110
|
+
else if (m.tab === "watchlists") main = watchlistsTab();
|
|
111
|
+
else if (m.tab === "alerts") main = alertsTab();
|
|
112
|
+
else main = html`<div class="stack">${filterForm()}${resultsTable()}</div>`;
|
|
113
|
+
const keep = document.activeElement?.id;
|
|
114
|
+
setHtml($(root, "#c-body"), html`${m.profile ? "" : tabs}${main}`);
|
|
115
|
+
if (keep) document.getElementById(keep)?.focus();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const fail = (r) => { if (["not_signed_in", "unauthorized", "not_live", "offline"].includes(r.code)) { m.error = r; draw(); } else refusalToast(r); };
|
|
119
|
+
|
|
120
|
+
const loadWatchlists = async () => {
|
|
121
|
+
const r = await get("/api/creators/watchlists");
|
|
122
|
+
if (!r.ok) { if (["not_signed_in", "unauthorized", "not_live", "offline", "plan_required"].includes(r.code)) { m.error = r; draw(); } else { m.watchlists = []; refusalToast(r); } return; }
|
|
123
|
+
m.watchlists = r.watchlists;
|
|
124
|
+
if (!m.watchTo) m.watchTo = r.watchlists[0]?.name || "Watching";
|
|
125
|
+
draw();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const search = async () => {
|
|
129
|
+
const f = m.filters;
|
|
130
|
+
const q = new URLSearchParams();
|
|
131
|
+
for (const [k, v] of Object.entries(f)) if (v) q.set(k, v);
|
|
132
|
+
m.searching = true; draw();
|
|
133
|
+
const r = await get(`/api/creators/search?${q}`);
|
|
134
|
+
m.searching = false;
|
|
135
|
+
if (!r.ok) { m.results = null; if (r.code === "bad_request") toast(r.refusal, { kind: "error", detail: r.fix }); else fail(r); draw(); return; }
|
|
136
|
+
m.results = r.creators; draw();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const openProfile = async (key) => {
|
|
140
|
+
const [platform, handle] = key.split("/");
|
|
141
|
+
m.profileBusy = true; m.study = null;
|
|
142
|
+
const r = await get(`/api/creators/profile?platform=${encodeURIComponent(platform)}&handle=${encodeURIComponent(handle)}`);
|
|
143
|
+
m.profileBusy = false;
|
|
144
|
+
if (!r.ok) { fail(r); return; }
|
|
145
|
+
m.profile = r.profile; draw();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
title: "Creators",
|
|
150
|
+
mount(el) {
|
|
151
|
+
root = el;
|
|
152
|
+
setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Creators</span><h1>Creators</h1><p class="sub">Find creators in a niche, and study the formats and hooks that work for them.</p></div></div><div id="c-body"></div>`);
|
|
153
|
+
el.addEventListener("input", (e) => { const n = e.target.name; if (n && n in m.filters) m.filters[n] = e.target.value; });
|
|
154
|
+
el.addEventListener("change", (e) => { const n = e.target.name; if (n && n in m.filters) m.filters[n] = e.target.value; if (e.target.id === "c-watchlist") m.watchTo = e.target.value; });
|
|
155
|
+
el.addEventListener("submit", (e) => { if (e.target.id === "c-form") { e.preventDefault(); search(); } });
|
|
156
|
+
el.addEventListener("click", async (e) => {
|
|
157
|
+
const t = e.target.closest("button"); if (!t) return;
|
|
158
|
+
if (t.dataset.tab) { m.tab = t.dataset.tab; draw(); if (m.tab === "watchlists") loadWatchlists(); return; }
|
|
159
|
+
if (t.dataset.open) { openProfile(t.dataset.open); return; }
|
|
160
|
+
if (t.hasAttribute("data-back")) { m.profile = null; draw(); return; }
|
|
161
|
+
if (t.dataset.list) {
|
|
162
|
+
const r = await get(`/api/creators/watchlist?id=${encodeURIComponent(t.dataset.list)}`);
|
|
163
|
+
if (!r.ok) { fail(r); return; }
|
|
164
|
+
m.openList = r.watchlist; m.items = r.items; draw(); return;
|
|
165
|
+
}
|
|
166
|
+
if (t.dataset.unwatch && m.openList) {
|
|
167
|
+
const [platform, handle] = t.dataset.unwatch.split("/");
|
|
168
|
+
const r = await api("/api/creators/watch", { action: "remove", watchlist: m.openList.name, platform, handle });
|
|
169
|
+
if (!r.ok) { refusalToast(r); return; }
|
|
170
|
+
toast(`Removed @${handle}`, { detail: m.openList.name });
|
|
171
|
+
const again = await get(`/api/creators/watchlist?id=${encodeURIComponent(m.openList.id)}`);
|
|
172
|
+
if (again.ok) m.items = again.items;
|
|
173
|
+
loadWatchlists(); return;
|
|
174
|
+
}
|
|
175
|
+
if (t.dataset.watch && m.profile) {
|
|
176
|
+
m.profileBusy = true; draw();
|
|
177
|
+
const r = await api("/api/creators/watch", { action: "add", watchlist: m.watchTo || "Watching", platform: m.profile.platform, handle: m.profile.handle });
|
|
178
|
+
m.profileBusy = false;
|
|
179
|
+
if (!r.ok) { refusalToast(r); draw(); return; }
|
|
180
|
+
m.profile.watchlists = [...new Set([...(m.profile.watchlists || []), r.watchlist.id])];
|
|
181
|
+
m.watchTo = r.watchlist.name;
|
|
182
|
+
toast(r.created ? `Watching @${m.profile.handle}` : `Already watching @${m.profile.handle}`, { detail: r.watchlist.name });
|
|
183
|
+
draw(); loadWatchlists(); return;
|
|
184
|
+
}
|
|
185
|
+
if (t.hasAttribute("data-study") && m.profile) {
|
|
186
|
+
m.profileBusy = true; draw();
|
|
187
|
+
const r = await api("/api/creators/study", { platform: m.profile.platform, handle: m.profile.handle });
|
|
188
|
+
m.profileBusy = false;
|
|
189
|
+
if (!r.ok) { refusalToast(r); draw(); return; }
|
|
190
|
+
m.study = r; toast("Formats saved for your agent", { detail: r.file }); draw(); return;
|
|
191
|
+
}
|
|
192
|
+
if (t.hasAttribute("data-check-alerts")) {
|
|
193
|
+
m.alertsBusy = true; draw();
|
|
194
|
+
const r = await get("/api/creators/alerts");
|
|
195
|
+
m.alertsBusy = false;
|
|
196
|
+
if (!r.ok) { fail(r); draw(); return; }
|
|
197
|
+
m.alerts = r; draw();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
draw();
|
|
201
|
+
(async () => {
|
|
202
|
+
const n = await get("/api/topics/niches");
|
|
203
|
+
if (n.ok) { m.niches = n.niches; draw(); }
|
|
204
|
+
loadWatchlists();
|
|
205
|
+
})();
|
|
206
|
+
},
|
|
207
|
+
update() {},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Account keys: one form per platform. Write only. A saved value is never shown again.
|
|
2
|
+
import { html, setHtml, $, $$, api, toast, refusalToast, store, rel, abs } from "./core.js";
|
|
3
|
+
import { emptyState } from "./components.js";
|
|
4
|
+
|
|
5
|
+
// Which stored fields each platform needs. `account` fields belong to the account, `series` fields to each of its series.
|
|
6
|
+
const GROUPS = [
|
|
7
|
+
{ key: "youtube", name: "YouTube", account: ["youtubeApiKey"], series: ["googleClientId", "googleClientSecret", "youtubeRefreshToken"] },
|
|
8
|
+
{ key: "tiktok", name: "TikTok", account: ["tiktokClientKey", "tiktokClientSecret"], series: ["tiktokRefreshToken", "tiktokOpenId"] },
|
|
9
|
+
{ key: "instagram", name: "Instagram", account: ["metaAppId", "metaAppSecret"], series: ["igUserId", "igAccessToken"], note: "The Meta app id and secret also serve Facebook." },
|
|
10
|
+
{ key: "facebook", name: "Facebook", account: [], series: ["fbPageId", "fbPageToken"], note: "Uses the Meta app id and secret saved under Instagram." },
|
|
11
|
+
{ key: "reddit", name: "Reddit sources", account: ["redditClientId", "redditClientSecret"], series: [] },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function keysView() {
|
|
15
|
+
let root, accountId = null, shape = "", confirming = null;
|
|
16
|
+
const creds = () => store.state?.credentials || [];
|
|
17
|
+
const find = (kind, owner, field) => creds().find((c) => c.ownerKind === kind && c.ownerId === owner && c.field === field);
|
|
18
|
+
const seriesOf = (acc) => (store.state?.series || []).filter((x) => x.accountId === acc);
|
|
19
|
+
const key = (kind, owner, field) => `${kind}|${owner}|${field}`;
|
|
20
|
+
|
|
21
|
+
function row(kind, owner, field) {
|
|
22
|
+
const k = key(kind, owner, field);
|
|
23
|
+
return html`<div class="sec-row" data-secret="${k}"><div class="sec-name"><label for="k-${k}"><span class="mono">${field}</span></label><span class="chip warn" data-state>Missing</span><span class="sub" data-when></span></div>
|
|
24
|
+
<div class="sec-form"><input type="password" id="k-${k}" data-field="${k}" autocomplete="new-password" spellcheck="false" placeholder="Leave blank to keep" aria-label="${field} value"><span data-clear></span></div></div>`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function build(s) {
|
|
28
|
+
const accs = s.accounts || [];
|
|
29
|
+
const acc = accs.find((a) => a.id === accountId) || accs[0];
|
|
30
|
+
accountId = acc?.id ?? null;
|
|
31
|
+
if (!acc) {
|
|
32
|
+
setHtml($(root, "#keys-body"), html`<div class="panel">${emptyState("series", "No account yet", "Create an account and a series first. Keys belong to an account and its series.")}</div>`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const series = seriesOf(acc.id);
|
|
36
|
+
const picker = accs.length > 1 ? html`<div class="field" style="max-width:320px"><label for="acct">Account</label><select id="acct">${accs.map((a) => html`<option value="${a.id}" ${a.id === acc.id ? "selected" : ""}>${a.name}</option>`)}</select></div>` : "";
|
|
37
|
+
setHtml($(root, "#keys-body"), html`<div class="stack">${picker}${GROUPS.map((g) => html`
|
|
38
|
+
<form class="panel" data-group="${g.key}"><div class="panel-head"><h2>${g.name}</h2><span class="sub">${g.note || "Account " + acc.name}</span></div><div class="panel-body">
|
|
39
|
+
${g.account.length ? html`<p class="group-label">Account · ${acc.name}</p>${g.account.map((f) => row("account", acc.id, f))}` : ""}
|
|
40
|
+
${g.series.length ? series.map((x) => html`<p class="group-label">Series · ${x.id}</p>${g.series.map((f) => row("series", x.id, f))}`) : ""}
|
|
41
|
+
${g.series.length && !series.length && !g.account.length ? html`<p class="muted">This account has no series yet.</p>` : ""}
|
|
42
|
+
<div class="form-foot"><button type="submit" class="btn">Save ${g.name} keys</button><span class="hint">Blank fields keep what is stored.</span></div></div></form>`)}</div>`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function paint() {
|
|
46
|
+
for (const r of $$(root, "[data-secret]")) {
|
|
47
|
+
const [kind, owner, field] = r.dataset.secret.split("|");
|
|
48
|
+
const c = find(kind, owner, field);
|
|
49
|
+
const chip = $(r, "[data-state]");
|
|
50
|
+
chip.className = `chip ${c ? "ok" : "warn"}`;
|
|
51
|
+
chip.textContent = c ? "Present" : "Missing";
|
|
52
|
+
const when = $(r, "[data-when]");
|
|
53
|
+
when.textContent = c ? (c.setAt ? `Set ${rel(c.setAt)}` : "Set earlier") : "";
|
|
54
|
+
when.title = c?.setAt ? abs(c.setAt) : "";
|
|
55
|
+
const k = r.dataset.secret;
|
|
56
|
+
const slot = $(r, "[data-clear]");
|
|
57
|
+
if (!c) setHtml(slot, html``);
|
|
58
|
+
else if (confirming === k) setHtml(slot, html`<span class="confirm" role="group" aria-label="Confirm clear">Clear ${field}?<button type="button" class="btn danger solid sm" data-act="clear-yes" data-k="${k}">Yes, clear</button><button type="button" class="btn ghost sm" data-act="clear-no">Keep it</button></span>`);
|
|
59
|
+
else setHtml(slot, html`<button type="button" class="btn secondary sm" data-act="clear" data-k="${k}">Clear</button>`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
title: "Keys",
|
|
65
|
+
mount(el) {
|
|
66
|
+
root = el;
|
|
67
|
+
setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Keys</span><h1>Keys</h1><p class="sub">Platform keys for this project, stored sealed. A saved value is never shown again; this page only says whether it is present.</p></div></div><div id="keys-body"></div>`);
|
|
68
|
+
el.addEventListener("change", (e) => {
|
|
69
|
+
if (e.target.id !== "acct") return;
|
|
70
|
+
accountId = e.target.value; shape = ""; confirming = null;
|
|
71
|
+
if (store.state) this.update(store.state);
|
|
72
|
+
});
|
|
73
|
+
el.addEventListener("submit", async (e) => {
|
|
74
|
+
const form = e.target.closest("form[data-group]");
|
|
75
|
+
if (!form) return;
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
const filled = $$(form, "input[data-field]").filter((i) => i.value.trim() !== "");
|
|
78
|
+
if (!filled.length) { toast("Nothing to save", { detail: "Blank fields keep the stored value." }); return; }
|
|
79
|
+
let saved = 0;
|
|
80
|
+
for (const input of filled) {
|
|
81
|
+
const [ownerKind, ownerId, field] = input.dataset.field.split("|");
|
|
82
|
+
const value = input.value;
|
|
83
|
+
input.value = "";
|
|
84
|
+
const r = await api("/api/secrets", { ownerKind, ownerId, field, value });
|
|
85
|
+
if (!r.ok) { refusalToast(r); for (const i of filled) i.value = ""; return; }
|
|
86
|
+
saved += 1;
|
|
87
|
+
}
|
|
88
|
+
toast(`${saved} key${saved === 1 ? "" : "s"} saved`);
|
|
89
|
+
store.refresh?.();
|
|
90
|
+
});
|
|
91
|
+
el.addEventListener("click", async (e) => {
|
|
92
|
+
const b = e.target.closest("[data-act]");
|
|
93
|
+
if (!b) return;
|
|
94
|
+
if (b.dataset.act === "clear") { confirming = b.dataset.k; paint(); }
|
|
95
|
+
if (b.dataset.act === "clear-no") { confirming = null; paint(); }
|
|
96
|
+
if (b.dataset.act === "clear-yes") {
|
|
97
|
+
const [ownerKind, ownerId, field] = b.dataset.k.split("|");
|
|
98
|
+
confirming = null;
|
|
99
|
+
const r = await api("/api/secrets/clear", { ownerKind, ownerId, field });
|
|
100
|
+
if (!r.ok) { paint(); return refusalToast(r); }
|
|
101
|
+
toast(`${field} cleared`);
|
|
102
|
+
store.refresh?.();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
update(s) {
|
|
107
|
+
const accs = s.accounts || [];
|
|
108
|
+
const acc = accs.find((a) => a.id === accountId) || accs[0];
|
|
109
|
+
const next = `${acc?.id}|${accs.map((a) => a.id).join(",")}|${seriesOf(acc?.id).map((x) => x.id).join(",")}`;
|
|
110
|
+
if (next !== shape) { shape = next; build(s); }
|
|
111
|
+
if (acc) paint();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|