feat: initial sbsports deployment setup

Add Coolify/Woodpecker CI config, .gitignore, and deployment scripts.
This commit is contained in:
alexandrump
2026-04-22 01:01:42 +02:00
commit 78c5ed52ac
32 changed files with 6088 additions and 0 deletions

45
server/db.js Normal file
View File

@@ -0,0 +1,45 @@
import Database from "better-sqlite3";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { mkdirSync } from "fs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const DATA_DIR = process.env.DATA_DIR || join(__dirname, "../data");
mkdirSync(DATA_DIR, { recursive: true });
const db = new Database(join(DATA_DIR, "db.sqlite"));
// Enable WAL for better concurrent read performance
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
db.exec(`
CREATE TABLE IF NOT EXISTS students (
id TEXT PRIMARY KEY,
firstName TEXT NOT NULL,
lastName TEXT NOT NULL,
age INTEGER,
sex TEXT,
weight REAL,
height REAL,
imc REAL
);
CREATE TABLE IF NOT EXISTS activities (
id TEXT PRIMARY KEY,
studentId TEXT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
type TEXT,
durationInput REAL,
durationUnit TEXT,
duration REAL,
displayDuration TEXT,
intensity TEXT,
lifestyle TEXT,
anxiety INTEGER DEFAULT 0,
grade TEXT,
date TEXT,
createdAt TEXT DEFAULT (datetime('now'))
);
`);
export default db;

150
server/index.js Normal file
View File

@@ -0,0 +1,150 @@
import express from "express";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import db from "./db.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
// Serve Vue build in production
const distPath = join(__dirname, "../dist");
app.use(express.static(distPath));
// ── Students ──────────────────────────────────────────────────────────────
app.get("/api/students", (_req, res) => {
res.json(
db.prepare("SELECT * FROM students ORDER BY lastName, firstName").all(),
);
});
app.post("/api/students", (req, res) => {
const { id, firstName, lastName, age, sex, weight, height, imc } = req.body;
db.prepare(
`
INSERT INTO students (id, firstName, lastName, age, sex, weight, height, imc)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(id, firstName, lastName, age, sex, weight, height, imc);
res.status(201).json({ id });
});
app.put("/api/students/:id", (req, res) => {
const { firstName, lastName, age, sex, weight, height, imc } = req.body;
const info = db
.prepare(
`
UPDATE students SET firstName=?, lastName=?, age=?, sex=?, weight=?, height=?, imc=?
WHERE id=?
`,
)
.run(firstName, lastName, age, sex, weight, height, imc, req.params.id);
if (info.changes === 0) return res.status(404).json({ error: "Not found" });
res.json({ ok: true });
});
app.delete("/api/students/:id", (req, res) => {
// CASCADE deletes linked activities automatically (FK ON DELETE CASCADE)
db.prepare("DELETE FROM students WHERE id=?").run(req.params.id);
res.json({ ok: true });
});
// ── Activities ────────────────────────────────────────────────────────────
app.get("/api/activities", (_req, res) => {
res.json(
db.prepare("SELECT * FROM activities ORDER BY createdAt DESC").all(),
);
});
app.post("/api/activities", (req, res) => {
const {
id,
studentId,
type,
durationInput,
durationUnit,
duration,
displayDuration,
intensity,
lifestyle,
anxiety,
grade,
date,
} = req.body;
db.prepare(
`
INSERT INTO activities
(id, studentId, type, durationInput, durationUnit, duration, displayDuration, intensity, lifestyle, anxiety, grade, date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
id,
studentId,
type,
durationInput,
durationUnit,
duration,
displayDuration,
intensity,
lifestyle,
anxiety ?? 0,
grade,
date,
);
res.status(201).json({ id });
});
app.put("/api/activities/:id", (req, res) => {
const {
type,
durationInput,
durationUnit,
duration,
displayDuration,
intensity,
lifestyle,
anxiety,
grade,
date,
} = req.body;
const info = db
.prepare(
`
UPDATE activities
SET type=?, durationInput=?, durationUnit=?, duration=?, displayDuration=?,
intensity=?, lifestyle=?, anxiety=?, grade=?, date=?
WHERE id=?
`,
)
.run(
type,
durationInput,
durationUnit,
duration,
displayDuration,
intensity,
lifestyle,
anxiety ?? 0,
grade,
date,
req.params.id,
);
if (info.changes === 0) return res.status(404).json({ error: "Not found" });
res.json({ ok: true });
});
app.delete("/api/activities/:id", (req, res) => {
db.prepare("DELETE FROM activities WHERE id=?").run(req.params.id);
res.json({ ok: true });
});
// ── SPA fallback ──────────────────────────────────────────────────────────
app.get("*", (_req, res) => {
res.sendFile(join(distPath, "index.html"));
});
app.listen(PORT, () => console.log(`SB Sports API running on :${PORT}`));