/** * QUIXZOOM Capture Pipeline — API Server * * Huvudserver som exponerar: * - Upload API * - Status API * - Dataset API * - Admin API * * Teknik: Node.js, Express, Redis */ const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const Redis = require('ioredis'); const uploadRouter = require('./upload'); const app = express(); const PORT = process.env.PORT || 3000; // Redis-anslutning const redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379, password: process.env.REDIS_PASSWORD, }); // Middleware app.use(helmet()); app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'], methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], })); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minuter max: 100, // Max 100 requests per window message: { error: 'För många förfrågningar, försök igen senare' }, }); app.use('/api/', limiter); // Body parsing app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), version: '1.0.0', }); }); // API-routes app.use('/api', uploadRouter); // Dataset API — hämta träningsdata app.get('/api/dataset/:version', async (req, res) => { try { const { version } = req.params; const { format = 'json', limit = 1000, offset = 0 } = req.query; // Hämta dataset från Redis eller R2 const datasetKey = `quixzoom:dataset:${version}`; const datasetInfo = await redis.hgetall(datasetKey); if (!datasetInfo || Object.keys(datasetInfo).length === 0) { return res.status(404).json({ error: 'Dataset not found' }); } res.json({ version, format, limit: parseInt(limit), offset: parseInt(offset), total: parseInt(datasetInfo.total || 0), createdAt: datasetInfo.createdAt, url: `https://${process.env.R2_BUCKET_NAME}.r2.cloudflarestorage.com/datasets/${version}.jsonl`, }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Dataset API — lista alla versioner app.get('/api/datasets', async (req, res) => { try { const keys = await redis.keys('quizoom:dataset:*'); const datasets = []; for (const key of keys) { const info = await redis.hgetall(key); datasets.push({ version: key.replace('quixzoom:dataset:', ''), ...info, }); } res.json({ datasets }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Admin API — statistik app.get('/api/stats', async (req, res) => { try { const stats = { uploads: { total: parseInt(await redis.get('quixzoom:stats:uploads:total') || 0), today: parseInt(await redis.get(`quixzoom:stats:uploads:${new Date().toISOString().split('T')[0]}`) || 0), }, processing: { pending: await redis.llen('quixzoom:processing:queue'), completed: parseInt(await redis.get('quixzoom:stats:processing:completed') || 0), failed: parseInt(await redis.get('quixzoom:stats:processing:failed') || 0), }, taxonomy: { tagged: parseInt(await redis.get('quixzoom:stats:taxonomy:tagged') || 0), pending: parseInt(await redis.get('quixzoom:stats:taxonomy:pending') || 0), }, contributors: { total: parseInt(await redis.get('quixzoom:stats:contributors:total') || 0), active: parseInt(await redis.get('quixzoom:stats:contributors:active') || 0), }, }; res.json(stats); } catch (error) { res.status(500).json({ error: error.message }); } }); // Admin API — köstatus app.get('/api/queue/status', async (req, res) => { try { const queues = { upload: await redis.llen('quixzoom:upload:queue'), processing: await redis.llen('quixzoom:processing:queue'), ai: await redis.llen('quixzoom:ai:queue'), taxonomy: await redis.llen('quixzoom:taxonomy:queue'), dataset: await redis.llen('quixzoom:dataset:queue'), }; res.json({ queues }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Felhantering app.use((err, req, res, next) => { console.error('API Error:', err); res.status(err.status || 500).json({ error: err.message || 'Internal server error', stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, }); }); // 404 app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found' }); }); // Starta server app.listen(PORT, () => { console.log(`[SERVER] QUIXZOOM Capture Pipeline API running on port ${PORT}`); console.log(`[SERVER] Environment: ${process.env.NODE_ENV || 'development'}`); }); module.exports = app;