@danhachuel/pandanews 0.2.0 → 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 +14 -0
- package/backend/app/main.py +7 -1
- package/package.json +1 -2
- package/scripts/install.js +20 -0
- package/scripts/start.js +100 -41
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
|
|
13
|
+
## 0.2.01 (2026-10-23)
|
|
14
|
+
|
|
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
|
|
16
|
+
- **Auto-restart**: se o backend encerrar com código anormal (ex.: 103), reinicia sozinho em 3s (até 5 tentativas) — a UI não cai junto
|
|
17
|
+
- `install`: correção idempotente dos caminhos absolutos do `pyvenv.cfg` após migração de runtime (venv movida herdava caminhos antigos)
|
|
18
|
+
|
|
5
19
|
## 0.2.00 (2026-10-23)
|
|
6
20
|
|
|
7
21
|
- **UI React completa** (Fase 2): Vite + TypeScript + Tailwind, sidebar com 7 seções, topbar com seletor de blog e tema claro/escuro
|
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/install.js
CHANGED
|
@@ -121,6 +121,25 @@ function migrateLegacyRuntime(home) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// Venvs carregam caminhos absolutos no pyvenv.cfg; após migração, aponta-os
|
|
125
|
+
// para a localização atual do Python (idempotente).
|
|
126
|
+
function fixVenvPaths(home) {
|
|
127
|
+
const cfgPath = join(home, '.runtime', 'venv', 'pyvenv.cfg');
|
|
128
|
+
if (!existsSync(cfgPath)) return;
|
|
129
|
+
const newPythonDir = join(home, '.runtime', 'python');
|
|
130
|
+
const exeRel = process.platform === 'win32' ? 'python.exe' : join('bin', 'python3');
|
|
131
|
+
let content = readFileSync(cfgPath, 'utf8');
|
|
132
|
+
const fixed = content.replace(
|
|
133
|
+
/^(home|executable|base_executable)\s*=.*$/gim,
|
|
134
|
+
(_m, key) => `${key} = ${key.toLowerCase() === 'home' ? newPythonDir : join(newPythonDir, exeRel)}`,
|
|
135
|
+
);
|
|
136
|
+
if (fixed !== content) {
|
|
137
|
+
content = fixed;
|
|
138
|
+
writeFileSync(cfgPath, content);
|
|
139
|
+
console.log('Caminhos do virtualenv corrigidos (pyvenv.cfg).');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
124
143
|
async function downloadPython(home, platform) {
|
|
125
144
|
const url = pythonArchiveUrl(platform);
|
|
126
145
|
const runtimeDir = join(home, '.runtime');
|
|
@@ -201,6 +220,7 @@ export async function runInstall({ skipPythonDeps = false } = {}) {
|
|
|
201
220
|
} else {
|
|
202
221
|
console.log('Virtualenv já presente.');
|
|
203
222
|
}
|
|
223
|
+
fixVenvPaths(home);
|
|
204
224
|
|
|
205
225
|
// Dependências: só roda o pip quando o fingerprint muda.
|
|
206
226
|
const fpFile = join(home, '.runtime', '.deps-fingerprint');
|
package/scripts/start.js
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
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
|
-
//
|
|
8
|
-
|
|
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
|
+
//
|
|
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
|
+
import { createWriteStream, existsSync, readFileSync } from 'node:fs';
|
|
9
13
|
import { spawn } from 'node:child_process';
|
|
10
14
|
import { createServer, request as httpRequest } from 'node:http';
|
|
15
|
+
import net from 'node:net';
|
|
11
16
|
import { join, extname } from 'node:path';
|
|
12
17
|
import { resolveHome, runInstall, PKG_ROOT } from './install.js';
|
|
13
18
|
|
|
14
|
-
const
|
|
19
|
+
const API_PORT_BASE = Number(process.env.PANDANEWS_API_PORT || 8000);
|
|
15
20
|
const UI_PORT_BASE = Number(process.env.PANDANEWS_UI_PORT || 4030);
|
|
16
|
-
const
|
|
21
|
+
const PORT_MAX_TRIES = 20;
|
|
22
|
+
const BACKEND_MAX_RESTARTS = 5;
|
|
23
|
+
const BACKEND_RESTART_DELAY_MS = 3000;
|
|
24
|
+
const BACKEND_HEALTHY_UPTIME_MS = 60000;
|
|
25
|
+
|
|
26
|
+
let activeBackendPort = API_PORT_BASE;
|
|
17
27
|
|
|
18
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";
|
|
19
29
|
|
|
@@ -52,14 +62,31 @@ function placeholderPage() {
|
|
|
52
62
|
</html>`;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
//
|
|
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).
|
|
56
83
|
function proxyApi(req, res) {
|
|
57
84
|
const options = {
|
|
58
85
|
hostname: '127.0.0.1',
|
|
59
|
-
port:
|
|
86
|
+
port: activeBackendPort,
|
|
60
87
|
path: req.url,
|
|
61
88
|
method: req.method,
|
|
62
|
-
headers: { ...req.headers, host: `127.0.0.1:${
|
|
89
|
+
headers: { ...req.headers, host: `127.0.0.1:${activeBackendPort}` },
|
|
63
90
|
};
|
|
64
91
|
const proxied = httpRequest(options, (upstream) => {
|
|
65
92
|
res.writeHead(upstream.statusCode || 502, upstream.headers);
|
|
@@ -67,7 +94,7 @@ function proxyApi(req, res) {
|
|
|
67
94
|
});
|
|
68
95
|
proxied.on('error', () => {
|
|
69
96
|
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
70
|
-
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}` }));
|
|
71
98
|
});
|
|
72
99
|
req.pipe(proxied);
|
|
73
100
|
}
|
|
@@ -109,8 +136,8 @@ function createUiServer() {
|
|
|
109
136
|
}
|
|
110
137
|
|
|
111
138
|
function listenUi(port, attempt) {
|
|
112
|
-
if (attempt >
|
|
113
|
-
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}.`);
|
|
114
141
|
console.error('Encerre algum processo que as ocupe ou defina PANDANEWS_UI_PORT.');
|
|
115
142
|
process.exit(1);
|
|
116
143
|
}
|
|
@@ -126,19 +153,61 @@ function listenUi(port, attempt) {
|
|
|
126
153
|
});
|
|
127
154
|
server.listen(port, '127.0.0.1', () => {
|
|
128
155
|
uiServer = server;
|
|
129
|
-
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.`);
|
|
130
157
|
console.log('Pressione Ctrl+C para encerrar.');
|
|
131
|
-
openBrowser(`http://127.0.0.1:${port}`);
|
|
132
158
|
});
|
|
133
159
|
}
|
|
134
160
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
let backendProc = null;
|
|
162
|
+
let backendRestarts = 0;
|
|
163
|
+
|
|
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).
|
|
167
|
+
function startBackend(home) {
|
|
168
|
+
console.log(`Iniciando backend FastAPI em http://127.0.0.1:${activeBackendPort} ...`);
|
|
169
|
+
const uvicorn = spawn(
|
|
170
|
+
venvBin(home),
|
|
171
|
+
['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(activeBackendPort)],
|
|
172
|
+
{
|
|
173
|
+
cwd: PKG_ROOT,
|
|
174
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
175
|
+
env: { ...process.env, PANDANEWS_HOME: home },
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
backendProc = uvicorn;
|
|
179
|
+
|
|
180
|
+
const logPath = join(home, 'data', 'backend.log');
|
|
181
|
+
const logStream = createWriteStream(logPath, { flags: 'a' });
|
|
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.
|
|
185
|
+
uvicorn.stdout.on('data', (chunk) => {
|
|
186
|
+
logStream.write(chunk);
|
|
187
|
+
});
|
|
188
|
+
uvicorn.stderr.on('data', (chunk) => {
|
|
189
|
+
process.stderr.write(chunk);
|
|
190
|
+
logStream.write(chunk);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const startedAt = Date.now();
|
|
194
|
+
uvicorn.on('exit', (code) => {
|
|
195
|
+
logStream.end(`--- backend encerrou com código ${code} ---\n`);
|
|
196
|
+
if (code === 0 || code === null) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (Date.now() - startedAt > BACKEND_HEALTHY_UPTIME_MS) {
|
|
200
|
+
backendRestarts = 0; // rodou saudável por tempo suficiente: zera o contador
|
|
201
|
+
}
|
|
202
|
+
backendRestarts += 1;
|
|
203
|
+
if (backendRestarts > BACKEND_MAX_RESTARTS) {
|
|
204
|
+
console.error(`Backend caiu ${backendRestarts - 1}x seguidas — desistindo.`);
|
|
205
|
+
console.error(`Log completo do backend: ${logPath}`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
console.error(`Backend encerrou com código ${code} — reiniciando em 3s (tentativa ${backendRestarts}/${BACKEND_MAX_RESTARTS})...`);
|
|
209
|
+
setTimeout(() => startBackend(home), BACKEND_RESTART_DELAY_MS);
|
|
210
|
+
});
|
|
142
211
|
}
|
|
143
212
|
|
|
144
213
|
export async function runStart() {
|
|
@@ -154,28 +223,18 @@ export async function runStart() {
|
|
|
154
223
|
return;
|
|
155
224
|
}
|
|
156
225
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
cwd: PKG_ROOT,
|
|
163
|
-
stdio: 'inherit',
|
|
164
|
-
env: { ...process.env, PANDANEWS_HOME: home },
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
uvicorn.on('exit', (code) => {
|
|
168
|
-
if (code !== 0 && code !== null) {
|
|
169
|
-
console.error(`Backend encerrou com código ${code}.`);
|
|
170
|
-
process.exit(code ?? 1);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
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
|
+
}
|
|
173
231
|
|
|
232
|
+
startBackend(home);
|
|
174
233
|
listenUi(UI_PORT_BASE, 1);
|
|
175
234
|
|
|
176
235
|
process.on('exit', () => {
|
|
177
236
|
if (uiServer) uiServer.close();
|
|
178
|
-
|
|
237
|
+
if (backendProc) backendProc.kill();
|
|
179
238
|
});
|
|
180
239
|
process.on('SIGINT', () => process.exit(0));
|
|
181
240
|
process.on('SIGTERM', () => process.exit(0));
|