@danhachuel/thunderbolt 0.3.96 → 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 +44 -0
- package/hermes_ui/update_manager.py +108 -0
- package/package.json +1 -1
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
|
|
@@ -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()
|
|
@@ -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