@danhachuel/thunderbolt 0.5.40 → 0.5.41

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.
Files changed (2) hide show
  1. package/app/main.py +83 -1
  2. package/package.json +1 -1
package/app/main.py CHANGED
@@ -1442,6 +1442,88 @@ def render_home_update_controls() -> None:
1442
1442
  st.rerun()
1443
1443
 
1444
1444
 
1445
+ def _growth_score_color(score: float) -> str:
1446
+ if score <= 30:
1447
+ return "#ef4444"
1448
+ if score < 70:
1449
+ return "#f59e0b"
1450
+ return "#22c55e"
1451
+
1452
+
1453
+ def _growth_channel_metrics(channel: dict[str, Any]) -> tuple[list[dict[str, Any]], float]:
1454
+ """Calculate transparent local growth notes without fabricating Studio metrics."""
1455
+ channel_id = str(channel.get("id") or "")
1456
+ tasks = [item for item in read_json("tasks.json", []) if isinstance(item, dict) and str(item.get("channel_id") or "") == channel_id]
1457
+ completed = [item for item in tasks if str(item.get("state") or item.get("status") or "").lower() == "done"]
1458
+ with_thumbnail = [item for item in tasks if str(item.get("thumbnail_path") or (item.get("artifacts") or {}).get("thumbnail") or "").strip()]
1459
+ with_title = [item for item in tasks if str(item.get("title") or "").strip()]
1460
+ subscriber_count = channel.get("subscriber_count")
1461
+ view_count = channel.get("view_count")
1462
+ video_count = channel.get("video_count")
1463
+
1464
+ def number(value: Any) -> float | None:
1465
+ try:
1466
+ return float(value) if value is not None and str(value).strip() else None
1467
+ except (TypeError, ValueError):
1468
+ return None
1469
+
1470
+ subscribers, views, videos = number(subscriber_count), number(view_count), number(video_count)
1471
+ demand = 70 if views is not None and views >= 1_000_000 else 55 if views is not None and views >= 100_000 else 50
1472
+ thumbnail = round((len(with_thumbnail) / len(tasks)) * 100) if tasks else 50
1473
+ title = round((len(with_title) / len(tasks)) * 100) if tasks else 50
1474
+ conversion = round(min(100, (subscribers / views) * 10000)) if subscribers is not None and views and views > 0 else 50
1475
+ cadence = round(min(100, len(tasks) * 12.5)) if tasks else 50
1476
+ metrics = [
1477
+ {"label": "Demanda validada", "score": demand, "value": f"{views:,.0f} visualizações" if views is not None else "Sem dados de visualizações", "source": "Canal YouTube" if views is not None else "Requer dados do canal"},
1478
+ {"label": "Qualidade da thumbnail", "score": thumbnail, "value": f"{len(with_thumbnail)}/{len(tasks)} tarefas com thumbnail" if tasks else "Sem tarefas analisadas", "source": "Artefactos locais"},
1479
+ {"label": "Qualidade do título", "score": title, "value": f"{len(with_title)}/{len(tasks)} tarefas com título" if tasks else "Sem tarefas analisadas", "source": "Artefactos locais"},
1480
+ {"label": "Hook — retenção inicial", "score": 50, "value": "Sem dados de retenção", "source": "Requer YouTube Studio"},
1481
+ {"label": "Ritmo e edição", "score": 50, "value": "Sem gráfico de retenção", "source": "Requer YouTube Studio"},
1482
+ {"label": "Origem do tráfego", "score": 50, "value": "Sem distribuição de tráfego", "source": "Requer YouTube Studio"},
1483
+ {"label": "Conversão em inscritos", "score": conversion, "value": f"{subscribers:,.0f} inscritos / {views:,.0f} visualizações" if subscribers is not None and views is not None else "Sem dados de inscritos e visualizações", "source": "Canal YouTube" if subscribers is not None and views is not None else "Requer dados do canal"},
1484
+ {"label": "Cadência de publicação", "score": cadence, "value": f"{len(tasks)} tarefas locais" if tasks else "Sem tarefas locais", "source": "Pipeline local"},
1485
+ ]
1486
+ available = [metric["score"] for metric in metrics if metric["source"] not in {"Requer YouTube Studio", "Requer dados do canal"}]
1487
+ overall = round(sum(available) / len(available)) if available else 50
1488
+ return metrics, overall
1489
+
1490
+
1491
+ def render_growth_youtube():
1492
+ st.title("Analista Growth Youtube")
1493
+ st.caption("Auditoria de crescimento baseada nos 3 pilares críticos e nas métricas secundárias do agente Growth.")
1494
+ channels = [item for item in read_json("channels.json", []) if isinstance(item, dict) and item.get("active", True) and is_youtube_channel_record(item)]
1495
+ if not channels:
1496
+ st.info("Cadastre pelo menos um canal YouTube em Canais YouTube para iniciar a análise.")
1497
+ return
1498
+ selected = st.selectbox("Canal a analisar", channels, format_func=lambda item: str(item.get("name") or "Canal sem nome"), key="growth_youtube_channel")
1499
+ metrics, overall = _growth_channel_metrics(selected)
1500
+ color = _growth_score_color(overall)
1501
+ st.markdown(
1502
+ f'<div style="border:1px solid #263447;border-radius:12px;padding:18px;margin:12px 0 20px;background:#101722;display:flex;align-items:center;justify-content:space-between;">'
1503
+ f'<div><div style="font-size:13px;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;">Nota geral do canal</div><div style="font-size:18px;font-weight:700;margin-top:5px;">{str(selected.get("name") or "Canal")}</div></div>'
1504
+ f'<div style="font-size:42px;font-weight:800;color:{color};line-height:1;">{overall}<span style="font-size:16px;color:#94a3b8;">/100</span></div></div>',
1505
+ unsafe_allow_html=True,
1506
+ )
1507
+ st.subheader("Dashboard de Growth")
1508
+ st.caption("Vermelho: 0–30 · Amarelo: 31–69 · Verde: 70–100. Métricas sem fonte local aparecem explicitamente como sem dados.")
1509
+ for row_start in range(0, len(metrics), 4):
1510
+ cols = st.columns(4, gap="small")
1511
+ for col, metric in zip(cols, metrics[row_start:row_start + 4]):
1512
+ with col:
1513
+ score = metric["score"]
1514
+ st.markdown(
1515
+ f'<div style="min-height:142px;border:1px solid #263447;border-radius:10px;padding:14px;background:#0d141e;">'
1516
+ f'<div style="font-size:13px;color:#cbd5e1;min-height:34px;">{metric["label"]}</div>'
1517
+ f'<div style="font-size:30px;font-weight:800;color:{_growth_score_color(score)};">{score}<span style="font-size:13px;color:#94a3b8;">/100</span></div>'
1518
+ f'<div style="font-size:12px;color:#e2e8f0;margin-top:5px;">{metric["value"]}</div>'
1519
+ f'<div style="font-size:11px;color:#64748b;margin-top:5px;">Fonte: {metric["source"]}</div></div>',
1520
+ unsafe_allow_html=True,
1521
+ )
1522
+ st.divider()
1523
+ st.subheader("Leitura estratégica")
1524
+ st.write("Priorize primeiro as métricas abaixo de 70. A referência do agente indica que Thumbnail + Título devem ser optimizados antes de alterações de conteúdo; Hook, Ritmo e Tráfego ficam pendentes de dados do YouTube Studio.")
1525
+
1526
+
1445
1527
  def render_dashboard():
1446
1528
  ui_language = current_ui_language()
1447
1529
  st.title("Thunderbolt")
@@ -8668,7 +8750,7 @@ def main():
8668
8750
  "Motion Control": lambda: render_motion_control(read_json("settings.json", {})),
8669
8751
  "UGC Products": lambda: render_ugc_products(read_json("settings.json", {})),
8670
8752
  "Redes Sociais": lambda: render_edit_placeholder("Redes Sociais", "Área reservada para a futura funcionalidade de redes sociais."),
8671
- "Analista Growth Youtube": lambda: render_edit_placeholder("Analista Growth Youtube", ""),
8753
+ "Analista Growth Youtube": render_growth_youtube,
8672
8754
  "Analista Growth Tiktok": lambda: render_edit_placeholder("Analista Growth Tiktok", ""),
8673
8755
  "Analista Growth Instagram": lambda: render_edit_placeholder("Analista Growth Instagram", ""),
8674
8756
  "Analista Facebook Pages": lambda: render_edit_placeholder("Analista Facebook Pages", ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.5.40",
3
+ "version": "0.5.41",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",