@julioventura/opensquad 0.1.17
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/README.md +433 -0
- package/_opensquad/config/playwright.config.json +11 -0
- package/_opensquad/core/architect.agent.yaml +112 -0
- package/_opensquad/core/best-practices/_catalog.yaml +126 -0
- package/_opensquad/core/best-practices/blog-post.md +132 -0
- package/_opensquad/core/best-practices/blog-seo.md +127 -0
- package/_opensquad/core/best-practices/brand-resolution-checklist.md +172 -0
- package/_opensquad/core/best-practices/copywriting.md +441 -0
- package/_opensquad/core/best-practices/data-analysis.md +401 -0
- package/_opensquad/core/best-practices/email-newsletter.md +118 -0
- package/_opensquad/core/best-practices/email-sales.md +110 -0
- package/_opensquad/core/best-practices/image-design.md +348 -0
- package/_opensquad/core/best-practices/instagram-feed.md +235 -0
- package/_opensquad/core/best-practices/instagram-reels.md +112 -0
- package/_opensquad/core/best-practices/instagram-stories.md +107 -0
- package/_opensquad/core/best-practices/linkedin-article.md +116 -0
- package/_opensquad/core/best-practices/linkedin-post.md +121 -0
- package/_opensquad/core/best-practices/researching.md +349 -0
- package/_opensquad/core/best-practices/review.md +269 -0
- package/_opensquad/core/best-practices/run-recovery.md +61 -0
- package/_opensquad/core/best-practices/social-networks-publishing.md +327 -0
- package/_opensquad/core/best-practices/squad-creation-checklist.md +32 -0
- package/_opensquad/core/best-practices/strategist.md +344 -0
- package/_opensquad/core/best-practices/technical-writing.md +365 -0
- package/_opensquad/core/best-practices/twitter-post.md +105 -0
- package/_opensquad/core/best-practices/twitter-thread.md +122 -0
- package/_opensquad/core/best-practices/whatsapp-broadcast.md +107 -0
- package/_opensquad/core/best-practices/youtube-script.md +122 -0
- package/_opensquad/core/best-practices/youtube-shorts.md +112 -0
- package/_opensquad/core/defaults/youtube-video-assembly.json +84 -0
- package/_opensquad/core/prompts/build.prompt.md +613 -0
- package/_opensquad/core/prompts/design.prompt.md +606 -0
- package/_opensquad/core/prompts/discovery.prompt.md +377 -0
- package/_opensquad/core/prompts/sherlock-instagram.md +123 -0
- package/_opensquad/core/prompts/sherlock-linkedin.md +73 -0
- package/_opensquad/core/prompts/sherlock-shared.md +684 -0
- package/_opensquad/core/prompts/sherlock-twitter.md +78 -0
- package/_opensquad/core/prompts/sherlock-youtube.md +85 -0
- package/_opensquad/core/runner.pipeline.md +743 -0
- package/_opensquad/core/skills.engine.md +384 -0
- package/bin/opensquad.js +108 -0
- package/dashboard/index.html +15 -0
- package/dashboard/package-lock.json +1964 -0
- package/dashboard/package.json +28 -0
- package/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female4_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female5_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female5_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female5_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female6_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female6_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female6_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male4_wave.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/backpack_blue.png +0 -0
- package/dashboard/public/assets/furniture/backpack_red.png +0 -0
- package/dashboard/public/assets/furniture/blinds.png +0 -0
- package/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
- package/dashboard/public/assets/furniture/bulletin_board.png +0 -0
- package/dashboard/public/assets/furniture/clock.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
- package/dashboard/public/assets/furniture/coffee_table.png +0 -0
- package/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
- package/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
- package/dashboard/public/assets/furniture/couch.png +0 -0
- package/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/cushion_blue.png +0 -0
- package/dashboard/public/assets/furniture/cushion_tan.png +0 -0
- package/dashboard/public/assets/furniture/desk_wood.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
- package/dashboard/public/assets/furniture/flowers1.png +0 -0
- package/dashboard/public/assets/furniture/flowers2.png +0 -0
- package/dashboard/public/assets/furniture/lamp_tan.png +0 -0
- package/dashboard/public/assets/furniture/lantern.png +0 -0
- package/dashboard/public/assets/furniture/monstera.png +0 -0
- package/dashboard/public/assets/furniture/monstera_small.png +0 -0
- package/dashboard/public/assets/furniture/picture_frame.png +0 -0
- package/dashboard/public/assets/furniture/plant1.png +0 -0
- package/dashboard/public/assets/furniture/plant2.png +0 -0
- package/dashboard/public/assets/furniture/plant3.png +0 -0
- package/dashboard/public/assets/furniture/plant_poof.png +0 -0
- package/dashboard/public/assets/furniture/plant_spindly.png +0 -0
- package/dashboard/public/assets/furniture/poster_blue.png +0 -0
- package/dashboard/public/assets/furniture/rug.png +0 -0
- package/dashboard/public/assets/furniture/succulent_blue.png +0 -0
- package/dashboard/public/assets/furniture/succulent_green.png +0 -0
- package/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
- package/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
- package/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
- package/dashboard/src/App.tsx +46 -0
- package/dashboard/src/components/RunDashboardButton.tsx +92 -0
- package/dashboard/src/components/SquadCard.tsx +49 -0
- package/dashboard/src/components/SquadSelector.tsx +67 -0
- package/dashboard/src/components/StatusBadge.tsx +32 -0
- package/dashboard/src/components/StatusBar.tsx +116 -0
- package/dashboard/src/hooks/useSquadSocket.ts +135 -0
- package/dashboard/src/lib/formatTime.ts +16 -0
- package/dashboard/src/lib/normalizeState.ts +25 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/office/AgentSprite.ts +241 -0
- package/dashboard/src/office/OfficeScene.ts +153 -0
- package/dashboard/src/office/PhaserGame.tsx +80 -0
- package/dashboard/src/office/RoomBuilder.ts +190 -0
- package/dashboard/src/office/assetKeys.ts +150 -0
- package/dashboard/src/office/palette.ts +32 -0
- package/dashboard/src/plugin/squadWatcher.ts +397 -0
- package/dashboard/src/store/useSquadStore.ts +56 -0
- package/dashboard/src/styles/globals.css +36 -0
- package/dashboard/src/types/state.ts +63 -0
- package/dashboard/src/vite-env.d.ts +1 -0
- package/dashboard/tsconfig.json +24 -0
- package/dashboard/vite.config.ts +13 -0
- package/package.json +59 -0
- package/public/sfx/slide-transition-sfx.mp3 +0 -0
- package/skills/README.md +84 -0
- package/skills/apify/SKILL.md +55 -0
- package/skills/blotato/SKILL.md +63 -0
- package/skills/canva/SKILL.md +60 -0
- package/skills/higgsfield/SKILL.md +147 -0
- package/skills/image-ai-generator/SKILL.md +124 -0
- package/skills/image-ai-generator/scripts/generate.py +175 -0
- package/skills/image-creator/SKILL.md +166 -0
- package/skills/image-creator/editorial-slide-template.js +645 -0
- package/skills/image-fetcher/SKILL.md +91 -0
- package/skills/imgbb-uploader/SKILL.md +73 -0
- package/skills/imgbb-uploader/scripts/upload.js +125 -0
- package/skills/instagram-publisher/README.md +36 -0
- package/skills/instagram-publisher/SKILL.md +231 -0
- package/skills/instagram-publisher/scripts/publish-playwright.js +418 -0
- package/skills/instagram-publisher/scripts/publish.js +521 -0
- package/skills/opensquad-agent-creator/SKILL.md +192 -0
- package/skills/opensquad-skill-creator/SKILL.md +420 -0
- package/skills/opensquad-skill-creator/agents/analyzer.md +274 -0
- package/skills/opensquad-skill-creator/agents/comparator.md +202 -0
- package/skills/opensquad-skill-creator/agents/grader.md +223 -0
- package/skills/opensquad-skill-creator/assets/eval_review.html +146 -0
- package/skills/opensquad-skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/opensquad-skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/opensquad-skill-creator/references/schemas.md +430 -0
- package/skills/opensquad-skill-creator/references/skill-format.md +235 -0
- package/skills/opensquad-skill-creator/scripts/__init__.py +0 -0
- package/skills/opensquad-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/opensquad-skill-creator/scripts/quick_validate.py +103 -0
- package/skills/opensquad-skill-creator/scripts/run_eval.py +310 -0
- package/skills/opensquad-skill-creator/scripts/utils.py +47 -0
- package/skills/pdf-extractor/SKILL.md +57 -0
- package/skills/pdf-extractor/scripts/extract.py +82 -0
- package/skills/resend/SKILL.md +80 -0
- package/skills/run-dashboard/README.md +93 -0
- package/skills/run-dashboard/SKILL.md +173 -0
- package/skills/run-dashboard/scripts/finalize-state.js +273 -0
- package/skills/run-dashboard/scripts/generate.js +1296 -0
- package/skills/run-dashboard/scripts/serve.js +135 -0
- package/skills/run-dashboard/templates/run-dashboard-simple.template.html +191 -0
- package/skills/run-dashboard/templates/run-dashboard.template.html +1164 -0
- package/skills/smtp-sender/SKILL.md +88 -0
- package/skills/smtp-sender/scripts/send.js +478 -0
- package/skills/template-designer/SKILL.md +201 -0
- package/skills/template-designer/base-templates/model-a.html +27 -0
- package/skills/template-designer/base-templates/model-b.html +31 -0
- package/skills/template-designer/base-templates/model-c.html +42 -0
- package/skills/youtube-publisher/SKILL.md +232 -0
- package/skills/youtube-publisher/scripts/publish.js +2078 -0
- package/src/agents-cli.js +158 -0
- package/src/agents.js +134 -0
- package/src/i18n.js +48 -0
- package/src/init.js +442 -0
- package/src/locales/en.json +79 -0
- package/src/locales/es.json +78 -0
- package/src/locales/pt-BR.json +78 -0
- package/src/logger.js +38 -0
- package/src/prompt.js +46 -0
- package/src/readme/README.md +146 -0
- package/src/runs.js +318 -0
- package/src/skills-cli.js +157 -0
- package/src/skills.js +146 -0
- package/src/supabase-cli.js +584 -0
- package/src/update.js +169 -0
- package/templates/_opensquad/.opensquad-version +1 -0
- package/templates/_opensquad/_investigations/.gitkeep +0 -0
- package/templates/ide-templates/antigravity/.agent/rules/opensquad.md +68 -0
- package/templates/ide-templates/antigravity/.agent/workflows/opensquad.md +102 -0
- package/templates/ide-templates/claude-code/.claude/skills/opensquad/SKILL.md +182 -0
- package/templates/ide-templates/claude-code/.mcp.json +8 -0
- package/templates/ide-templates/claude-code/CLAUDE.md +57 -0
- package/templates/ide-templates/codex/.agents/skills/opensquad/SKILL.md +6 -0
- package/templates/ide-templates/codex/AGENTS.md +120 -0
- package/templates/ide-templates/cursor/.cursor/commands/opensquad.md +9 -0
- package/templates/ide-templates/cursor/.cursor/mcp.json +8 -0
- package/templates/ide-templates/cursor/.cursor/rules/opensquad.mdc +62 -0
- package/templates/ide-templates/cursor/.cursorignore +3 -0
- package/templates/ide-templates/gemini-cli/.gemini/settings.json +8 -0
- package/templates/ide-templates/gemini-cli/.gemini/skills/opensquad/SKILL.md +186 -0
- package/templates/ide-templates/gemini-cli/GEMINI.md +57 -0
- package/templates/ide-templates/opencode/.opencode/commands/opensquad.md +9 -0
- package/templates/ide-templates/opencode/AGENTS.md +120 -0
- package/templates/ide-templates/qwen-code/.qwen/settings.json +8 -0
- package/templates/ide-templates/qwen-code/.qwen/skills/opensquad/SKILL.md +182 -0
- package/templates/ide-templates/qwen-code/QWEN.md +57 -0
- package/templates/ide-templates/trae/.trae/mcp.json +8 -0
- package/templates/ide-templates/trae/.trae/rules/opensquad.md +64 -0
- package/templates/ide-templates/vscode-copilot/.github/copilot-instructions.md +59 -0
- package/templates/ide-templates/vscode-copilot/.github/prompts/opensquad.prompt.md +209 -0
- package/templates/ide-templates/vscode-copilot/.vscode/mcp.json +8 -0
- package/templates/ide-templates/vscode-copilot/.vscode/settings.json +3 -0
- package/templates/package.json +8 -0
- package/templates/squads/.gitkeep +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-dashboard
|
|
3
|
+
description: Generate a modern DASHBOARD-RUN HTML snapshot plus JSON data for a completed squad run, including checklist, publishing links, artifacts, errors, and refreshable platform metrics when credentials are available.
|
|
4
|
+
description_pt-BR: Gera um DASHBOARD-RUN em HTML e um snapshot JSON para uma run concluída, com checklist, links de publicação, artefatos, erros e métricas atualizáveis quando houver credenciais.
|
|
5
|
+
type: script
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
script:
|
|
8
|
+
path: scripts/generate.js
|
|
9
|
+
runtime: node
|
|
10
|
+
invoke: "node --env-file=.env {skill_path}/scripts/generate.js --run-dir \"{run_dir}\""
|
|
11
|
+
env:
|
|
12
|
+
- INSTAGRAM_ACCESS_TOKEN
|
|
13
|
+
- INSTAGRAM_USER_ID
|
|
14
|
+
- FACEBOOK_PAGE_ID
|
|
15
|
+
- FACEBOOK_PAGE_ACCESS_TOKEN
|
|
16
|
+
- YOUTUBE_CLIENT_ID
|
|
17
|
+
- YOUTUBE_CLIENT_SECRET
|
|
18
|
+
- YOUTUBE_REFRESH_TOKEN
|
|
19
|
+
- YOUTUBE_CHANNEL_ID
|
|
20
|
+
- SUPABASE_URL
|
|
21
|
+
- SUPABASE_ANON_KEY
|
|
22
|
+
- SUPABASE_SERVICE_ROLE_KEY
|
|
23
|
+
categories: [reporting, dashboard, html, publishing, operations]
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Run Dashboard
|
|
27
|
+
|
|
28
|
+
Use this skill after a run completes to generate a standalone HTML dashboard and a sibling JSON snapshot inside the run folder.
|
|
29
|
+
|
|
30
|
+
Inside Opensquad, this skill is part of automatic run finalization. Do not stop to ask for separate confirmation before generating the dashboard. Run it, validate the files, and report the result.
|
|
31
|
+
|
|
32
|
+
The generated HTML should be branded as `DASHBOARD-RUN`.
|
|
33
|
+
|
|
34
|
+
## When to use
|
|
35
|
+
|
|
36
|
+
- Finalizing any completed run that should leave behind a visual operational report
|
|
37
|
+
- Regenerating a dashboard after publishing recovery, metric refresh, or manual edits to run artifacts
|
|
38
|
+
- Producing an editable HTML file that can be opened in a simple code-and-preview editor
|
|
39
|
+
- Closing the operational report after Instagram, Facebook, YouTube, and optional `jornal-matutino` outputs have already finished
|
|
40
|
+
|
|
41
|
+
## Output contract
|
|
42
|
+
|
|
43
|
+
Always generate both files in the run folder:
|
|
44
|
+
|
|
45
|
+
- `run-dashboard.html`
|
|
46
|
+
- `run-dashboard.data.json`
|
|
47
|
+
|
|
48
|
+
The HTML must stay standalone and editable with inline CSS/JS. The JSON is the refreshable data source for the dashboard.
|
|
49
|
+
|
|
50
|
+
If the squad `channel-config.yaml` declares `dashboard.static_publish.enabled: true`, generating the dashboard should also publish the latest and archived static snapshots automatically as part of the same finalization pass.
|
|
51
|
+
|
|
52
|
+
## Static publish mode
|
|
53
|
+
|
|
54
|
+
When you need to force static publish explicitly, generate it with:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
node --env-file=.env skills/run-dashboard/scripts/generate.js \
|
|
58
|
+
--workspace-root . \
|
|
59
|
+
--run-dir "squads/musicplay-club/output/2026-05-19-020010" \
|
|
60
|
+
--publish-static
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Prefer passing a concrete run folder. Skills that consume versioned artifacts may point to the sibling `v1/` directory, but the run dashboard itself should target the parent run folder.
|
|
64
|
+
|
|
65
|
+
If the squad channel config declares `dashboard.static_publish.publish_dir_env` and the corresponding env var points to the server webroot folder, the command publishes:
|
|
66
|
+
|
|
67
|
+
- latest snapshot as `index.html`
|
|
68
|
+
- archived snapshot as `<run-id>.html`
|
|
69
|
+
- supporting copied files under `assets/latest/` and `assets/<run-id>/`
|
|
70
|
+
|
|
71
|
+
The published HTML remains standalone with the embedded snapshot and does not require the local dashboard server to open.
|
|
72
|
+
|
|
73
|
+
If `dashboard.static_publish.transport` is set to `ftp`, the same command uploads the generated snapshot to the remote folder declared by `publish_dir_env`, using the FTP env keys declared in the squad config.
|
|
74
|
+
|
|
75
|
+
If `dashboard.static_publish.transport` is `both`, treat local publish and remote FTP publish as two separate validations. A successful write to the local publish dir does not prove the public site is updated.
|
|
76
|
+
|
|
77
|
+
Before treating a run as closed, preflight the static publish inputs declared in `channel-config.yaml`:
|
|
78
|
+
|
|
79
|
+
- the env key referenced by `publish_dir_env`
|
|
80
|
+
- the FTP env keys referenced by `dashboard.static_publish.ftp.*` when transport is `ftp` or `both`
|
|
81
|
+
|
|
82
|
+
If these env keys are missing, report the configuration defect explicitly. Do not misclassify it as a dashboard rendering failure.
|
|
83
|
+
|
|
84
|
+
After a `both` or `ftp` publish, validate the public latest URLs explicitly, not just the local files:
|
|
85
|
+
|
|
86
|
+
- `index.html`
|
|
87
|
+
- `run-dashboard.data.json`
|
|
88
|
+
- `youtube.html` when the run includes a YouTube link
|
|
89
|
+
|
|
90
|
+
Also validate the archive URL generated from the run ID. The archive file name comes from the dashboard payload `runId`, not from the publish timestamp shown in `publish-result.md`.
|
|
91
|
+
|
|
92
|
+
If the archive URLs update but the fixed latest URLs stay stale, treat that as a remote sync issue on the latest files. The operational recovery path is to overwrite the remote `index.html`, `run-dashboard.data.json`, and `youtube.html` explicitly via FTP using the freshly generated local publish directory contents, then revalidate the public URLs before closing the run.
|
|
93
|
+
|
|
94
|
+
If `dashboard.static_publish.enabled: true` is already present in the squad config, the default run-finalization call should do this automatically even without an explicit `--publish-static` flag.
|
|
95
|
+
|
|
96
|
+
## Required content
|
|
97
|
+
|
|
98
|
+
- Header with squad, run ID, status, date/time, and last refresh timestamp
|
|
99
|
+
- Checklist display summarizing what was completed in the run
|
|
100
|
+
- Publishing display showing where the content was posted
|
|
101
|
+
- Link buttons for Instagram, Facebook, and YouTube when URLs exist
|
|
102
|
+
- Error/problem display summarizing residual issues and blocking incidents
|
|
103
|
+
- Artifact display with the important run files used to build the dashboard
|
|
104
|
+
- Metrics display with the best snapshot available from Instagram, Facebook, and YouTube
|
|
105
|
+
|
|
106
|
+
## Metrics rules
|
|
107
|
+
|
|
108
|
+
- Instagram: try likes plus the first available metric among views, video views, plays, or impressions
|
|
109
|
+
- Facebook: try post reactions, comments, and shares when a Page token can be resolved
|
|
110
|
+
- YouTube: try views, likes, comments, and published timestamp using the channel OAuth credentials
|
|
111
|
+
- If a metric cannot be fetched, render `N/A` without failing the dashboard
|
|
112
|
+
|
|
113
|
+
## Refresh behavior
|
|
114
|
+
|
|
115
|
+
- The HTML refresh button should try to fetch `run-dashboard.data.json` from the same folder with cache-busting
|
|
116
|
+
- If the JSON cannot be fetched in the current environment, keep showing the embedded snapshot instead of breaking the page
|
|
117
|
+
- When the dashboard is served through the local `serve.js` helper, the refresh button should first call the local regeneration endpoint so the JSON snapshot and metrics are rebuilt before the page re-renders
|
|
118
|
+
|
|
119
|
+
## Visual previews
|
|
120
|
+
|
|
121
|
+
- When slide renders exist in `images/`, show them as preview cards and prefer linking each card to the editable `slide-xx.html` source
|
|
122
|
+
- When a YouTube thumbnail exists in `thumbs/` or is declared in `publish-result.md`, show it in the preview section and link it to the published video when possible
|
|
123
|
+
|
|
124
|
+
## Supabase policy
|
|
125
|
+
|
|
126
|
+
- Dashboard generation must not depend on Supabase
|
|
127
|
+
- If a future run persists dashboard snapshots, templates, profiles, or customization data in Supabase, every Opensquad-managed table name must use the `os_` prefix
|
|
128
|
+
- Examples: `os_profiles`, `os_run_reports`, `os_dashboard_templates`
|
|
129
|
+
|
|
130
|
+
## Default layout (simplified format — v2)
|
|
131
|
+
|
|
132
|
+
All squads should use the simplified `run-dashboard-simple.template.html` layout unless the squad's local config explicitly overrides it. The original complex template (`run-dashboard.template.html`) remains available for squads that need metrics refresh, collapsible panels, and modal zoom.
|
|
133
|
+
|
|
134
|
+
The simplified layout contains exactly these containers, in order:
|
|
135
|
+
|
|
136
|
+
1. **Header** — Squad name, run ID, publish date, status badge
|
|
137
|
+
2. **Row 1 (2 columns):**
|
|
138
|
+
- **Resumo do Tema** — Pauta, ângulo editorial, tom aplicado
|
|
139
|
+
- **Publicações** — Links clicáveis para Instagram, Facebook, YouTube e newsletter status
|
|
140
|
+
3. **Row 2 (full width):**
|
|
141
|
+
- **Legenda e Metadados** — Legenda completa + hashtags
|
|
142
|
+
4. **Carrossel Visual Produzido** — Grid de **4 slides por linha** (responsivo: 2 em tablet, 1 em mobile)
|
|
143
|
+
5. **Row 3 (3 columns):**
|
|
144
|
+
- **Descrição — Instagram** — Texto exato usado no post do Instagram
|
|
145
|
+
- **Descrição — Facebook** — Texto exato usado no post do Facebook
|
|
146
|
+
- **Descrição — YouTube** — Texto exato usado na descrição do vídeo (incluindo FONTES VERIFICADAS)
|
|
147
|
+
|
|
148
|
+
### Template placeholders
|
|
149
|
+
|
|
150
|
+
The template uses `{{PLACEHOLDER}}` tokens that agents must replace with actual run data:
|
|
151
|
+
|
|
152
|
+
| Placeholder | Source |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `{{SQUAD_NAME}}` | Squad directory name |
|
|
155
|
+
| `{{RUN_ID}}` | Run folder name |
|
|
156
|
+
| `{{TOPIC}}` | `content-package.md` → Tema central |
|
|
157
|
+
| `{{ANGLE}}` | `content-package.md` → Ângulo editorial |
|
|
158
|
+
| `{{TONE}}` | `content-package.md` → Tom aplicado |
|
|
159
|
+
| `{{IG_URL}}` | `publish-result.md` → Instagram URL |
|
|
160
|
+
| `{{FB_URL}}` | `publish-result.md` → Facebook URL |
|
|
161
|
+
| `{{YT_URL}}` | `publish-result.md` → YouTube URL |
|
|
162
|
+
| `{{CAPTION_FULL}}` | `content-package.md` → Legenda section |
|
|
163
|
+
| `{{HASHTAGS}}` | `content-package.md` → Hashtags section |
|
|
164
|
+
| `{{DESC_INSTAGRAM}}` | Exact caption sent to Instagram publish script |
|
|
165
|
+
| `{{DESC_FACEBOOK}}` | Exact caption sent to Facebook publish script |
|
|
166
|
+
| `{{DESC_YOUTUBE}}` | Exact description sent to YouTube publish script |
|
|
167
|
+
|
|
168
|
+
### Squads using this format
|
|
169
|
+
|
|
170
|
+
- `odontologia-news` — default
|
|
171
|
+
- `musicplay-club` — default
|
|
172
|
+
|
|
173
|
+
Any new squad inherits this layout unless its pipeline explicitly specifies `dashboard_template: complex`.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_ARGS = {
|
|
7
|
+
runDir: '',
|
|
8
|
+
workspaceRoot: process.cwd(),
|
|
9
|
+
force: false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const args = { ...DEFAULT_ARGS };
|
|
14
|
+
|
|
15
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
16
|
+
const current = argv[index];
|
|
17
|
+
if (current === '--run-dir' && index + 1 < argv.length) args.runDir = argv[++index];
|
|
18
|
+
else if (current === '--workspace-root' && index + 1 < argv.length) args.workspaceRoot = argv[++index];
|
|
19
|
+
else if (current === '--force') args.force = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return args;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
const args = parseArgs(process.argv);
|
|
27
|
+
if (!args.runDir) throw new Error('--run-dir is required');
|
|
28
|
+
|
|
29
|
+
const result = await ensureRunStateFile({
|
|
30
|
+
runDir: args.runDir,
|
|
31
|
+
workspaceRoot: args.workspaceRoot,
|
|
32
|
+
force: args.force,
|
|
33
|
+
strict: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.log(`${result.action} run state: ${result.statePath}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
40
|
+
|
|
41
|
+
export async function ensureRunStateFile(options) {
|
|
42
|
+
const runDir = resolve(options.runDir);
|
|
43
|
+
const workspaceRoot = resolve(options.workspaceRoot || process.cwd());
|
|
44
|
+
const statePath = join(runDir, 'state.json');
|
|
45
|
+
const existingState = await readJsonIfExists(statePath);
|
|
46
|
+
|
|
47
|
+
if (!options.force && isFinalizedState(existingState)) {
|
|
48
|
+
return { action: 'kept', state: existingState, statePath };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const squadName = inferSquadName(runDir);
|
|
52
|
+
let nextState = null;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const squadDir = join(workspaceRoot, 'squads', squadName);
|
|
56
|
+
const squadYaml = await readRequiredText(join(squadDir, 'squad.yaml'));
|
|
57
|
+
const squadCode = extractYamlScalar(squadYaml, 'code') || squadName;
|
|
58
|
+
const pipelineFile = extractPipelineFile(squadYaml) || 'pipeline/pipeline.yaml';
|
|
59
|
+
const pipelineYaml = await readRequiredText(join(squadDir, pipelineFile));
|
|
60
|
+
const agents = parseSquadParty(await readRequiredText(join(squadDir, 'squad-party.csv')));
|
|
61
|
+
const publishResult = parsePublishResult(await firstExistingText(runDir, ['publish-result.md', 'v1/publish-result.md']));
|
|
62
|
+
nextState = buildRunStateFromMetadata({
|
|
63
|
+
runDir,
|
|
64
|
+
squadCode,
|
|
65
|
+
totalSteps: countPipelineSteps(pipelineYaml),
|
|
66
|
+
agents,
|
|
67
|
+
existingState,
|
|
68
|
+
publishResult,
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (options.strict) throw error;
|
|
72
|
+
return { action: 'skipped', state: existingState || null, statePath };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!nextState) {
|
|
76
|
+
if (options.strict) {
|
|
77
|
+
throw new Error(`Could not infer final state for '${runDir}'. Add publish-result.md or rerun with a complete run folder.`);
|
|
78
|
+
}
|
|
79
|
+
return { action: 'skipped', state: existingState || null, statePath };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await writeFile(statePath, `${JSON.stringify(nextState, null, 2)}\n`, 'utf-8');
|
|
83
|
+
return { action: existingState ? 'updated' : 'created', state: nextState, statePath };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildRunStateFromMetadata({ runDir, squadCode, totalSteps, agents, existingState, publishResult }) {
|
|
87
|
+
const finalStatus = inferFinalStatus(existingState, publishResult);
|
|
88
|
+
if (!finalStatus) return null;
|
|
89
|
+
|
|
90
|
+
const finalTimestamp = inferFinalTimestamp(existingState, publishResult) || new Date().toISOString();
|
|
91
|
+
const startedAt = existingState?.startedAt || inferStartedAtFromRunDir(runDir) || finalTimestamp;
|
|
92
|
+
const baseState = {
|
|
93
|
+
squad: squadCode,
|
|
94
|
+
status: finalStatus,
|
|
95
|
+
step: {
|
|
96
|
+
current: totalSteps,
|
|
97
|
+
total: totalSteps,
|
|
98
|
+
label: finalStatus === 'failed' ? 'Pipeline failed' : 'Pipeline completed',
|
|
99
|
+
},
|
|
100
|
+
agents: agents.map((agent) => ({
|
|
101
|
+
...agent,
|
|
102
|
+
status: finalStatus === 'failed' ? 'error' : 'done',
|
|
103
|
+
})),
|
|
104
|
+
handoff: existingState?.handoff || null,
|
|
105
|
+
startedAt,
|
|
106
|
+
updatedAt: finalTimestamp,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (finalStatus === 'failed') {
|
|
110
|
+
return {
|
|
111
|
+
...baseState,
|
|
112
|
+
error: publishResult.error || existingState?.error || 'Run failed during publication or closure.',
|
|
113
|
+
failedAt: existingState?.failedAt || finalTimestamp,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...baseState,
|
|
119
|
+
completedAt: existingState?.completedAt || finalTimestamp,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function inferFinalStatus(existingState, publishResult) {
|
|
124
|
+
const existingStatus = normalizeValue(existingState?.status).toLowerCase();
|
|
125
|
+
if (existingStatus === 'completed' || existingStatus === 'failed') return existingStatus;
|
|
126
|
+
|
|
127
|
+
const publishStatus = normalizeValue(publishResult.status).toLowerCase();
|
|
128
|
+
if (publishStatus === 'published') return 'completed';
|
|
129
|
+
if (publishStatus === 'failed' || publishStatus === 'error') return 'failed';
|
|
130
|
+
if (publishResult.error && normalizeValue(publishResult.error).toLowerCase() !== 'null') return 'failed';
|
|
131
|
+
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function inferFinalTimestamp(existingState, publishResult) {
|
|
136
|
+
return existingState?.completedAt
|
|
137
|
+
|| existingState?.failedAt
|
|
138
|
+
|| publishResult.publishedAt
|
|
139
|
+
|| existingState?.updatedAt
|
|
140
|
+
|| '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function inferStartedAtFromRunDir(runDir) {
|
|
144
|
+
const normalized = runDir.replace(/\\/g, '/');
|
|
145
|
+
const runId = normalized.split('/').filter(Boolean).at(-1) || '';
|
|
146
|
+
const match = runId.match(/^(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})$/);
|
|
147
|
+
if (!match) return '';
|
|
148
|
+
|
|
149
|
+
const [, year, month, day, hours, minutes, seconds] = match;
|
|
150
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes), Number(seconds));
|
|
151
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
152
|
+
return date.toISOString();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function inferSquadName(runDir) {
|
|
156
|
+
const normalized = resolve(runDir).replace(/\\/g, '/');
|
|
157
|
+
const parts = normalized.split('/');
|
|
158
|
+
const squadsIndex = parts.lastIndexOf('squads');
|
|
159
|
+
return squadsIndex !== -1 ? parts[squadsIndex + 1] : 'unknown-squad';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function extractPipelineFile(content) {
|
|
163
|
+
const lines = content.split(/\r?\n/);
|
|
164
|
+
const pipelineIndex = lines.findIndex((line) => /^pipeline:\s*$/.test(line.trim()));
|
|
165
|
+
if (pipelineIndex === -1) return '';
|
|
166
|
+
|
|
167
|
+
for (let index = pipelineIndex + 1; index < lines.length; index += 1) {
|
|
168
|
+
const line = lines[index];
|
|
169
|
+
if (/^\S/.test(line)) break;
|
|
170
|
+
const match = line.match(/^\s+file:\s*"?([^"\n]+)"?\s*$/);
|
|
171
|
+
if (match) return match[1].trim();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function countPipelineSteps(content) {
|
|
178
|
+
const lines = content.split(/\r?\n/);
|
|
179
|
+
const stepsIndex = lines.findIndex((line) => /^steps:\s*$/.test(line.trim()));
|
|
180
|
+
if (stepsIndex === -1) return 0;
|
|
181
|
+
|
|
182
|
+
let total = 0;
|
|
183
|
+
for (let index = stepsIndex + 1; index < lines.length; index += 1) {
|
|
184
|
+
const line = lines[index];
|
|
185
|
+
if (/^\S/.test(line)) break;
|
|
186
|
+
if (/^\s*-\s+file:\s+/.test(line)) total += 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return total;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseSquadParty(content) {
|
|
193
|
+
const lines = content.split(/\r?\n/).filter(Boolean);
|
|
194
|
+
return lines.slice(1).map((line, index) => {
|
|
195
|
+
const [name, icon, path] = line.split(',');
|
|
196
|
+
return {
|
|
197
|
+
id: String(path || '')
|
|
198
|
+
.trim()
|
|
199
|
+
.replace(/^\.\/agents\//, '')
|
|
200
|
+
.replace(/\.agent\.md$/i, ''),
|
|
201
|
+
name: normalizeValue(name),
|
|
202
|
+
icon: normalizeValue(icon),
|
|
203
|
+
desk: {
|
|
204
|
+
col: (index % 3) + 1,
|
|
205
|
+
row: Math.floor(index / 3) + 1,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}).filter((entry) => entry.id && entry.name);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parsePublishResult(content) {
|
|
212
|
+
return {
|
|
213
|
+
status: extractLabeledValue(content, 'Status'),
|
|
214
|
+
publishedAt: extractLabeledValue(content, 'Publicado em'),
|
|
215
|
+
error: extractLabeledValue(content, 'Erro remanescente'),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function extractYamlScalar(content, key) {
|
|
220
|
+
const match = content.match(new RegExp(`^${key}:\\s*"?([^"\\n]+)"?\\s*$`, 'm'));
|
|
221
|
+
return match ? match[1].trim() : '';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function extractLabeledValue(content, label) {
|
|
225
|
+
if (!content) return '';
|
|
226
|
+
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
227
|
+
const match = content.match(new RegExp(`\\*\\*${escapedLabel}:\\*\\*\\s*(.+)`, 'i'));
|
|
228
|
+
return match ? normalizeValue(match[1]) : '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeValue(value) {
|
|
232
|
+
return String(value || '')
|
|
233
|
+
.replace(/^['"`<]+/, '')
|
|
234
|
+
.replace(/['"`>]+$/, '')
|
|
235
|
+
.trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isFinalizedState(state) {
|
|
239
|
+
if (!state || typeof state !== 'object') return false;
|
|
240
|
+
const status = normalizeValue(state.status).toLowerCase();
|
|
241
|
+
return (status === 'completed' && Boolean(state.completedAt)) || (status === 'failed' && Boolean(state.failedAt));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function firstExistingText(runDir, relativePaths) {
|
|
245
|
+
for (const relativePath of relativePaths) {
|
|
246
|
+
try {
|
|
247
|
+
return await readFile(join(runDir, relativePath), 'utf-8');
|
|
248
|
+
} catch {
|
|
249
|
+
// Try next file.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function readJsonIfExists(filePath) {
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(await readFile(filePath, 'utf-8'));
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function readRequiredText(filePath) {
|
|
265
|
+
return readFile(filePath, 'utf-8');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (process.argv[1] && resolve(process.argv[1]) === __filename) {
|
|
269
|
+
main().catch((error) => {
|
|
270
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
});
|
|
273
|
+
}
|