← 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}`);
});