feat: add production deployment and runtime safeguards
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8ce2fcdfea
commit
a9ef4d776d
@@ -0,0 +1,76 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { paths } from '../src/env.mjs';
|
||||
|
||||
export async function createBackup({ dataDir = paths.data, now = () => new Date(), retention = 14 } = {}) {
|
||||
const stamp = now().toISOString().slice(0, 10);
|
||||
const backupsDir = path.join(dataDir, 'backups');
|
||||
const target = path.join(backupsDir, `maquina-pessoal-${stamp}`);
|
||||
const dbPath = dataDir === paths.data ? paths.db : path.join(dataDir, 'maquina-pessoal.db');
|
||||
const authPath = dataDir === paths.data ? paths.whatsappAuth : path.join(dataDir, 'runtime', 'whatsapp-auth');
|
||||
|
||||
await fs.mkdir(target, { recursive: true });
|
||||
await copyDatabaseBackup(dbPath, path.join(target, 'maquina-pessoal.db'));
|
||||
await fs.cp(authPath, path.join(target, 'whatsapp-auth'), { recursive: true });
|
||||
await pruneBackups(backupsDir, retention);
|
||||
|
||||
return { file: target };
|
||||
}
|
||||
|
||||
async function copyDatabaseBackup(source, destination) {
|
||||
try {
|
||||
const db = new DatabaseSync(source, { readOnly: true });
|
||||
try {
|
||||
db.exec(`VACUUM INTO ${sqlString(destination)};`);
|
||||
return;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch {
|
||||
await copyDatabaseFiles(source, destination);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDatabaseFiles(source, destination) {
|
||||
await fs.copyFile(source, destination);
|
||||
|
||||
for (const suffix of ['-wal', '-shm']) {
|
||||
try {
|
||||
await fs.copyFile(`${source}${suffix}`, `${destination}${suffix}`);
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sqlString(value) {
|
||||
return `'${String(value).replaceAll("'", "''")}'`;
|
||||
}
|
||||
|
||||
async function pruneBackups(backupsDir, retention) {
|
||||
const keep = Math.max(0, retention);
|
||||
const entries = await fs.readdir(backupsDir, { withFileTypes: true });
|
||||
const backupDirs = entries
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith('maquina-pessoal-'))
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
|
||||
for (const name of backupDirs.slice(0, Math.max(0, backupDirs.length - keep))) {
|
||||
await fs.rm(path.join(backupsDir, name), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function isCliEntryPoint(moduleUrl = import.meta.url, argvPath = process.argv[1]) {
|
||||
return Boolean(argvPath) && moduleUrl === pathToFileURL(argvPath).href;
|
||||
}
|
||||
|
||||
if (isCliEntryPoint()) {
|
||||
createBackup().then((result) => console.log(`backup criado: ${result.file}`)).catch((error) => {
|
||||
console.error(`backup falhou: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
+22
-7
@@ -1,4 +1,10 @@
|
||||
import { loadDotEnv, APP_PORT, paths } from '../src/env.mjs';
|
||||
import { loadDotEnv, APP_HOST, APP_PORT, paths, telegramConfig } from '../src/env.mjs';
|
||||
import { createTelegramAlerter } from '../src/alerts/telegram.mjs';
|
||||
import {
|
||||
hasCampaignsPausedByRestart,
|
||||
notifyStartupState,
|
||||
startInitialWhatsAppReconnect
|
||||
} from '../src/alerts/runtime-alerts.mjs';
|
||||
import { openDb, ensureSeeds, recoverPendingOutreach, updateOutreachMessageStatus } from '../src/db.mjs';
|
||||
import { createJobManager, recoverOrphans } from '../src/jobs/manager.mjs';
|
||||
import { createAppServer } from '../src/server/index.mjs';
|
||||
@@ -18,8 +24,12 @@ await ensureSeeds(db, { log });
|
||||
recoverOrphans(db, log);
|
||||
recoverPendingOutreach(db);
|
||||
pauseCampaignsAfterRestart(db);
|
||||
const campaignsPausedByRestart = hasCampaignsPausedByRestart(db);
|
||||
recoverReservedCampaignQueue(db);
|
||||
|
||||
const tg = telegramConfig();
|
||||
const alerter = createTelegramAlerter({ enabled: tg.enabled, token: tg.botToken, chatId: tg.chatId, log });
|
||||
|
||||
const jobs = createJobManager({ db });
|
||||
const whatsapp = createWhatsAppManager({
|
||||
authDir: paths.whatsappAuth,
|
||||
@@ -27,16 +37,21 @@ const whatsapp = createWhatsAppManager({
|
||||
onMessageStatus: (event) => updateOutreachMessageStatus(db, event),
|
||||
onInboundMessage: (event) => recordInboundReply(db, event)
|
||||
});
|
||||
if (await hasWhatsAppSession(paths.whatsappAuth)) {
|
||||
whatsapp.connect().catch((error) => log(`whatsapp: reconexao inicial falhou: ${error.message}`));
|
||||
}
|
||||
void notifyStartupState({ alerter, whatsappStatus: whatsapp.getStatus().state, campaignsPausedByRestart, log });
|
||||
await startInitialWhatsAppReconnect({
|
||||
hasSession: () => hasWhatsAppSession(paths.whatsappAuth),
|
||||
whatsapp,
|
||||
alerter,
|
||||
log
|
||||
});
|
||||
const server = createAppServer({ db, jobs, whatsapp, log });
|
||||
const stopScheduler = startOutreachScheduler({ db, whatsapp, log });
|
||||
|
||||
const port = APP_PORT;
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
log(`Maquina Comercial Pessoal rodando em http://127.0.0.1:${port}`);
|
||||
log('somente este computador tem acesso (127.0.0.1). Ctrl+C para encerrar.');
|
||||
server.listen(port, APP_HOST, () => {
|
||||
log(`Maquina Comercial Pessoal rodando em http://${APP_HOST}:${port}`);
|
||||
if (APP_HOST === '127.0.0.1') log('somente este computador tem acesso (127.0.0.1). Ctrl+C para encerrar.');
|
||||
else log('modo container/rede: proteja o acesso com proxy HTTPS e senha forte.');
|
||||
});
|
||||
|
||||
let shuttingDown = false;
|
||||
|
||||
Reference in New Issue
Block a user