const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const bodyParser = require('body-parser'); const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(bodyParser.json()); app.use(express.static(path.join(__dirname, '../public'))); // Ensure data directory exists const dataDir = path.join(__dirname, '../data'); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } // Initialize SQLite database const dbPath = process.env.DATABASE_PATH || path.join(dataDir, 'tasks.db'); const db = new sqlite3.Database(dbPath, (err) => { if (err) { console.error('Error opening database:', err); } else { console.log('Connected to SQLite database at:', dbPath); initDatabase(); } }); function initDatabase() { db.run(` CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT, size TEXT NOT NULL, reward TEXT, completed INTEGER DEFAULT 0, archived INTEGER DEFAULT 0, expand_description INTEGER DEFAULT 0, position INTEGER, subtasks TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `, (err) => { if (err) return; // Migrate: add columns if missing on existing databases db.run(`ALTER TABLE tasks ADD COLUMN archived INTEGER DEFAULT 0`, () => {}); db.run(`ALTER TABLE tasks ADD COLUMN expand_description INTEGER DEFAULT 0`, () => {}); }); } // API Routes // Get tasks — ?archived=1 returns archived, default returns active app.get('/api/tasks', (req, res) => { const archived = req.query.archived === '1' ? 1 : 0; db.all('SELECT * FROM tasks WHERE archived = ? ORDER BY position ASC, created_at ASC', [archived], (err, rows) => { if (err) { console.error('Error fetching tasks:', err); return res.status(500).json({ error: 'Failed to fetch tasks' }); } const tasks = rows.map(row => ({ id: row.id, title: row.title, description: row.description, size: row.size, reward: row.reward, completed: row.completed === 1, archived: row.archived === 1, expandDescription: row.expand_description === 1, subtasks: row.subtasks ? JSON.parse(row.subtasks) : [] })); res.json(tasks); }); }); // Create a new task app.post('/api/tasks', (req, res) => { const { title, description, size, reward, expandDescription } = req.body; const id = Date.now().toString(); db.run( `INSERT INTO tasks (id, title, description, size, reward, expand_description, archived, position) VALUES (?, ?, ?, ?, ?, ?, 0, (SELECT COALESCE(MAX(position), 0) + 1 FROM tasks WHERE archived = 0))`, [id, title, description || '', size, reward || '', expandDescription ? 1 : 0], function(err) { if (err) { console.error('Error creating task:', err); return res.status(500).json({ error: 'Failed to create task' }); } res.json({ id, title, description: description || '', size, reward: reward || '', completed: false, archived: false, expandDescription: !!expandDescription, subtasks: [] }); } ); }); // Update a task app.put('/api/tasks/:id', (req, res) => { const { id } = req.params; const { title, description, size, reward, completed, expandDescription, subtasks } = req.body; db.run( `UPDATE tasks SET title = ?, description = ?, size = ?, reward = ?, completed = ?, expand_description = ?, subtasks = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [title, description || '', size, reward || '', completed ? 1 : 0, expandDescription ? 1 : 0, JSON.stringify(subtasks || []), id], function(err) { if (err) { console.error('Error updating task:', err); return res.status(500).json({ error: 'Failed to update task' }); } if (this.changes === 0) return res.status(404).json({ error: 'Task not found' }); res.json({ success: true }); } ); }); // Archive a task app.put('/api/tasks/:id/archive', (req, res) => { const { id } = req.params; db.run( `UPDATE tasks SET archived = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [id], function(err) { if (err) return res.status(500).json({ error: 'Failed to archive task' }); if (this.changes === 0) return res.status(404).json({ error: 'Task not found' }); res.json({ success: true }); } ); }); // Unarchive (restore) a task app.put('/api/tasks/:id/unarchive', (req, res) => { const { id } = req.params; db.run( `UPDATE tasks SET archived = 0, position = (SELECT COALESCE(MAX(position), 0) + 1 FROM tasks WHERE archived = 0), updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [id], function(err) { if (err) return res.status(500).json({ error: 'Failed to unarchive task' }); if (this.changes === 0) return res.status(404).json({ error: 'Task not found' }); res.json({ success: true }); } ); }); // Delete a task app.delete('/api/tasks/:id', (req, res) => { const { id } = req.params; db.run('DELETE FROM tasks WHERE id = ?', [id], function(err) { if (err) return res.status(500).json({ error: 'Failed to delete task' }); if (this.changes === 0) return res.status(404).json({ error: 'Task not found' }); res.json({ success: true }); }); }); // Reorder tasks app.post('/api/tasks/reorder', (req, res) => { const taskIds = req.body; if (!Array.isArray(taskIds)) return res.status(400).json({ error: 'Invalid task order data' }); const promises = taskIds.map((id, index) => { return new Promise((resolve, reject) => { db.run('UPDATE tasks SET position = ? WHERE id = ?', [index, id], (err) => { if (err) reject(err); else resolve(); }); }); }); Promise.all(promises) .then(() => res.json({ success: true })) .catch(err => { console.error('Error reordering tasks:', err); res.status(500).json({ error: 'Failed to reorder tasks' }); }); }); // Serve the main HTML file app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../public/index.html')); }); app.listen(PORT, () => { console.log(`Learning Progress Tracker running on http://localhost:${PORT}`); }); process.on('SIGINT', () => { db.close((err) => { if (err) console.error('Error closing database:', err); else console.log('Database connection closed'); process.exit(0); }); });