import express from 'express'; import pg from 'pg'; const { Pool } = pg; import multer from 'multer'; import { randomUUID } from 'crypto'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PORT = 7060; const UPLOAD_DIR = '/opt/amos/data/quixzoom-uploads'; // PostgreSQL connection const pool = new Pool({ host: 'localhost', port: 5432, database: 'amos', user: 'postgres', password: 'quixzo…2026' }); fs.mkdirSync(UPLOAD_DIR, { recursive: true }); const app = express(); app.use(express.json()); const upload = multer({ dest: UPLOAD_DIR, limits: { fileSize: 50 * 1024 * 1024 }, }); // Serve uploaded images app.use('/uploads', express.static(UPLOAD_DIR)); // Health check app.get('/health', async (req, res) => { try { const result = await pool.query('SELECT COUNT(*) FROM quixzoom.missions'); res.json({ status: 'ok', service: 'quixzoom-mission-engine', port: PORT, missions_total: parseInt(result.rows[0].count), uptime_seconds: process.uptime(), timestamp: new Date().toISOString() }); } catch (e) { res.status(500).json({ status: 'error', error: e.message }); } }); // GET /missions — list all missions app.get('/missions', async (req, res) => { try { const result = await pool.query(` SELECT id, title, description, latitude as lat, longitude as lon, area_name as address, status, reward_credits as reward_sek, required_photos, created_at, expires_at FROM quixzoom.missions ORDER BY created_at DESC `); res.json({ missions: result.rows, count: result.rows.length }); } catch (e) { res.status(500).json({ error: e.message }); } }); // GET /api/v1/missions — API format for admin app.get('/api/v1/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 pool.query(query, params); res.json({ missions: result.rows, count: result.rows.length, total: parseInt((await pool.query('SELECT COUNT(*) FROM quixzoom.missions')).rows[0].count) }); } catch (e) { res.status(500).json({ error: e.message }); } }); // GET /api/v1/stats — dashboard stats app.get('/api/v1/stats', async (req, res) => { try { const missionsResult = await pool.query('SELECT COUNT(*) FROM quixzoom.missions'); const activeResult = await pool.query("SELECT COUNT(*) FROM quixzoom.missions WHERE status = 'active'"); const submissionsResult = await pool.query('SELECT COUNT(*) FROM quixzoom.submissions'); const contributorsResult = await pool.query('SELECT COUNT(DISTINCT user_id) FROM quixzoom.submissions'); res.json({ missions_total: parseInt(missionsResult.rows[0].count), missions_active: parseInt(activeResult.rows[0].count), submissions_total: parseInt(submissionsResult.rows[0].count), contributors_total: parseInt(contributorsResult.rows[0].count) }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Start server app.listen(PORT, () => { console.log(`quixzoom-mission (PostgreSQL) running on port ${PORT}`); });