#!/usr/bin/env node /** * LANDVEX MASTER API * ================== * Single source of truth för ALLA system. * Läser från: amos, aamos_ledger, landvex, wavult_identity * Exponerar: Enhetligt REST API för admin, appar, externa system * * Regler: * 1. INGEN mock-data * 2. INGEN hårdkodad data * 3. ALLT kommer från databas * 4. ALLA ändringar går via API:et */ import express from 'express'; import pg from 'pg'; const { Pool } = pg; import cors from 'cors'; const app = express(); app.use(cors()); app.use(express.json()); const PORT = process.env.PORT || 7073; // ═══════════════════════════════════════════════════════════════ // DATABASUPPKOPLINGAR // ═══════════════════════════════════════════════════════════════ const pools = { amos: new Pool({ host: 'localhost', port: 5432, database: 'amos', user: 'postgres', password: 'quixzo…2026' }), ledger: new Pool({ host: 'localhost', port: 5432, database: 'aamos_ledger', user: 'postgres', password: 'quixzo…2026' }), landvex: new Pool({ host: 'localhost', port: 5432, database: 'landvex', user: 'postgres', password: 'quixzo…2026' }) }; // Health check för alla databaser async function checkDatabases() { const status = {}; for (const [name, pool] of Object.entries(pools)) { try { const start = Date.now(); await pool.query('SELECT 1'); status[name] = { ok: true, latency_ms: Date.now() - start }; } catch (e) { status[name] = { ok: false, error: e.message }; } } return status; } // ═══════════════════════════════════════════════════════════════ // MIDDLEWARE // ═══════════════════════════════════════════════════════════════ app.use((req, res, next) => { console.log(`${new Date().toISOString()} ${req.method} ${req.path}`); next(); }); // ═══════════════════════════════════════════════════════════════ // HEALTH & STATUS // ═══════════════════════════════════════════════════════════════ app.get('/health', async (req, res) => { const dbStatus = await checkDatabases(); const allOk = Object.values(dbStatus).every(s => s.ok); res.status(allOk ? 200 : 503).json({ status: allOk ? 'ok' : 'degraded', service: 'landvex-master-api', version: '1.0.0', databases: dbStatus, timestamp: new Date().toISOString() }); }); // ═══════════════════════════════════════════════════════════════ // QUIXZOOM ENDPOINTS (från amos-databasen) // ═══════════════════════════════════════════════════════════════ app.get('/api/v1/quixzoom/stats', async (req, res) => { try { const [missions, active, submissions, contributors] = await Promise.all([ pools.amos.query('SELECT COUNT(*) FROM quixzoom.missions'), pools.amos.query("SELECT COUNT(*) FROM quixzoom.missions WHERE status = 'active'"), pools.amos.query('SELECT COUNT(*) FROM quixzoom.submissions'), pools.amos.query('SELECT COUNT(DISTINCT user_id) FROM quixzoom.claims') ]); res.json({ missions_total: parseInt(missions.rows[0].count), missions_active: parseInt(active.rows[0].count), submissions_total: parseInt(submissions.rows[0].count), contributors_total: parseInt(contributors.rows[0].count), source: 'amos.quixzoom', cached: false }); } catch (e) { res.status(500).json({ error: e.message, source: 'amos.quixzoom' }); } }); app.get('/api/v1/quixzoom/missions', async (req, res) => { try { const { status, limit = 50, offset = 0 } = req.query; let query = ` SELECT m.id, m.title, m.description, m.latitude as lat, m.longitude as lon, m.area_name as address, m.status, m.reward_credits as reward_sek, m.required_photos, m.created_at, m.expires_at, COUNT(s.id) as submission_count FROM quixzoom.missions m LEFT JOIN quixzoom.submissions s ON s.mission_id = m.id `; const params = []; if (status) { query += ' WHERE m.status = $1'; params.push(status); } query += ` GROUP BY m.id ORDER BY m.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`; params.push(limit, offset); const result = await pools.amos.query(query, params); const totalResult = await pools.amos.query('SELECT COUNT(*) FROM quixzoom.missions'); res.json({ missions: result.rows, count: result.rows.length, total: parseInt(totalResult.rows[0].count), source: 'amos.quixzoom.missions' }); } catch (e) { res.status(500).json({ error: e.message, source: 'amos.quixzoom.missions' }); } }); app.get('/api/v1/quixzoom/missions/:id', async (req, res) => { try { const result = await pools.amos.query(` SELECT * FROM quixzoom.missions WHERE id = $1 `, [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Mission not found' }); } res.json(result.rows[0]); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/v1/quixzoom/contributors', async (req, res) => { try { const result = await pools.amos.query(` SELECT u.id, u.email, u.display_name as name, u.city, u.status, COUNT(DISTINCT s.id) as submission_count, COALESCE(SUM(s.reward_credits), 0) as total_earnings FROM quixzoom.users u LEFT JOIN quixzoom.submissions s ON s.user_id = u.id GROUP BY u.id ORDER BY total_earnings DESC LIMIT 50 `); res.json({ contributors: result.rows, count: result.rows.length, source: 'amos.quixzoom.users' }); } catch (e) { res.status(500).json({ error: e.message }); } }); // ═══════════════════════════════════════════════════════════════ // LEDGER ENDPOINTS (från aamos_ledger-databasen) // ═══════════════════════════════════════════════════════════════ app.get('/api/v1/ledger/accounts', async (req, res) => { try { const result = await pools.ledger.query(` SELECT a.id, a.code as account_number, a.name, a.account_type, COALESCE(SUM(jl.debit), 0) as total_debit, COALESCE(SUM(jl.credit), 0) as total_credit, COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) as balance FROM accounts a LEFT JOIN journal_lines jl ON jl.account_id = a.id GROUP BY a.id ORDER BY a.code `); res.json({ accounts: result.rows, count: result.rows.length, source: 'aamos_ledger.accounts' }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/v1/ledger/trial-balance', async (req, res) => { try { const result = await pools.ledger.query(` SELECT a.code as account_number, a.name, a.account_type, COALESCE(SUM(jl.debit), 0) as total_debit, COALESCE(SUM(jl.credit), 0) as total_credit FROM accounts a LEFT JOIN journal_lines jl ON jl.account_id = a.id GROUP BY a.id HAVING COALESCE(SUM(jl.debit), 0) > 0 OR COALESCE(SUM(jl.credit), 0) > 0 ORDER BY a.code `); const totalDebit = result.rows.reduce((sum, r) => sum + parseFloat(r.total_debit), 0); const totalCredit = result.rows.reduce((sum, r) => sum + parseFloat(r.total_credit), 0); res.json({ rows: result.rows, total_debit: totalDebit, total_credit: totalCredit, balanced: Math.abs(totalDebit - totalCredit) < 0.01, source: 'aamos_ledger.journal_lines' }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/v1/ledger/journal-entries', async (req, res) => { try { const { limit = 50, offset = 0 } = req.query; const result = await pools.ledger.query(` SELECT je.id, je.entry_number, je.description, je.entry_date, je.status, jl.account_id, a.account_number, a.name as account_name, jl.debit, jl.credit FROM journal_entries je JOIN journal_lines jl ON jl.journal_entry_id = je.id JOIN accounts a ON a.id = jl.account_id ORDER BY je.entry_date DESC, je.entry_number DESC LIMIT $1 OFFSET $2 `, [limit, offset]); res.json({ entries: result.rows, count: result.rows.length, source: 'aamos_ledger.journal_entries' }); } catch (e) { res.status(500).json({ error: e.message }); } }); // ═══════════════════════════════════════════════════════════════ // SYSTEM ENDPOINTS // ═══════════════════════════════════════════════════════════════ app.get('/api/v1/system/services', async (req, res) => { // Hämta faktisk status från systemd const services = [ { name: 'aamos-ledger', port: 3250, db: 'aamos_ledger' }, { name: 'quixzoom-mission', port: 7060, db: 'amos' }, { name: 'landvex-api', port: 8081, db: 'landvex' }, { name: 'quixzoom-api', port: 8080, db: 'amos' }, { name: 'nginx', port: 80, db: null } ]; res.json({ services }); }); // ═══════════════════════════════════════════════════════════════ // START // ═══════════════════════════════════════════════════════════════ app.listen(PORT, '0.0.0.0', async () => { console.log(`╔══════════════════════════════════════════════════════════════╗`); console.log(`║ LANDVEX MASTER API v1.0.0 ║`); console.log(`║ Single Source of Truth ║`); console.log(`╠══════════════════════════════════════════════════════════════╣`); console.log(`║ Port: ${PORT} ║`); console.log(`║ Databases: amos, aamos_ledger, landvex ║`); console.log(`╠══════════════════════════════════════════════════════════════╣`); console.log(`║ Endpoints: ║`); console.log(`║ GET /health ║`); console.log(`║ GET /api/v1/quixzoom/stats ║`); console.log(`║ GET /api/v1/quixzoom/missions ║`); console.log(`║ GET /api/v1/quixzoom/contributors ║`); console.log(`║ GET /api/v1/ledger/accounts ║`); console.log(`║ GET /api/v1/ledger/trial-balance ║`); console.log(`║ GET /api/v1/ledger/journal-entries ║`); console.log(`╚══════════════════════════════════════════════════════════════╝`); const dbStatus = await checkDatabases(); console.log('\nDatabase status:', dbStatus); });