← back to Ga Global Viewer
server.js
144 lines
'use strict';
/**
* ga-global-viewer — Express server
*
* GET / → serves the dashboard HTML
* GET /api/stats → returns stats JSON (from cache or fresh fetch)
* GET /api/refresh → triggers a fresh python3 fetch_stats.py --cache run
*/
const express = require('express');
const { execFile, spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const PORT = 9768;
const CACHE_FILE = path.join(__dirname, 'cache', 'stats.json');
const FETCH_SCRIPT = path.join(__dirname, 'fetch_stats.py');
// Track whether a refresh is currently running
let refreshRunning = false;
let refreshLog = [];
const app = express();
app.use(express.json());
// ── /api/stats ────────────────────────────────────────────────────────────────
app.get('/api/stats', (req, res) => {
// If cache exists, serve it
if (fs.existsSync(CACHE_FILE)) {
try {
const raw = fs.readFileSync(CACHE_FILE, 'utf8');
const data = JSON.parse(raw);
data._cache_hit = true;
data._cache_age_s = Math.floor((Date.now() - fs.statSync(CACHE_FILE).mtimeMs) / 1000);
return res.json(data);
} catch (e) {
// fall through to fresh fetch
}
}
// No cache — run the python script synchronously (first-run)
res.setHeader('Content-Type', 'application/json');
res.setHeader('X-Fetching', 'true');
runFetch((err, data) => {
if (err) {
return res.status(500).json({
auth_ok: false,
auth_error: `Failed to run fetch_stats.py: ${err.message}`,
properties_count: 0,
per_property: [],
global: { totals: {sessions:0,users:0,pageviews:0}, timeseries:[], top_pages:[], top_sources:[] },
});
}
data._cache_hit = false;
res.json(data);
});
});
// ── /api/refresh ──────────────────────────────────────────────────────────────
app.get('/api/refresh', (req, res) => {
if (refreshRunning) {
return res.json({ status: 'already_running', log: refreshLog });
}
refreshRunning = true;
refreshLog = ['Starting fresh GA4 data pull...'];
res.json({ status: 'started' });
const child = spawn('python3', [FETCH_SCRIPT, '--cache'], {
cwd: __dirname,
env: { ...process.env, PYTHONUNBUFFERED: '1' },
});
child.stdout.on('data', d => refreshLog.push(d.toString().trim()));
child.stderr.on('data', d => refreshLog.push(d.toString().trim()));
child.on('close', code => {
refreshRunning = false;
refreshLog.push(`Done (exit ${code})`);
});
});
// ── /api/refresh-status ───────────────────────────────────────────────────────
app.get('/api/refresh-status', (req, res) => {
res.json({ running: refreshRunning, log: refreshLog });
});
// ── /api/properties ───────────────────────────────────────────────────────────
app.get('/api/properties', (req, res) => {
const pPath = path.join(
process.env.HOME, '.config', 'ga-analytics-agent', 'properties.json'
);
if (!fs.existsSync(pPath)) {
return res.json({ count: 0, properties: {} });
}
try {
const d = JSON.parse(fs.readFileSync(pPath, 'utf8'));
res.json({
count: Object.keys(d.properties || {}).length,
account: d.account_display_name,
generated: d._generated,
properties: d.properties || {},
});
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// ── static (public/) ──────────────────────────────────────────────────────────
app.use(express.static(path.join(__dirname, 'public')));
// ── root → dashboard ──────────────────────────────────────────────────────────
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// ── helpers ───────────────────────────────────────────────────────────────────
function runFetch(cb) {
execFile('python3', [FETCH_SCRIPT, '--cache'], {
cwd: __dirname,
timeout: 300_000, // 5 min for full fleet run
env: { ...process.env, PYTHONUNBUFFERED: '1' },
}, (err, stdout, stderr) => {
if (err) return cb(err);
if (fs.existsSync(CACHE_FILE)) {
try {
return cb(null, JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')));
} catch (e) {
return cb(e);
}
}
try {
return cb(null, JSON.parse(stdout));
} catch (e) {
return cb(new Error(`JSON parse error: ${e.message}\nstdout: ${stdout.slice(0,200)}`));
}
});
}
// ── start ─────────────────────────────────────────────────────────────────────
app.listen(PORT, () => {
console.log(`ga-global-viewer running at http://localhost:${PORT}`);
console.log(`Cache: ${CACHE_FILE}`);
});