spawner/user-template-dictionary/app.py
XPS\Micro e811c4fe3d feat: Add user-template-dictionary with SQLite persistence
- New template: user-template-dictionary with Flask backend
- Features: Add/Edit/Delete words, SQLite database per user
- Persistent storage: Docker Volumes mount to /data/
- Modern HTML/CSS/JS Frontend with error handling
- REST API: GET/POST/PUT/DELETE endpoints
- Health checks and comprehensive logging
- Comprehensive documentation in docs/templates/DICTIONARY_TEMPLATE.md
- Updated templates.json and .env.example

Files:
- user-template-dictionary/Dockerfile
- user-template-dictionary/app.py
- user-template-dictionary/requirements.txt
- user-template-dictionary/templates/index.html
- docs/templates/DICTIONARY_TEMPLATE.md
- templates.json (updated)
- .env.example (updated)
2026-03-18 15:57:23 +01:00

268 lines
7.9 KiB
Python

"""
Persönliches Wörterbuch - Flask Backend mit SQLite
Speichert Wörter und Bedeutungen in einer persistenten Datenbank pro Benutzer
"""
from flask import Flask, render_template, request, jsonify
from datetime import datetime
import sqlite3
import os
import logging
app = Flask(__name__)
# Logging konfigurieren
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Pfad zur persistenten Datenbank (wird als Docker Volume gemountet)
DB_PATH = "/data/app.db"
os.makedirs("/data", exist_ok=True)
logger.info(f"[DICTIONARY] Database path: {DB_PATH}")
def get_db():
"""Verbindung zur SQLite-Datenbank"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
"""Erstelle Tabelle beim Start (falls noch nicht vorhanden)"""
conn = get_db()
cursor = conn.cursor()
# Prüfe ob Tabelle bereits existiert
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='words'
""")
if not cursor.fetchone():
logger.info("[DICTIONARY] Creating 'words' table...")
cursor.execute('''
CREATE TABLE words (
id INTEGER PRIMARY KEY AUTOINCREMENT,
word TEXT NOT NULL UNIQUE,
meaning TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
logger.info("[DICTIONARY] Table 'words' created successfully")
else:
logger.info("[DICTIONARY] Table 'words' already exists")
conn.close()
@app.route('/')
def index():
"""Hauptseite mit HTML-Interface"""
return render_template('index.html')
@app.route('/health')
def health():
"""Health Check Endpoint für Docker"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT 1")
conn.close()
return {'status': 'ok', 'database': 'connected'}, 200
except Exception as e:
logger.error(f"[DICTIONARY] Health check failed: {e}")
return {'status': 'error', 'message': str(e)}, 500
@app.route('/api/words', methods=['GET'])
def get_words():
"""Alle gespeicherten Wörter abrufen"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT id, word, meaning, created_at FROM words ORDER BY created_at DESC')
words = [dict(row) for row in cursor.fetchall()]
conn.close()
logger.info(f"[DICTIONARY] Retrieved {len(words)} words")
return jsonify({
'words': words,
'count': len(words)
}), 200
except Exception as e:
logger.error(f"[DICTIONARY] Error retrieving words: {e}")
return jsonify({'error': 'Fehler beim Abrufen der Wörter'}), 500
@app.route('/api/words', methods=['POST'])
def add_word():
"""Neues Wort hinzufügen"""
try:
data = request.get_json()
if not data:
return jsonify({'error': 'Keine Daten empfangen'}), 400
word = data.get('word', '').strip()
meaning = data.get('meaning', '').strip()
# Validierung
if not word or not meaning:
return jsonify({'error': 'Wort und Bedeutung sind erforderlich'}), 400
if len(word) > 255:
return jsonify({'error': 'Wort ist zu lang (max. 255 Zeichen)'}), 400
if len(meaning) > 2000:
return jsonify({'error': 'Bedeutung ist zu lang (max. 2000 Zeichen)'}), 400
conn = get_db()
cursor = conn.cursor()
try:
cursor.execute(
'INSERT INTO words (word, meaning) VALUES (?, ?)',
(word, meaning)
)
conn.commit()
word_id = cursor.lastrowid
# Neuen Eintrag zurück
cursor.execute('SELECT id, word, meaning, created_at FROM words WHERE id = ?', (word_id,))
new_word = dict(cursor.fetchone())
conn.close()
logger.info(f"[DICTIONARY] Word added: '{word}'")
return jsonify(new_word), 201
except sqlite3.IntegrityError:
conn.close()
logger.warning(f"[DICTIONARY] Duplicate word: '{word}'")
return jsonify({'error': f'Das Wort "{word}" existiert bereits'}), 409
except Exception as e:
logger.error(f"[DICTIONARY] Error adding word: {e}")
return jsonify({'error': 'Fehler beim Speichern des Wortes'}), 500
@app.route('/api/words/<int:word_id>', methods=['PUT'])
def update_word(word_id):
"""Wort aktualisieren"""
try:
data = request.get_json()
if not data:
return jsonify({'error': 'Keine Daten empfangen'}), 400
word = data.get('word', '').strip()
meaning = data.get('meaning', '').strip()
# Validierung
if not word or not meaning:
return jsonify({'error': 'Wort und Bedeutung sind erforderlich'}), 400
conn = get_db()
cursor = conn.cursor()
try:
cursor.execute(
'UPDATE words SET word = ?, meaning = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
(word, meaning, word_id)
)
conn.commit()
if cursor.rowcount == 0:
conn.close()
return jsonify({'error': 'Wort nicht gefunden'}), 404
# Aktualisiertes Wort zurück
cursor.execute('SELECT id, word, meaning, created_at FROM words WHERE id = ?', (word_id,))
updated_word = dict(cursor.fetchone())
conn.close()
logger.info(f"[DICTIONARY] Word updated: ID {word_id}")
return jsonify(updated_word), 200
except sqlite3.IntegrityError:
conn.close()
logger.warning(f"[DICTIONARY] Duplicate word on update: '{word}'")
return jsonify({'error': f'Das Wort "{word}" existiert bereits'}), 409
except Exception as e:
logger.error(f"[DICTIONARY] Error updating word: {e}")
return jsonify({'error': 'Fehler beim Aktualisieren des Wortes'}), 500
@app.route('/api/words/<int:word_id>', methods=['DELETE'])
def delete_word(word_id):
"""Wort löschen"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('DELETE FROM words WHERE id = ?', (word_id,))
conn.commit()
if cursor.rowcount == 0:
conn.close()
return jsonify({'error': 'Wort nicht gefunden'}), 404
conn.close()
logger.info(f"[DICTIONARY] Word deleted: ID {word_id}")
return '', 204
except Exception as e:
logger.error(f"[DICTIONARY] Error deleting word: {e}")
return jsonify({'error': 'Fehler beim Löschen des Wortes'}), 500
@app.route('/api/stats')
def get_stats():
"""Statistiken über die Wörterbuch-Datenbank"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) as total FROM words')
total = cursor.fetchone()['total']
cursor.execute('SELECT MAX(created_at) as last_added FROM words')
last_added = cursor.fetchone()['last_added']
conn.close()
return jsonify({
'total_words': total,
'last_added': last_added,
'database': 'sqlite3',
'storage': '/data/app.db'
}), 200
except Exception as e:
logger.error(f"[DICTIONARY] Error getting stats: {e}")
return jsonify({'error': 'Fehler beim Abrufen der Statistiken'}), 500
@app.errorhandler(404)
def not_found(error):
"""404 Handler"""
return jsonify({'error': 'Endpoint nicht gefunden'}), 404
@app.errorhandler(500)
def server_error(error):
"""500 Handler"""
logger.error(f"[DICTIONARY] Server error: {error}")
return jsonify({'error': 'Interner Fehler'}), 500
if __name__ == '__main__':
logger.info("[DICTIONARY] Starting Flask application...")
init_db()
logger.info("[DICTIONARY] Database initialized")
app.run(host='0.0.0.0', port=8080, debug=False)