@danhachuel/thunderbolt 0.3.95 → 0.3.97

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/app/main.py CHANGED
@@ -4,6 +4,7 @@ import hashlib
4
4
  import json
5
5
  import mimetypes
6
6
  import re
7
+ import time
7
8
  from contextlib import nullcontext
8
9
  from datetime import date, datetime, timezone
9
10
  import sys
@@ -22,7 +23,7 @@ try:
22
23
  except (OSError, json.JSONDecodeError):
23
24
  APP_VERSION = ""
24
25
 
25
- from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, set_channel_defaults, transition_task, update_channel, update_channel_video
26
+ from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, retry_task_with_current_settings, set_channel_defaults, transition_task, update_channel, update_channel_video
26
27
  from hermes_ui.drafts import list_drafts, save_draft
27
28
  from hermes_ui.automation_worker import load_worker_status
28
29
  from hermes_ui.pipeline_worker import load_pipeline_worker_status, recover_stale_tasks, STALE_TASK_SECONDS, WORKER_HEARTBEAT_TIMEOUT_SECONDS
@@ -49,6 +50,7 @@ from hermes_ui.logs import list_logs, logs_to_rows
49
50
  from hermes_ui.languages import LANGUAGE_CODES, VIDEO_LANGUAGE_CODES, LANGUAGE_FLAG_DATA_URIS, language_code, language_label, ui_language_menu_label, ui_text, video_language_label, video_language_options
50
51
  from hermes_ui.api_key_tests import test_apify_credentials, test_influencer_database, test_innertube_api_key, test_kaggle_credentials, test_material_source_credentials, test_media_provider_card, test_nano_banana_credentials, test_postiz_credentials, test_telegram_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
51
52
  from hermes_ui.tutorials import tutorial_body, tutorial_caption, tutorial_title
53
+ from hermes_ui.update_manager import check_version, update_to_latest
52
54
 
53
55
  from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
54
56
  from hermes_ui.script_generation import generate_script_document
@@ -1269,6 +1271,48 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1269
1271
 
1270
1272
  def render_dashboard():
1271
1273
  ui_language = current_ui_language()
1274
+ update_area, version_area = st.columns([1.45, 4.55])
1275
+ with update_area:
1276
+ st.markdown(
1277
+ """
1278
+ <style>
1279
+ div[data-testid="stButton"] button[kind="primary"] {
1280
+ background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
1281
+ color: #ffffff;
1282
+ border: 1px solid #8b5cf6;
1283
+ font-weight: 700;
1284
+ box-shadow: 0 8px 20px rgba(79, 70, 229, 0.28);
1285
+ }
1286
+ div[data-testid="stButton"] button[kind="primary"]:hover {
1287
+ border-color: #c4b5fd;
1288
+ filter: brightness(1.08);
1289
+ }
1290
+ </style>
1291
+ """,
1292
+ unsafe_allow_html=True,
1293
+ )
1294
+ if st.button("Atualizar Versão", key="home_update_version", use_container_width=True, type="primary", icon=":material/system_update:"):
1295
+ with st.spinner("A instalar a versão mais recente…"):
1296
+ st.session_state["home_update_result"] = update_to_latest(APP_VERSION)
1297
+ with version_area:
1298
+ cache_key = "home_update_version_check"
1299
+ checked_at_key = "home_update_version_checked_at"
1300
+ if not st.session_state.get(cache_key) or time.monotonic() - float(st.session_state.get(checked_at_key, 0)) > 300:
1301
+ st.session_state[cache_key] = check_version(APP_VERSION)
1302
+ st.session_state[checked_at_key] = time.monotonic()
1303
+ version_status = st.session_state[cache_key]
1304
+ if version_status.update_available:
1305
+ st.info(f"Nova versão disponível: {version_status.latest_version}. A versão actual é {APP_VERSION or 'desconhecida'}.")
1306
+ elif version_status.error:
1307
+ st.caption(f"Versão actual: {APP_VERSION or 'desconhecida'} · verificação de actualização indisponível.")
1308
+ else:
1309
+ st.caption(f"Versão actual: {APP_VERSION or 'desconhecida'} · já está actualizada ({version_status.latest_version}).")
1310
+ update_result = st.session_state.get("home_update_result")
1311
+ if update_result is not None:
1312
+ if update_result.ok:
1313
+ st.success(update_result.message)
1314
+ else:
1315
+ st.error(update_result.message)
1272
1316
  st.title("Thunderbolt")
1273
1317
  st.caption(ui_text("Interface local para operação e automação de conteúdo faceless", ui_language))
1274
1318
  summary = pipeline_summary()
@@ -4054,7 +4098,7 @@ def render_automation():
4054
4098
 
4055
4099
  st.divider()
4056
4100
  st.subheader("Vídeos cadastrados")
4057
- st.caption("Start retoma as etapas já concluídas e só gera novamente o que ainda não estiver pronto. Apagar remove o card da fila após confirmação e preserva os artefactos locais.")
4101
+ st.caption("Start retoma as etapas já concluídas e só gera novamente o que ainda não estiver pronto. Em tarefas falhadas ou bloqueadas, a nova tentativa lê as chaves, prioridades e configurações actualmente guardadas. Apagar remove o card da fila após confirmação e preserva os artefactos locais.")
4058
4102
  tasks = load_video_tasks_for_catalog()
4059
4103
  if not tasks:
4060
4104
  st.info("Ainda não existem vídeos cadastrados.")
@@ -4074,7 +4118,10 @@ def render_automation():
4074
4118
  start_col, stop_col, delete_col = st.columns(3)
4075
4119
  with start_col:
4076
4120
  if st.button("Start", key=f"automation_start_{task['id']}", use_container_width=True, disabled=state not in {"to_do", "blocked", "failed"}):
4077
- transition_task(task["id"], "doing")
4121
+ if state in {"failed", "blocked"}:
4122
+ retry_task_with_current_settings(task["id"])
4123
+ else:
4124
+ transition_task(task["id"], "doing")
4078
4125
  st.rerun()
4079
4126
  with stop_col:
4080
4127
  if st.button("Stop", key=f"automation_stop_{task['id']}", use_container_width=True, disabled=state != "doing"):
@@ -328,6 +328,37 @@ def transition_task(task_id: str, state: str | None = None, stage: str | None =
328
328
  return None
329
329
 
330
330
 
331
+ def retry_task_with_current_settings(task_id: str) -> dict[str, Any] | None:
332
+ """Queue a failed or blocked task without persisting API credentials or provider snapshots.
333
+
334
+ The pipeline reloads settings.json immediately before every execution, so a
335
+ retry always uses the currently saved API keys, active provider priorities,
336
+ and provider endpoints while retaining the task's completed artefacts.
337
+ """
338
+ tasks = read_json("tasks.json", [])
339
+ for task in tasks:
340
+ if task.get("id") != task_id:
341
+ continue
342
+ previous_state = str(task.get("state") or "")
343
+ if previous_state not in {"failed", "blocked"}:
344
+ raise ValueError("Apenas tarefas falhadas ou bloqueadas podem ser retomadas.")
345
+ try:
346
+ retry_count = int(task.get("retry_count") or 0)
347
+ except (TypeError, ValueError):
348
+ retry_count = 0
349
+ task["state"] = "to_do"
350
+ task["error"] = None
351
+ task["retry_count"] = retry_count + 1
352
+ task["retry_requested_at"] = now()
353
+ task["retry_config_source"] = "settings.json_at_execution"
354
+ for field in ("failure_api", "failure_provider", "failure_service", "failure_config_fields"):
355
+ task.pop(field, None)
356
+ task["updated_at"] = now()
357
+ write_json("tasks.json", tasks)
358
+ return task
359
+ return None
360
+
361
+
331
362
  def pipeline_summary() -> dict[str, Any]:
332
363
  tasks = read_json("tasks.json", [])
333
364
  channels = read_json("channels.json", [])
@@ -0,0 +1,108 @@
1
+ """Verificação e actualização local da versão distribuída pelo NPM.
2
+
3
+ O módulo não recebe nem manipula credenciais. A instalação só é iniciada após o
4
+ clique explícito do utilizador na interface local.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import subprocess
11
+ from dataclasses import dataclass
12
+ from typing import Any, Callable
13
+
14
+ import requests
15
+
16
+
17
+ PACKAGE_NAME = "@danhachuel/thunderbolt"
18
+ REGISTRY_URL = "https://registry.npmjs.org/@danhachuel/thunderbolt/latest"
19
+ UPDATE_TIMEOUT_SECONDS = 20 * 60
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class VersionCheck:
24
+ """Version information displayed on the local home page."""
25
+
26
+ current_version: str
27
+ latest_version: str = ""
28
+ error: str = ""
29
+
30
+ @property
31
+ def update_available(self) -> bool:
32
+ return bool(self.latest_version and self.current_version and self.latest_version != self.current_version)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class UpdateResult:
37
+ """Sanitised result of an explicit local package update request."""
38
+
39
+ ok: bool
40
+ latest_version: str = ""
41
+ message: str = ""
42
+
43
+
44
+ def latest_package_version(*, timeout: int = 8, get: Callable[..., Any] = requests.get) -> str:
45
+ """Return the latest public package version without sending local configuration."""
46
+ response = get(REGISTRY_URL, timeout=timeout)
47
+ response.raise_for_status()
48
+ payload = response.json()
49
+ version = str(payload.get("version") or "").strip() if isinstance(payload, dict) else ""
50
+ if not version:
51
+ raise ValueError("O registry NPM não devolveu uma versão válida.")
52
+ return version
53
+
54
+
55
+ def check_version(current_version: str, *, timeout: int = 8, get: Callable[..., Any] = requests.get) -> VersionCheck:
56
+ """Fetch only the package metadata needed for the version badge."""
57
+ current = str(current_version or "").strip()
58
+ try:
59
+ return VersionCheck(current_version=current, latest_version=latest_package_version(timeout=timeout, get=get))
60
+ except (requests.RequestException, ValueError) as exc:
61
+ return VersionCheck(current_version=current, error=f"Não foi possível verificar actualizações agora ({type(exc).__name__}).")
62
+
63
+
64
+ def update_command() -> list[str]:
65
+ """Build the same cross-platform install command documented for Thunderbolt."""
66
+ executable = "npx.cmd" if os.name == "nt" else "npx"
67
+ return [executable, "--yes", "--prefer-online", PACKAGE_NAME, "install"]
68
+
69
+
70
+ def update_to_latest(
71
+ current_version: str,
72
+ *,
73
+ timeout: int = UPDATE_TIMEOUT_SECONDS,
74
+ get: Callable[..., Any] = requests.get,
75
+ run: Callable[..., Any] = subprocess.run,
76
+ ) -> UpdateResult:
77
+ """Install the latest package only after an explicit UI action.
78
+
79
+ The running local process keeps its current code until it is restarted. No
80
+ subprocess output is returned to the UI, preventing accidental display of
81
+ environment values from third-party installers.
82
+ """
83
+ status = check_version(current_version, get=get)
84
+ if status.error:
85
+ return UpdateResult(False, message=status.error)
86
+ if not status.update_available:
87
+ return UpdateResult(True, latest_version=status.latest_version, message="O Thunderbolt já está na versão mais recente.")
88
+ try:
89
+ completed = run(
90
+ update_command(),
91
+ stdin=subprocess.DEVNULL,
92
+ stdout=subprocess.DEVNULL,
93
+ stderr=subprocess.DEVNULL,
94
+ timeout=timeout,
95
+ check=False,
96
+ )
97
+ except (OSError, subprocess.TimeoutExpired) as exc:
98
+ return UpdateResult(False, latest_version=status.latest_version, message=f"Não foi possível concluir a actualização ({type(exc).__name__}).")
99
+ if int(getattr(completed, "returncode", 1)) != 0:
100
+ return UpdateResult(False, latest_version=status.latest_version, message="A actualização não foi concluída. Feche processos Thunderbolt em execução e tente novamente.")
101
+ return UpdateResult(
102
+ True,
103
+ latest_version=status.latest_version,
104
+ message=(
105
+ f"A versão {status.latest_version} foi instalada. Reinicie o Thunderbolt para abrir a versão nova; "
106
+ "os dados e configurações locais foram preservados."
107
+ ),
108
+ )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.95",
3
+ "version": "0.3.97",
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",