@danhachuel/pandanews 0.2.0 → 0.2.1
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 +6 -0
- package/backend/app/main.py +1 -1
- package/package.json +1 -1
- package/scripts/install.js +20 -0
- package/scripts/start.js +59 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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.01 (2026-10-23)
|
|
6
|
+
|
|
7
|
+
- **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
|
|
8
|
+
- **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
|
|
9
|
+
- `install`: correção idempotente dos caminhos absolutos do `pyvenv.cfg` após migração de runtime (venv movida herdava caminhos antigos)
|
|
10
|
+
|
|
5
11
|
## 0.2.00 (2026-10-23)
|
|
6
12
|
|
|
7
13
|
- **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
package/package.json
CHANGED
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
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
// antes de subir os servidores. A UI começa em PANDANEWS_UI_PORT (padrão 4030)
|
|
6
6
|
// e, se a porta estiver ocupada, tenta automaticamente a seguinte (4031, ...).
|
|
7
7
|
// Requisições /api/* são proxies para o backend em 127.0.0.1:8000.
|
|
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.
|
|
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';
|
|
11
15
|
import { join, extname } from 'node:path';
|
|
@@ -14,6 +18,9 @@ import { resolveHome, runInstall, PKG_ROOT } from './install.js';
|
|
|
14
18
|
const BACKEND_PORT = Number(process.env.PANDANEWS_API_PORT || 8000);
|
|
15
19
|
const UI_PORT_BASE = Number(process.env.PANDANEWS_UI_PORT || 4030);
|
|
16
20
|
const UI_PORT_MAX_TRIES = 20;
|
|
21
|
+
const BACKEND_MAX_RESTARTS = 5;
|
|
22
|
+
const BACKEND_RESTART_DELAY_MS = 3000;
|
|
23
|
+
const BACKEND_HEALTHY_UPTIME_MS = 60000;
|
|
17
24
|
|
|
18
25
|
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
26
|
|
|
@@ -141,41 +148,75 @@ async function openBrowser(url) {
|
|
|
141
148
|
}
|
|
142
149
|
}
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!existsSync(join(home, '.runtime', '.ready'))) {
|
|
147
|
-
console.log('Runtime não encontrado — instalando tudo (Python portátil + dependências)...');
|
|
148
|
-
await runInstall({});
|
|
149
|
-
}
|
|
150
|
-
if (!existsSync(venvBin(home))) {
|
|
151
|
-
console.log('Runtime Python ausente. Execute o comando com `install` primeiro:');
|
|
152
|
-
console.log(' npx --yes @danhachuel/pandanews install');
|
|
153
|
-
process.exitCode = 1;
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
151
|
+
let backendProc = null;
|
|
152
|
+
let backendRestarts = 0;
|
|
156
153
|
|
|
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).
|
|
156
|
+
function startBackend(home) {
|
|
157
157
|
console.log(`Iniciando backend FastAPI em http://127.0.0.1:${BACKEND_PORT} ...`);
|
|
158
158
|
const uvicorn = spawn(
|
|
159
159
|
venvBin(home),
|
|
160
160
|
['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(BACKEND_PORT)],
|
|
161
161
|
{
|
|
162
162
|
cwd: PKG_ROOT,
|
|
163
|
-
stdio: 'inherit',
|
|
163
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
164
164
|
env: { ...process.env, PANDANEWS_HOME: home },
|
|
165
165
|
},
|
|
166
166
|
);
|
|
167
|
+
backendProc = uvicorn;
|
|
168
|
+
|
|
169
|
+
const logPath = join(home, 'data', 'backend.log');
|
|
170
|
+
const logStream = createWriteStream(logPath, { flags: 'a' });
|
|
171
|
+
logStream.write(`\n--- ${new Date().toISOString()} — iniciando backend (pid ${uvicorn.pid}) ---\n`);
|
|
172
|
+
uvicorn.stdout.on('data', (chunk) => {
|
|
173
|
+
process.stdout.write(chunk);
|
|
174
|
+
logStream.write(chunk);
|
|
175
|
+
});
|
|
176
|
+
uvicorn.stderr.on('data', (chunk) => {
|
|
177
|
+
process.stderr.write(chunk);
|
|
178
|
+
logStream.write(chunk);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const startedAt = Date.now();
|
|
167
182
|
uvicorn.on('exit', (code) => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
logStream.end(`--- backend encerrou com código ${code} ---\n`);
|
|
184
|
+
if (code === 0 || code === null) {
|
|
185
|
+
return;
|
|
171
186
|
}
|
|
187
|
+
if (Date.now() - startedAt > BACKEND_HEALTHY_UPTIME_MS) {
|
|
188
|
+
backendRestarts = 0; // rodou saudável por tempo suficiente: zera o contador
|
|
189
|
+
}
|
|
190
|
+
backendRestarts += 1;
|
|
191
|
+
if (backendRestarts > BACKEND_MAX_RESTARTS) {
|
|
192
|
+
console.error(`Backend caiu ${backendRestarts - 1}x seguidas — desistindo.`);
|
|
193
|
+
console.error(`Log completo do backend: ${logPath}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
console.error(`Backend encerrou com código ${code} — reiniciando em 3s (tentativa ${backendRestarts}/${BACKEND_MAX_RESTARTS})...`);
|
|
197
|
+
setTimeout(() => startBackend(home), BACKEND_RESTART_DELAY_MS);
|
|
172
198
|
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function runStart() {
|
|
202
|
+
const home = resolveHome();
|
|
203
|
+
if (!existsSync(join(home, '.runtime', '.ready'))) {
|
|
204
|
+
console.log('Runtime não encontrado — instalando tudo (Python portátil + dependências)...');
|
|
205
|
+
await runInstall({});
|
|
206
|
+
}
|
|
207
|
+
if (!existsSync(venvBin(home))) {
|
|
208
|
+
console.log('Runtime Python ausente. Execute o comando com `install` primeiro:');
|
|
209
|
+
console.log(' npx --yes @danhachuel/pandanews install');
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
173
213
|
|
|
214
|
+
startBackend(home);
|
|
174
215
|
listenUi(UI_PORT_BASE, 1);
|
|
175
216
|
|
|
176
217
|
process.on('exit', () => {
|
|
177
218
|
if (uiServer) uiServer.close();
|
|
178
|
-
|
|
219
|
+
if (backendProc) backendProc.kill();
|
|
179
220
|
});
|
|
180
221
|
process.on('SIGINT', () => process.exit(0));
|
|
181
222
|
process.on('SIGTERM', () => process.exit(0));
|