Files
wortkraft/server/index.js
T

120 lines
3.8 KiB
JavaScript

import express from 'express';
import cors from 'cors';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 80;
const DATA_DIR = process.env.DATA_DIR || path.resolve(__dirname, '../data_store');
const STATE_FILE = path.join(DATA_DIR, 'wortkraft_state.json');
const TRANSLATE_CACHE_FILE = path.join(DATA_DIR, 'translations_cache.json');
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
function readJson(filePath, fallback) {
try {
if (fs.existsSync(filePath)) {
const raw = fs.readFileSync(filePath, 'utf8');
return JSON.parse(raw);
}
} catch (err) {
console.error(`[WORTKRAFT] Error reading ${filePath}:`, err);
}
return fallback;
}
function writeJson(filePath, obj) {
try {
fs.writeFileSync(filePath, JSON.stringify(obj, null, 2), 'utf8');
return true;
} catch (err) {
console.error(`[WORTKRAFT] Error writing ${filePath}:`, err);
return false;
}
}
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// Healthcheck endpoint for Docker & Homepage
app.get('/api/health', (_req, res) => {
res.json({
status: 'ok',
app: 'wortkraft',
timestamp: new Date().toISOString(),
uptimeSeconds: Math.floor(process.uptime())
});
});
// Get persisted learning state (SRS deck, streak, custom words & stories)
app.get('/api/state', (_req, res) => {
const state = readJson(STATE_FILE, null);
res.json({ exists: Boolean(state), state });
});
// Save learning state
app.post('/api/state', (req, res) => {
const payload = req.body;
if (!payload || typeof payload !== 'object') {
return res.status(400).json({ error: 'Invalid state payload' });
}
payload.updatedAt = new Date().toISOString();
const ok = writeJson(STATE_FILE, payload);
res.json({ success: ok, updatedAt: payload.updatedAt });
});
// On-the-fly German -> English word translation fallback for custom texts
app.get('/api/translate', async (req, res) => {
const word = String(req.query.word || '').trim();
if (!word) {
return res.status(400).json({ error: 'Missing word parameter' });
}
const key = word.toLowerCase();
const cache = readJson(TRANSLATE_CACHE_FILE, {});
if (cache[key]) {
return res.json({ word, english: cache[key], cached: true });
}
try {
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(word)}&langpair=de|en`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
const translated = data?.responseData?.translatedText;
if (translated && translated.toLowerCase() !== key) {
cache[key] = translated;
writeJson(TRANSLATE_CACHE_FILE, cache);
return res.json({ word, english: translated, cached: false });
}
}
} catch (err) {
console.warn('[WORTKRAFT] Translation fallback error:', err.message);
}
res.json({ word, english: null });
});
// Serve static frontend files from project root
const PUBLIC_DIR = path.resolve(__dirname, '..');
app.use('/data', express.static(path.join(PUBLIC_DIR, 'data')));
app.get('/styles.css', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'styles.css')));
app.get('/app.js', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'app.js')));
app.get('/', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'index.html')));
app.use((req, res, next) => {
if (req.method === 'GET' && !req.path.startsWith('/api')) {
return res.sendFile(path.join(PUBLIC_DIR, 'index.html'));
}
next();
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`[WORTKRAFT] Server listening on port ${PORT}`);
console.log(`[WORTKRAFT] Persistent data directory: ${DATA_DIR}`);
});