first commit
This commit is contained in:
commit
0e1ce93def
12 files changed
+1693
No files matched your search
@@ -0,0 +1,9 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const dbPath = path.join(__dirname, '..', 'db', 'tachyon.db');
|
||||
|
||||
export const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
@@ -0,0 +1,21 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { newsRouter } from './routes/news.js';
|
||||
import { mediaRouter } from './routes/media.js';
|
||||
import { statsRouter } from './routes/stats.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.use('/api/news', newsRouter);
|
||||
app.use('/api/media', mediaRouter);
|
||||
app.use('/api/stats', statsRouter);
|
||||
|
||||
app.get('/api/health', (req, res) => res.json({ ok: true }));
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`tachyon-api listening on http://localhost:${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Router } from 'express';
|
||||
import { db } from '../db.js';
|
||||
|
||||
export const mediaRouter = Router();
|
||||
|
||||
// GET /api/media?limit=12
|
||||
mediaRouter.get('/', (req, res) => {
|
||||
const limit = Math.min(Number(req.query.limit) || 24, 100);
|
||||
const rows = db
|
||||
.prepare('SELECT id, title, media_type, url, created_at FROM media_items ORDER BY created_at DESC LIMIT ?')
|
||||
.all(limit);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// POST /api/media { title, media_type: 'image' | 'video', url }
|
||||
mediaRouter.post('/', (req, res) => {
|
||||
const { title, media_type, url } = req.body ?? {};
|
||||
if (!title || !url || !['image', 'video'].includes(media_type)) {
|
||||
return res.status(400).json({ error: 'title, url, and media_type (image|video) are required' });
|
||||
}
|
||||
const result = db
|
||||
.prepare('INSERT INTO media_items (title, media_type, url) VALUES (?, ?, ?)')
|
||||
.run(title, media_type, url);
|
||||
const created = db
|
||||
.prepare('SELECT id, title, media_type, url, created_at FROM media_items WHERE id = ?')
|
||||
.get(result.lastInsertRowid);
|
||||
res.status(201).json(created);
|
||||
});
|
||||
|
||||
mediaRouter.delete('/:id', (req, res) => {
|
||||
const result = db.prepare('DELETE FROM media_items WHERE id = ?').run(req.params.id);
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'not found' });
|
||||
res.status(204).end();
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Router } from 'express';
|
||||
import { db } from '../db.js';
|
||||
|
||||
export const newsRouter = Router();
|
||||
|
||||
// GET /api/news?limit=10
|
||||
newsRouter.get('/', (req, res) => {
|
||||
const limit = Math.min(Number(req.query.limit) || 20, 100);
|
||||
const rows = db
|
||||
.prepare('SELECT id, title, content, created_at FROM news_posts ORDER BY created_at DESC LIMIT ?')
|
||||
.all(limit);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// POST /api/news { title, content }
|
||||
newsRouter.post('/', (req, res) => {
|
||||
const { title, content } = req.body ?? {};
|
||||
if (!title || !content) {
|
||||
return res.status(400).json({ error: 'title and content are required' });
|
||||
}
|
||||
const result = db
|
||||
.prepare('INSERT INTO news_posts (title, content) VALUES (?, ?)')
|
||||
.run(title, content);
|
||||
const created = db
|
||||
.prepare('SELECT id, title, content, created_at FROM news_posts WHERE id = ?')
|
||||
.get(result.lastInsertRowid);
|
||||
res.status(201).json(created);
|
||||
});
|
||||
|
||||
// DELETE /api/news/:id
|
||||
newsRouter.delete('/:id', (req, res) => {
|
||||
const result = db.prepare('DELETE FROM news_posts WHERE id = ?').run(req.params.id);
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'not found' });
|
||||
res.status(204).end();
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Router } from 'express';
|
||||
import { db } from '../db.js';
|
||||
|
||||
export const statsRouter = Router();
|
||||
|
||||
// GET /api/stats/overview
|
||||
// Bundles everything the "Overview" panel needs in one call, since the
|
||||
// frontend renders all three columns (users / activity / general) together.
|
||||
statsRouter.get('/overview', (req, res) => {
|
||||
const currentUsers = db
|
||||
.prepare(`
|
||||
SELECT u.id, u.username, s.started_at
|
||||
FROM sessions s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.ended_at IS NULL
|
||||
ORDER BY s.started_at DESC
|
||||
LIMIT 20
|
||||
`)
|
||||
.all();
|
||||
|
||||
const recentActivity = db
|
||||
.prepare('SELECT id, description, created_at FROM activity_log ORDER BY created_at DESC LIMIT 20')
|
||||
.all();
|
||||
|
||||
const statRows = db.prepare('SELECT key, value FROM site_stats').all();
|
||||
const stats = Object.fromEntries(statRows.map((r) => [r.key, r.value]));
|
||||
|
||||
const totalUsers = db.prepare('SELECT COUNT(*) AS count FROM users').get().count;
|
||||
const mostRecentUser = db
|
||||
.prepare('SELECT username, id FROM users ORDER BY joined_at DESC LIMIT 1')
|
||||
.get();
|
||||
|
||||
res.json({
|
||||
currentUsers,
|
||||
recentActivity,
|
||||
general: {
|
||||
users: { total: totalUsers, mostRecent: mostRecentUser ?? null },
|
||||
reports: {
|
||||
named: stats.reports_named ?? '0',
|
||||
associated: stats.reports_associated ?? '0',
|
||||
logged: stats.reports_logged ?? '0',
|
||||
},
|
||||
build: {
|
||||
pipeline: stats.build_pipeline ?? 'unknown',
|
||||
buildNumber: stats.build_number ?? '0',
|
||||
version: stats.version ?? '0.0.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// GET /api/stats/users (just the current-users list, if you want it standalone)
|
||||
statsRouter.get('/users', (req, res) => {
|
||||
const rows = db
|
||||
.prepare(`
|
||||
SELECT u.id, u.username, s.started_at
|
||||
FROM sessions s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.ended_at IS NULL
|
||||
ORDER BY s.started_at DESC
|
||||
`)
|
||||
.all();
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// POST /api/stats/session/start { username }
|
||||
statsRouter.post('/session/start', (req, res) => {
|
||||
const { username } = req.body ?? {};
|
||||
if (!username) return res.status(400).json({ error: 'username is required' });
|
||||
|
||||
let user = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||||
if (!user) {
|
||||
const result = db.prepare('INSERT INTO users (username) VALUES (?)').run(username);
|
||||
user = { id: result.lastInsertRowid };
|
||||
}
|
||||
db.prepare('UPDATE users SET last_seen_at = strftime(\'%Y-%m-%dT%H:%M:%fZ\', \'now\') WHERE id = ?').run(user.id);
|
||||
const session = db.prepare('INSERT INTO sessions (user_id) VALUES (?)').run(user.id);
|
||||
res.status(201).json({ sessionId: session.lastInsertRowid, userId: user.id });
|
||||
});
|
||||
|
||||
// POST /api/stats/session/end { sessionId }
|
||||
statsRouter.post('/session/end', (req, res) => {
|
||||
const { sessionId } = req.body ?? {};
|
||||
if (!sessionId) return res.status(400).json({ error: 'sessionId is required' });
|
||||
const result = db
|
||||
.prepare('UPDATE sessions SET ended_at = strftime(\'%Y-%m-%dT%H:%M:%fZ\', \'now\') WHERE id = ? AND ended_at IS NULL')
|
||||
.run(sessionId);
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'no active session with that id' });
|
||||
res.status(204).end();
|
||||
});
|
||||
Reference in new issue
Block a user