import os import sys import json import time import logging import requests import urllib3 from flask import Flask, request, jsonify, render_template_string, url_for from playwright.sync_api import sync_playwright # Suprimir warnings de SSL urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Configuração de logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) app = Flask(__name__) # Constantes da API STF STF_API_URL = "https://jurisprudencia.stf.jus.br/api/search/search" # Cache de token token_cache = {"token": None, "expires_at": 0} HEADERS = { "Accept": "application/json, text/plain, */*", "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://jurisprudencia.stf.jus.br/pages/search", "Origin": "https://jurisprudencia.stf.jus.br" } # ============================================ # Funções de token e busca # ============================================ def get_fresh_token(force_refresh=False): global token_cache if not force_refresh and token_cache["token"] and time.time() < token_cache["expires_at"]: logger.info("Usando token em cache") return token_cache["token"] logger.info("Obtendo novo token via Playwright" + (" (forçado)" if force_refresh else "")) try: with sync_playwright() as p: browser = p.chromium.launch(headless=True, args=['--no-sandbox']) context = browser.new_context( viewport={'width': 1920, 'height': 1080}, user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ) page = context.new_page() page.goto("https://jurisprudencia.stf.jus.br/pages/search", wait_until='domcontentloaded', timeout=30000) page.wait_for_timeout(3000) cookies = context.cookies() token = None for cookie in cookies: if cookie.get('name') == 'aws-waf-token': token = cookie.get('value') break browser.close() if token: token_cache["token"] = token token_cache["expires_at"] = time.time() + 600 logger.info(f"Token obtido: {token[:30]}...") return token else: logger.warning("Token não encontrado nos cookies") return None except Exception as e: logger.error(f"Erro ao obter token: {str(e)}") return None def search_stf(query: str, bases: list, page: int = 1, page_size: int = 20): token = get_fresh_token() if not token: return {"error": "Não foi possível obter token de acesso"}, 503 from_idx = (page - 1) * page_size should_filters = [] for base in bases: should_filters.append({"term": {"base": base}}) payload = { "query": { "bool": { "filter": [{"bool": {"should": should_filters, "minimum_should_match": 1}}], "must": [{"bool": {"should": [ {"query_string": {"fields": ["ementa_texto"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}}, {"query_string": {"fields": ["decisao_texto"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}}, {"query_string": {"fields": ["acordao_ata"], "query": query, "default_operator": "AND", "fuzziness": "AUTO:4,7"}} ]}}] } }, "_source": ["id", "titulo", "ementa_texto", "decisao_texto", "acordao_ata", "processo_codigo_completo", "relator_processo_nome", "orgao_julgador", "julgamento_data", "publicacao_data", "inteiro_teor_url", "base"], "size": page_size, "from": from_idx, "sort": [{"julgamento_data": {"order": "desc"}}], "track_total_hits": True } headers = HEADERS.copy() headers['Cookie'] = f'aws-waf-token={token}' try: response = requests.post(STF_API_URL, headers=headers, json=payload, verify=False, timeout=30) if response.status_code == 200: return response.json() elif response.status_code == 202: token = get_fresh_token(force_refresh=True) if token: headers['Cookie'] = f'aws-waf-token={token}' response = requests.post(STF_API_URL, headers=headers, json=payload, verify=False, timeout=30) if response.status_code == 200: return response.json() return {"error": "Token expirado e renovação falhou"}, 503 else: return {"error": f"API retornou status {response.status_code}"}, response.status_code except Exception as e: return {"error": str(e)}, 502 # ============================================ # CSS compartilhado # ============================================ SHARED_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300&family=DM+Sans:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap'); :root { --bg: #0e1018; --bg-1: #13161f; --bg-2: #191d28; --bg-3: #1f2435; --gold: #c9a84c; --gold-lt: #e8cf82; --gold-dim: #7a6030; --gold-glow: rgba(201,168,76,.12); --gold-glow2: rgba(201,168,76,.05); --gold-bd: rgba(201,168,76,.22); --blue: #3d7de8; --blue-dim: rgba(61,125,232,.13); --blue-bd: rgba(61,125,232,.30); --purple: #7c5cbf; --purple-dim: rgba(124,92,191,.13); --purple-bd: rgba(124,92,191,.30); --tx: #e6e8ee; --tx-2: #8e91a8; --tx-3: #484c62; --bd: rgba(255,255,255,.07); --bd-2: rgba(255,255,255,.11); --red-dim: rgba(239,68,68,.12); --red-bd: rgba(239,68,68,.28); --serif: 'Cormorant Garamond', Georgia, serif; --sans: 'DM Sans', system-ui, sans-serif; --mono: 'JetBrains Mono', monospace; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { scroll-behavior: smooth; overflow-x: hidden; } body { font-family: var(--sans); background: var(--bg); color: var(--tx); min-height: 100vh; -webkit-font-smoothing: antialiased; line-height: 1.6; overflow-x: hidden; width: 100%; } /* Grain */ body::after { content: ''; position: fixed; inset: 0; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E"); pointer-events: none; z-index: 9999; opacity: .5; } ::selection { background: var(--gold-glow); color: var(--tx); } ::-webkit-scrollbar { width: 3px; } ::-webkit-scrollbar-thumb { background: var(--gold-dim); border-radius: 3px; } /* ── TOPBAR ── */ .topbar { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--bd); position: sticky; top: 0; z-index: 100; background: rgba(14,16,24,.92); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); width: 100%; box-sizing: border-box; } .brand { display: flex; align-items: center; gap: 14px; text-decoration: none; } .brand-logo { height: 52px; width: auto; object-fit: contain; } .brand-sep { width: 1px; height: 20px; background: var(--bd-2); } .brand-label { font-family: var(--mono); font-size: .65rem; letter-spacing: .18em; text-transform: uppercase; color: var(--tx-3); } .topbar-right { display: flex; align-items: center; gap: 14px; } .status-dot { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: .68rem; color: var(--tx-3); letter-spacing: .06em; } .status-dot::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 6px rgba(74,222,128,.6); } /* ── SHELL ── */ .shell { max-width: 1060px; margin: 0 auto; padding: 0 16px 80px; width: 100%; box-sizing: border-box; } /* ── HERO ── */ .hero { padding: 56px 0 44px; text-align: center; position: relative; align-items: center;} .hero-glow { position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 600px; height: 300px; background: radial-gradient(ellipse at center top, rgba(201,168,76,.08) 0%, transparent 70%); pointer-events: none; } .hero-logo-wrap { margin-bottom: 28px; margin-top: 82px; } .hero-logo { height: 186px; width: auto; object-fit: contain; filter: drop-shadow(0 0 24px rgba(201,168,76,.15)); } .hero-sub { font-size: .8rem; font-family: var(--mono); letter-spacing: .2em; text-transform: uppercase; color: var(--tx-3); margin-bottom: 10px; } .hero-title { font-family: var(--serif); font-size: clamp(1.8rem, 4vw, 3rem); font-weight: 300; color: var(--tx); line-height: 1.15; margin-bottom: 10px; } .hero-title em { font-style: italic; color: var(--gold-lt); } /* ── SEARCH AREA ── */ .search-wrap { margin-top: 36px; display: flex; flex-direction: column; align-items: center; /* centraliza os filhos horizontalmente */ width: 100%; } /* Filtros de base — linha simples e centralizada */ .bases-line { display: flex; align-items: center; justify-content: center; /* centraliza o conteúdo interno */ gap: 20px; margin-bottom: 14px; font-size: .8rem; color: var(--tx-3); width: 100%; /* ocupa toda a largura para que o centro seja relativo à tela */ } .base-check { display: flex; align-items: center; gap: 7px; cursor: pointer; user-select: none; color: var(--tx-2); transition: color .2s; } .base-check:hover { color: var(--tx); } .base-check input { display: none; } .check-box { width: 15px; height: 15px; border: 1px solid var(--bd-2); border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: all .2s; flex-shrink: 0; } .base-check input:checked + .check-box { border-color: var(--gold-dim); background: var(--gold-glow); } .base-check input:checked + .check-box::after { content: ''; width: 6px; height: 6px; border-radius: 2px; background: var(--gold); } .base-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; } .dot-ac { background: var(--blue); } .dot-de { background: var(--purple); } /* Input row */ .search-row { display: flex; gap: 10px; align-items: center; background: var(--bg-1); border: 1px solid var(--bd-2); border-radius: 14px; padding: 6px 12px 6px 16px; transition: border-color .25s, box-shadow .25s; width: 100%; box-sizing: border-box; } .search-row:focus-within { border-color: var(--gold-dim); box-shadow: 0 0 0 3px var(--gold-glow), 0 8px 32px rgba(0,0,0,.3); } .search-input { flex: 1; background: transparent; border: none; outline: none; font-family: var(--sans); font-size: .95rem; color: var(--tx); padding: 10px 0; min-width: 0; } .search-input::placeholder { color: var(--tx-3); } .search-btn { background: linear-gradient(135deg, #6b4f1a, var(--gold)); border: none; border-radius: 10px; padding: 11px 26px; font-family: var(--sans); font-size: .85rem; font-weight: 500; color: #0e1018; letter-spacing: .04em; cursor: pointer; white-space: nowrap; display: flex; align-items: center; gap: 8px; transition: opacity .2s, transform .15s; flex-shrink: 0; } .search-btn:hover { opacity: .88; transform: translateY(-1px); } .search-btn:disabled { opacity: .35; cursor: not-allowed; transform: none; } /* ── STATS CARDS ── */ .stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 32px 0 0; display: none; } .stat-card { background: var(--bg-1); border: 1px solid var(--bd); border-radius: 14px; padding: 20px 22px; text-align: center; position: relative; overflow: hidden; transition: border-color .2s; } .stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--gold-dim), transparent); opacity: .5; } .stat-card:hover { border-color: var(--gold-bd); } .stat-val { font-family: var(--serif); font-size: 2rem; font-weight: 300; color: var(--gold-lt); line-height: 1; margin-bottom: 4px; } .stat-label { font-family: var(--mono); font-size: .63rem; letter-spacing: .12em; text-transform: uppercase; color: var(--tx-3); } /* ── DIVIDER ── */ .section-divider { display: none; align-items: center; gap: 14px; margin: 28px 0 20px; } .div-line { flex: 1; height: 1px; background: var(--bd); } .div-label { font-family: var(--mono); font-size: .65rem; letter-spacing: .14em; text-transform: uppercase; color: var(--tx-3); white-space: nowrap; } /* ── RESULT CARDS ── */ .result-card { background: var(--bg-1); border: 1px solid var(--bd); border-radius: 16px; padding: 22px 20px; margin-bottom: 12px; position: relative; overflow: hidden; transition: border-color .22s, box-shadow .22s, transform .15s; animation: riseUp .3s ease both; width: 100%; box-sizing: border-box; } @keyframes riseUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } /* Accent linha esquerda — neutro */ .result-card::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: rgba(255,255,255,.08); transition: opacity .2s; } .result-card:hover::before { background: var(--gold-dim); } /* Corner circuit decoration */ .result-card::after { content: ''; position: absolute; right: 0; top: 0; width: 80px; height: 80px; background: radial-gradient(circle at top right, rgba(255,255,255,.025) 0%, transparent 70%); pointer-events: none; } .result-card:hover { border-color: var(--gold-bd); box-shadow: 0 0 0 1px var(--gold-bd), 0 12px 40px rgba(0,0,0,.35); transform: translateY(-1px); } /* Card header */ .card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 14px; } .card-title { font-family: var(--serif); font-size: 1.08rem; font-weight: 400; color: var(--tx); line-height: 1.35; flex: 1; } .card-num { font-family: var(--mono); font-size: .62rem; color: var(--tx-3); flex-shrink: 0; margin-top: 3px; } /* Tipo tag */ .tipo-tag { display: inline-flex; align-items: center; gap: 5px; font-family: var(--mono); font-size: .58rem; letter-spacing: .05em; padding: 3px 8px; border-radius: 20px; font-weight: 500; flex-shrink: 0; margin-top: 2px; white-space: nowrap; } .tipo-tag.acordaos { background: var(--blue-dim); color: #7ab4ff; border: 1px solid var(--blue-bd); } .tipo-tag.decisoes { background: var(--purple-dim); color: #b39dff; border: 1px solid var(--purple-bd); } .tipo-tag-dot { width: 5px; height: 5px; border-radius: 50%; } .tipo-tag.acordaos .tipo-tag-dot { background: var(--blue); } .tipo-tag.decisoes .tipo-tag-dot { background: var(--purple); } /* Meta pills */ .meta-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid var(--bd); } .meta-pill { font-size: .70rem; color: var(--tx-2); background: var(--bg-2); border: 1px solid var(--bd); border-radius: 20px; padding: 3px 10px; display: flex; align-items: center; gap: 4px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .meta-pill-icon { opacity: .55; font-size: .75rem; } /* Field blocks */ .field-block { border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.07); border-left: 2px solid rgba(255,255,255,.12); } .field-block.acordao { border-left-color: var(--gold-dim); } .field-label { font-family: var(--mono); font-size: .60rem; letter-spacing: .14em; text-transform: uppercase; font-weight: 500; margin-bottom: 7px; display: block; color: var(--tx-3); } .field-block.acordao .field-label { color: var(--gold-dim); } .field-text { font-size: .84rem; color: var(--tx-2); line-height: 1.7; white-space: pre-wrap; max-height: 200px; overflow-y: auto; } .field-text::-webkit-scrollbar { width: 2px; } .field-text::-webkit-scrollbar-thumb { background: var(--bd-2); } /* Card footer */ .card-foot { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; } .btn-ghost { font-family: var(--sans); font-size: .77rem; color: var(--tx-2); background: var(--bg-2); border: 1px solid var(--bd); border-radius: 20px; padding: 7px 16px; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; transition: color .2s, border-color .2s; cursor: pointer; } .btn-ghost:hover { color: var(--tx); border-color: var(--bd-2); } .btn-gold { font-family: var(--sans); font-size: .77rem; color: var(--gold); background: var(--gold-glow); border: 1px solid var(--gold-bd); border-radius: 20px; padding: 7px 16px; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; transition: background .2s; } .btn-gold:hover { background: rgba(201,168,76,.18); } /* ── LOADER ── */ #loader { display: none; text-align: center; padding: 60px 0; } .spinner { width: 32px; height: 32px; margin: 0 auto 14px; border: 2px solid var(--bd); border-top-color: var(--gold); border-radius: 50%; animation: spin .75s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .loader-txt { font-family: var(--mono); font-size: .72rem; color: var(--tx-3); letter-spacing: .1em; } /* ── ERROR ── */ .err-box { display: none; background: var(--red-dim); border: 1px solid var(--red-bd); border-radius: 12px; padding: 16px 20px; font-size: .88rem; color: #fca5a5; margin-top: 16px; } /* ── EMPTY ── */ .empty-box { display: none; text-align: center; padding: 70px 0; color: var(--tx-3); } .empty-icon { font-size: 2rem; margin-bottom: 10px; opacity: .4; } /* ── PAGINATION ── */ .pag-wrap { display: none; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px; margin-top: 36px; } .pag-btn { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--bd); background: transparent; color: var(--tx-2); cursor: pointer; font-size: .83rem; display: flex; align-items: center; justify-content: center; transition: all .18s; font-family: var(--sans); } .pag-btn:hover:not(:disabled) { border-color: var(--gold-bd); color: var(--gold); } .pag-btn.active { background: var(--gold-glow); border-color: var(--gold-bd); color: var(--gold); } .pag-btn:disabled { opacity: .25; cursor: default; } .pag-info { font-family: var(--mono); font-size: .68rem; color: var(--tx-3); padding: 0 10px; letter-spacing: .06em; } /* ── FOOTER ── */ .footer { border-top: 1px solid var(--bd); margin-top: 70px; padding-top: 24px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; } .footer-brand { font-family: var(--serif); font-size: .9rem; color: var(--tx-3); font-style: italic; } .footer-brand span { color: var(--gold); } .footer-note { font-family: var(--mono); font-size: .63rem; color: var(--tx-3); letter-spacing: .04em; } /* ── DOC PAGE SPECIFICS ── */ .back-link { display: inline-flex; align-items: center; gap: 7px; font-size: .8rem; color: var(--tx-2); text-decoration: none; border: 1px solid var(--bd); border-radius: 20px; padding: 7px 16px; transition: color .2s, border-color .2s; } .back-link:hover { color: var(--tx); border-color: var(--bd-2); } .doc-header-card { background: var(--bg-1); border: 1px solid var(--bd); border-radius: 16px; padding: 28px 30px; margin: 28px 0 20px; position: relative; overflow: hidden; } .doc-header-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--gold-dim), transparent); opacity: .6; } .doc-title { font-family: var(--serif); font-size: 1.5rem; font-weight: 400; color: var(--tx); margin-bottom: 18px; line-height: 1.3; } .doc-field { border-radius: 12px; padding: 20px 22px; margin-bottom: 14px; background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.07); border-left: 2px solid rgba(255,255,255,.12); } .doc-field.acordao { border-left-color: var(--gold-dim); } .doc-field-label { font-family: var(--mono); font-size: .62rem; letter-spacing: .14em; text-transform: uppercase; font-weight: 500; margin-bottom: 12px; display: block; color: var(--tx-3); } .doc-field.acordao .doc-field-label { color: var(--gold-dim); } .doc-field-text { font-size: .9rem; color: var(--tx-2); line-height: 1.78; white-space: pre-wrap; } .inteiro-teor-link { display: inline-flex; align-items: center; gap: 8px; font-family: var(--sans); font-size: .85rem; color: var(--gold); text-decoration: none; border: 1px solid var(--gold-bd); border-radius: 10px; padding: 12px 22px; margin-top: 16px; background: var(--gold-glow); transition: background .2s; } .inteiro-teor-link:hover { background: rgba(201,168,76,.18); } /* SVG icon inline helper */ .ico { display: inline-block; vertical-align: middle; } """ # ============================================ # Rota principal (HTML) # ============================================ @app.route('/') def index(): return render_template_string("""
Nenhum resultado para esta busca.