@danhachuel/pandanews 0.1.3 → 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 +19 -0
- package/README.md +5 -1
- package/backend/app/main.py +23 -4
- package/backend/app/routers/blogs.py +87 -0
- package/backend/app/routers/configuracoes.py +58 -0
- package/backend/app/routers/dashboard.py +59 -0
- package/backend/app/routers/llm.py +16 -0
- package/backend/app/routers/logs.py +38 -0
- package/backend/app/routers/noticias.py +65 -0
- package/backend/app/schemas.py +71 -1
- package/backend/core/config.py +42 -1
- package/backend/core/database.py +86 -4
- package/backend/core/llm_router.py +89 -4
- package/frontend/dist/assets/index-BaPdaYIz.css +1 -0
- package/frontend/dist/assets/index-D0e-vr7T.js +70 -0
- package/frontend/dist/favicon.svg +11 -0
- package/frontend/dist/index.html +17 -0
- package/package.json +1 -1
- package/scripts/build.js +12 -3
- package/scripts/install.js +20 -0
- package/scripts/start.js +112 -21
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<rect width="100" height="100" rx="20" fill="#0b1220"/>
|
|
3
|
+
<circle cx="25" cy="28" r="13" fill="#e6edf3"/>
|
|
4
|
+
<circle cx="75" cy="28" r="13" fill="#e6edf3"/>
|
|
5
|
+
<circle cx="50" cy="52" r="30" fill="#e6edf3"/>
|
|
6
|
+
<ellipse cx="37" cy="48" rx="9" ry="11" fill="#0b1220" transform="rotate(-15 37 48)"/>
|
|
7
|
+
<ellipse cx="63" cy="48" rx="9" ry="11" fill="#0b1220" transform="rotate(15 63 48)"/>
|
|
8
|
+
<circle cx="38" cy="49" r="3" fill="#22c55e"/>
|
|
9
|
+
<circle cx="62" cy="49" r="3" fill="#22c55e"/>
|
|
10
|
+
<ellipse cx="50" cy="62" rx="5" ry="4" fill="#0b1220"/>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>PandaNews — Automação de blogs de notícias com IA</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
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-D0e-vr7T.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BaPdaYIz.css">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="root"></div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/package.json
CHANGED
package/scripts/build.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Compila o frontend de desenvolvimento (frontend/ -> frontend/dist).
|
|
2
2
|
// A UI React + Vite + Tailwind é inicializada na Fase 2 do plano.
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -16,10 +16,19 @@ export async function runBuild() {
|
|
|
16
16
|
}
|
|
17
17
|
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
18
18
|
console.log('Instalando dependências do frontend...');
|
|
19
|
-
|
|
19
|
+
// shell: true — obrigatório para .cmd no Windows (Node >= 18)
|
|
20
|
+
const install = spawnSync(npmCmd, ['install'], { cwd: frontendDir, stdio: 'inherit', shell: true });
|
|
20
21
|
if (install.status !== 0) throw new Error('Falha ao instalar dependências do frontend.');
|
|
21
22
|
console.log('Compilando o frontend...');
|
|
22
|
-
const build = spawnSync(npmCmd, ['run', 'build'], { cwd: frontendDir, stdio: 'inherit' });
|
|
23
|
+
const build = spawnSync(npmCmd, ['run', 'build'], { cwd: frontendDir, stdio: 'inherit', shell: true });
|
|
23
24
|
if (build.status !== 0) throw new Error('Falha ao compilar o frontend.');
|
|
24
25
|
console.log('Frontend compilado em frontend/dist.');
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
// Entry point para execução direta: node scripts/build.js
|
|
29
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
30
|
+
runBuild().catch((err) => {
|
|
31
|
+
console.error(`pandanews build: ${err && err.message}`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
});
|
|
34
|
+
}
|
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
|
@@ -4,15 +4,25 @@
|
|
|
4
4
|
// provisionado, executa o install completo (Python portátil + dependências)
|
|
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
|
+
//
|
|
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';
|
|
8
13
|
import { spawn } from 'node:child_process';
|
|
9
|
-
import { createServer } from 'node:http';
|
|
14
|
+
import { createServer, request as httpRequest } from 'node:http';
|
|
10
15
|
import { join, extname } from 'node:path';
|
|
11
16
|
import { resolveHome, runInstall, PKG_ROOT } from './install.js';
|
|
12
17
|
|
|
13
18
|
const BACKEND_PORT = Number(process.env.PANDANEWS_API_PORT || 8000);
|
|
14
19
|
const UI_PORT_BASE = Number(process.env.PANDANEWS_UI_PORT || 4030);
|
|
15
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;
|
|
24
|
+
|
|
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";
|
|
16
26
|
|
|
17
27
|
function venvBin(home) {
|
|
18
28
|
return process.platform === 'win32'
|
|
@@ -34,27 +44,70 @@ const MIME = {
|
|
|
34
44
|
function placeholderPage() {
|
|
35
45
|
return `<!DOCTYPE html>
|
|
36
46
|
<html lang="pt-BR">
|
|
37
|
-
<head
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="utf-8">
|
|
49
|
+
<title>PandaNews</title>
|
|
50
|
+
<link rel="icon" href="${FAVICON_URI}">
|
|
51
|
+
</head>
|
|
38
52
|
<body style="font-family: Inter, system-ui, sans-serif; background: #0b1220; color: #e6edf3; display: flex; min-height: 100vh; align-items: center; justify-content: center;">
|
|
39
53
|
<div style="text-align: center;">
|
|
40
54
|
<h1 style="font-size: 3rem; margin: 0;">🐼 PandaNews</h1>
|
|
41
|
-
<p style="color: #9aa7b5;">Backend ativo em <a style="color: #22c55e;" href="
|
|
42
|
-
<p style="color: #9aa7b5;">A UI React completa chega na versão 0.2.00
|
|
55
|
+
<p style="color: #9aa7b5;">Backend ativo em <a style="color: #22c55e;" href="/api/v1/health">/api/v1/health</a></p>
|
|
56
|
+
<p style="color: #9aa7b5;">A UI React completa chega na versão 0.2.00.</p>
|
|
43
57
|
</div>
|
|
44
58
|
</body>
|
|
45
59
|
</html>`;
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
// Encaminha /api/* para o backend FastAPI em 127.0.0.1:8000.
|
|
63
|
+
function proxyApi(req, res) {
|
|
64
|
+
const options = {
|
|
65
|
+
hostname: '127.0.0.1',
|
|
66
|
+
port: BACKEND_PORT,
|
|
67
|
+
path: req.url,
|
|
68
|
+
method: req.method,
|
|
69
|
+
headers: { ...req.headers, host: `127.0.0.1:${BACKEND_PORT}` },
|
|
70
|
+
};
|
|
71
|
+
const proxied = httpRequest(options, (upstream) => {
|
|
72
|
+
res.writeHead(upstream.statusCode || 502, upstream.headers);
|
|
73
|
+
upstream.pipe(res);
|
|
74
|
+
});
|
|
75
|
+
proxied.on('error', () => {
|
|
76
|
+
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}` }));
|
|
78
|
+
});
|
|
79
|
+
req.pipe(proxied);
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
let uiServer = null;
|
|
49
83
|
|
|
50
84
|
function createUiServer() {
|
|
51
85
|
const distDir = join(PKG_ROOT, 'frontend', 'dist');
|
|
52
86
|
return createServer((req, res) => {
|
|
87
|
+
if (req.url.startsWith('/api')) {
|
|
88
|
+
proxyApi(req, res);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
53
91
|
const url = req.url.split('?')[0];
|
|
54
92
|
const filePath = join(distDir, url === '/' ? 'index.html' : url.slice(1));
|
|
55
93
|
if (!existsSync(filePath)) {
|
|
56
|
-
|
|
57
|
-
|
|
94
|
+
// SPA (BrowserRouter): rotas sem extensão caem no index.html quando o
|
|
95
|
+
// build existe; sem build, mostra o placeholder.
|
|
96
|
+
const indexHtml = join(distDir, 'index.html');
|
|
97
|
+
const hasBuild = existsSync(indexHtml);
|
|
98
|
+
const isAsset = extname(url) !== '';
|
|
99
|
+
if (hasBuild && !isAsset) {
|
|
100
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
101
|
+
res.end(readFileSync(indexHtml));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!hasBuild) {
|
|
105
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
106
|
+
res.end(placeholderPage());
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
110
|
+
res.end('Not found');
|
|
58
111
|
return;
|
|
59
112
|
}
|
|
60
113
|
res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] || 'application/octet-stream' });
|
|
@@ -95,6 +148,56 @@ async function openBrowser(url) {
|
|
|
95
148
|
}
|
|
96
149
|
}
|
|
97
150
|
|
|
151
|
+
let backendProc = null;
|
|
152
|
+
let backendRestarts = 0;
|
|
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
|
+
console.log(`Iniciando backend FastAPI em http://127.0.0.1:${BACKEND_PORT} ...`);
|
|
158
|
+
const uvicorn = spawn(
|
|
159
|
+
venvBin(home),
|
|
160
|
+
['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(BACKEND_PORT)],
|
|
161
|
+
{
|
|
162
|
+
cwd: PKG_ROOT,
|
|
163
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
164
|
+
env: { ...process.env, PANDANEWS_HOME: home },
|
|
165
|
+
},
|
|
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();
|
|
182
|
+
uvicorn.on('exit', (code) => {
|
|
183
|
+
logStream.end(`--- backend encerrou com código ${code} ---\n`);
|
|
184
|
+
if (code === 0 || code === null) {
|
|
185
|
+
return;
|
|
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);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
98
201
|
export async function runStart() {
|
|
99
202
|
const home = resolveHome();
|
|
100
203
|
if (!existsSync(join(home, '.runtime', '.ready'))) {
|
|
@@ -108,24 +211,12 @@ export async function runStart() {
|
|
|
108
211
|
return;
|
|
109
212
|
}
|
|
110
213
|
|
|
111
|
-
|
|
112
|
-
const uvicorn = spawn(
|
|
113
|
-
venvBin(home),
|
|
114
|
-
['-m', 'uvicorn', 'backend.app.main:app', '--host', '127.0.0.1', '--port', String(BACKEND_PORT)],
|
|
115
|
-
{ cwd: PKG_ROOT, stdio: 'inherit' },
|
|
116
|
-
);
|
|
117
|
-
uvicorn.on('exit', (code) => {
|
|
118
|
-
if (code !== 0 && code !== null) {
|
|
119
|
-
console.error(`Backend encerrou com código ${code}.`);
|
|
120
|
-
process.exit(code ?? 1);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
214
|
+
startBackend(home);
|
|
124
215
|
listenUi(UI_PORT_BASE, 1);
|
|
125
216
|
|
|
126
217
|
process.on('exit', () => {
|
|
127
218
|
if (uiServer) uiServer.close();
|
|
128
|
-
|
|
219
|
+
if (backendProc) backendProc.kill();
|
|
129
220
|
});
|
|
130
221
|
process.on('SIGINT', () => process.exit(0));
|
|
131
222
|
process.on('SIGTERM', () => process.exit(0));
|