chore: import maquina-comercial-pessoal history via subtree
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: maquina-comercial-pessoal git-subtree-mainline:f5af038e19git-subtree-split:a9ef4d776d
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { loadDotEnv, paths } from '../src/env.mjs';
|
||||
import { downloadRfbDataset } from '../src/extract/rfb-downloader.mjs';
|
||||
|
||||
await loadDotEnv();
|
||||
|
||||
downloadRfbDataset({
|
||||
rawDir: paths.rfbRaw,
|
||||
onProgress: (progress) => {
|
||||
const pct = progress.pct == null ? '' : `${progress.pct}% `;
|
||||
console.log(`[RFB] ${pct}${progress.message}`);
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.cancelled) process.exitCode = 2;
|
||||
else console.log(`[RFB] snapshot ${result.snapshot} pronto: ${result.downloaded.length} baixados, ${result.skipped.length} reutilizados.`);
|
||||
}).catch((error) => {
|
||||
console.error(`[RFB] erro: ${error.stack || error.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$target = Join-Path $root "Maquina-Comercial-Pessoal.exe"
|
||||
$source = Join-Path $root "launcher\Program.cs"
|
||||
$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
|
||||
$sdk = if ($dotnet) { & dotnet --list-sdks 2>$null } else { $null }
|
||||
|
||||
if ($sdk) {
|
||||
$project = Join-Path $root "launcher\Maquina-Comercial-Pessoal.csproj"
|
||||
$publish = Join-Path $root "launcher\publish"
|
||||
dotnet publish $project -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true -o $publish
|
||||
$exe = Join-Path $publish "Maquina-Comercial-Pessoal.exe"
|
||||
Copy-Item -LiteralPath $exe -Destination $target -Force
|
||||
} else {
|
||||
$csc = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
|
||||
if (-not (Test-Path -LiteralPath $csc)) {
|
||||
throw "Compilador C# nao encontrado: $csc"
|
||||
}
|
||||
& $csc /nologo /target:exe /out:$target $source
|
||||
}
|
||||
|
||||
Write-Host "Executavel gerado em: $target"
|
||||
@@ -0,0 +1,123 @@
|
||||
import { loadDotEnv } from '../src/env.mjs';
|
||||
import { openDb, ensureSeeds } from '../src/db.mjs';
|
||||
import { runCapture, createCaptureRun } from '../src/capture/runner.mjs';
|
||||
import { describeSources } from '../src/sources/index.mjs';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createJobLease } from '../src/jobs/lease.mjs';
|
||||
|
||||
// CLI de captura (uso avancado / testes). A interface principal e o app:
|
||||
// node scripts/servidor.mjs (ou o Maquina-Comercial-Pessoal.exe)
|
||||
//
|
||||
// Exemplos:
|
||||
// node scripts/capturar-leads.mjs --sources=mock
|
||||
// node scripts/capturar-leads.mjs --sources=cnpj-base --uf=BA --cities="Santo Antonio de Jesus"
|
||||
// node scripts/capturar-leads.mjs --sources=cnpj-base,overpass --uf=BA --cities=all --segments=all
|
||||
|
||||
await loadDotEnv();
|
||||
|
||||
const args = new Map(
|
||||
process.argv.slice(2).map((arg) => {
|
||||
const [key, value = 'true'] = arg.replace(/^--/, '').split('=');
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
|
||||
function usage() {
|
||||
const fontes = describeSources().map((s) => ` ${s.id} — ${s.label}${s.disponivel ? '' : ` (indisponivel: ${s.motivo})`}`);
|
||||
return [
|
||||
'Uso: node scripts/capturar-leads.mjs --sources=<ids> [opcoes]',
|
||||
'',
|
||||
'Opcoes:',
|
||||
' --sources=cnpj-base,overpass fontes separadas por virgula (ou "mock")',
|
||||
' --uf=BA estado alvo',
|
||||
' --cities=all | "A,B,C" cidades (all = todas as cidades da UF na base)',
|
||||
' --segments=all | "a,b,c" nichos pelo nome (all = todos os 57)',
|
||||
' --segment-limit=N usa so os N primeiros nichos',
|
||||
' --city-limit=N usa so as N primeiras cidades',
|
||||
' --max=10 max de resultados por consulta (fontes de API)',
|
||||
' --query-limit=0 max total de consultas (0 = sem limite)',
|
||||
'',
|
||||
'Fontes:',
|
||||
...fontes
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (args.has('help')) {
|
||||
console.log(usage());
|
||||
return;
|
||||
}
|
||||
|
||||
const db = openDb();
|
||||
await ensureSeeds(db, { log: (m) => console.log(`[captura] ${m}`) });
|
||||
|
||||
const uf = (args.get('uf') || 'BA').toUpperCase();
|
||||
const sources = (args.get('sources') || 'mock').split(',').map((s) => s.trim()).filter(Boolean);
|
||||
|
||||
let segments = db.prepare('SELECT * FROM segments WHERE active = 1 ORDER BY id').all().map((row) => ({
|
||||
name: row.name,
|
||||
label: row.label,
|
||||
interest: row.interest,
|
||||
queries: JSON.parse(row.queries_json),
|
||||
cnaeCodes: JSON.parse(row.cnae_codes_json),
|
||||
overpassTags: JSON.parse(row.overpass_tags_json),
|
||||
pains: JSON.parse(row.pains_json)
|
||||
}));
|
||||
|
||||
const segmentsArg = args.get('segments');
|
||||
if (segmentsArg && segmentsArg !== 'all') {
|
||||
const wanted = new Set(segmentsArg.split(',').map((s) => s.trim()));
|
||||
segments = segments.filter((s) => wanted.has(s.name));
|
||||
}
|
||||
const segmentLimit = Number(args.get('segment-limit') || 0);
|
||||
if (segmentLimit > 0) segments = segments.slice(0, segmentLimit);
|
||||
|
||||
let cities;
|
||||
const citiesArg = args.get('cities') || 'all';
|
||||
if (citiesArg === 'all') {
|
||||
cities = db.prepare('SELECT nome FROM municipios WHERE uf = ? ORDER BY nome').all(uf).map((r) => r.nome);
|
||||
} else {
|
||||
cities = citiesArg.split(',').map((c) => c.trim()).filter(Boolean);
|
||||
}
|
||||
const cityLimit = Number(args.get('city-limit') || 0);
|
||||
if (cityLimit > 0) cities = cities.slice(0, cityLimit);
|
||||
|
||||
const params = {
|
||||
sources,
|
||||
uf,
|
||||
cities,
|
||||
segments,
|
||||
limits: {
|
||||
maxPerQuery: Number(args.get('max') || 10),
|
||||
queryLimit: Number(args.get('query-limit') || 0),
|
||||
googlePages: Number(args.get('google-pages') || 3)
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`[captura] fontes: ${sources.join(', ')} | UF: ${uf} | cidades: ${cities.length} | nichos: ${segments.length}`);
|
||||
|
||||
const active = db.prepare("SELECT id FROM capture_runs WHERE status IN ('pending','running')").get();
|
||||
if (active) throw new Error(`ja existe uma captura em andamento (#${active.id})`);
|
||||
const runId = createCaptureRun(db, { ...params, segments: segments.map((s) => s.name) });
|
||||
const lease = createJobLease({ db, table: 'capture_runs', id: runId, ownerToken: randomUUID() });
|
||||
let summary;
|
||||
try {
|
||||
summary = await runCapture({
|
||||
db,
|
||||
runId,
|
||||
params,
|
||||
emit: (p) => console.log(`[captura] ${p.phase}: ${p.message || ''}`),
|
||||
log: (m) => console.log(`[captura] ${m}`)
|
||||
});
|
||||
} finally {
|
||||
lease.close();
|
||||
}
|
||||
|
||||
console.log(`[captura] resultado: ${summary.raw} brutos, ${summary.new} novos, ${summary.dup} duplicados (${summary.status})`);
|
||||
db.close();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[captura] erro: ${error.stack || error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { loadDotEnv, paths } from '../src/env.mjs';
|
||||
import { openDb, ensureSeeds } from '../src/db.mjs';
|
||||
import { runOutreachBatch } from '../src/outreach/run.mjs';
|
||||
import { createWhatsAppManager, hasWhatsAppSession } from '../src/whatsapp/manager.mjs';
|
||||
|
||||
// Dispara um lote pela sessao local existente, respeitando as mesmas regras do servidor.
|
||||
|
||||
await loadDotEnv();
|
||||
|
||||
const log = (message) => console.log(`[outreach] ${message}`);
|
||||
|
||||
const db = openDb();
|
||||
await ensureSeeds(db, { log });
|
||||
const whatsapp = createWhatsAppManager({ authDir: paths.whatsappAuth, log });
|
||||
if (await hasWhatsAppSession(paths.whatsappAuth)) {
|
||||
await whatsapp.connect().catch((error) => log(`conexao local falhou: ${error.message}`));
|
||||
}
|
||||
|
||||
const resumo = await runOutreachBatch({ db, whatsapp, log });
|
||||
console.log(JSON.stringify(resumo, null, 2));
|
||||
|
||||
await whatsapp.close();
|
||||
db.close();
|
||||
@@ -0,0 +1,90 @@
|
||||
import path from 'node:path';
|
||||
import { loadDotEnv, paths } from '../src/env.mjs';
|
||||
import { openDb, ensureSeeds } from '../src/db.mjs';
|
||||
import { extractUf } from '../src/extract/rfb-extractor.mjs';
|
||||
import { downloadRfbDataset, validateRfbRawFiles } from '../src/extract/rfb-downloader.mjs';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createJobLease } from '../src/jobs/lease.mjs';
|
||||
|
||||
// Extrai a base CNPJ de uma UF inteira a partir dos zips da Receita Federal:
|
||||
// node scripts/extrair-base-cnpj.mjs --uf=BA
|
||||
// Por padrao baixa/retoma os arquivos necessarios antes de extrair. Use
|
||||
// --no-download somente quando a base local completa ja estiver disponivel.
|
||||
|
||||
await loadDotEnv();
|
||||
|
||||
const args = new Map(
|
||||
process.argv.slice(2).map((arg) => {
|
||||
const [key, value = 'true'] = arg.replace(/^--/, '').split('=');
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
|
||||
const uf = (args.get('uf') || '').toUpperCase();
|
||||
if (!/^[A-Z]{2}$/.test(uf)) {
|
||||
console.error('Uso: node scripts/extrair-base-cnpj.mjs --uf=BA');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rawDir = args.get('raw-dir') || paths.rfbRaw;
|
||||
const outCsv = path.join(paths.cnpjDir, `estabelecimentos-${uf}.csv`);
|
||||
const checkpointPath = path.join(paths.cnpjDir, `checkpoint-estabelecimentos-${uf}.ndjson`);
|
||||
|
||||
async function main() {
|
||||
const db = openDb();
|
||||
await ensureSeeds(db, { log: (m) => console.log(`[extrair] ${m}`) });
|
||||
|
||||
const active = db.prepare("SELECT id, uf FROM extractions WHERE status IN ('pending','running')").get();
|
||||
if (active) throw new Error(`ja existe uma extracao em andamento (#${active.id}, ${active.uf})`);
|
||||
const inserted = db.prepare(
|
||||
"INSERT INTO extractions (uf, status, started_at) VALUES (?, 'running', datetime('now'))"
|
||||
).run(uf);
|
||||
const extractionId = Number(inserted.lastInsertRowid);
|
||||
const lease = createJobLease({ db, table: 'extractions', id: extractionId, ownerToken: randomUUID() });
|
||||
|
||||
try {
|
||||
if (!args.has('no-download')) {
|
||||
await downloadRfbDataset({
|
||||
rawDir,
|
||||
onProgress: (p) => console.log(`[download] ${p.pct ?? 0}% ${p.message}`)
|
||||
});
|
||||
}
|
||||
await validateRfbRawFiles(rawDir);
|
||||
const result = await extractUf({
|
||||
uf,
|
||||
rawDir,
|
||||
outCsv,
|
||||
checkpointPath,
|
||||
db,
|
||||
log: (m) => console.log(`[extrair] ${m}`),
|
||||
onProgress: (p) => {
|
||||
console.log(`[extrair] ${p.pct}% ${p.phase}: ${p.message}`);
|
||||
db.prepare('UPDATE extractions SET progress_json = ? WHERE id = ?')
|
||||
.run(JSON.stringify(p), extractionId);
|
||||
}
|
||||
});
|
||||
|
||||
if (result.cancelled) {
|
||||
db.prepare("UPDATE extractions SET status = 'cancelled', finished_at = datetime('now') WHERE id = ?")
|
||||
.run(extractionId);
|
||||
} else {
|
||||
db.prepare(
|
||||
`UPDATE extractions SET status = 'done', csv_path = ?, total_rows = ?, with_phone = ?,
|
||||
municipios_count = ?, finished_at = datetime('now') WHERE id = ?`
|
||||
).run(result.csvPath, result.totalRows, result.withPhone, result.municipiosCount, extractionId);
|
||||
}
|
||||
} catch (error) {
|
||||
db.prepare(
|
||||
"UPDATE extractions SET status = 'error', error = ?, finished_at = datetime('now') WHERE id = ?"
|
||||
).run(String(error.message || error), extractionId);
|
||||
throw error;
|
||||
} finally {
|
||||
lease.close();
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[extrair] erro: ${error.stack || error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { loadDotEnv } from '../src/env.mjs';
|
||||
import { openDb, transaction } from '../src/db.mjs';
|
||||
import { scoreQualidade, scoreComercial, scoreGeral, classificarScoreGeral } from '../src/scoring.mjs';
|
||||
|
||||
// Recalcula score_qualidade/score_comercial/score/classificacao/prioridade/
|
||||
// produto_principal de TODOS os leads a partir dos dados ja gravados + da
|
||||
// afinidade/produtos configurados por nicho (tabela segments). Nao e um
|
||||
// script de uso unico: roda de novo (`npm run recalc-scores`) sempre que o
|
||||
// catalogo de afinidade/produtos em prospeccao/config.json mudar.
|
||||
//
|
||||
// Roda com o servidor parado (evita concorrencia de escrita no WAL).
|
||||
|
||||
await loadDotEnv();
|
||||
|
||||
const BATCH_SIZE = 2000;
|
||||
|
||||
function produtoPrincipalDe(produtos) {
|
||||
return produtos.length ? [...produtos].sort((a, b) => b.estrelas - a.estrelas)[0].nome : '';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const db = openDb();
|
||||
|
||||
const segmentos = new Map(
|
||||
db.prepare('SELECT name, affinity, products_json FROM segments').all()
|
||||
.map((row) => [row.name, { affinity: row.affinity, products: JSON.parse(row.products_json) }])
|
||||
);
|
||||
|
||||
const update = db.prepare(`
|
||||
UPDATE leads SET
|
||||
score = ?, classificacao = ?, motivo_classificacao = ?,
|
||||
score_qualidade = ?, score_comercial = ?, prioridade = ?,
|
||||
produto_principal = ?, produtos_json = ?, updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const total = db.prepare('SELECT COUNT(*) AS t FROM leads').get().t;
|
||||
const porPrioridade = { 1: 0, 2: 0, 3: 0, 4: 0 };
|
||||
let processed = 0;
|
||||
let lastId = 0;
|
||||
|
||||
const page = db.prepare('SELECT * FROM leads WHERE id > ? ORDER BY id LIMIT ?');
|
||||
|
||||
while (true) {
|
||||
const rows = page.all(lastId, BATCH_SIZE);
|
||||
if (!rows.length) break;
|
||||
|
||||
transaction(db, () => {
|
||||
for (const lead of rows) {
|
||||
const info = segmentos.get(lead.segmento) || { affinity: 'media', products: [] };
|
||||
const qualidade = scoreQualidade({
|
||||
whatsapp: lead.whatsapp, cnpj: lead.cnpj, empresa: lead.empresa,
|
||||
cidade: lead.cidade, segmento: lead.segmento, cnae: lead.cnae, website: lead.website
|
||||
});
|
||||
const comercial = scoreComercial({
|
||||
whatsapp: lead.whatsapp, cnpj: lead.cnpj, website: lead.website,
|
||||
email: lead.email, affinity: info.affinity
|
||||
});
|
||||
const geral = scoreGeral(qualidade.score, comercial);
|
||||
const tier = classificarScoreGeral(geral);
|
||||
const produtoPrincipal = produtoPrincipalDe(info.products);
|
||||
|
||||
update.run(
|
||||
geral, tier.id, `score geral ${geral} (qualidade ${qualidade.score}, comercial ${comercial})`,
|
||||
qualidade.score, comercial, tier.prioridade,
|
||||
produtoPrincipal, JSON.stringify(info.products),
|
||||
lead.id
|
||||
);
|
||||
porPrioridade[tier.prioridade] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
processed += rows.length;
|
||||
lastId = rows[rows.length - 1].id;
|
||||
console.log(`${processed}/${total} leads recalculados...`);
|
||||
}
|
||||
|
||||
console.log('Concluido. Distribuicao por prioridade:', porPrioridade);
|
||||
db.close();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Erro no recalculo de scores:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
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';
|
||||
import { startOutreachScheduler } from '../src/outreach/scheduler.mjs';
|
||||
import { pauseCampaignsAfterRestart, recoverReservedCampaignQueue } from '../src/outreach/campaign-worker.mjs';
|
||||
import { recordInboundReply } from '../src/outreach/inbound.mjs';
|
||||
import { createWhatsAppManager, hasWhatsAppSession } from '../src/whatsapp/manager.mjs';
|
||||
|
||||
// Servidor local da Maquina Comercial Pessoal.
|
||||
|
||||
await loadDotEnv();
|
||||
|
||||
const log = (message) => console.log(`[servidor] ${message}`);
|
||||
|
||||
const db = openDb();
|
||||
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,
|
||||
log,
|
||||
onMessageStatus: (event) => updateOutreachMessageStatus(db, event),
|
||||
onInboundMessage: (event) => recordInboundReply(db, event)
|
||||
});
|
||||
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, 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;
|
||||
async function shutdown() {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
log('encerrando...');
|
||||
const watchdog = setTimeout(() => process.exit(1), 30_000);
|
||||
watchdog.unref();
|
||||
|
||||
const shutdownTasks = [
|
||||
stopScheduler().catch((error) => log(`outreach: falha ao encerrar scheduler: ${error.message}`)),
|
||||
jobs.close().catch((error) => log(`jobs: falha ao encerrar: ${error.message}`)),
|
||||
whatsapp.close().catch((error) => log(`whatsapp: falha ao encerrar: ${error.message}`))
|
||||
];
|
||||
let resourceTimer;
|
||||
const resourcesTimedOut = await Promise.race([
|
||||
Promise.allSettled(shutdownTasks).then(() => false),
|
||||
new Promise((resolve) => {
|
||||
resourceTimer = setTimeout(() => resolve(true), 20_000);
|
||||
resourceTimer.unref();
|
||||
})
|
||||
]);
|
||||
clearTimeout(resourceTimer);
|
||||
if (resourcesTimedOut) log('encerramento dos recursos excedeu o prazo; prosseguindo');
|
||||
|
||||
recoverOrphans(db, log);
|
||||
let httpTimer;
|
||||
const httpTimedOut = await Promise.race([
|
||||
new Promise((resolve) => server.close(resolve)).then(() => false),
|
||||
new Promise((resolve) => {
|
||||
httpTimer = setTimeout(() => resolve(true), 5_000);
|
||||
httpTimer.unref();
|
||||
})
|
||||
]);
|
||||
clearTimeout(httpTimer);
|
||||
if (httpTimedOut) log('encerramento HTTP excedeu o prazo; prosseguindo');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
Reference in New Issue
Block a user