feat: add in-app release notes and what's new dialog
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user