feat: add in-app release notes and what's new dialog

This commit is contained in:
2026-07-28 12:59:12 +02:00
parent 8903452e96
commit 216f53c641
8 changed files with 336 additions and 18 deletions
+112 -11
View File
@@ -22,6 +22,7 @@ import os
from datetime import datetime
import json
import re
import socket
import unicodedata
app = Flask(__name__)
@@ -59,6 +60,8 @@ BUILD_INFO_FILE = "build_info.json"
DOCS_DIR = os.path.join(app.root_path, "docs")
DEFAULT_DOC_LANGUAGE = "de"
USER_MANUAL_FILENAME = "user_manual.md"
RECENT_CHANGES_FILENAME = "recent_changes.md"
WHATS_NEW_SUMMARY_FILE = os.path.join(app.root_path, "static", "recent_changes.json")
UNKNOWN_BUILD_INFO = {
"version": "unknown",
"branch": "unknown",
@@ -91,10 +94,36 @@ def load_build_info():
BUILD_INFO = load_build_info()
def load_whats_new_summary():
"""Load compact release notes for the What's New dialog."""
try:
with open(WHATS_NEW_SUMMARY_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
print(f"[What's New Error] {e}")
return []
items = data.get("items") if isinstance(data, dict) else None
if not isinstance(items, list):
return []
return [
item.strip()
for item in items
if isinstance(item, str) and item.strip()
][:5]
WHATS_NEW_SUMMARY = load_whats_new_summary()
@app.context_processor
def inject_build_info():
"""Make build metadata available in all templates."""
return {"build_info": BUILD_INFO}
"""Make build metadata and release-note summary available in templates."""
return {
"build_info": BUILD_INFO,
"whats_new_summary": WHATS_NEW_SUMMARY
}
# ============================================================================
# AUTHENTICATION
@@ -132,6 +161,23 @@ def log_access(username, endpoint, method, status=200):
except Exception as e:
print(f"[Logging Error] {e}")
def find_free_port(start_port=5000, max_attempts=10, host="0.0.0.0"):
"""Find the first available TCP port at or above start_port."""
for port in range(start_port, start_port + max_attempts):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind((host, port))
except OSError:
continue
return port
raise RuntimeError(
f"No free port found from {start_port} to "
f"{start_port + max_attempts - 1}"
)
# ============================================================================
# ROUTES
# ============================================================================
@@ -151,9 +197,14 @@ def build_manual_renderer():
return renderer
def get_document_path(filename, language=DEFAULT_DOC_LANGUAGE):
"""Return the Markdown path for a language-specific help document."""
return os.path.join(DOCS_DIR, language, filename)
def get_manual_path(language=DEFAULT_DOC_LANGUAGE):
"""Return the Markdown path for a language-specific manual."""
return os.path.join(DOCS_DIR, language, USER_MANUAL_FILENAME)
return get_document_path(USER_MANUAL_FILENAME, language)
def get_manual_screenshot_dir(language=DEFAULT_DOC_LANGUAGE):
@@ -199,13 +250,13 @@ def add_heading_ids_and_toc(tokens):
return toc
def render_user_manual(language=DEFAULT_DOC_LANGUAGE):
"""Read and render the Markdown user manual into HTML and a generated TOC."""
def render_markdown_document(filename, language=DEFAULT_DOC_LANGUAGE):
"""Read and render a Markdown help document into HTML and a generated TOC."""
renderer = build_manual_renderer()
manual_path = get_manual_path(language)
document_path = get_document_path(filename, language)
with open(manual_path, "r", encoding="utf-8") as manual_file:
markdown_text = manual_file.read()
with open(document_path, "r", encoding="utf-8") as document_file:
markdown_text = document_file.read()
markdown_text = rewrite_manual_asset_paths(markdown_text, language)
tokens = renderer.parse(markdown_text)
@@ -213,6 +264,16 @@ def render_user_manual(language=DEFAULT_DOC_LANGUAGE):
html = renderer.renderer.render(tokens, renderer.options, {})
return Markup(html), toc
def render_user_manual(language=DEFAULT_DOC_LANGUAGE):
"""Read and render the Markdown user manual."""
return render_markdown_document(USER_MANUAL_FILENAME, language)
def render_recent_changes(language=DEFAULT_DOC_LANGUAGE):
"""Read and render the Markdown recent changes document."""
return render_markdown_document(RECENT_CHANGES_FILENAME, language)
@app.route("/", methods=["GET"])
@auth.login_required
def index():
@@ -236,6 +297,9 @@ def user_manual():
manual_html, toc = render_user_manual(DEFAULT_DOC_LANGUAGE)
return render_template(
"user_manual.html",
document_title="User Manual",
document_subtitle="RollCalc technical user documentation",
error_title="User Manual unavailable",
manual_html=manual_html,
toc=toc,
render_error=None
@@ -244,6 +308,9 @@ def user_manual():
app.logger.exception("Could not render user manual")
return render_template(
"user_manual.html",
document_title="User Manual",
document_subtitle="RollCalc technical user documentation",
error_title="User Manual unavailable",
manual_html=None,
toc=[],
render_error=(
@@ -252,6 +319,39 @@ def user_manual():
)
), 500
@app.route("/help/recent-changes", methods=["GET"])
@auth.login_required
def recent_changes():
"""Render recent changes inside the RollCalc Help layout."""
log_access(auth.current_user(), "/help/recent-changes", "GET")
try:
changes_html, toc = render_recent_changes(DEFAULT_DOC_LANGUAGE)
return render_template(
"user_manual.html",
document_title="Recent Changes",
document_subtitle="RollCalc release notes",
error_title="Recent Changes unavailable",
manual_html=changes_html,
toc=toc,
render_error=None
)
except Exception:
app.logger.exception("Could not render recent changes")
return render_template(
"user_manual.html",
document_title="Recent Changes",
document_subtitle="RollCalc release notes",
error_title="Recent Changes unavailable",
manual_html=None,
toc=[],
render_error=(
"Recent changes could not be loaded. "
"Please contact the RollCalc maintainer if the problem persists."
)
), 500
@app.route("/help/user-manual/screenshots/<path:filename>", methods=["GET"])
@auth.login_required
def manual_screenshot(filename, language=DEFAULT_DOC_LANGUAGE):
@@ -317,21 +417,22 @@ def internal_error(e):
if __name__ == "__main__":
os.makedirs("templates", exist_ok=True)
os.makedirs("static", exist_ok=True)
port = find_free_port(start_port=5000, max_attempts=10, host="0.0.0.0")
print("""
print(f"""
╔═══════════════════════════════════════════════════════════╗
║ Naue Roll Calculator - V14 + QoL Update ║
║ HTTP Basic Auth Enabled ║
║ ║
║ ⚠️ BEFORE PRODUCTION: Update passwords in app.py ║
║ ║
║ Starten auf: http://localhost:5000 ║
║ Starten auf: http://localhost:{port:<5} ║
╚═══════════════════════════════════════════════════════════╝
""")
app.run(
host="0.0.0.0",
port=5000,
port=port,
debug=False,
use_reloader=False
)