Feste ~20er-Taxonomie geseedet (de/en/sv, published; bestehende Kategorien werden wiederverwendet) + Tabelle category_batches. src/lib/classifyWords.js: findet in Pairs verwendete Wörter ohne Kategorie und klassifiziert sie per Haiku gegen die feste Liste. - Stundenjob über die Message Batches API (asynchron, ~50% günstiger): submit/collect-Ticks, in index.js nach Boot + stündlich. - Sofortiger synchroner One-Shot-Backfill (classifyWordsSync) für Live-Test ohne 24h-Verzug. Beides materialisiert pair_categories via derivePairCategories. POST /api/categories/auto-assign (admin): ?sync=true = Sofort-Backfill, sonst ein Batch-Tick. Entkoppelt von generate-words und Publish. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
2.8 KiB
JavaScript
75 lines
2.8 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const auth = require('./middleware/auth');
|
|
const { pool } = require('./db');
|
|
const migrate = require('./db-migrate');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// Health check — always 200 so Coolify doesn't kill the container
|
|
app.get('/health', async (req, res) => {
|
|
let db = 'connected';
|
|
try {
|
|
await pool.query('SELECT 1');
|
|
} catch (err) {
|
|
db = err.message;
|
|
}
|
|
res.json({ status: 'ok', db });
|
|
});
|
|
|
|
// Public routes
|
|
app.use('/auth', require('./routes/auth'));
|
|
app.use('/auth/feed', require('./routes/feed').router);
|
|
|
|
// Routes — protected by Bearer token
|
|
app.use('/api', auth, require('./routes/index'));
|
|
app.use('/api/pictures', auth, require('./routes/pictures'));
|
|
app.use('/api/words', auth, require('./routes/words'));
|
|
app.use('/api/categories', auth, require('./routes/categories'));
|
|
app.use('/api/objects', auth, require('./routes/objects'));
|
|
app.use('/api/pairs', auth, require('./routes/pairs'));
|
|
app.use('/api/questions', auth, require('./routes/questions'));
|
|
app.use('/api/statements', auth, require('./routes/statements'));
|
|
app.use('/api/blocklist', auth, require('./routes/blocklist'));
|
|
app.use('/api/languages', auth, require('./routes/languages'));
|
|
app.use('/api/user-names', auth, require('./routes/user-names'));
|
|
app.use('/api/users-public', auth, require('./routes/users-public'));
|
|
app.use('/api/users', auth, require('./routes/users'));
|
|
app.use('/api/audios', auth, require('./routes/audios'));
|
|
app.use('/api/tts-settings', auth, require('./routes/tts-settings'));
|
|
app.use('/api/claude', auth, require('./routes/claude'));
|
|
app.use('/api/pipeline', auth, require('./routes/pipeline'));
|
|
|
|
// 404
|
|
app.use((req, res) => {
|
|
res.status(404).json({ error: 'Not found' });
|
|
});
|
|
|
|
// Error handler
|
|
app.use((err, req, res, next) => {
|
|
console.error(err.stack);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
|
|
migrate()
|
|
.then(() => {
|
|
app.listen(PORT, '0.0.0.0', () => console.log(`snakkimo-API running on port ${PORT}`));
|
|
// Hängengebliebene Pipeline-Läufe (z.B. nach Redeploy) wieder aufnehmen
|
|
require('./lib/pipeline').resumePending()
|
|
.catch(err => console.error('Pipeline-Resume fehlgeschlagen:', err));
|
|
|
|
// Automatische Wort-Kategorisierung (Message Batches API): kurz nach Boot + stündlich.
|
|
// Submit/Collect-Ticks, entkoppelt von generate-words und Publish.
|
|
const { runCategorizationTick } = require('./lib/classifyWords');
|
|
const HOUR = 60 * 60 * 1000;
|
|
const tick = () => runCategorizationTick().catch(err => console.error('Auto-Kategorisierung:', err.message));
|
|
setTimeout(tick, 30_000);
|
|
setInterval(tick, HOUR);
|
|
})
|
|
.catch(err => { console.error('Migration failed:', err); process.exit(1); });
|