#!/usr/bin/env python3
"""Fehler-Melde-Server fuer brettspiel-news.de
Mini-HTTP-Server auf Port 8888. Nur das Formular + API.
Isoliert von allen anderen Diensten. Kein Flask, nur stdlib."""
import http.server
import json
import sqlite3
import os
import urllib.parse
import urllib.request
from datetime import datetime
PORT = 8888
HOST = "127.0.0.1"
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fehler.db")
TOKEN = os.environ.get("FEHLER_TOKEN", "br3ttsp1el-f3hl3r-2026")
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
# ═══════════════════════════════════════════════════
# SQLite
# ═══════════════════════════════════════════════════
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS fehler (
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id TEXT NOT NULL,
melder_name TEXT NOT NULL,
melder_email TEXT DEFAULT '',
fehler_text TEXT NOT NULL,
status TEXT DEFAULT 'offen',
loesung TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now','localtime')),
updated_at TEXT DEFAULT (datetime('now','localtime'))
)
""")
conn.commit()
conn.close()
def save_report(article_id, name, email, text):
conn = sqlite3.connect(DB_PATH)
conn.execute(
"INSERT INTO fehler (article_id, melder_name, melder_email, fehler_text) VALUES (?,?,?,?)",
(str(article_id), name, email, text)
)
rid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
conn.commit()
conn.close()
return rid
def get_stats():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
total = conn.execute("SELECT COUNT(*) as c FROM fehler").fetchone()["c"]
offen = conn.execute("SELECT COUNT(*) as c FROM fehler WHERE status='offen'").fetchone()["c"]
behoben = conn.execute("SELECT COUNT(*) as c FROM fehler WHERE status='behoben'").fetchone()["c"]
verworfen = conn.execute("SELECT COUNT(*) as c FROM fehler WHERE status='verworfen'").fetchone()["c"]
conn.close()
return {"total": total, "offen": offen, "behoben": behoben, "verworfen": verworfen}
def get_meldungen(status_filter=None, limit=100):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
if status_filter:
rows = conn.execute(
"SELECT * FROM fehler WHERE status=? ORDER BY created_at DESC LIMIT ?",
(status_filter, limit)
).fetchall()
else:
rows = conn.execute(
"SELECT * FROM fehler ORDER BY created_at DESC LIMIT ?",
(limit,)
).fetchall()
conn.close()
return [dict(r) for r in rows]
def update_status(rid, status, loesung):
conn = sqlite3.connect(DB_PATH)
conn.execute(
"UPDATE fehler SET status=?, loesung=?, updated_at=datetime('now','localtime') WHERE id=?",
(status, loesung, rid)
)
conn.commit()
conn.close()
# ═══════════════════════════════════════════════════
# Basti-Benachrichtigung (Datei-basiert)
# ═══════════════════════════════════════════════════
FEHLER_EINGANG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "eingang")
def notify_basti(report_id, article_id, name, text):
"""Schreibt Fehlermeldung als .md-Datei für Basti."""
os.makedirs(FEHLER_EINGANG, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
fname = f"{ts}_{report_id}.md"
fpath = os.path.join(FEHLER_EINGANG, fname)
url = f"https://www.brettspiel-news.de/index.php/de/component/content/article/{article_id}"
content = f"""# 🐛 Fehlermeldung #{report_id}
**Artikel:** [#{article_id}]({url})
**Gemeldet von:** {name}
**Datum:** {datetime.now().strftime('%d.%m.%Y %H:%M')}
## Fehlerbeschreibung
{text}
---
*Status: offen | Bearbeiter: Basti*
"""
with open(fpath, "w") as f:
f.write(content)
print(f"[OK] Basti benachrichtigt → {fname}")
return fpath
# ═══════════════════════════════════════════════════
# HTML-Formular (inline, self-contained)
# ═══════════════════════════════════════════════════
FORM_HTML = r"""
Fehler melden
"""
# ═══════════════════════════════════════════════════
# JSON-Helfer
# ═══════════════════════════════════════════════════
def json_response(handler, status, data):
body = json.dumps(data, ensure_ascii=False).encode()
handler.send_response(status)
handler.send_header("Content-Type", "application/json; charset=utf-8")
handler.send_header("Access-Control-Allow-Origin", "*")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
handler.wfile.write(body)
# ═══════════════════════════════════════════════════
# HTTP-Handler
# ═══════════════════════════════════════════════════
class FehlerHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
p = urllib.parse.urlparse(self.path)
# Formular: / oder /form
if p.path in ("/", "/form"):
html = FORM_HTML.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(html)))
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
self.end_headers()
self.wfile.write(html)
return
# Stats (nur localhost)
if p.path == "/api/stats":
if self.client_address[0] not in ("127.0.0.1", "::1"):
json_response(self, 403, {"error": "forbidden"})
return
json_response(self, 200, get_stats())
return
# Meldungen (nur localhost)
if p.path == "/api/meldungen":
if self.client_address[0] not in ("127.0.0.1", "::1"):
json_response(self, 403, {"error": "forbidden"})
return
qs = urllib.parse.parse_qs(p.query)
status = qs.get("status", [None])[0]
json_response(self, 200, get_meldungen(status))
return
self.send_error(404)
def do_POST(self):
# CORS-Preflight
if self.path == "/report":
length = int(self.headers.get("Content-Length", 0))
if length == 0:
json_response(self, 400, {"error": "Leerer Request"})
return
body = self.rfile.read(length)
try:
data = json.loads(body)
except json.JSONDecodeError:
json_response(self, 400, {"error": "Ungültiges JSON"})
return
article_id = data.get("article_id", "")
melder_name = data.get("melder_name", "").strip()
melder_email = data.get("melder_email", "").strip()
fehler_text = data.get("fehler_text", "").strip()
if not melder_name:
json_response(self, 400, {"error": "Name ist erforderlich"})
return
if not fehler_text:
json_response(self, 400, {"error": "Fehlerbeschreibung ist erforderlich"})
return
rid = save_report(article_id, melder_name, melder_email, fehler_text)
notify_basti(rid, article_id, melder_name, fehler_text)
json_response(self, 201, {"id": rid, "message": "Fehler gemeldet \u2014 danke!"})
return
# Status-Update (mit Token)
if self.path == "/api/update":
length = int(self.headers.get("Content-Length", 0))
data = json.loads(self.rfile.read(length))
if data.get("token") != TOKEN:
json_response(self, 403, {"error": "forbidden"})
return
update_status(data["id"], data["status"], data.get("loesung", ""))
json_response(self, 200, {"ok": True})
return
self.send_error(404)
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def log_message(self, format, *args):
print(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}")
# ═══════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════
if __name__ == "__main__":
init_db()
print(f"🐛 Fehler-Server auf http://{HOST}:{PORT}")
print(f" Formular: http://{HOST}:{PORT}/?article_id=123")
print(f" Stats: http://{HOST}:{PORT}/api/stats")
httpd = http.server.HTTPServer((HOST, PORT), FehlerHandler)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n👋 Bye!")
httpd.server_close()