@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 +8 -0
- package/backend/app/main.py +7 -1
- package/package.json +1 -2
- package/scripts/start.js +52 -34
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
|
package/backend/app/main.py
CHANGED
|
@@ -19,7 +19,7 @@ async def lifespan(_: FastAPI):
|
|
|
19
19
|
yield
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
app = FastAPI(title="PandaNews API", version="0.2.
|
|
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.
|
|
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
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
10
|
-
// processo morrer de forma anormal, o backend reinicia sozinho (até 5
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
86
|
+
port: activeBackendPort,
|
|
67
87
|
path: req.url,
|
|
68
88
|
method: req.method,
|
|
69
|
-
headers: { ...req.headers, host: `127.0.0.1:${
|
|
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:${
|
|
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 >
|
|
120
|
-
console.error(`Nenhuma porta livre entre ${UI_PORT_BASE} e ${UI_PORT_BASE +
|
|
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
|
|
155
|
-
//
|
|
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:${
|
|
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(
|
|
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
|
|