backend: add static serving + multi-stage Dockerfile, website: add API test dashboard

This commit is contained in:
sumit-plena
2026-07-07 17:03:33 +05:30
parent 1dd97cf059
commit 317d60ed63
78 changed files with 6100 additions and 40 deletions

View File

@@ -1,13 +1,14 @@
PORT=8080
NODE_ENV=development
DATABASE_URL=libsql://db-dev-63adec08f5fd35eadac3-sumit22.aws-ap-south-1.turso.io
DATABASE_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3ODI3Mzg5MjcsImlkIjoiMDE5ZjEzODUtMzUwMS03ODA0LWExZTEtY2JlN2E4NDlhM2I2Iiwia2lkIjoiczltT1N1V3ZwX2pib21leUZZNzR3RVNoN1NOak12RmVDc3EtZk5YdHY1NCIsInJpZCI6IjU5OTg0M2RkLTc4NWItNDhhZS04MDI0LTgxODM2YmQzZDE0MSJ9.TsozKx4lkITuiGiQ7FQDR_xl1LiZtzExx6nDkrZgefNXI7QPWiKYrSTgapqNMlO9QiGzE_cPbcClHvCPnmgMDg
S3_ENDPOINT=https://t3.storage.dev
S3_BUCKET=plena-staging
S3_PREFIX=backends/6892099c9a4be35629c49e8c/6a426fea762485cd9a634857/dev/
S3_ACCESS_KEY_ID=tid_VozbolUlzrxAAhDfPdanwUzAMqsO_E_EroKpPZnCcwOuXlHUKd
S3_SECRET_ACCESS_KEY=tsec_C6sLdJDir0Kz+4abePDcJIdah+zA7dni1Z5nNY3lH5f4-dd+Nr+GohZuXgZwetr1Hv-270
S3_SESSION_TOKEN=
PROJECT_ID=6a426fea762485cd9a634857
API_PREFIX=
CORS_ORIGIN=*
LOG_LEVEL=debug
PROJECT_ID=6a1d3e08d7c7b2a82853a3dd
DATABASE_URL=libsql://db-17b1345995727386ada9-sumit22.aws-ap-south-1.turso.io
DATABASE_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3ODIxOTUxMDksImlkIjoiMDE5ZWVlZDctNTAwMS03MjkwLWE4YmEtY2FlMjMwMDkzOTcxIiwicmlkIjoiOGVlNjZiZmItNmI5NS00MjdiLTlhYjMtZDljODRkMTVlZDljIn0.8QLPZHplF5ofeifQz9vrAeVp3R0mN_2bELWH4WsSvRovsz4T6jr2c1J6fTStBXOvRTkatwQ7FOdpVWNfFGI4Ag
S3_ENDPOINT=https://t3.storage.dev
S3_BUCKET=plena-staging
S3_PREFIX=backends/template-project/
S3_ACCESS_KEY_ID=tid_bBVTJLLQFGYJFbjMnPYjKvPtYZRzVXGFMrATpuMvySWEkgCEME
S3_SECRET_ACCESS_KEY=tsec_7R_ZSENyKt_7GiIyse2Y4ZgZDwsqzOWajWKrg6CJrfiHOwVPIExYMp0PtVUpEUV7hmCPip
S3_SESSION_TOKEN=

1
backend/.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules
.DS_Store
.env

View File

@@ -1,11 +1,16 @@
FROM oven/bun:1.2.18 AS base
FROM oven/bun:1.2.18 AS website-builder
WORKDIR /website
COPY website/package.json ./
RUN bun install
COPY website/ .
RUN bun run build
FROM oven/bun:1.2.18
WORKDIR /app
COPY package.json bun.lock tsconfig.json ./
RUN bun install --frozen-lockfile
COPY src ./src
COPY backend/package.json backend/bun.lock backend/tsconfig.json ./
RUN bun install --frozen-lockfile --production
COPY backend/src ./src
COPY --from=website-builder /website/dist ./public
ENV NODE_ENV=production
ENV PORT=8080

View File

@@ -8,7 +8,6 @@
"cors": "^2.8.5",
"dotenv": "^17.2.0",
"express": "^5.1.0",
"node-cron": "^4.5.0",
},
"devDependencies": {
"@types/cors": "^2.8.19",
@@ -61,7 +60,7 @@
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
@@ -127,8 +126,6 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-cron": ["node-cron@4.5.0", "", {}, "sha512-4Trh+kjvbXokyJkwQumvD5YAgeJfgHLR/sKyu71uSmxfCR5QMO1hldpvmFZOICN5pLgNY+J5Y8+ar3XKo5/4tQ=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],

View File

@@ -11,8 +11,7 @@
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^17.2.0",
"express": "^5.1.0",
"node-cron": "^4.5.0"
"express": "^5.1.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",

23
backend/public/index.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Backend Running</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; color: #333; }
.card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); text-align: center; }
h1 { margin: 0 0 0.5rem; font-size: 1.5rem; }
p { margin: 0.25rem 0; color: #666; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="card">
<h1>Backend Running</h1>
<p>API <a href="/api/health">/api/health</a></p>
<p>Frontend build not found — run <code>bun run build</code> in website/</p>
</div>
</body>
</html>

View File

@@ -1,12 +1,15 @@
import 'dotenv/config';
import path from 'path';
import { fileURLToPath } from 'url';
import cors from 'cors';
import express, { type NextFunction, type Request, type Response } from 'express';
import cron from 'node-cron';
import { buildApiPath, getConfig } from './config';
import { TursoClient } from './lib/db';
import { createHealthRouter } from './routes/health';
import { createTodosRouter } from './routes/todos';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const config = getConfig();
const app = express();
const db = new TursoClient(config);
@@ -19,24 +22,14 @@ const apiRoot = config.apiPrefix || '/';
app.use(apiRoot, createHealthRouter(config));
app.use(apiRoot, createTodosRouter(config, db));
// 1. Cron Job A: Runs every 3 minutes
cron.schedule('*/3 * * * *', () => {
console.log('[Cron A] Heartbeat executing every 3 minutes: ' + new Date().toISOString());
});
const publicDir = path.join(__dirname, '../public');
app.use(express.static(publicDir));
// 2. Cron Job B: Runs every 10 minutes
cron.schedule('*/10 * * * *', () => {
console.log('[Cron B] Heartbeat executing every 10 minutes: ' + new Date().toISOString());
});
// 3. Cloud Run Cron Tick endpoint (No-op to wake up event loop)
app.post('/_cron/tick', (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || authHeader !== 'Bearer ' + process.env.CRON_TOKEN) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
console.log('[Tick] Heartbeat received. Waking up event loop.');
res.status(200).json({ success: true });
app.get('*', (req, res, next) => {
if (req.path.startsWith(apiRoot)) return next();
res.sendFile(path.join(publicDir, 'index.html'), (err) => {
if (err) next();
});
});
app.use((_req, res) => {