@danhachuel/pandanews 0.2.1 → 0.2.2

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Todas as versões publicadas são registradas aqui antes do publish (regra em AGENTS.md). A etiqueta visual usa PATCH de dois dígitos (ex.: 0.1.00); o registry npm normaliza para a versão canônica (ex.: 0.1.0).
4
4
 
5
+ ## 0.2.02 (2026-10-24)
6
+
7
+ - **O sistema NUNCA abre o navegador**: o app exibe o endereço no terminal e o usuário decide quando abrir (dependência `open` removida)
8
+ - **Terminal limpo**: logs de acesso do uvicorn (stdout — `GET /health`, `GET /api/v1/*` etc.) vão apenas para `data/backend.log`; o console mantém somente stderr (banner de inicialização, WARNING/ERROR e tracebacks)
9
+ - **Fallback de porta também para o backend**: se 8000 estiver ocupada (ex.: outra instância), o backend sobe em 8001, 8002... e o proxy da UI segue automaticamente — junto com o fallback já existente da UI (4030, 4031...)
10
+ - Novo endpoint `GET /health` na raiz (200 OK) para clientes que esperam esse caminho — elimina os 404; o health canônico segue em `/api/v1/health`
11
+ - `data/backend.log` continua registrando TODAS as requisições (incluindo `/health`) para auditoria — o filtro no logger `uvicorn.access` (Python) foi deliberadamente evitado: removeria `/health` também do arquivo de auditoria; o silenciamento é feito apenas no console
12
+
5
13
  ## 0.2.01 (2026-10-23)
6
14
 
7
15
  - **Blindagem do backend**: saída do uvicorn espelhada no console e gravada em `<home>/data/backend.log` — queda anormal registra o motivo para diagnóstico
@@ -19,7 +19,7 @@ async def lifespan(_: FastAPI):
19
19
  yield
20
20
 
21
21
 
22
- app = FastAPI(title="PandaNews API", version="0.2.1", lifespan=lifespan)
22
+ app = FastAPI(title="PandaNews API", version="0.2.2", lifespan=lifespan)
23
23
 
24
24
  app.add_middleware(
25
25
  CORSMiddleware,
@@ -39,3 +39,9 @@ app.include_router(logs.router, prefix="/api/v1")
39
39
  @app.get("/api/v1/health")
40
40
  def health() -> dict:
41
41
  return {"status": "ok", "service": "pandanews", "version": app.version}
42
+
43
+
44
+ @app.get("/health")
45
+ def health_root() -> dict:
46
+ """Alias na raiz para clientes que esperam /health (extensões, monitores)."""
47
+ return {"status": "ok", "service": "pandanews", "version": app.version}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/pandanews",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",
@@ -43,7 +43,6 @@
43
43
  "extract-zip": "^2.0.1",
44
44
  "fs-extra": "^11.3.0",
45
45
  "node-fetch": "^3.3.2",
46
- "open": "^10.2.0",
47
46
  "ora": "^8.2.0"
48
47
  }
49
48
  }
package/scripts/start.js CHANGED
@@ -1,27 +1,30 @@
1
- // Inicia o PandaNews: backend FastAPI (127.0.0.1:8000) + UI (127.0.0.1:4030+).
1
+ // Inicia o PandaNews: backend FastAPI (127.0.0.1:8000+) + UI (127.0.0.1:4030+).
2
2
  //
3
- // Fluxo de um único comando (padrão Thunderbolt): se o runtime não estiver
4
- // provisionado, executa o install completo (Python portátil + dependências)
5
- // antes de subir os servidores. A UI começa em PANDANEWS_UI_PORT (padrão 4030)
6
- // e, se a porta estiver ocupada, tenta automaticamente a seguinte (4031, ...).
7
- // Requisições /api/* são proxies para o backend em 127.0.0.1:8000.
3
+ // Fluxo de um único comando: se o runtime não estiver provisionado, executa o
4
+ // install completo (Python portátil + dependências) antes de subir os
5
+ // servidores. Ambas as portas têm fallback automático (API: 8000, 8001, ...;
6
+ // UI: 4030, 4031, ...). O sistema NÃO abre o navegador: o endereço é exibido
7
+ // no terminal e o usuário decide quando abrir.
8
8
  //
9
- // Robustez: o output do backend é gravado em <home>/data/backend.log e, se o
10
- // processo morrer de forma anormal, o backend reinicia sozinho (até 5 vezes)
11
- // — a UI não cai junto.
9
+ // Robustez: o output do backend é gravado em <home>/data/backend.log (completo)
10
+ // e, se o processo morrer de forma anormal, o backend reinicia sozinho (até 5
11
+ // vezes) — a UI não cai junto. Logs de acesso não poluem o console.
12
12
  import { createWriteStream, existsSync, readFileSync } from 'node:fs';
13
13
  import { spawn } from 'node:child_process';
14
14
  import { createServer, request as httpRequest } from 'node:http';
15
+ import net from 'node:net';
15
16
  import { join, extname } from 'node:path';
16
17
  import { resolveHome, runInstall, PKG_ROOT } from './install.js';
17
18
 
18
- const BACKEND_PORT = Number(process.env.PANDANEWS_API_PORT || 8000);
19
+ const API_PORT_BASE = Number(process.env.PANDANEWS_API_PORT || 8000);
19
20
  const UI_PORT_BASE = Number(process.env.PANDANEWS_UI_PORT || 4030);
20
- const UI_PORT_MAX_TRIES = 20;
21
+ const PORT_MAX_TRIES = 20;
21
22
  const BACKEND_MAX_RESTARTS = 5;
22
23
  const BACKEND_RESTART_DELAY_MS = 3000;
23
24
  const BACKEND_HEALTHY_UPTIME_MS = 60000;
24
25
 
26
+ let activeBackendPort = API_PORT_BASE;
27
+
25
28
  const FAVICON_URI = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%230b1220'/%3E%3Ccircle cx='25' cy='28' r='13' fill='%23e6edf3'/%3E%3Ccircle cx='75' cy='28' r='13' fill='%23e6edf3'/%3E%3Ccircle cx='50' cy='52' r='30' fill='%23e6edf3'/%3E%3Cellipse cx='37' cy='48' rx='9' ry='11' fill='%230b1220' transform='rotate(-15 37 48)'/%3E%3Cellipse cx='63' cy='48' rx='9' ry='11' fill='%230b1220' transform='rotate(15 63 48)'/%3E%3Ccircle cx='38' cy='49' r='3' fill='%2322c55e'/%3E%3Ccircle cx='62' cy='49' r='3' fill='%2322c55e'/%3E%3Cellipse cx='50' cy='62' rx='5' ry='4' fill='%230b1220'/%3E%3C/svg%3E";
26
29
 
27
30
  function venvBin(home) {
@@ -59,14 +62,31 @@ function placeholderPage() {
59
62
  </html>`;
60
63
  }
61
64
 
62
- // Encaminha /api/* para o backend FastAPI em 127.0.0.1:8000.
65
+ // Testa se uma porta pode ser vinculada (probe real de bind).
66
+ function probePort(port) {
67
+ return new Promise((resolve) => {
68
+ const srv = net.createServer();
69
+ srv.once('error', () => resolve(false));
70
+ srv.once('listening', () => srv.close(() => resolve(true)));
71
+ srv.listen(port, '127.0.0.1');
72
+ });
73
+ }
74
+
75
+ async function findFreePort(base, maxTries) {
76
+ for (let port = base; port < base + maxTries; port++) {
77
+ if (await probePort(port)) return port;
78
+ }
79
+ throw new Error(`Nenhuma porta livre entre ${base} e ${base + maxTries - 1}`);
80
+ }
81
+
82
+ // Encaminha /api/* para o backend FastAPI (na porta ativa).
63
83
  function proxyApi(req, res) {
64
84
  const options = {
65
85
  hostname: '127.0.0.1',
66
- port: BACKEND_PORT,
86
+ port: activeBackendPort,
67
87
  path: req.url,
68
88
  method: req.method,
69
- headers: { ...req.headers, host: `127.0.0.1:${BACKEND_PORT}` },
89
+ headers: { ...req.headers, host: `127.0.0.1:${activeBackendPort}` },
70
90
  };
71
91
  const proxied = httpRequest(options, (upstream) => {
72
92
  res.writeHead(upstream.statusCode || 502, upstream.headers);
@@ -74,7 +94,7 @@ function proxyApi(req, res) {
74
94
  });
75
95
  proxied.on('error', () => {
76
96
  res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
77
- res.end(JSON.stringify({ detail: `Backend PandaNews não responde em 127.0.0.1:${BACKEND_PORT}` }));
97
+ res.end(JSON.stringify({ detail: `Backend PandaNews não responde em 127.0.0.1:${activeBackendPort}` }));
78
98
  });
79
99
  req.pipe(proxied);
80
100
  }
@@ -116,8 +136,8 @@ function createUiServer() {
116
136
  }
117
137
 
118
138
  function listenUi(port, attempt) {
119
- if (attempt > UI_PORT_MAX_TRIES) {
120
- console.error(`Nenhuma porta livre entre ${UI_PORT_BASE} e ${UI_PORT_BASE + UI_PORT_MAX_TRIES - 1}.`);
139
+ if (attempt > PORT_MAX_TRIES) {
140
+ console.error(`Nenhuma porta livre entre ${UI_PORT_BASE} e ${UI_PORT_BASE + PORT_MAX_TRIES - 1}.`);
121
141
  console.error('Encerre algum processo que as ocupe ou defina PANDANEWS_UI_PORT.');
122
142
  process.exit(1);
123
143
  }
@@ -133,31 +153,22 @@ function listenUi(port, attempt) {
133
153
  });
134
154
  server.listen(port, '127.0.0.1', () => {
135
155
  uiServer = server;
136
- console.log(`PandaNews ativo em http://127.0.0.1:${port}`);
156
+ console.log(`PandaNews ativo em http://127.0.0.1:${port} — abra no seu navegador.`);
137
157
  console.log('Pressione Ctrl+C para encerrar.');
138
- openBrowser(`http://127.0.0.1:${port}`);
139
158
  });
140
159
  }
141
160
 
142
- async function openBrowser(url) {
143
- try {
144
- const { default: open } = await import('open');
145
- await open(url);
146
- } catch {
147
- /* ambiente sem navegador (ex.: CI) */
148
- }
149
- }
150
-
151
161
  let backendProc = null;
152
162
  let backendRestarts = 0;
153
163
 
154
- // Sobe o backend com saída espelhada no console e em <home>/data/backend.log.
155
- // Se o processo morrer de forma anormal, reinicia sozinho (até 5 tentativas).
164
+ // Sobe o backend com saída completa em <home>/data/backend.log; o console
165
+ // mantém apenas stderr (banner, erros, tracebacks). Se o processo morrer de
166
+ // forma anormal, reinicia sozinho (até 5 tentativas).
156
167
  function startBackend(home) {
157
- console.log(`Iniciando backend FastAPI em http://127.0.0.1:${BACKEND_PORT} ...`);
168
+ console.log(`Iniciando backend FastAPI em http://127.0.0.1:${activeBackendPort} ...`);
158
169
  const uvicorn = spawn(
159
170
  venvBin(home),
160
- ['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(BACKEND_PORT)],
171
+ ['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(activeBackendPort)],
161
172
  {
162
173
  cwd: PKG_ROOT,
163
174
  stdio: ['inherit', 'pipe', 'pipe'],
@@ -168,9 +179,10 @@ function startBackend(home) {
168
179
 
169
180
  const logPath = join(home, 'data', 'backend.log');
170
181
  const logStream = createWriteStream(logPath, { flags: 'a' });
171
- logStream.write(`\n--- ${new Date().toISOString()} — iniciando backend (pid ${uvicorn.pid}) ---\n`);
182
+ logStream.write(`\n--- ${new Date().toISOString()} — iniciando backend (pid ${uvicorn.pid}, porta ${activeBackendPort}) ---\n`);
183
+ // Logs de acesso do uvicorn (stdout) vão SOMENTE para o arquivo: o terminal
184
+ // fica limpo. O stderr (banner, WARNING/ERROR e tracebacks) é visível.
172
185
  uvicorn.stdout.on('data', (chunk) => {
173
- process.stdout.write(chunk);
174
186
  logStream.write(chunk);
175
187
  });
176
188
  uvicorn.stderr.on('data', (chunk) => {
@@ -211,6 +223,12 @@ export async function runStart() {
211
223
  return;
212
224
  }
213
225
 
226
+ // Fallback de portas: API em 8000, 8001, ...; UI em 4030, 4031, ...
227
+ activeBackendPort = await findFreePort(API_PORT_BASE, PORT_MAX_TRIES);
228
+ if (activeBackendPort !== API_PORT_BASE) {
229
+ console.log(`Porta ${API_PORT_BASE} ocupada — backend usando ${activeBackendPort}.`);
230
+ }
231
+
214
232
  startBackend(home);
215
233
  listenUi(UI_PORT_BASE, 1);
216
234