This commit is contained in:
kolmeasi
2026-06-02 08:29:38 -03:00
parent 6cc98a4531
commit 57fc64f120
15 changed files with 1292264 additions and 1908331 deletions

View File

@@ -1281,6 +1281,342 @@ with tab0:
with st.expander("Divergências"):
for e in r["errors"]: st.text(e)
# ── Atualização Total ─────────────────────────────────────────────────────
st.divider()
st.markdown("### 🌐 Atualização Total")
st.markdown(
"**O que faz:** verifica novos aeródromos no catálogo ICEA "
"e baixa **apenas os dados novos** para cada aeródromo que já "
"possui observações no banco. Recomendado a cada 2 meses.\n\n"
"> **Nota:** aeródromos que ainda **não têm dados** no banco não são "
"coletados aqui — apenas adicionados ao catálogo. "
"Para coletar o histórico completo de todos os aeródromos, "
"use o script `_apps/collect_all.py` (pode levar horas/dias)."
)
_at_c1, _at_c2, _ = st.columns([1, 1, 3])
with _at_c1:
_at_headless = not st.checkbox("Mostrar Chrome", value=False, key="at_headless")
with _at_c2:
_at_validate = st.checkbox("Validar amostras", value=False, key="at_validate")
_at_submitted = st.button(
"🌐 Iniciar Atualização Total",
type="primary",
help="Atualiza catálogo + coleta dados novos para todos os aeródromos com dados no banco",
)
if _at_submitted:
import db as _db_at
_at_conn_check = _db_at.get_connection(DB_PATH)
_db_at.ensure_schema(_at_conn_check)
_at_aeros = _db_at.list_aerodromes_with_data(_at_conn_check)
_at_conn_check.close()
if not _at_aeros:
st.warning(
"Nenhum aeródromo com dados no banco. "
"Use o formulário acima para coletar dados primeiro."
)
else:
_at_n_total = len(_at_aeros)
_at_q: queue.Queue = queue.Queue()
_at_results: list[dict] = []
_at_log_lines: list[str] = []
_at_prog_bar = st.progress(0.0)
_at_stat_txt = st.empty()
_at_log_box = st.empty()
_at_table_box = st.empty()
_at_aero_list = [d["icao"] for d in _at_aeros]
_at_aero_meta = {d["icao"]: d for d in _at_aeros}
def _at_worker() -> None:
try:
# Phase 1 — refresh aerodrome catalog
_at_q.put(("status", "Fase 1 — Atualizando catálogo ICEA…"))
from scraper_meteorologia import fetch_aerodrome_catalog as _fat
import db as _dbi
_new_cat = _fat(headless=_at_headless)
_cat_conn = _dbi.get_connection(DB_PATH)
_dbi.ensure_schema(_cat_conn)
_n_new = _dbi.upsert_aerodromes(_cat_conn, _new_cat)
_cat_conn.close()
_at_q.put(("log",
f"[catálogo] {len(_new_cat)} aeródromos no portal "
f"| {_n_new} registros atualizados"
))
# Phase 2 — forward-only update for each aerodrome
for _i, _icao in enumerate(_at_aero_list):
_at_q.put(("aero_start", _icao, _i))
try:
from pipeline import run_pipeline as _rp
_res = _rp(
aerodrome = _icao,
dados_dir = DADOS_DIR,
db_path = DB_PATH,
preproc_dir = PREPROC_DIR,
all_years = False,
update_only = True,
headless = _at_headless,
n_samples = 5 if _at_validate else 0,
do_validate = _at_validate,
do_cleanup = True,
log = lambda m: _at_q.put(("log", m)),
progress = lambda p, m: None,
)
_at_q.put(("aero_done", _icao, _res))
except Exception as _exc:
_at_q.put(("aero_error", _icao, str(_exc)[:120]))
except Exception as _exc:
_at_q.put(("log", f"ERRO: {_exc}"))
finally:
_at_q.put(("done",))
_at_t = threading.Thread(target=_at_worker, daemon=True)
_at_t.start()
_at_n_done = 0
while _at_t.is_alive() or not _at_q.empty():
try:
_item = _at_q.get(timeout=0.4)
except queue.Empty:
continue
if _item[0] == "done":
break
elif _item[0] == "status":
_at_stat_txt.caption(_item[1])
elif _item[0] == "log":
_at_log_lines.append(_item[1])
_at_log_box.code("\n".join(_at_log_lines[-60:]))
elif _item[0] == "aero_start":
_, _icao, _i = _item
_at_prog_bar.progress(_i / max(_at_n_total, 1))
_at_stat_txt.caption(
f"Fase 2 — {_icao} ({_i + 1} / {_at_n_total})"
)
_meta = _at_aero_meta.get(_icao, {})
_at_results.append({
"ICAO": _icao,
"Nome": _meta.get("nome", ""),
"UF": _meta.get("uf", ""),
"Status": "⏳ Coletando…",
"Novas linhas": "",
})
_at_table_box.dataframe(
_at_results, use_container_width=True, hide_index=True
)
elif _item[0] == "aero_done":
_, _icao, _res = _item
_at_n_done += 1
_new = _res.get("n_upserted", 0)
for _row in _at_results:
if _row["ICAO"] == _icao:
_row["Status"] = "✅ Concluído"
_row["Novas linhas"] = f"{_new:,}"
break
_at_table_box.dataframe(
_at_results, use_container_width=True, hide_index=True
)
elif _item[0] == "aero_error":
_, _icao, _err = _item
for _row in _at_results:
if _row["ICAO"] == _icao:
_row["Status"] = "❌ Erro"
_row["Novas linhas"] = _err[:50]
break
_at_table_box.dataframe(
_at_results, use_container_width=True, hide_index=True
)
_at_t.join()
_at_prog_bar.progress(1.0)
st.cache_data.clear()
_at_stat_txt.empty()
st.success(
f"✅ Atualização Total concluída — "
f"{_at_n_done} / {_at_n_total} aeródromos atualizados."
)
st.rerun()
# ── Coleta Total — Histórico Completo ─────────────────────────────────────
st.divider()
st.markdown("### 🗄️ Coleta Total — Histórico Completo")
st.markdown(
"Coleta o **histórico completo** de todos os aeródromos do catálogo "
"que ainda **não têm dados** no banco. "
"Aeródromos com dados são ignorados automaticamente.\n\n"
"> ⚠️ Esta operação pode demorar **horas ou dias** dependendo do número "
"de aeródromos. Não feche o dashboard durante a coleta. "
"Em caso de interrupção, basta clicar novamente — os aeródromos já "
"coletados serão pulados automaticamente."
)
# Status summary
if _db_available():
import db as _db_ct_info
_ct_conn_info = _db_ct_info.get_connection(DB_PATH)
_db_ct_info.ensure_schema(_ct_conn_info)
_ct_catalog = _db_ct_info.list_all_aerodromes(_ct_conn_info)
_ct_with_data = set(_db_ct_info.list_aerodromes(_ct_conn_info))
_ct_conn_info.close()
_ct_total = len(_ct_catalog)
_ct_done = len(_ct_with_data)
_ct_pending = _ct_total - _ct_done
else:
_ct_total = _ct_done = _ct_pending = 0
_ct_info_col1, _ct_info_col2, _ct_info_col3 = st.columns(3)
_ct_info_col1.metric("No catálogo", _ct_total)
_ct_info_col2.metric("Com dados", _ct_done)
_ct_info_col3.metric("Aguardando coleta", _ct_pending)
_ct_opt1, _ct_opt2, _ct_opt3 = st.columns([1, 1, 2])
with _ct_opt1:
_ct_headless = not st.checkbox("Mostrar Chrome", value=False, key="ct_headless")
with _ct_opt2:
_ct_max = int(st.number_input(
"Limite de aeródromos (0 = todos)",
min_value=0, max_value=200, value=0, step=1, key="ct_max",
help="Útil para testes. 0 processa todos os pendentes.",
))
if _ct_pending == 0 and _ct_total > 0:
st.info("✅ Todos os aeródromos do catálogo já têm dados no banco.")
elif _ct_total == 0:
st.warning(
"Catálogo vazio. Clique em **Atualizar catálogo ICEA** (acima) para "
"baixar a lista de aeródromos disponíveis."
)
_ct_submitted = st.button(
"🗄️ Iniciar Coleta Total",
type="primary",
disabled=(_ct_pending == 0),
help="Coleta o histórico completo para os aeródromos sem dados no banco",
)
if _ct_submitted and _ct_pending > 0:
_ct_q: queue.Queue = queue.Queue()
_ct_results: list[dict] = []
_ct_log_lines: list[str] = []
_ct_prog_bar = st.progress(0.0)
_ct_stat_txt = st.empty()
_ct_log_box = st.empty()
_ct_table_box = st.empty()
# Build metadata dict for display
_ct_meta = {d["icao"]: d for d in _ct_catalog}
_ct_max_arg = _ct_max if _ct_max > 0 else None
def _ct_worker() -> None:
try:
from collect_all import collect_all as _ca
def _ct_on_start(icao: str, idx: int, total: int, nome: str) -> None:
_ct_q.put(("aero_start", icao, idx, total, nome))
def _ct_on_done(icao: str, event: str, result: dict) -> None:
_ct_q.put(("aero_done", icao, event, result))
_ca(
resume = True,
max_aerodromes = _ct_max_arg,
headless = _ct_headless,
n_samples = 0,
do_cleanup = True,
log = lambda m: _ct_q.put(("log", m)),
on_start = _ct_on_start,
on_done = _ct_on_done,
)
except Exception as _exc:
_ct_q.put(("log", f"ERRO: {_exc}"))
finally:
_ct_q.put(("done",))
_ct_t = threading.Thread(target=_ct_worker, daemon=True)
_ct_t.start()
_ct_n_done = 0
_ct_n_total_run = _ct_max_arg or _ct_pending
while _ct_t.is_alive() or not _ct_q.empty():
try:
_citem = _ct_q.get(timeout=0.4)
except queue.Empty:
continue
if _citem[0] == "done":
break
elif _citem[0] == "log":
_ct_log_lines.append(_citem[1])
_ct_log_box.code("\n".join(_ct_log_lines[-80:]))
elif _citem[0] == "aero_start":
_, _icao, _idx, _tot, _nome = _citem
_ct_prog_bar.progress((_idx - 1) / max(_tot, 1))
_ct_stat_txt.caption(f"Coletando {_icao}{_idx} / {_tot}")
_meta = _ct_meta.get(_icao, {})
_ct_results.append({
"ICAO": _icao,
"Nome": _meta.get("nome", _nome),
"UF": _meta.get("uf", ""),
"Status": "⏳ Coletando…",
"Período": "",
"Linhas": "",
})
_ct_table_box.dataframe(
_ct_results, use_container_width=True, hide_index=True
)
elif _citem[0] == "aero_done":
_, _icao, _event, _res = _citem
_status_map = {
"completed": "✅ Concluído",
"skipped": "⏭️ Já tinha dados",
"failed": "❌ Erro",
"interrupted": "⏸️ Interrompido",
}
_st = _status_map.get(_event, _event)
for _row in _ct_results:
if _row["ICAO"] == _icao:
_row["Status"] = _st
if _event == "completed":
_ct_n_done += 1
_row["Período"] = (
f"{_res.get('period_start','')}"
f"{_res.get('period_end','')}"
)
_row["Linhas"] = f"{_res.get('rows', 0):,}"
elif _event == "failed":
_row["Período"] = _res.get("error", "")[:40]
break
_ct_table_box.dataframe(
_ct_results, use_container_width=True, hide_index=True
)
_ct_t.join()
_ct_prog_bar.progress(1.0)
st.cache_data.clear()
_ct_stat_txt.empty()
st.success(
f"✅ Coleta Total concluída — {_ct_n_done} aeródromos coletados."
)
st.rerun()
# ── Visão Geral ───────────────────────────────────────────────────────────────
with tab1:
st.plotly_chart(chart_overview(anl_r), use_container_width=True)