@danhachuel/thunderbolt 0.7.51 → 0.7.53

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.
@@ -506,7 +506,8 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
506
506
  st.write(f"**{_clean(profile.get('name')) or 'Sem nome'}**")
507
507
  st.caption(f"{_clean(profile.get('handle')) or _clean(profile.get('url')) or 'sem handler'}")
508
508
  _render_instagram_bio(_profile_bio(profile))
509
- st.caption(f"{_profile_country(profile) or 'País não definido'} · {_clean(profile.get('language')) or 'Idioma não definido'}")
509
+ country = _profile_country(profile)
510
+ st.caption(f"{_country_display(country) if country else 'País não definido'} · {_clean(profile.get('language')) or 'Idioma não definido'}")
510
511
  with header_cols[2]:
511
512
  st.metric("posts", _metric(_profile_metric(profile, "post_count", "posts", "media_count")))
512
513
  with header_cols[3]:
@@ -587,7 +588,8 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
587
588
  with block_cols[1]:
588
589
  st.markdown(f"**handler**\n\n{_clean(profile.get('handle')) or '—'}")
589
590
  with block_cols[2]:
590
- st.markdown(f"**País**\n\n{_profile_country(profile) or '—'}")
591
+ country = _profile_country(profile)
592
+ st.markdown(f"**País**\n\n{_country_display(country) if country else '—'}")
591
593
  with block_cols[3]:
592
594
  st.markdown(f"**Idioma**\n\n{_clean(profile.get('language')) or '—'}")
593
595
 
@@ -5,6 +5,7 @@ import os
5
5
  import shutil
6
6
  import tempfile
7
7
  import time
8
+ from threading import RLock
8
9
  from contextlib import contextmanager
9
10
  from copy import deepcopy
10
11
  from datetime import datetime, timezone
@@ -24,6 +25,9 @@ SEED_BLUEPRINTS = ROOT / "seed" / "blueprints"
24
25
  SEED_THUMBNAIL_BLUEPRINTS = SEED_BLUEPRINTS / "thumbnails"
25
26
  SEED_TIKTOK_PROMPT_MASTERS = ROOT / "seed" / "prompt_masters"
26
27
 
28
+ _READ_CACHE_LOCK = RLock()
29
+ _READ_CACHE: dict[str, tuple[tuple[int, int], Any]] = {}
30
+
27
31
  DEFAULTS: dict[str, Any] = {
28
32
  "channels.json": [],
29
33
  "channel_videos.json": [],
@@ -720,6 +724,19 @@ def _load_json_unlocked(path: Path) -> Any:
720
724
  return json.load(handle)
721
725
 
722
726
 
727
+ def _read_fingerprint(path: Path) -> tuple[int, int] | None:
728
+ try:
729
+ stat = path.stat()
730
+ except OSError:
731
+ return None
732
+ return stat.st_mtime_ns, stat.st_size
733
+
734
+
735
+ def _invalidate_read_cache(path: Path) -> None:
736
+ with _READ_CACHE_LOCK:
737
+ _READ_CACHE.pop(str(path), None)
738
+
739
+
723
740
  def _recover_json_unlocked(name: str, path: Path, default: Any | None) -> Any:
724
741
  backup = _corrupt_backup(path)
725
742
  if name in _PROTECTED_STATE_FILES:
@@ -743,6 +760,13 @@ def _recover_json_unlocked(name: str, path: Path, default: Any | None) -> Any:
743
760
  def read_json(name: str, default: Any | None = None) -> Any:
744
761
  ensure_storage()
745
762
  path = STATE / name
763
+ fingerprint = _read_fingerprint(path)
764
+ cache_key = str(path)
765
+ if fingerprint is not None:
766
+ with _READ_CACHE_LOCK:
767
+ cached = _READ_CACHE.get(cache_key)
768
+ if cached is not None and cached[0] == fingerprint:
769
+ return deepcopy(cached[1])
746
770
  with _state_lock(path, read_only=True):
747
771
  try:
748
772
  data = _load_json_unlocked(path)
@@ -753,7 +777,12 @@ def read_json(name: str, default: Any | None = None) -> Any:
753
777
  migrated, changed = _migrate_settings(data)
754
778
  if changed:
755
779
  _atomic_write_unlocked(path, migrated)
756
- return migrated
780
+ result = migrated
781
+ current_fingerprint = _read_fingerprint(path)
782
+ if current_fingerprint is not None:
783
+ with _READ_CACHE_LOCK:
784
+ _READ_CACHE[cache_key] = (current_fingerprint, deepcopy(result))
785
+ return deepcopy(result)
757
786
 
758
787
 
759
788
  def update_json(name: str, default: Any, mutator: Callable[[Any], Any]) -> Any:
@@ -767,6 +796,7 @@ def update_json(name: str, default: Any, mutator: Callable[[Any], Any]) -> Any:
767
796
  current = _recover_json_unlocked(name, path, default)
768
797
  result = mutator(current)
769
798
  _atomic_write_unlocked(path, current)
799
+ _invalidate_read_cache(path)
770
800
  return result
771
801
 
772
802
 
@@ -774,6 +804,7 @@ def write_json(name: str, data: Any) -> None:
774
804
  """Persist a state JSON file using :func:`atomic_write`."""
775
805
  ensure_storage()
776
806
  atomic_write(STATE / name, data)
807
+ _invalidate_read_cache(STATE / name)
777
808
 
778
809
 
779
810
  def append_json(name: str, item: dict[str, Any]) -> dict[str, Any]:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.51",
3
+ "version": "0.7.53",
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",
package/scripts/cli.mjs CHANGED
@@ -262,16 +262,19 @@ const proxy = http.createServer((request, response) => {
262
262
  headers: { ...request.headers, host: `127.0.0.1:${backendPort}`, "accept-encoding": "identity" },
263
263
  }, (upstreamResponse) => {
264
264
  const responseHeaders = { ...upstreamResponse.headers };
265
- // Streamlit entrega HTML/JS de uma instância local que pode mudar após
266
- // uma atualização via npx. Não permitir que o browser reaproveite uma
267
- // página ou bundle da instância anterior.
268
- delete responseHeaders.etag;
269
- delete responseHeaders["last-modified"];
270
- responseHeaders["cache-control"] = "no-store, no-cache, must-revalidate, max-age=0";
271
- responseHeaders.pragma = "no-cache";
272
- responseHeaders.expires = "0";
273
265
  const contentType = String(responseHeaders["content-type"] || "").toLowerCase();
274
266
  const isHtml = contentType.includes("text/html") && !responseHeaders["content-encoding"];
267
+ // O documento HTML contém o estado da sessão e pode mudar após uma
268
+ // actualização via npx; os bundles estáticos do Streamlit, por outro lado,
269
+ // devem ser reutilizados pelo browser. Desactivar cache em todos os
270
+ // recursos tornava cada aba muito mais pesada e multiplicava downloads.
271
+ if (isHtml) {
272
+ delete responseHeaders.etag;
273
+ delete responseHeaders["last-modified"];
274
+ responseHeaders["cache-control"] = "no-store, no-cache, must-revalidate, max-age=0";
275
+ responseHeaders.pragma = "no-cache";
276
+ responseHeaders.expires = "0";
277
+ }
275
278
  if (!isHtml) {
276
279
  response.writeHead(upstreamResponse.statusCode || 502, responseHeaders);
277
280
  upstreamResponse.pipe(response);