commit bd658a822be0e0dd2a90bb991a929a5e342a4423 Author: Ada Date: Sun Apr 5 21:07:13 2026 -0400 Initial commit: Project Tracker - Node.js + Express + SQLite backend, dark-themed frontend\n\n- Express REST API (GET/POST/PUT/DELETE /api/projects)\n- SQLite database with better-sqlite3\n- Dark-themed single-page UI with filter bar and drill-down panel\n- nginx reverse proxy config\n- Deployment script diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..9d0fa4b --- /dev/null +++ b/backend/package.json @@ -0,0 +1,14 @@ +{ + "name": "project-tracker-backend", + "version": "1.0.0", + "description": "Project Tracker API", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "better-sqlite3": "^11.0.0", + "cors": "^2.8.5", + "express": "^4.18.2" + } +} diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..796a77b --- /dev/null +++ b/backend/server.js @@ -0,0 +1,103 @@ +const express = require('express'); +const sqlite3 = require('better-sqlite3'); +const cors = require('cors'); +const fs = require('fs'); +const path = require('path'); + +const app = express(); +const PORT = 3000; +const DB_PATH = '/opt/project-tracker/data/projects.db'; +const DATA_DIR = '/opt/project-tracker/data'; + +// Ensure data directory exists +if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); +} + +// Initialize database +const db = new sqlite3(DB_PATH); + +// Create table +db.exec(` + CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + priority TEXT NOT NULL, + url TEXT DEFAULT '', + notes TEXT DEFAULT '', + status TEXT DEFAULT 'Backlog', + owner TEXT DEFAULT 'Ada', + tags TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ) +`); + +// Middleware +app.use(cors()); +app.use(express.json()); + +// GET all projects +app.get('/api/projects', (req, res) => { + const { status } = req.query; + let stmt = status + ? db.prepare('SELECT * FROM projects WHERE status = ? ORDER BY CASE priority WHEN \'High\' THEN 1 WHEN \'Med-High\' THEN 2 WHEN \'Medium\' THEN 3 WHEN \'Low\' THEN 4 ELSE 5 END, created_at DESC') + : db.prepare('SELECT * FROM projects ORDER BY CASE priority WHEN \'High\' THEN 1 WHEN \'Med-High\' THEN 2 WHEN \'Medium\' THEN 3 WHEN \'Low\' THEN 4 ELSE 5 END, created_at DESC'); + const rows = status ? stmt.all(status) : stmt.all(); + res.json(rows); +}); + +// GET single project +app.get('/api/projects/:id', (req, res) => { + const stmt = db.prepare('SELECT * FROM projects WHERE id = ?'); + const row = stmt.get(req.params.id); + if (row) res.json(row); + else res.status(404).json({ error: 'Not found' }); +}); + +// POST create project +app.post('/api/projects', (req, res) => { + const { name, priority, url, notes, status, owner, tags } = req.body; + if (!name || !priority) { + return res.status(400).json({ error: 'name and priority are required' }); + } + const stmt = db.prepare(` + INSERT INTO projects (name, priority, url, notes, status, owner, tags) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + const result = stmt.run(name, priority, url || '', notes || '', status || 'Backlog', owner || 'Ada', tags || ''); + res.json({ id: result.lastInsertRowid, message: 'Created' }); +}); + +// PUT update project +app.put('/api/projects/:id', (req, res) => { + const { name, priority, url, notes, status, owner, tags } = req.body; + const fields = []; + const values = []; + if (name !== undefined) { fields.push('name = ?'); values.push(name); } + if (priority !== undefined) { fields.push('priority = ?'); values.push(priority); } + if (url !== undefined) { fields.push('url = ?'); values.push(url); } + if (notes !== undefined) { fields.push('notes = ?'); values.push(notes); } + if (status !== undefined) { fields.push('status = ?'); values.push(status); } + if (owner !== undefined) { fields.push('owner = ?'); values.push(owner); } + if (tags !== undefined) { fields.push('tags = ?'); values.push(tags); } + fields.push('updated_at = datetime(\'now\')'); + values.push(req.params.id); + if (fields.length === 1) { + return res.status(400).json({ error: 'No fields to update' }); + } + const stmt = db.prepare(`UPDATE projects SET ${fields.join(', ')} WHERE id = ?`); + stmt.run(...values); + res.json({ message: 'Updated' }); +}); + +// DELETE project +app.delete('/api/projects/:id', (req, res) => { + const stmt = db.prepare('DELETE FROM projects WHERE id = ?'); + stmt.run(req.params.id); + res.json({ message: 'Deleted' }); +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`Project Tracker API running on port ${PORT}`); +}); diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..70298c1 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Project Tracker Deployment Script +# Run on project-tracker (10.10.11.103) as root + +set -e + +echo "=== Project Tracker Deployment ===" + +# 1. Install dependencies +echo "[1/6] Installing nginx, node, npm..." +apt-get update -qq +apt-get install -y -qq nginx nodejs npm > /dev/null 2>&1 + +# 2. Create directories +echo "[2/6] Creating directories..." +mkdir -p /opt/project-tracker/backend /opt/project-tracker/data /opt/project-tracker/frontend + +# 3. Install Node dependencies +echo "[3/6] Installing Node dependencies..." +cd /opt/project-tracker/backend +npm install --silent express better-sqlite3 cors + +# 4. Deploy backend server.js (already created locally and needs to be copied) +# 5. Deploy frontend (already created locally and needs to be copied) +# 6. Start services... + +# 7. Set up systemd service +echo "[4/6] Setting up systemd service..." +cat > /etc/systemd/system/project-tracker.service << 'EOF' +[Unit] +Description=Project Tracker API +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/project-tracker/backend +ExecStart=/usr/bin/node server.js +Restart=always +RestartSec=5 +User=root + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable project-tracker +systemctl restart project-tracker + +# 8. Configure nginx +echo "[5/6] Configuring nginx..." +cp /opt/project-tracker/nginx.conf /etc/nginx/sites-available/project-tracker +ln -sf /etc/nginx/sites-available/project-tracker /etc/nginx/sites-enabled/project-tracker +nginx -t && systemctl reload nginx + +# 9. Verify +echo "[6/6] Verifying..." +sleep 2 +curl -s http://localhost:3000/api/projects | head -c 100 +echo "" +curl -s -o /dev/null -w "Frontend: %{http_code}\n" http://localhost/ +echo "" +systemctl status project-tracker --no-pager | grep -E 'Active|loaded' +echo "" +echo "=== Done! ===" +echo "Access at: http://10.10.11.103" diff --git a/frontend/favicon.png b/frontend/favicon.png new file mode 100644 index 0000000..bff6233 Binary files /dev/null and b/frontend/favicon.png differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d549c7a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,684 @@ + + + + + + Project Tracker + + + + + +
+

Project Tracker

+ +
+ +
+ + + + + + +
+ +
+
Loading...
+
+ + +
+
+
+

Project

+ +
+
+ +
+
+ + +
+
+ + + + + + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..5e42ed7 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name _; + + # Serve frontend static files + root /opt/project-tracker/frontend; + index index.html; + + # API proxy to Node backend + location /api/ { + proxy_pass http://127.0.0.1:3000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # Static files + location / { + try_files $uri $uri/ /index.html; + } +}