← back to George Gmail
fleet-autoresponder-ui.js
108 lines
#!/usr/bin/env node
/**
* Fleet Auto-Responder — admin UI dashboard.
*
* A small standalone Express viewer (127.0.0.1:9768) for the fleet
* auto-responder. Deliberately NOT part of the George prod server — it
* reads the responder's own data files (ledger + run-log + state) so it
* needs no George deploy and no Gmail OAuth.
*
* Shows:
* - inquiries received per fleet domain
* - auto-replies sent (timestamp + recipient)
* - spam-skips with the reason each was skipped
* - the current reply template (HTML preview)
* - an on/off toggle for the responder
* - last-run time
*
* Data sources (all written by fleet-autoresponder.js):
* data/fleet-autoresponder-ledger.json per-sender reply timestamps
* data/fleet-autoresponder-runlog.jsonl append-only per-message records
* data/fleet-autoresponder-state.json last-run + on/off toggle
*/
'use strict';
const fs = require('fs');
const path = require('path');
const express = require('express');
const responder = require('./fleet-autoresponder');
const { RUNLOG_PATH, STATE_PATH, LEDGER_PATH, FLEET,
buildHtmlBody, buildPlainBody, loadState, saveState } = responder;
const PORT = process.env.FLEET_UI_PORT || 9768;
const app = express();
app.use(express.json());
// ─── Data readers ────────────────────────────────────────────────────────────
function readRunLog() {
try {
return fs.readFileSync(RUNLOG_PATH, 'utf8')
.split('\n').filter(Boolean)
.map(l => { try { return JSON.parse(l); } catch { return null; } })
.filter(Boolean);
} catch { return []; }
}
function readLedger() {
try { return JSON.parse(fs.readFileSync(LEDGER_PATH, 'utf8')); }
catch { return { replies: {} }; }
}
// ─── API ─────────────────────────────────────────────────────────────────────
app.get('/api/data', (req, res) => {
const log = readRunLog();
const state = loadState();
const ledger = readLedger();
const replies = log.filter(r => r.event === 'reply');
const skips = log.filter(r => r.event === 'skip');
// Per-domain inquiry tally (a "received" inquiry = anything the responder
// touched — replied OR skipped — that resolved to a fleet domain).
const perDomain = {};
for (const s of FLEET) perDomain[s.domain] = { domain: s.domain, siteName: s.siteName, received: 0, replied: 0, skipped: 0 };
for (const r of log) {
if (!r.fleetDomain || !perDomain[r.fleetDomain]) continue;
perDomain[r.fleetDomain].received++;
if (r.event === 'reply') perDomain[r.fleetDomain].replied++;
if (r.event === 'skip') perDomain[r.fleetDomain].skipped++;
}
res.json({
state,
fleetCount: FLEET.length,
totals: {
inquiries: log.length,
replied: replies.length,
skipped: skips.length,
uniqueSenders: Object.keys(ledger.replies || {}).length,
},
perDomain: Object.values(perDomain).sort((a, b) => b.received - a.received),
replies: replies.slice(-200).reverse(),
skips: skips.slice(-200).reverse(),
});
});
// Current reply template — rendered for a sample domain so the UI can preview.
app.get('/api/template', (req, res) => {
const domain = (req.query.domain || (FLEET[0] && FLEET[0].domain) || 'example.com').toLowerCase();
res.json({ domain, html: buildHtmlBody(domain), text: buildPlainBody(domain) });
});
// On/off toggle for the responder.
app.post('/api/toggle', (req, res) => {
const state = loadState();
state.enabled = !!req.body.enabled;
state.toggledAt = new Date().toISOString();
saveState(state);
res.json({ ok: true, enabled: state.enabled });
});
app.get('/healthz', (_req, res) => res.json({ ok: true }));
app.use(express.static(path.join(__dirname, 'public-fleet-ui')));
app.listen(PORT, '127.0.0.1', () => {
console.log(`[fleet-autoresponder-ui] listening on http://127.0.0.1:${PORT}`);
});