const express = require('express'); const path = require('path'); const { Pool } = require('pg'); const app = express(); app.use(express.json()); // ── Ships/Oops API (migrated from calvana) ──────────────────────────────── const poolConfig = { host: process.env.SHIPS_DB_HOST || 'localhost', port: parseInt(process.env.SHIPS_DB_PORT || '5432'), user: process.env.SHIPS_DB_USER || 'dokploy', password: process.env.SHIPS_DB_PASSWORD || '', database: process.env.SHIPS_DB_NAME || 'calvana', min: 0, max: 10, }; let pool = null; const getPool = () => { if (!pool) { pool = new Pool(poolConfig); } return pool; }; // Health check app.get('/api/health', async (req, res) => { try { await getPool().query('SELECT 1'); res.json({ status: 'ok', db: 'connected' }); } catch (e) { res.status(500).json({ status: 'error', db: e.message }); } }); // GET all ships app.get('/api/ships', async (req, res) => { try { const { rows } = await getPool().query( 'SELECT * FROM ships ORDER BY created_at DESC' ); res.json(rows); } catch (e) { res.status(500).json({ error: e.message }); } }); // GET ships/showcase — top shipped items for homepage widget app.get('/api/ships/showcase', async (req, res) => { try { const { rows } = await getPool().query( "SELECT * FROM ships WHERE status = 'shipped' ORDER BY created_at DESC LIMIT 10" ); res.json(rows); } catch (e) { res.status(500).json({ error: e.message }); } }); // GET stats — totals for homepage counters app.get('/api/stats', async (req, res) => { try { const p = getPool(); const [shipsTotal, shipsShipped, oopsTotal] = await Promise.all([ p.query('SELECT COUNT(*) FROM ships'), p.query("SELECT COUNT(*) FROM ships WHERE status = 'shipped'"), p.query('SELECT COUNT(*) FROM oops'), ]); res.json({ total: parseInt(shipsTotal.rows[0].count), shipped: parseInt(shipsShipped.rows[0].count), oops: parseInt(oopsTotal.rows[0].count), }); } catch (e) { res.status(500).json({ error: e.message }); } }); // POST ship app.post('/api/ships', async (req, res) => { const { title, status, metric, details } = req.body; if (!title) return res.status(400).json({ error: 'title required' }); try { const { rows } = await getPool().query( 'INSERT INTO ships (title, status, metric, details) VALUES ($1, $2, $3, $4) RETURNING *', [title, status || 'planned', metric || null, details || null] ); res.status(201).json(rows[0]); } catch (e) { res.status(500).json({ error: e.message }); } }); // PATCH ship app.patch('/api/ships/:id', async (req, res) => { const { id } = req.params; const { title, status, metric, details } = req.body; try { const sets = []; const vals = []; let i = 1; if (title !== undefined) { sets.push(`title=$${i++}`); vals.push(title); } if (status !== undefined) { sets.push(`status=$${i++}`); vals.push(status); } if (metric !== undefined) { sets.push(`metric=$${i++}`); vals.push(metric); } if (details !== undefined) { sets.push(`details=$${i++}`); vals.push(details); } if (sets.length === 0) return res.status(400).json({ error: 'nothing to update' }); sets.push(`updated_at=NOW()`); vals.push(id); const { rows } = await getPool().query( `UPDATE ships SET ${sets.join(', ')} WHERE id=$${i} RETURNING *`, vals ); if (rows.length === 0) return res.status(404).json({ error: 'not found' }); res.json(rows[0]); } catch (e) { res.status(500).json({ error: e.message }); } }); // DELETE ship app.delete('/api/ships/:id', async (req, res) => { const { id } = req.params; try { const { rows } = await getPool().query( 'DELETE FROM ships WHERE id=$1 RETURNING *', [id] ); if (rows.length === 0) return res.status(404).json({ error: 'not found' }); res.json({ deleted: true, ship: rows[0] }); } catch (e) { res.status(500).json({ error: e.message }); } }); // GET oops app.get('/api/oops', async (req, res) => { try { const { rows } = await getPool().query( 'SELECT * FROM oops ORDER BY created_at DESC' ); res.json(rows); } catch (e) { res.status(500).json({ error: e.message }); } }); // POST oops app.post('/api/oops', async (req, res) => { const { description, fix_time, commit_link } = req.body; if (!description) return res.status(400).json({ error: 'description required' }); try { const { rows } = await getPool().query( 'INSERT INTO oops (description, fix_time, commit_link) VALUES ($1, $2, $3) RETURNING *', [description, fix_time || null, commit_link || null] ); res.status(201).json(rows[0]); } catch (e) { res.status(500).json({ error: e.message }); } }); // DELETE oops app.delete('/api/oops/:id', async (req, res) => { const { id } = req.params; try { const { rows } = await getPool().query( 'DELETE FROM oops WHERE id=$1 RETURNING *', [id] ); if (rows.length === 0) return res.status(404).json({ error: 'not found' }); res.json({ deleted: true, oops: rows[0] }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Redirect /work/ to first case study (matches old nginx behaviour) app.get('/work/', (req, res) => res.redirect(301, '/work/charityright/')); // Serve static files from html/ directory app.use(express.static(path.join(__dirname, 'html'))); // SPA fallback: send index.html for unmatched routes app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'html', 'index.html')); }); const PORT = process.env.PORT || 3000; app.listen(PORT, '0.0.0.0', () => { console.log(`QuikCue Site server listening on :${PORT}`); });