caarleexx commited on
Commit
f2ab215
·
verified ·
1 Parent(s): 4b105ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -785
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import os
3
  import sys
4
  import json
@@ -8,8 +7,6 @@ import requests
8
  import urllib3
9
  from flask import Flask, request, jsonify, render_template_string
10
  from playwright.sync_api import sync_playwright
11
- import traceback
12
- import re
13
 
14
  # Suprimir warnings de SSL
15
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -20,438 +17,15 @@ logger = logging.getLogger(__name__)
20
 
21
  app = Flask(__name__)
22
 
23
- # ============================================
24
- # PAYLOAD EXATO DA BUSCA (conforme curl enviado)
25
- # ============================================
26
- PAYLOAD_BUSCA_BASE = {
27
- "query": {
28
- "function_score": {
29
-
30
- "query": {
31
- "bool": {
32
- "filter": [
33
- {"query_string": {
34
- "default_operator": "AND",
35
- "fields": [
36
- "processo_codigo_completo",
37
- "acordao_ata^3",
38
- "documental_acordao_mesmo_sentido_lista_texto",
39
- "documental_doutrina_texto",
40
- "documental_indexacao_texto",
41
- "documental_jurisprudencia_citada_texto",
42
- "documental_legislacao_citada_texto",
43
- "documental_observacao_texto",
44
- "documental_publicacao_lista_texto",
45
- "documental_tese_tema_texto^3",
46
- "documental_tese_texto^3",
47
- "ementa_texto^3",
48
- "ministro_facet",
49
- "revisor_processo_nome",
50
- "orgao_julgador",
51
- "partes_lista_texto",
52
- "procedencia_geografica_completo",
53
- "processo_classe_processual_unificada_extenso",
54
- "titulo^6",
55
- "colac_numero",
56
- "colac_pagina",
57
- "decisao_texto^2",
58
- "documental_decisao_mesmo_sentido_lista_texto",
59
- "processo_precedente_texto",
60
- "sumula_texto^3",
61
- "ramo_direito^1",
62
- "situacao_sumula^1",
63
- "materia_noticia^1",
64
- "titulo_noticia^3",
65
- "resumo_noticia^3",
66
- "conteudo_noticia^1",
67
- "ramo_noticia^1",
68
- "inteiro_teor_texto"
69
- ],
70
- "query": "__TERMO__",
71
- "type": "cross_fields",
72
- "fuzziness": "AUTO:4,7",
73
- "analyzer": "radical_search_analyzer",
74
- "quote_analyzer": "standard_analyzer",
75
- "quote_field_suffix": ".standard"
76
- }}
77
- ],
78
- "must": [],
79
- "should": [
80
- {"query_string": {
81
- "default_operator": "AND",
82
- "fields": [
83
- "acordao_ata^3",
84
- "documental_doutrina_texto",
85
- "documental_indexacao_texto",
86
- "documental_jurisprudencia_citada_texto",
87
- "documental_observacao_texto",
88
- "documental_tese_tema_texto^3",
89
- "documental_tese_texto^3",
90
- "ementa_texto^3",
91
- "titulo^6",
92
- "decisao_texto^2",
93
- "sumula_texto^3",
94
- "ramo_direito^1",
95
- "situacao_sumula^1",
96
- "materia_noticia^1",
97
- "titulo_noticia^3",
98
- "resumo_noticia^3",
99
- "conteudo_noticia^1",
100
- "ramo_noticia^1",
101
- "inteiro_teor_texto"
102
- ],
103
- "query": "__TERMO__",
104
- "tie_breaker": 1,
105
- "fuzziness": "AUTO:4,7",
106
- "analyzer": "radical_search_analyzer",
107
- "quote_analyzer": "standard_analyzer",
108
- "quote_field_suffix": ".standard"
109
- }},
110
- {"query_string": {
111
- "default_operator": "and",
112
- "type": "phrase",
113
- "tie_breaker": 1,
114
- "phrase_slop": 20,
115
- "fields": [
116
- "acordao_ata^3",
117
- "documental_tese_tema_texto^3",
118
- "documental_tese_texto^3",
119
- "ementa_texto^3",
120
- "decisao_texto^2",
121
- "situacao_sumula^1",
122
- "titulo_noticia^3",
123
- "resumo_noticia^3",
124
- "conteudo_noticia^1",
125
- "inteiro_teor_texto^0.5"
126
- ],
127
- "query": "__TERMO__",
128
- "fuzziness": "AUTO:4,7",
129
- "analyzer": "radical_search_analyzer",
130
- "quote_analyzer": "standard_analyzer",
131
- "quote_field_suffix": ".standard"
132
- }},
133
- {"query_string": {
134
- "default_operator": "and",
135
- "type": "phrase",
136
- "tie_breaker": 1,
137
- "phrase_slop": 5,
138
- "fields": [
139
- "documental_acordao_mesmo_sentido_lista_texto",
140
- "documental_doutrina_texto",
141
- "documental_indexacao_texto",
142
- "documental_jurisprudencia_citada_texto",
143
- "documental_legislacao_citada_texto",
144
- "documental_observacao_texto",
145
- "partes_lista_texto",
146
- "processo_precedente_texto",
147
- "documental_decisao_mesmo_sentido_lista_texto"
148
- ],
149
- "query": "__TERMO__",
150
- "fuzziness": "AUTO:4,7",
151
- "analyzer": "radical_search_analyzer",
152
- "quote_analyzer": "standard_analyzer",
153
- "quote_field_suffix": ".standard"
154
- }},
155
- {"query_string": {
156
- "default_operator": "and",
157
- "type": "phrase",
158
- "phrase_slop": 1,
159
- "fields": [
160
- "documental_publicacao_lista_texto",
161
- "ministro_facet",
162
- "revisor_processo_nome",
163
- "orgao_julgador",
164
- "ramo_direito^1",
165
- "ramo_noticia^1",
166
- "procedencia_geografica_completo",
167
- "processo_classe_processual_unificada_extenso",
168
- "titulo^6",
169
- "colac_numero",
170
- "colac_pagina",
171
- "sumula_texto^3"
172
- ],
173
- "query": "__TERMO__",
174
- "fuzziness": "AUTO:4,7",
175
- "boost": 0,
176
- "analyzer": "radical_search_analyzer",
177
- "quote_analyzer": "standard_analyzer",
178
- "quote_field_suffix": ".standard"
179
- }}
180
- ]
181
- }
182
- }
183
- }
184
- },
185
- "_source": [
186
- "base", "_id", "id", "dg_unique", "titulo", "ministro_facet",
187
- "procedencia_geografica_completo", "procedencia_geografica_pais_sigla",
188
- "procedencia_geografica_uf_sigla", "procedencia_geografica_uf_extenso",
189
- "processo_codigo_completo", "processo_classe_processual_unificada_extenso",
190
- "processo_classe_processual_unificada_classe_sigla",
191
- "processo_classe_processual_unificada_incidente_sigla", "processo_numero",
192
- "julgamento_data", "publicacao_data", "is_decisao_presidencia",
193
- "relator_processo_nome", "presidente_nome", "relator_decisao_nome",
194
- "acordao_ata", "decisao_texto", "partes_lista_texto",
195
- "acompanhamento_processual_url", "dje_url",
196
- "documental_publicacao_lista_texto", "documental_decisao_mesmo_sentido_lista_texto",
197
- "documental_decisao_mesmo_sentido_lista_html",
198
- "documental_decisao_mesmo_sentido_is_secundario",
199
- "documental_legislacao_citada_texto", "documental_indexacao_texto",
200
- "documental_observacao_texto", "documental_observacao_html",
201
- "documental_doutrina_texto", "externo_seq_objeto_incidente",
202
- "dg_atualizado_em", "dg_unique", "volume_informativo", "ramo_noticia",
203
- "materia_noticia", "titulo_noticia", "resumo_noticia", "conteudo_noticia",
204
- "is_covid", "tipo_julgamento", "acompanhamento_processual_url",
205
- "informativo_url", "pesquisa_url", "audio_url", "video_url",
206
- "numero_noticias_url", "numero_noticias_processo", "informativo_url",
207
- "periodo_inicio_data", "periodo_fim_data", "processo_lista_texto",
208
- "sumula_numero", "orgao_julgador", "is_vinculante", "sumula_texto",
209
- "situacao_sumula", "ramo_direito", "aprovacao_url",
210
- "processo_precedente_texto", "processo_precedente_html",
211
- "processo_classe_processual_unificada_sigla", "is_questao_ordem",
212
- "is_repercussao_geral_admissibilidade", "is_repercussao_geral_merito",
213
- "is_repercussao_geral_recurso_interno", "is_repercussao_geral",
214
- "is_processo_antigo", "is_colac", "colac_numero", "colac_pagina",
215
- "revisor_processo_nome", "relator_acordao_nome",
216
- "julgamento_is_sessao_virtual", "republicacao_data", "ementa_texto",
217
- "inteiro_teor_url", "documental_acordao_mesmo_sentido_lista_texto",
218
- "documental_acordao_mesmo_sentido_lista_html",
219
- "documental_acordao_mesmo_sentido_is_secundario",
220
- "documental_jurisprudencia_citada_texto", "documental_assunto_texto",
221
- "documental_tese_tipo", "documental_tese_texto", "documental_tese_tema_texto",
222
- "old_seq_colac", "old_seq_repercussao_geral", "old_seq_sjur", "ods_onu"
223
- ],
224
- "aggs": {
225
- "base_agg": {
226
- "filters": {
227
- "filters": {
228
- "acordaos": {"match": {"base": "acordaos"}},
229
- "sumulas": {"match": {"base": "sumulas"}},
230
- "decisoes": {"match": {"base": "decisoes"}},
231
- "informativos": {"match": {"base": "novo_informativo"}}
232
- }
233
- }
234
- },
235
 
236
-
237
- "processo_classe_processual_unificada_classe_sigla_agg": {
238
- "aggs": {
239
- "processo_classe_processual_unificada_classe_sigla_agg": {
240
- "terms": {"field": "processo_classe_processual_unificada_classe_sigla.keyword", "size": 200, "execution_hint": "map"}
241
- }
242
- },
243
- "filter": {"bool": {"must": [{"term": {"base": "acordaos"}}]}}
244
- },
245
- "procedencia_geografica_uf_sigla_agg": {
246
- "aggs": {
247
- "procedencia_geografica_uf_sigla_agg": {
248
- "terms": {"field": "procedencia_geografica_uf_sigla", "size": 200, "execution_hint": "map"}
249
- }
250
- },
251
- "filter": {"bool": {"must": [{"term": {"base": "acordaos"}}]}}
252
- }
253
- },
254
- "size": 250,
255
- "from": 0,
256
- "post_filter": {
257
- "bool": {
258
- "must": [{"term": {"base": "acordaos"}}],
259
- "should": []
260
- }
261
- },
262
- "sort": [{"_score": "desc"}],
263
- "highlight": {
264
- "highlight_query": {
265
- "bool": {
266
- "filter": [
267
- {"query_string": {
268
- "default_operator": "AND",
269
- "fields": [
270
- "processo_codigo_completo",
271
- "acordao_ata^3",
272
- "documental_acordao_mesmo_sentido_lista_texto",
273
- "documental_doutrina_texto",
274
- "documental_indexacao_texto",
275
- "documental_jurisprudencia_citada_texto",
276
- "documental_legislacao_citada_texto",
277
- "documental_observacao_texto",
278
- "documental_publicacao_lista_texto",
279
- "documental_tese_tema_texto^3",
280
- "documental_tese_texto^3",
281
- "ementa_texto^3",
282
- "ministro_facet",
283
- "revisor_processo_nome",
284
- "orgao_julgador",
285
- "partes_lista_texto",
286
- "procedencia_geografica_completo",
287
- "processo_classe_processual_unificada_extenso",
288
- "titulo^6",
289
- "colac_numero",
290
- "colac_pagina",
291
- "decisao_texto^2",
292
- "documental_decisao_mesmo_sentido_lista_texto",
293
- "processo_precedente_texto",
294
- "sumula_texto^3",
295
- "ramo_direito^1",
296
- "situacao_sumula^1",
297
- "materia_noticia^1",
298
- "titulo_noticia^3",
299
- "resumo_noticia^3",
300
- "conteudo_noticia^1",
301
- "ramo_noticia^1",
302
- "inteiro_teor_texto"
303
- ],
304
- "query": "__TERMO__",
305
- "type": "cross_fields",
306
- "fuzziness": "AUTO:4,7",
307
- "analyzer": "radical_search_analyzer",
308
- "quote_analyzer": "standard_analyzer",
309
- "quote_field_suffix": ".standard"
310
- }}
311
- ],
312
- "must": [],
313
- "should": [
314
- {"query_string": {
315
- "default_operator": "AND",
316
- "fields": [
317
- "acordao_ata^3",
318
- "documental_doutrina_texto",
319
- "documental_indexacao_texto",
320
- "documental_jurisprudencia_citada_texto",
321
- "documental_observacao_texto",
322
- "documental_tese_tema_texto^3",
323
- "documental_tese_texto^3",
324
- "ementa_texto^3",
325
- "titulo^6",
326
- "decisao_texto^2",
327
- "sumula_texto^3",
328
- "ramo_direito^1",
329
- "situacao_sumula^1",
330
- "materia_noticia^1",
331
- "titulo_noticia^3",
332
- "resumo_noticia^3",
333
- "conteudo_noticia^1",
334
- "ramo_noticia^1",
335
- "inteiro_teor_texto"
336
- ],
337
- "query": "__TERMO__",
338
- "tie_breaker": 1,
339
- "fuzziness": "AUTO:4,7",
340
- "analyzer": "radical_search_analyzer",
341
- "quote_analyzer": "standard_analyzer",
342
- "quote_field_suffix": ".standard"
343
- }},
344
- {"query_string": {
345
- "default_operator": "and",
346
- "type": "phrase",
347
- "tie_breaker": 1,
348
- "phrase_slop": 20,
349
- "fields": [
350
- "acordao_ata^3",
351
- "documental_tese_tema_texto^3",
352
- "documental_tese_texto^3",
353
- "ementa_texto^3",
354
- "decisao_texto^2",
355
- "situacao_sumula^1",
356
- "titulo_noticia^3",
357
- "resumo_noticia^3",
358
- "conteudo_noticia^1",
359
- "inteiro_teor_texto^0.5"
360
- ],
361
- "query": "__TERMO__",
362
- "fuzziness": "AUTO:4,7",
363
- "analyzer": "radical_search_analyzer",
364
- "quote_analyzer": "standard_analyzer",
365
- "quote_field_suffix": ".standard"
366
- }},
367
- {"query_string": {
368
- "default_operator": "and",
369
- "type": "phrase",
370
- "tie_breaker": 1,
371
- "phrase_slop": 5,
372
- "fields": [
373
- "documental_acordao_mesmo_sentido_lista_texto",
374
- "documental_doutrina_texto",
375
- "documental_indexacao_texto",
376
- "documental_jurisprudencia_citada_texto",
377
- "documental_legislacao_citada_texto",
378
- "documental_observacao_texto",
379
- "partes_lista_texto",
380
- "processo_precedente_texto",
381
- "documental_decisao_mesmo_sentido_lista_texto"
382
- ],
383
- "query": "__TERMO__",
384
- "fuzziness": "AUTO:4,7",
385
- "analyzer": "radical_search_analyzer",
386
- "quote_analyzer": "standard_analyzer",
387
- "quote_field_suffix": ".standard"
388
- }},
389
- {"query_string": {
390
- "default_operator": "and",
391
- "type": "phrase",
392
- "phrase_slop": 1,
393
- "fields": [
394
- "documental_publicacao_lista_texto",
395
- "ministro_facet",
396
- "revisor_processo_nome",
397
- "orgao_julgador",
398
- "ramo_direito^1",
399
- "ramo_noticia^1",
400
- "procedencia_geografica_completo",
401
- "processo_classe_processual_unificada_extenso",
402
- "titulo^6",
403
- "colac_numero",
404
- "colac_pagina",
405
- "sumula_texto^3"
406
- ],
407
- "query": "__TERMO__",
408
- "fuzziness": "AUTO:4,7",
409
- "boost": 0,
410
- "analyzer": "radical_search_analyzer",
411
- "quote_analyzer": "standard_analyzer",
412
- "quote_field_suffix": ".standard"
413
- }}
414
- ]
415
- }
416
- },
417
- "number_of_fragments": 64,
418
- "fragment_size": 300,
419
- "order": "score",
420
- "pre_tags": ["<em>"],
421
- "post_tags": ["</em>"],
422
- "fields": {
423
- "ementa_texto": {"fragment_size": 24000, "matched_fields": ["ementa_texto", "ementa_texto.standard"], "type": "fvh"},
424
- "sumula_texto": {"number_of_fragments": 0, "matched_fields": ["sumula_texto", "sumula_texto.standard"], "type": "fvh"},
425
- "materia_noticia": {"matched_fields": ["materia_noticia", "materia_noticia.standard"], "type": "fvh"},
426
- "titulo_noticia": {"matched_fields": ["titulo_noticia", "titulo_noticia.standard"], "type": "fvh"},
427
- "resumo_noticia": {"fragment_size": 5000, "matched_fields": ["resumo_noticia", "resumo_noticia.standard"], "type": "fvh"},
428
- "conteudo_noticia": {"fragment_size": 50000, "matched_fields": ["conteudo_noticia", "conteudo_noticia.standard"], "type": "fvh"},
429
- "acordao_ata": {"fragment_size": 600, "matched_fields": ["acordao_ata", "acordao_ata.standard"], "type": "fvh"},
430
- "decisao_texto": {"fragment_size": 1200, "matched_fields": ["decisao_texto", "decisao_texto.standard"], "type": "fvh"},
431
- "documental_tese_texto": {"fragment_size": 2000, "matched_fields": ["documental_tese_texto", "documental_tese_texto.standard"], "type": "fvh"},
432
- "documental_tese_tema_texto": {"fragment_size": 2000, "matched_fields": ["documental_tese_tema_texto", "documental_tese_tema_texto.standard"], "type": "fvh"},
433
- "documental_observacao_texto": {"matched_fields": ["documental_observacao_texto", "documental_observacao_texto.standard"], "type": "fvh"},
434
- "documental_indexacao_texto": {"matched_fields": ["documental_indexacao_texto", "documental_indexacao_texto.standard"], "type": "fvh"},
435
- "documental_legislacao_citada_texto": {"matched_fields": ["documental_legislacao_citada_texto", "documental_legislacao_citada_texto.standard"], "type": "fvh"},
436
- "documental_jurisprudencia_citada_texto": {"matched_fields": ["documental_jurisprudencia_citada_texto", "documental_jurisprudencia_citada_texto.standard"], "type": "fvh"},
437
- "documental_doutrina_texto": {"matched_fields": ["documental_doutrina_texto", "documental_doutrina_texto.standard"], "type": "fvh"},
438
- "partes_lista_texto": {"matched_fields": ["partes_lista_texto", "partes_lista_texto.standard"], "type": "fvh"},
439
- "documental_publicacao_lista_texto": {"matched_fields": ["documental_publicacao_lista_texto", "documental_publicacao_lista_texto.standard"], "type": "fvh"},
440
- "documental_acordao_mesmo_sentido_lista_texto": {"matched_fields": ["documental_acordao_mesmo_sentido_lista_texto", "documental_acordao_mesmo_sentido_lista_texto.standard"], "type": "fvh"},
441
- "documental_decisao_mesmo_sentido_lista_texto": {"matched_fields": ["documental_decisao_mesmo_sentido_lista_texto", "documental_decisao_mesmo_sentido_lista_texto.standard"], "type": "fvh"},
442
- "processo_precedente_texto": {"matched_fields": ["processo_precedente_texto", "processo_precedente_texto.standard"], "type": "fvh"},
443
- "procedencia_geografica_completo": {"matched_fields": ["procedencia_geografica_completo", "procedencia_geografica_completo.standard"], "type": "fvh"},
444
- "inteiro_teor_texto": {"fragment_size": 600, "matched_fields": ["inteiro_teor_texto", "inteiro_teor_texto.standard"], "type": "fvh"}
445
- }
446
- },
447
- "track_total_hits": True
448
- }
449
 
450
- # ============================================
451
- # Constantes da API e cache (mantidos)
452
- # ============================================
453
- URL_API = "https://jurisprudencia.stf.jus.br/api/search/search"
454
- URL_API_GET = "https://jurisprudencia.stf.jus.br/api/search/get"
455
  HEADERS = {
456
  "Accept": "application/json, text/plain, */*",
457
  "Content-Type": "application/json",
@@ -460,8 +34,6 @@ HEADERS = {
460
  "Origin": "https://jurisprudencia.stf.jus.br"
461
  }
462
 
463
- token_cache = {"token": None, "expires_at": 0}
464
-
465
  # ============================================
466
  # Funções de token e busca
467
  # ============================================
@@ -501,286 +73,199 @@ def get_fresh_token(force_refresh=False):
501
  logger.error(f"Erro ao obter token: {str(e)}")
502
  return None
503
 
504
- def search_with_token(token, payload):
 
 
505
  if not token:
506
- return {"success": False, "error": "Token não fornecido"}
507
- headers = HEADERS.copy()
508
- headers['Cookie'] = f'aws-waf-token={token}'
509
- try:
510
- response = requests.post(URL_API, headers=headers, json=payload, verify=False, timeout=30)
511
- logger.info(f"Resposta: status {response.status_code}")
512
- if response.status_code == 200:
513
- return {"success": True, "data": response.json()}
514
- elif response.status_code in (403, 202):
515
- token_cache["token"] = None
516
- return {"success": False, "error": "Token expirado ou desafio", "status": response.status_code}
517
- else:
518
- return {"success": False, "error": f"HTTP {response.status_code}", "status": response.status_code, "text": response.text[:500]}
519
- except Exception as e:
520
- logger.error(f"Erro na requisição: {str(e)}")
521
- return {"success": False, "error": str(e)}
 
 
522
 
523
- def get_document_raw(token, doc_id):
524
- if not token:
525
- return {"success": False, "error": "Token não fornecido"}
526
  headers = HEADERS.copy()
527
  headers['Cookie'] = f'aws-waf-token={token}'
528
- headers.pop("Content-Type", None)
529
- url = f"{URL_API_GET}/{doc_id}"
530
  try:
531
- response = requests.get(url, headers=headers, verify=False, timeout=30)
532
  if response.status_code == 200:
533
- return {"success": True, "data": response.json()}
534
- elif response.status_code in (403, 202):
535
- token_cache["token"] = None
536
- return {"success": False, "error": "Token expirado", "status": response.status_code}
 
 
 
 
 
 
537
  else:
538
- return {"success": False, "error": f"HTTP {response.status_code}"}
539
  except Exception as e:
540
- return {"success": False, "error": str(e)}
541
 
542
  # ============================================
543
- # HTML TEMPLATE (mantido, mas omitido para brevidade)
544
  # ============================================
545
- HTML_TEMPLATE = """<!DOCTYPE html>
 
 
 
546
  <html>
547
  <head>
548
- <title>⚖️ STF Jurisprudência</title>
549
  <meta charset="utf-8">
550
  <meta name="viewport" content="width=device-width, initial-scale=1">
551
  <style>
552
- body { font-family: 'Segoe UI', Roboto, system-ui, sans-serif; max-width: 1600px; margin: 0 auto; padding: 20px; background: #f0f2f5; }
553
- .container { background: white; border-radius: 12px; padding: 30px; box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
554
- h1 { color: #1a1a2e; border-bottom: 3px solid #4CAF50; padding-bottom: 15px; }
555
- .info-box { background: #e8f0fe; border-left: 5px solid #2196F3; padding: 15px 20px; margin: 20px 0; border-radius: 8px; }
556
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px,1fr)); gap:15px; margin:25px 0; }
557
- .stat-card { background: white; border:1px solid #e0e0e0; border-radius:10px; padding:20px; text-align:center; }
558
- .stat-value { font-size:32px; font-weight:bold; color:#1a73e8; }
559
- .button-group { display:flex; gap:15px; flex-wrap:wrap; margin:25px 0; }
560
- button { background:#1a73e8; color:white; border:none; padding:14px 28px; border-radius:8px; cursor:pointer; }
561
- button:hover { background:#1557b0; }
562
- .loading { display:inline-block; width:20px; height:20px; border:3px solid rgba(255,255,255,0.3); border-top:3px solid white; border-radius:50%; animation:spin 1s linear infinite; }
563
- @keyframes spin { 0% { transform:rotate(0deg); } 100% { transform:rotate(360deg); } }
564
- pre { background:#f8f9fa; border:1px solid #e0e0e0; border-radius:10px; padding:20px; overflow:auto; max-height:600px; }
565
- .success { background:#e6f4ea; border-left:5px solid #34a853; padding:15px; margin:15px 0; }
566
- .error { background:#fce8e6; border-left:5px solid #ea4335; padding:15px; margin:15px 0; }
567
- .token-box { background:#1a1a2e; color:#e0e0e0; padding:15px; border-radius:8px; font-family:monospace; margin:15px 0; }
568
- .filters { background:#f8f9fa; border-radius:8px; padding:20px; margin:20px 0; border:1px solid #e0e0e0; }
569
- .filter-group { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px,1fr)); gap:15px; }
570
- .filter-item label { display:block; margin-bottom:8px; color:#5f6368; font-size:14px; font-weight:500; }
571
- .filter-item input, .filter-item select { width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; }
572
  </style>
573
  </head>
574
  <body>
575
  <div class="container">
576
- <h1>STF Jurisprudência - Visualizador</h1>
577
- <div class="info-box">
578
- <strong>📌 API de Jurisprudência do STF</strong><br>
579
- • Documentos disponíveis: <span id="totalDocs">carregando...</span>
580
- </div>
581
- <div class="stats-grid">
582
- <div class="stat-card"><div class="stat-value" id="requestsCount">0</div><div class="stat-label">Requisições</div></div>
583
- <div class="stat-card"><div class="stat-value" id="successCount">0</div><div class="stat-label">Sucessos</div></div>
584
- <div class="stat-card"><div class="stat-value" id="failCount">0</div><div class="stat-label">Falhas</div></div>
585
- <div class="stat-card"><div class="stat-value" id="docsCount">0</div><div class="stat-label">Docs Obtidos</div></div>
586
  </div>
587
- <div class="filters">
588
- <h3>🔍 Filtros de Busca</h3>
589
- <div class="filter-group">
590
- <div class="filter-item"><label>Quantidade</label><select id="pageSize"><option value="5">5</option><option value="10">10</option><option value="25">25</option><option value="50">50</option><option value="100" selected>100</option></select></div>
591
- <div class="filter-item"><label>Ordenar por</label><select id="sortOrder"><option value="desc">Mais recentes</option><option value="asc">Mais antigos</option></select></div>
592
- <div class="filter-item"><label>Base</label><select id="base"><option value="acordaos">Acórdãos</option><option value="decisoes">Decisões</option><option value="sumulas">Súmulas</option><option value="informativos">Informativos</option><option value="">Todas</option></select></div>
593
- <div class="filter-item"><label>Busca por ID</label><input type="text" id="docId" placeholder="Ex: sjur505215"></div>
594
- </div>
595
- </div>
596
- <div class="button-group">
597
- <button id="testBtn" onclick="runSearch()"><span class="loading" id="loading" style="display:none;"></span>🔍 Buscar Documentos</button>
598
- <button id="getByIdBtn" class="secondary" onclick="getDocumentById()">📄 Buscar por ID</button>
599
- <button id="downloadBtn" class="download" onclick="downloadJSON()" disabled>📥 Download JSON</button>
600
- <button id="copyBtn" class="secondary" onclick="copyToken()">🔑 Copiar Token</button>
601
- </div>
602
- <div id="tokenDisplay" style="display:none;" class="token-box"><span class="token-label">🔐 Token AWS WAF</span><span id="tokenValue"></span></div>
603
- <div id="result" style="margin-top:25px;"></div>
604
  </div>
 
605
  <script>
606
- let lastResult = null, lastToken = null, requestsCount = 0, successCount = 0, failCount = 0, docsCount = 0;
607
- function displayResult(data) {
608
- if (!data?.result?.hits?.hits) return document.getElementById('result').innerHTML = '<div class="error">Nenhum documento encontrado</div>';
609
- const hits = data.result.hits.hits, total = data.result.hits.total?.value || 0;
610
- let html = `<div class="success">✅ Encontrados ${total.toLocaleString()} documentos. Exibindo ${hits.length} resultados.</div>`;
611
- hits.forEach((hit, idx) => {
612
- const src = hit._source || {}, docId = src.id || hit._id || `doc_${idx}`;
613
- html += `<div style="border:1px solid #ccc; margin-bottom:10px; padding:10px;"><h3>${src.titulo || src.processo_codigo_completo || docId}</h3>`;
614
- html += `<p><strong>ID:</strong> ${docId}</p>`;
615
- html += `<p><strong>Relator:</strong> ${src.relator_processo_nome || ''}</p>`;
616
- html += `<p><strong>Órgão:</strong> ${src.orgao_julgador || ''}</p>`;
617
- html += `<p><strong>Data:</strong> ${src.julgamento_data || ''}</p>`;
618
- html += `<p><strong>Ementa:</strong> ${(src.ementa_texto || '').substring(0,500)}...</p>`;
619
- if (src.inteiro_teor_url) html += `<p><a href="${src.inteiro_teor_url}" target="_blank">📄 Inteiro Teor</a></p>`;
620
- html += `<button onclick="fetchFullDocument('${docId}')">📄 Obter completo</button>`;
621
- html += `</div>`;
622
- });
623
- document.getElementById('result').innerHTML = html;
624
- }
625
- async function runSearch() {
626
- const btn = document.getElementById('testBtn'), loading = document.getElementById('loading'), resultDiv = document.getElementById('result');
627
- btn.disabled = true; loading.style.display = 'inline-block'; resultDiv.innerHTML = '<div class="info-box">⏳ Buscando...</div>';
628
- try {
629
- const resp = await fetch('/api/search-advanced', {
630
- method:'POST', headers:{'Content-Type':'application/json'},
631
- body:JSON.stringify({ pageSize: parseInt(document.getElementById('pageSize').value), sortOrder: document.getElementById('sortOrder').value, base: document.getElementById('base').value || undefined })
632
- });
633
- const data = await resp.json();
634
- requestsCount++; document.getElementById('requestsCount').textContent = requestsCount;
635
- if (data.success) {
636
- successCount++; document.getElementById('successCount').textContent = successCount;
637
- if (data.data?.result?.hits) {
638
- docsCount = data.data.result.hits.hits.length;
639
- document.getElementById('docsCount').textContent = docsCount;
640
- if (data.data.result.hits.total?.value) document.getElementById('totalDocs').textContent = data.data.result.hits.total.value.toLocaleString();
641
- }
642
- lastResult = data.data; lastToken = data.token;
643
- if (lastToken) { document.getElementById('tokenValue').textContent = lastToken; document.getElementById('tokenDisplay').style.display = 'block'; document.getElementById('downloadBtn').disabled = false; }
644
- displayResult(data.data);
645
- } else {
646
- failCount++; document.getElementById('failCount').textContent = failCount;
647
- resultDiv.innerHTML = '<div class="error">❌ Falha</div><pre>'+JSON.stringify(data,null,2)+'</pre>';
648
- }
649
- } catch(e) {
650
- failCount++; document.getElementById('failCount').textContent = failCount;
651
- resultDiv.innerHTML = '<div class="error">❌ Erro: '+e.message+'</div>';
652
- } finally {
653
- btn.disabled = false; loading.style.display = 'none';
654
  }
655
- }
656
- async function getDocumentById() {
657
- const docId = document.getElementById('docId').value.trim();
658
- if (!docId) return alert('Digite um ID');
659
- const btn = document.getElementById('getByIdBtn'), loading = document.getElementById('loading'), resultDiv = document.getElementById('result');
660
- btn.disabled = true; loading.style.display = 'inline-block'; resultDiv.innerHTML = '<div class="info-box">⏳ Buscando...</div>';
 
 
661
  try {
662
- const resp = await fetch(`/api/document/${docId}`);
663
- const data = await resp.json();
664
- requestsCount++; document.getElementById('requestsCount').textContent = requestsCount;
665
- if (data.success && data.document) {
666
- successCount++; document.getElementById('successCount').textContent = successCount; docsCount++; document.getElementById('docsCount').textContent = docsCount;
667
- lastResult = { result: { hits: { hits: [data.document] } } };
668
- displayResult(lastResult);
669
  } else {
670
- failCount++; document.getElementById('failCount').textContent = failCount;
671
- resultDiv.innerHTML = '<div class="error">❌ Documento não encontrado</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  }
673
- } catch(e) {
674
- failCount++; document.getElementById('failCount').textContent = failCount;
675
- resultDiv.innerHTML = '<div class="error">❌ Erro: '+e.message+'</div>';
676
  } finally {
677
- btn.disabled = false; loading.style.display = 'none';
 
678
  }
679
  }
680
- async function fetchFullDocument(docId) {
681
- try {
682
- const resp = await fetch(`/api/document-raw/${docId}`);
683
- const data = await resp.json();
684
- if (data.success) {
685
- alert('Documento completo obtido! Verifique o console.');
686
- console.log('Documento completo:', data.document);
687
- } else alert('Erro: ' + (data.error || 'desconhecido'));
688
- } catch(e) { alert('Erro: ' + e.message); }
689
- }
690
- function downloadJSON() {
691
- if (!lastResult) return alert('Nenhum resultado');
692
- const dataStr = JSON.stringify(lastResult, null, 2);
693
- const blob = new Blob([dataStr], {type:'application/json'});
694
- const url = URL.createObjectURL(blob);
695
- const a = document.createElement('a');
696
- a.href = url; a.download = `stf_${new Date().toISOString().slice(0,10)}.json`;
697
- document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
698
- }
699
- function copyToken() { if (lastToken) navigator.clipboard.writeText(lastToken).then(()=>alert('✅ Token copiado!')); else alert('Nenhum token'); }
700
- window.onload = async () => {
701
- try {
702
- const resp = await fetch('/api/health');
703
- const data = await resp.json();
704
- if (data.total_docs) document.getElementById('totalDocs').textContent = data.total_docs.toLocaleString();
705
- } catch(e) { document.getElementById('totalDocs').textContent = 'indisponível'; }
706
- };
707
  </script>
708
  </body>
709
- </html>"""
 
710
 
711
  # ============================================
712
- # Rotas
713
  # ============================================
714
- @app.route('/')
715
- def index():
716
- try:
717
- return render_template_string(HTML_TEMPLATE)
718
- except Exception as e:
719
- return f"Erro no template: {str(e)}<br><pre>{traceback.format_exc()}</pre>", 500
720
-
721
- @app.route('/api/search-advanced', methods=['POST'])
722
- def search_advanced():
723
- data = request.json or {}
724
- page_size = data.get('pageSize', 100)
725
- sort_order = data.get('sortOrder', 'desc')
726
- base = data.get('base')
727
-
728
- token = get_fresh_token()
729
- if not token:
730
- return jsonify({"success": False, "error": "Token não disponível"}), 500
731
-
732
- payload = PAYLOAD_BUSCA_BASE.copy()
733
- payload["size"] = min(page_size, 250)
734
- if base:
735
- payload["post_filter"]["bool"]["must"] = [{"term": {"base": base}}]
736
- # Substituir placeholder se houver (não usado aqui)
737
- result = search_with_token(token, payload)
738
- if result.get("success"):
739
- return jsonify({"success": True, "token": token, "data": result["data"], "timestamp": time.time()})
740
- else:
741
- if result.get("status") in (403, 202):
742
- token = get_fresh_token(force_refresh=True)
743
- if token:
744
- result = search_with_token(token, payload)
745
- if result.get("success"):
746
- return jsonify({"success": True, "token": token, "data": result["data"], "timestamp": time.time()})
747
- return jsonify({"success": False, "error": result.get("error")}), 500
748
 
749
- @app.route('/api/document/<doc_id>', methods=['GET'])
750
- def get_document(doc_id):
751
- token = get_fresh_token()
752
- if not token:
753
- return jsonify({"error": "Não foi possível obter token"}), 500
754
- payload = {
755
- "query": {"ids": {"values": [doc_id]}},
756
- "_source": ["id", "titulo", "ementa_texto", "processo_codigo_completo", "relator_processo_nome", "orgao_julgador", "julgamento_data", "inteiro_teor_url"],
757
- "size": 1
758
- }
759
- result = search_with_token(token, payload)
760
- if result.get("success") and result["data"].get("hits", {}).get("hits"):
761
- doc = result["data"]["hits"]["hits"][0]
762
- return jsonify({"success": True, "document": doc})
763
- else:
764
- return jsonify({"success": False, "error": result.get("error", "Documento não encontrado")}), 404
 
 
 
 
 
 
 
765
 
766
- @app.route('/api/document-raw/<doc_id>', methods=['GET'])
767
- def get_document_raw_endpoint(doc_id):
768
- token = get_fresh_token()
769
- if not token:
770
- return jsonify({"error": "Não foi possível obter token"}), 500
771
- result = get_document_raw(token, doc_id)
772
- if result.get("success"):
773
- return jsonify({"success": True, "document": result["data"]})
774
- else:
775
- if result.get("status") == 403:
776
- token_cache["token"] = None
777
- token = get_fresh_token(force_refresh=True)
778
- if token:
779
- result = get_document_raw(token, doc_id)
780
- if result.get("success"):
781
- return jsonify({"success": True, "document": result["data"]})
782
- return jsonify({"success": False, "error": result.get("error", "Falha")}), 500
783
 
 
 
 
784
  @app.route('/api/health', methods=['GET'])
785
  def health():
786
  playwright_status = False
@@ -790,110 +275,10 @@ def health():
790
  playwright_status = True
791
  except:
792
  pass
793
- total_docs = None
794
- token = get_fresh_token()
795
- if token:
796
- try:
797
- res = search_with_token(token, {"size": 0, "track_total_hits": True})
798
- if res.get("success") and res["data"].get("hits", {}).get("total"):
799
- total_docs = res["data"]["hits"]["total"]["value"]
800
- except:
801
- pass
802
  return jsonify({
803
  "status": "healthy",
804
- "timestamp": time.time(),
805
  "playwright_ready": playwright_status,
806
- "token_cached": bool(token_cache["token"]),
807
- "token_expires_in": max(0, token_cache["expires_at"] - time.time()) if token_cache["expires_at"] else 0,
808
- "total_docs": total_docs,
809
- "python_version": sys.version
810
- })
811
-
812
- @app.route('/api/token', methods=['GET'])
813
- def get_token():
814
- token = get_fresh_token()
815
- if token:
816
- return jsonify({"success": True, "token": token, "expires_in": max(0, token_cache["expires_at"] - time.time())})
817
- else:
818
- return jsonify({"success": False, "error": "Não foi possível obter token"}), 500
819
-
820
- @app.route('/api/clear-cache', methods=['POST'])
821
- def clear_cache():
822
- global token_cache
823
- token_cache = {"token": None, "expires_at": 0}
824
- return jsonify({"success": True, "message": "Cache limpo"})
825
-
826
- # ============================================
827
- # ENDPOINT SIMPLIFICADO /busca (com payload exato)
828
- # ============================================
829
- @app.route('/busca')
830
- def busca_simplificada():
831
- """
832
- Endpoint público para busca simplificada de jurisprudência.
833
- Usa o payload completo do STF e retorna apenas campos essenciais.
834
- Exemplo: /busca?q=dano%20moral
835
- """
836
- query = request.args.get('q', '')
837
- if not query:
838
- return jsonify({"erro": "Parâmetro 'q' é obrigatório"}), 400
839
-
840
- token = get_fresh_token()
841
- if not token:
842
- return jsonify({"erro": "Não foi possível obter token de acesso"}), 503
843
-
844
- # Substitui placeholder no payload pelo termo
845
- payload_str = json.dumps(PAYLOAD_BUSCA_BASE).replace("__TERMO__", query)
846
- payload = json.loads(payload_str)
847
-
848
- headers = HEADERS.copy()
849
- headers['Cookie'] = f'aws-waf-token={token}'
850
-
851
- try:
852
- response = requests.post(URL_API, headers=headers, json=payload, verify=False, timeout=30)
853
- except Exception as e:
854
- return jsonify({"erro": f"Falha na comunicação com a API: {str(e)}"}), 502
855
-
856
- # Se recebeu 202, tenta renovar e faz segunda tentativa
857
- if response.status_code == 202:
858
- logger.info("Recebido status 202, forçando renovação do token...")
859
- token = get_fresh_token(force_refresh=True)
860
- if token:
861
- headers['Cookie'] = f'aws-waf-token={token}'
862
- try:
863
- response = requests.post(URL_API, headers=headers, json=payload, verify=False, timeout=30)
864
- except Exception as e:
865
- return jsonify({"erro": f"Falha na comunicação com a API após renovação: {str(e)}"}), 502
866
- else:
867
- return jsonify({"erro": "Não foi possível renovar o token"}), 503
868
-
869
- if response.status_code != 200:
870
- return jsonify({"erro": f"API retornou status {response.status_code}"}), response.status_code
871
-
872
- data = response.json()
873
- hits = data.get('result', {}).get('hits', {}).get('hits', [])
874
-
875
- resultados = []
876
- for hit in hits:
877
- source = hit.get('_source', {})
878
- item = {
879
- "id": source.get('id') or hit.get('_id'),
880
- "titulo": source.get('titulo'),
881
- "processo": source.get('processo_codigo_completo'),
882
- "relator": source.get('relator_processo_nome'),
883
- "orgao": source.get('orgao_julgador'),
884
- "data": source.get('julgamento_data'),
885
- "ementa": source.get('ementa_texto'),
886
- "url_documento": source.get('inteiro_teor_url'),
887
- "score": hit.get('_score')
888
- }
889
- # Remove campos nulos
890
- item = {k: v for k, v in item.items() if v is not None}
891
- resultados.append(item)
892
-
893
- return jsonify({
894
- "q": query,
895
- "total": data.get('result', {}).get('hits', {}).get('total', {}).get('value', 0),
896
- "resultados": resultados
897
  })
898
 
899
  if __name__ == '__main__':
@@ -904,13 +289,7 @@ if __name__ == '__main__':
904
  except:
905
  pass
906
  logger.info("="*50)
907
- logger.info("🚀 Iniciando aplicação STF Jurisprudência")
908
  logger.info("="*50)
909
- try:
910
- with sync_playwright() as p:
911
- p.chromium.launch(headless=True).close()
912
- logger.info("✅ Playwright pronto para uso")
913
- except Exception as e:
914
- logger.warning(f"⚠️ Playwright pode não estar configurado: {e}")
915
  port = int(os.environ.get('PORT', 7860))
916
  app.run(host='0.0.0.0', port=port, debug=False)
 
 
1
  import os
2
  import sys
3
  import json
 
7
  import urllib3
8
  from flask import Flask, request, jsonify, render_template_string
9
  from playwright.sync_api import sync_playwright
 
 
10
 
11
  # Suprimir warnings de SSL
12
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
 
17
 
18
  app = Flask(__name__)
19
 
20
+ # Constantes da API STF (endpoint simplificado)
21
+ STF_API_URL = "https://jurisprudencia.stf.jus.br/api/search/search" # ou use o /busca se preferir
22
+ # Mas vamos usar o mesmo endpoint /busca que já implementamos no próprio app
23
+ # Melhor: vamos usar o próprio endpoint /busca do nosso app, que por sua vez chama a API STF.
24
+ # Então não precisamos chamar a API STF diretamente aqui; o frontend fará fetch para /api/busca.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # Cache de token (ainda necessário para as chamadas à API STF)
27
+ token_cache = {"token": None, "expires_at": 0}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
 
 
 
 
 
29
  HEADERS = {
30
  "Accept": "application/json, text/plain, */*",
31
  "Content-Type": "application/json",
 
34
  "Origin": "https://jurisprudencia.stf.jus.br"
35
  }
36
 
 
 
37
  # ============================================
38
  # Funções de token e busca
39
  # ============================================
 
73
  logger.error(f"Erro ao obter token: {str(e)}")
74
  return None
75
 
76
+ def search_stf(query: str):
77
+ """Faz a busca na API do STF usando o payload completo"""
78
+ token = get_fresh_token()
79
  if not token:
80
+ return {"error": "Não foi possível obter token de acesso"}, 503
81
+
82
+ # Payload completo (pode ser simplificado se quiser, mas usaremos o mesmo)
83
+ # Vamos usar o payload básico que busca em todos os campos
84
+ payload = {
85
+ "query": {
86
+ "query_string": {
87
+ "query": query,
88
+ "default_operator": "AND",
89
+ "fields": ["ementa_texto", "acordao_ata", "decisao_texto", "titulo"],
90
+ "fuzziness": "AUTO:4,7"
91
+ }
92
+ },
93
+ "_source": ["id", "titulo", "ementa_texto", "processo_codigo_completo", "relator_processo_nome", "orgao_julgador", "julgamento_data", "inteiro_teor_url"],
94
+ "size": 20,
95
+ "sort": [{"julgamento_data": {"order": "desc"}}],
96
+ "track_total_hits": True
97
+ }
98
 
 
 
 
99
  headers = HEADERS.copy()
100
  headers['Cookie'] = f'aws-waf-token={token}'
101
+
 
102
  try:
103
+ response = requests.post(STF_API_URL, headers=headers, json=payload, verify=False, timeout=30)
104
  if response.status_code == 200:
105
+ return response.json()
106
+ elif response.status_code == 202:
107
+ # Tenta renovar o token uma vez
108
+ token = get_fresh_token(force_refresh=True)
109
+ if token:
110
+ headers['Cookie'] = f'aws-waf-token={token}'
111
+ response = requests.post(STF_API_URL, headers=headers, json=payload, verify=False, timeout=30)
112
+ if response.status_code == 200:
113
+ return response.json()
114
+ return {"error": "Token expirado e renovação falhou"}, 503
115
  else:
116
+ return {"error": f"API retornou status {response.status_code}"}, response.status_code
117
  except Exception as e:
118
+ return {"error": str(e)}, 502
119
 
120
  # ============================================
121
+ # Rota principal (HTML)
122
  # ============================================
123
+ @app.route('/')
124
+ def index():
125
+ return render_template_string("""
126
+ <!DOCTYPE html>
127
  <html>
128
  <head>
129
+ <title>Busca STF</title>
130
  <meta charset="utf-8">
131
  <meta name="viewport" content="width=device-width, initial-scale=1">
132
  <style>
133
+ body { font-family: 'Segoe UI', Roboto, system-ui, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
134
+ .container { background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
135
+ h1 { color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
136
+ .search-box { display: flex; gap: 10px; margin: 20px 0; }
137
+ .search-box input { flex: 1; padding: 12px; font-size: 16px; border: 1px solid #ddd; border-radius: 4px; }
138
+ .search-box button { padding: 12px 24px; background: #4CAF50; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }
139
+ .search-box button:hover { background: #45a049; }
140
+ .search-box button:disabled { background: #ccc; cursor: not-allowed; }
141
+ .result { border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin: 10px 0; background: #fafafa; }
142
+ .result h3 { margin: 0 0 8px; color: #1a0dab; }
143
+ .result .meta { color: #666; font-size: 14px; margin-bottom: 10px; }
144
+ .result .ementa { font-size: 14px; line-height: 1.6; color: #333; white-space: pre-wrap; }
145
+ .result a { color: #1a73e8; text-decoration: none; }
146
+ .result a:hover { text-decoration: underline; }
147
+ .loading { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(0,0,0,0.1); border-top-color: #4CAF50; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 10px; }
148
+ @keyframes spin { to { transform: rotate(360deg); } }
149
+ .error { color: #d93025; background: #fce8e6; border-left: 4px solid #ea4335; padding: 15px; margin: 10px 0; border-radius: 4px; }
 
 
 
150
  </style>
151
  </head>
152
  <body>
153
  <div class="container">
154
+ <h1>⚖️ Busca na Jurisprudência do STF</h1>
155
+ <div class="search-box">
156
+ <input type="text" id="query" placeholder="Digite sua busca (ex: dano moral mordida cachorro)" value="">
157
+ <button id="searchBtn" onclick="doSearch()">Buscar</button>
 
 
 
 
 
 
158
  </div>
159
+ <div id="loading" style="display: none;" class="loading"></div>
160
+ <div id="results"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  </div>
162
+
163
  <script>
164
+ async function doSearch() {
165
+ const query = document.getElementById('query').value.trim();
166
+ if (!query) {
167
+ alert('Digite algo para buscar');
168
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
170
+ const btn = document.getElementById('searchBtn');
171
+ const loading = document.getElementById('loading');
172
+ const resultsDiv = document.getElementById('results');
173
+
174
+ btn.disabled = true;
175
+ loading.style.display = 'inline-block';
176
+ resultsDiv.innerHTML = '';
177
+
178
  try {
179
+ const response = await fetch(`/api/busca?q=${encodeURIComponent(query)}`);
180
+ const data = await response.json();
181
+
182
+ if (data.error) {
183
+ resultsDiv.innerHTML = `<div class="error">Erro: ${data.error}</div>`;
 
 
184
  } else {
185
+ const resultados = data.resultados || [];
186
+ if (resultados.length === 0) {
187
+ resultsDiv.innerHTML = '<p>Nenhum resultado encontrado.</p>';
188
+ } else {
189
+ let html = `<p>Encontrados ${data.total || resultados.length} resultados.</p>`;
190
+ resultados.forEach(item => {
191
+ html += `
192
+ <div class="result">
193
+ <h3>${item.titulo || item.processo || 'Sem título'}</h3>
194
+ <div class="meta">
195
+ ${item.relator ? `Relator: ${item.relator} |` : ''}
196
+ ${item.orgao ? `${item.orgao} |` : ''}
197
+ ${item.data ? `Data: ${item.data} |` : ''}
198
+ ${item.id ? `ID: ${item.id}` : ''}
199
+ </div>
200
+ <div class="ementa">${(item.ementa || '').replace(/\\n/g, '<br>')}</div>
201
+ ${item.url_documento ? `<p><a href="${item.url_documento}" target="_blank">📄 Inteiro Teor</a></p>` : ''}
202
+ </div>
203
+ `;
204
+ });
205
+ resultsDiv.innerHTML = html;
206
+ }
207
  }
208
+ } catch (err) {
209
+ resultsDiv.innerHTML = `<div class="error">Erro na requisição: ${err.message}</div>`;
 
210
  } finally {
211
+ btn.disabled = false;
212
+ loading.style.display = 'none';
213
  }
214
  }
215
+
216
+ // Permitir busca ao pressionar Enter
217
+ document.getElementById('query').addEventListener('keypress', function(e) {
218
+ if (e.key === 'Enter') {
219
+ doSearch();
220
+ }
221
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  </script>
223
  </body>
224
+ </html>
225
+ """)
226
 
227
  # ============================================
228
+ # Endpoint /api/busca (retorna JSON)
229
  # ============================================
230
+ @app.route('/api/busca')
231
+ def busca_api():
232
+ query = request.args.get('q', '')
233
+ if not query:
234
+ return jsonify({"error": "Parâmetro 'q' obrigatório"}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
+ result = search_stf(query)
237
+ if isinstance(result, tuple):
238
+ return jsonify(result[0]), result[1]
239
+
240
+ # Converte para o formato simplificado de saída
241
+ hits = result.get('result', {}).get('hits', {}).get('hits', [])
242
+ resultados = []
243
+ for hit in hits:
244
+ src = hit.get('_source', {})
245
+ item = {
246
+ "id": src.get('id') or hit.get('_id'),
247
+ "titulo": src.get('titulo'),
248
+ "processo": src.get('processo_codigo_completo'),
249
+ "relator": src.get('relator_processo_nome'),
250
+ "orgao": src.get('orgao_julgador'),
251
+ "data": src.get('julgamento_data'),
252
+ "ementa": src.get('ementa_texto'),
253
+ "url_documento": src.get('inteiro_teor_url'),
254
+ "score": hit.get('_score')
255
+ }
256
+ # Remove campos vazios
257
+ item = {k: v for k, v in item.items() if v is not None}
258
+ resultados.append(item)
259
 
260
+ return jsonify({
261
+ "q": query,
262
+ "total": result.get('result', {}).get('hits', {}).get('total', {}).get('value', len(resultados)),
263
+ "resultados": resultados
264
+ })
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
+ # ============================================
267
+ # Endpoint de saúde (opcional)
268
+ # ============================================
269
  @app.route('/api/health', methods=['GET'])
270
  def health():
271
  playwright_status = False
 
275
  playwright_status = True
276
  except:
277
  pass
 
 
 
 
 
 
 
 
 
278
  return jsonify({
279
  "status": "healthy",
 
280
  "playwright_ready": playwright_status,
281
+ "token_cached": bool(token_cache["token"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  })
283
 
284
  if __name__ == '__main__':
 
289
  except:
290
  pass
291
  logger.info("="*50)
292
+ logger.info("🚀 Iniciando app simplificado STF")
293
  logger.info("="*50)
 
 
 
 
 
 
294
  port = int(os.environ.get('PORT', 7860))
295
  app.run(host='0.0.0.0', port=port, debug=False)