Migrate calvana API into quikcue-site: add Node.js server, update Dockerfile, add migrated HTML pages
This commit is contained in:
+8
-4
@@ -1,4 +1,8 @@
|
|||||||
FROM nginx:alpine
|
FROM node:22-alpine
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
WORKDIR /app
|
||||||
COPY html/ /usr/share/nginx/html/
|
COPY package.json .
|
||||||
EXPOSE 80
|
RUN npm install
|
||||||
|
COPY server.js .
|
||||||
|
COPY html/ ./html/
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>QuikCue — Hire</title>
|
||||||
|
<meta name="description" content="If you want velocity with control — let's talk.">
|
||||||
|
<meta property="og:title" content="QuikCue — Hire">
|
||||||
|
<meta property="og:description" content="If you want velocity with control — let's talk.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://quikcue.com/hire">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<link rel="canonical" href="https://quikcue.com/hire">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<div class="nav-inner">
|
||||||
|
<a href="/" class="logo">QuikCue</a>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/manifesto">/manifesto</a>
|
||||||
|
<a href="/live">/live</a>
|
||||||
|
<a href="/hire" class="active">/hire</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="page">
|
||||||
|
<h1 class="hero-title">If you're reading this,<br>you <span class="accent">already know.</span></h1>
|
||||||
|
<div class="hire-ctas">
|
||||||
|
<a href="mailto:omair@quikcue.com?subject=QuikCue%20-%20Repo%20%2B%20Metric" class="btn btn-cta">Give me a repo + one metric →</a>
|
||||||
|
<a href="mailto:omair@quikcue.com?subject=QuikCue%20-%207-day%20proof" class="btn btn-primary">Give me 7 days. I'll prove it. →</a>
|
||||||
|
</div>
|
||||||
|
<p class="hire-note">
|
||||||
|
If you want safe hands, hire safe people.<br>
|
||||||
|
If you want velocity with control — let's talk.
|
||||||
|
</p>
|
||||||
|
<p class="hire-referral">PS — Umar pointed me here. If this turns into a hire, I want him to get paid.</p>
|
||||||
|
<footer>
|
||||||
|
<p class="footer-tagline">I break rules. Not production.</p>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+4
-4
@@ -724,7 +724,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/pricing">Pricing</a>
|
<a href="/pricing">Pricing</a>
|
||||||
<a href="https://calvana.quikcue.com/live" target="_blank">Live Feed</a>
|
<a href="/live">Live Feed</a>
|
||||||
<a href="https://git.quikcue.com" target="_blank">Git</a>
|
<a href="https://git.quikcue.com" target="_blank">Git</a>
|
||||||
<a href="mailto:omair@quikcue.com">Contact</a>
|
<a href="mailto:omair@quikcue.com">Contact</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -776,13 +776,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Live Feed ──
|
// ── Live Feed ──
|
||||||
const CALVANA_API = 'https://calvana.quikcue.com/api';
|
const SHIPS_API = '/api';
|
||||||
|
|
||||||
async function loadLiveFeed() {
|
async function loadLiveFeed() {
|
||||||
try {
|
try {
|
||||||
const [shipsRes, statsRes] = await Promise.all([
|
const [shipsRes, statsRes] = await Promise.all([
|
||||||
fetch(`${CALVANA_API}/ships/showcase`),
|
fetch(`${SHIPS_API}/ships/showcase`),
|
||||||
fetch(`${CALVANA_API}/stats`)
|
fetch(`${SHIPS_API}/stats`)
|
||||||
]);
|
]);
|
||||||
if (shipsRes.ok) renderShips(await shipsRes.json());
|
if (shipsRes.ok) renderShips(await shipsRes.json());
|
||||||
if (statsRes.ok) renderStats(await statsRes.json());
|
if (statsRes.ok) renderStats(await statsRes.json());
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>QuikCue — I don't apply. I deploy.</title>
|
||||||
|
<meta name="description" content="Most applications prove the past. This one proves the next 7 days. Built live. Shipped now.">
|
||||||
|
<meta property="og:title" content="QuikCue — I don't apply. I deploy.">
|
||||||
|
<meta property="og:description" content="You're hiring engineers. I'm showing you what changes when you hire an engine.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://quikcue.com/manifesto">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<link rel="canonical" href="https://quikcue.com/manifesto">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<div class="nav-inner">
|
||||||
|
<a href="/" class="logo">QuikCue</a>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/manifesto" class="active">/manifesto</a>
|
||||||
|
<a href="/live">/live</a>
|
||||||
|
<a href="/hire">/hire</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="page">
|
||||||
|
<h1 class="hero-title">I don't apply.<br><span class="accent">I deploy.</span></h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
You're hiring engineers. I'm showing you what changes when you hire an engine.<br>
|
||||||
|
This application is a product. Built for you. Right now.
|
||||||
|
</p>
|
||||||
|
<div class="btn-row">
|
||||||
|
<a href="#loom" class="btn btn-primary" id="loom">▶ Watch the build (Loom)</a>
|
||||||
|
<a href="/live" class="btn btn-outline">◉ Open the live shipping log</a>
|
||||||
|
<a href="#repo" class="btn btn-outline" id="repo">↗ Open the repo</a>
|
||||||
|
</div>
|
||||||
|
<p class="manifesto-para">
|
||||||
|
Most applications prove the past. I'm proving the next 7 days.<br>
|
||||||
|
Build → test → ship → observe → iterate… at speed, without breaking reality.
|
||||||
|
</p>
|
||||||
|
<footer>
|
||||||
|
<p class="footer-tagline">I break rules. Not production.</p>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -12,6 +12,15 @@ server {
|
|||||||
|
|
||||||
absolute_redirect off;
|
absolute_redirect off;
|
||||||
|
|
||||||
|
# Proxy /api to quikcue-api service (ships/oops live ship log)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://quikcue-api:3001;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
# Redirect /work/ to first case study (no bare index = was 403)
|
# Redirect /work/ to first case study (no bare index = was 403)
|
||||||
location = /work/ {
|
location = /work/ {
|
||||||
return 301 /work/charityright/;
|
return 301 /work/charityright/;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "quikcue-site",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"pg": "^8.13.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user