@danhachuel/pandanews 0.4.10 → 0.5.0

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.
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
11
- <script type="module" crossorigin src="/assets/index-Iz5spM-E.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-ExoYg5_I.css">
11
+ <script type="module" crossorigin src="/assets/index-Dj6FuR5_.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-CM6InlUs.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/pandanews",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "description": "SaaS local de criação e automação de múltiplos blogs de notícias com IA (FastAPI + React + Tailwind).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/start.js CHANGED
@@ -39,6 +39,53 @@ function venvBin(home) {
39
39
  : join(home, '.runtime', 'venv', 'bin', 'python');
40
40
  }
41
41
 
42
+ // ------------------------------------------------------------------------
43
+ // JANELAS DE CONSOLE NO WINDOWS — porque subimos o python BASE e não o venv
44
+ //
45
+ // Medido neste repositório (spawn com detached + windowsHide, stdio no
46
+ // arquivo): `venv/Scripts/python.exe` das distribuições python-build-
47
+ // standalone é um REDIRECIONADOR — ele re-executa o interpretador real
48
+ // (`runtime/python/python.exe`) e esse NETO nasce SEM as flags de
49
+ // ocultação do pai. O Windows então aloca um console NOVO para o neto:
50
+ // 1 processo filho + 1 hospedeiro de console (conhost/WindowsTerminal)
51
+ // novos, ou seja, uma janela de terminal na cara do usuário.
52
+ //
53
+ // Subindo o interpretador base diretamente, com o site-packages do venv no
54
+ // PYTHONPATH, temos um ÚNICO processo: 0 filhos, 0 console novo. As
55
+ // dependências são exatamente as mesmas (verificado: import do app inteiro
56
+ // com o python base + PYTHONPATH).
57
+ //
58
+ // No Unix o `venv/bin/python` é um symlink para o base — sem redirector,
59
+ // sem neto —, então lá seguimos usando o venv normalmente.
60
+ // ------------------------------------------------------------------------
61
+ function baseBin(home) {
62
+ return process.platform === 'win32'
63
+ ? join(home, '.runtime', 'python', 'python.exe')
64
+ : null;
65
+ }
66
+
67
+ function venvSitePackages(home) {
68
+ return join(home, '.runtime', 'venv', 'Lib', 'site-packages');
69
+ }
70
+
71
+ function pythonBin(home) {
72
+ if (process.platform === 'win32') {
73
+ const base = baseBin(home);
74
+ if (base && existsSync(base)) return base;
75
+ }
76
+ return venvBin(home);
77
+ }
78
+
79
+ function pythonEnv(home) {
80
+ const env = { ...process.env, PANDANEWS_HOME: home, PYTHONUNBUFFERED: '1' };
81
+ if (process.platform === 'win32') {
82
+ // python base só enxerga as dependências do venv via PYTHONPATH
83
+ const site = venvSitePackages(home);
84
+ if (existsSync(site)) env.PYTHONPATH = site;
85
+ }
86
+ return env;
87
+ }
88
+
42
89
  const MIME = {
43
90
  '.html': 'text/html; charset=utf-8',
44
91
  '.js': 'text/javascript; charset=utf-8',
@@ -84,10 +131,16 @@ async function findFreePort(base, maxTries) {
84
131
  }
85
132
 
86
133
  // Encerra o processo por PID e (no Windows) a árvore de processos /T.
134
+ // Corre SEMPRE silenciosamente: windowsHide + stdio ignore — um taskkill
135
+ // que piscasse uma janela de console negaria a regra permanente do projeto.
87
136
  function encerrarPorPid(pid, motivo) {
88
137
  try {
89
138
  if (process.platform === 'win32') {
90
- spawnSync('taskkill', ['/pid', String(pid), '/f', '/t'], { windowsHide: true, detached: true });
139
+ spawnSync('taskkill', ['/pid', String(pid), '/f', '/t'], {
140
+ windowsHide: true, // CREATE_NO_WINDOW — sem tela preta
141
+ detached: true, // grupo isolado, imune a Ctrl+C do console
142
+ stdio: 'ignore', // taskkill não escreve no terminal do launcher
143
+ });
91
144
  } else {
92
145
  process.kill(pid, 'SIGTERM');
93
146
  }
@@ -223,6 +276,8 @@ async function esperarBackend(port) {
223
276
 
224
277
  // Sobe o uvicorn em grupo de processos próprio: detached (novo grupo no
225
278
  // Windows), windowsHide (sem janela preta) e stdio direto no arquivo de log.
279
+ // No Windows sobe o interpretador BASE + PYTHONPATH do venv — um único
280
+ // processo, sem o redirector que abria a janela de console (ver pythonBin).
226
281
  async function startBackend(home) {
227
282
  const port = await findFreePort(API_PORT_BASE, PORT_MAX_TRIES);
228
283
  if (port !== API_PORT_BASE) {
@@ -238,14 +293,14 @@ async function startBackend(home) {
238
293
 
239
294
  console.log(`Iniciando backend FastAPI em http://127.0.0.1:${port} ... (log: ${logPath})`);
240
295
  const python = spawn(
241
- venvBin(home),
296
+ pythonBin(home),
242
297
  ['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(port)],
243
298
  {
244
299
  cwd: PKG_ROOT,
245
300
  detached: true, // CREATE_NEW_PROCESS_GROUP no Windows — imune a Ctrl+C do console
246
301
  windowsHide: true, // sem janela de console fantasma
247
302
  stdio: ['ignore', logFd, logFd], // saída direto no arquivo (append) — não herda console
248
- env: { ...process.env, PANDANEWS_HOME: home, PYTHONUNBUFFERED: '1' },
303
+ env: pythonEnv(home),
249
304
  },
250
305
  );
251
306
  closeSync(logFd); // o filho tem sua própria cópia herdada
@@ -269,22 +324,67 @@ async function startBackend(home) {
269
324
  }
270
325
 
271
326
  // Watchdog de saúde: spawn destacado não emite eventos de exit confiáveis —
272
- // a porta é polidada a cada 10s e 3 falhas consecutivas reerguem o backend.
327
+ // a porta é polidada a cada 10s. FALHAS_PARA_REINICIAR leituras ruins
328
+ // consecutivas disparam um reinício, mas no MÁXIMO MAX_REINICIOS vezes:
329
+ // sem essa trava, um backend que não consegue subir de jeito nenhum (porta
330
+ // tomada por órfão, dependência quebrada) viraria um loop infinito de spawns
331
+ // — e cada spawn no Windows é candidato a uma janela de console.
332
+ const FALHAS_PARA_REINICIAR = 3;
333
+ const MAX_REINICIOS = 3;
334
+
273
335
  let falhasSaude = 0;
336
+ let reinicios = 0; // tentativas de reinício mal-sucedidas EM SEQUÊNCIA
337
+
274
338
  function iniciarWatchdog(home) {
275
- setInterval(async () => {
339
+ const timer = setInterval(async () => {
276
340
  if (await portaAberta(activeBackendPort)) {
341
+ //backend respondendo: zera as duas contagens (queda pontual não punida)
277
342
  falhasSaude = 0;
343
+ reinicios = 0;
278
344
  return;
279
345
  }
346
+
280
347
  falhasSaude += 1;
281
- if (falhasSaude >= 3) {
282
- console.error('Backend não responde — reiniciando...');
283
- falhasSaude = 0;
348
+ if (falhasSaude < FALHAS_PARA_REINICIAR) return;
349
+
350
+ // --- teto de tentativas atingido: para tudo em vez de loop infinito ---
351
+ if (reinicios >= MAX_REINICIOS) {
352
+ clearInterval(timer);
353
+ // encerra o backend zumbi por código, silenciosamente
354
+ if (backendPid) encerrarPorPid(backendPid, `Backend (PID ${backendPid}) encerrado após ${MAX_REINICIOS} falhas.`);
355
+ console.error('');
356
+ console.error(`O backend não respondeu em http://127.0.0.1:${activeBackendPort} após ${MAX_REINICIOS} tentativas de reinício.`);
357
+ console.error('O launcher está sendo encerrado para não entrar em loop infinito.');
358
+ console.error(`Diagnóstico completo em: ${join(home, 'data', 'backend.log')}`);
359
+ console.error('Se a porta estiver ocupada por outra instância, encerre-a ou defina PANDANEWS_API_PORT.');
360
+ process.exit(1);
361
+ return;
362
+ }
363
+
364
+ falhasSaude = 0;
365
+
366
+ // --- reaproveita backend vivo em vez de empilhar outra instância ---
367
+ // backend duplicado era o que fazia a porta "morrer" para o proxy e
368
+ // disparava reinício atrás de reinício.
369
+ const existente = await backendJaRodando(home);
370
+ if (existente) {
371
+ console.log(`Backend já em execução (PID ${existente.pid}) na porta ${existente.port} — reaproveitando (sem subir outra instância).`);
372
+ activeBackendPort = existente.port;
284
373
  backendPid = null;
285
- await startBackend(home);
374
+ return;
375
+ }
376
+
377
+ // --- reinício ---
378
+ reinicios += 1;
379
+ console.error(`Backend não responde — reiniciando... (tentativa ${reinicios} de ${MAX_REINICIOS})`);
380
+ backendPid = null;
381
+ const subiu = await startBackend(home);
382
+ if (subiu) {
383
+ // conseguiu subir: a próxima leitura saudável zera o contador
384
+ console.log('Backend reerguido pelo watchdog.');
286
385
  }
287
- }, 10000).unref();
386
+ }, 10000);
387
+ timer.unref();
288
388
  }
289
389
 
290
390
  // ---------------------------------------------------------------- execução