From 216f53c6419b18c0d7d40f93c604dd333125b1e4 Mon Sep 17 00:00:00 2001
From: Martin Tazl
Date: Tue, 28 Jul 2026 12:59:12 +0200
Subject: [PATCH] feat: add in-app release notes and what's new dialog
---
PROJECT_KNOWLEDGE.md | 25 ++++++
README.md | 27 ++++++
app.py | 123 +++++++++++++++++++++++++---
docs/de/recent_changes.md | 9 ++
static/recent_changes.json | 8 ++
templates/help_index.html | 7 ++
templates/roll_calculator.html | 145 ++++++++++++++++++++++++++++++++-
templates/user_manual.html | 10 +--
8 files changed, 336 insertions(+), 18 deletions(-)
create mode 100644 docs/de/recent_changes.md
create mode 100644 static/recent_changes.json
diff --git a/PROJECT_KNOWLEDGE.md b/PROJECT_KNOWLEDGE.md
index 411f7e3..7ccc804 100644
--- a/PROJECT_KNOWLEDGE.md
+++ b/PROJECT_KNOWLEDGE.md
@@ -18,6 +18,7 @@ Backend:
- `/` renders the active calculator template.
- `/help` renders the authenticated Help landing page.
- `/help/user-manual` renders `docs/de/user_manual.md` server-side inside the RollCalc Help layout.
+- `/help/recent-changes` renders `docs/de/recent_changes.md` server-side inside the RollCalc Help layout.
- `/help/user-manual/screenshots/` serves German manual screenshots from `docs/de/screenshots/`.
- `/static/` is intended to serve static files behind Basic Auth.
- `/api/health` returns health/version information.
@@ -77,6 +78,30 @@ Rendering rules:
The Help pages use the same HTTP Basic Auth protection as the calculator.
+### Recent Changes and What's New
+
+Full user-facing release notes are maintained in:
+
+```text
+docs/de/recent_changes.md
+```
+
+The calculator header links directly to `/help/user-manual`. The Help landing page lists only existing documents, currently User Manual and Recent Changes.
+
+The startup What's New dialog uses `build_info.version` and is suppressed when the version is empty or `unknown`. The last acknowledged version is stored only in browser `localStorage` under:
+
+```text
+rollcalc_last_seen_version
+```
+
+The short dialog item list is maintained separately in:
+
+```text
+static/recent_changes.json
+```
+
+This JSON is a compact summary for the dialog, not the complete release history. Keep it aligned with the most relevant current entries from `docs/de/recent_changes.md`.
+
## Authentication
RollCalc uses HTTP Basic Auth. User entries in `BETA_USERS` have this shape:
diff --git a/README.md b/README.md
index e5e0f94..4a375b0 100644
--- a/README.md
+++ b/README.md
@@ -94,6 +94,7 @@ Implemented routes:
| `/` | `GET` | Basic Auth | Renders `templates/roll_calculator.html`. |
| `/help` | `GET` | Basic Auth | Renders the Help landing page. |
| `/help/user-manual` | `GET` | Basic Auth | Reads `docs/de/user_manual.md`, renders it as HTML, and displays it in the RollCalc Help layout. |
+| `/help/recent-changes` | `GET` | Basic Auth | Reads `docs/de/recent_changes.md`, renders it as HTML, and displays it in the RollCalc Help layout. |
| `/help/user-manual/screenshots/` | `GET` | Basic Auth | Serves German user-manual screenshots from `docs/de/screenshots/` only. |
| `/static/` | `GET` | Basic Auth | Intended protected static-file serving from `static/`. |
| `/api/health` | `GET` | Basic Auth | Returns app health and version. |
@@ -132,8 +133,16 @@ The user manual is available at:
/help/user-manual
```
+Recent changes are available at:
+
+```text
+/help/recent-changes
+```
+
`docs/de/user_manual.md` is the leading source for the German user manual. The app reads this Markdown file on request and renders it server-side with `markdown-it-py`; no generated static HTML copy is maintained. Raw HTML in the Markdown source is disabled during rendering.
+`docs/de/recent_changes.md` is the leading source for full German release notes. It is rendered through the same authenticated Markdown infrastructure.
+
The documentation directory is prepared for additional languages:
```text
@@ -153,6 +162,24 @@ docs/de/screenshots/
The application rewrites those relative Markdown image paths to the authenticated manual screenshot route, which is limited to the active language's screenshot directory.
+### What's New Dialog
+
+The calculator page shows a compact What's New dialog once per build version. The current version comes from `build_info.version`; if that value is empty or `unknown`, the dialog is not shown.
+
+The last acknowledged version is stored only in the browser:
+
+```text
+rollcalc_last_seen_version
+```
+
+The short dialog summary is maintained in:
+
+```text
+static/recent_changes.json
+```
+
+Keep `docs/de/recent_changes.md` as the complete release-note source and `static/recent_changes.json` as the short 3-5 item summary for the startup dialog.
+
## Frontend Entry Point
The active UI is `templates/roll_calculator.html`.
diff --git a/app.py b/app.py
index b40267e..da95ea3 100644
--- a/app.py
+++ b/app.py
@@ -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/", 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
)
diff --git a/docs/de/recent_changes.md b/docs/de/recent_changes.md
new file mode 100644
index 0000000..8a7daa4
--- /dev/null
+++ b/docs/de/recent_changes.md
@@ -0,0 +1,9 @@
+# Recent Changes
+
+## Version 0.4
+
+- User Manual integrated into RollCalc.
+- Help area added for internal documentation.
+- Capacity Analysis presentation improved.
+- Governing Constraint highlighted more clearly.
+- Remaining Capacity added to the Capacity Summary.
diff --git a/static/recent_changes.json b/static/recent_changes.json
new file mode 100644
index 0000000..bb8016f
--- /dev/null
+++ b/static/recent_changes.json
@@ -0,0 +1,8 @@
+{
+ "items": [
+ "User Manual integrated",
+ "Capacity Analysis improved",
+ "Governing Constraint clarified",
+ "Remaining Capacity added"
+ ]
+}
diff --git a/templates/help_index.html b/templates/help_index.html
index 525938a..205f500 100644
--- a/templates/help_index.html
+++ b/templates/help_index.html
@@ -57,6 +57,13 @@
Open User Manual
+
+ Recent Changes
+
+ Compact release notes for relevant user-facing changes in RollCalc.
+
+ Open Recent Changes
+
diff --git a/templates/roll_calculator.html b/templates/roll_calculator.html
index 7a7c53f..d31b31c 100644
--- a/templates/roll_calculator.html
+++ b/templates/roll_calculator.html
@@ -146,6 +146,59 @@
background: #b8c7bf;
cursor: not-allowed;
}
+ .whats-new-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 9000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ background: rgba(20, 28, 34, 0.55);
+ }
+ .whats-new-overlay.is-hidden { display: none; }
+ .whats-new-dialog {
+ width: min(520px, 100%);
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 18px 42px rgba(0, 0, 0, 0.24);
+ overflow: hidden;
+ }
+ .whats-new-header {
+ padding: 16px 20px;
+ background: #1F5438;
+ color: #fff;
+ }
+ .whats-new-header h2 { font-size: 18px; margin: 0 0 4px; }
+ .whats-new-version { font-size: 12px; opacity: 0.88; }
+ .whats-new-content {
+ padding: 18px 20px 8px;
+ font-size: 14px;
+ line-height: 1.5;
+ color: #333;
+ }
+ .whats-new-content ul { margin: 8px 0 0 20px; }
+ .whats-new-content li { margin-bottom: 5px; }
+ .whats-new-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ padding: 16px 20px 18px;
+ border-top: 1px solid #e0e8f0;
+ background: #f8fafa;
+ }
+ .whats-new-btn {
+ border: none;
+ border-radius: 6px;
+ padding: 9px 14px;
+ font-size: 13px;
+ font-weight: 700;
+ cursor: pointer;
+ }
+ .whats-new-btn-primary { background: #2BAC70; color: #fff; }
+ .whats-new-btn-primary:hover { background: #1F5438; }
+ .whats-new-btn-secondary { background: #fff; color: #1F5438; border: 1px solid #a8c8ba; }
+ .whats-new-btn-secondary:hover { background: #eef4f1; }
/* ============================================
LOAD OPTIMIZER STYLES
@@ -453,6 +506,8 @@
@media (max-width: 768px) {
.header { padding: 18px 16px; }
.header-inner { align-items: flex-start; }
+ .whats-new-actions { flex-direction: column; }
+ .whats-new-btn { width: 100%; }
.lo-accordion-header h3 { font-size: 14px; }
.lo-preset-buttons { flex-direction: column; }
.lo-preset-btn { width: 100%; }
@@ -496,13 +551,29 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -1573,12 +1644,81 @@ document.addEventListener('DOMContentLoaded', () => {
window.loadOptimizerUI = new LoadOptimizerUI();
});
+const ROLLCALC_BUILD_VERSION = {{ build_info.version|tojson }};
+const ROLLCALC_WHATS_NEW_ITEMS = {{ whats_new_summary|tojson }};
+const ROLLCALC_LAST_SEEN_VERSION_KEY = 'rollcalc_last_seen_version';
+
+function shouldShowWhatsNew(currentVersion, storedVersion) {
+ return !!currentVersion &&
+ currentVersion !== 'unknown' &&
+ currentVersion !== storedVersion;
+}
+
+function markWhatsNewSeen(currentVersion) {
+ if (!currentVersion || currentVersion === 'unknown') return;
+ try {
+ localStorage.setItem(ROLLCALC_LAST_SEEN_VERSION_KEY, currentVersion);
+ } catch (e) {
+ console.warn('Could not store RollCalc release note state', e);
+ }
+}
+
+function renderWhatsNewItems(items) {
+ const list = document.getElementById('whatsNewList');
+ if (!list) return;
+
+ list.innerHTML = '';
+ items.forEach(item => {
+ const li = document.createElement('li');
+ li.textContent = item;
+ list.appendChild(li);
+ });
+}
+
+function maybeShowWhatsNew() {
+ const overlay = document.getElementById('whatsNewOverlay');
+ const versionValue = document.getElementById('whatsNewVersion');
+ const continueBtn = document.getElementById('whatsNewContinueBtn');
+ const openChangesBtn = document.getElementById('whatsNewOpenChangesBtn');
+ const items = Array.isArray(ROLLCALC_WHATS_NEW_ITEMS)
+ ? ROLLCALC_WHATS_NEW_ITEMS
+ : [];
+
+ if (!overlay || !versionValue || !continueBtn || !openChangesBtn || items.length === 0) return;
+
+ let storedVersion = null;
+ try {
+ storedVersion = localStorage.getItem(ROLLCALC_LAST_SEEN_VERSION_KEY);
+ } catch (e) {
+ storedVersion = null;
+ }
+
+ if (!shouldShowWhatsNew(ROLLCALC_BUILD_VERSION, storedVersion)) return;
+
+ versionValue.textContent = ROLLCALC_BUILD_VERSION;
+ renderWhatsNewItems(items);
+ overlay.classList.remove('is-hidden');
+
+ continueBtn.addEventListener('click', () => {
+ markWhatsNewSeen(ROLLCALC_BUILD_VERSION);
+ overlay.classList.add('is-hidden');
+ }, { once: true });
+
+ openChangesBtn.addEventListener('click', () => {
+ markWhatsNewSeen(ROLLCALC_BUILD_VERSION);
+ window.location.href = {{ url_for('recent_changes')|tojson }};
+ }, { once: true });
+}
+
document.addEventListener('DOMContentLoaded', () => {
const overlay = document.getElementById('disclaimerOverlay');
const checkbox = document.getElementById('disclaimerAccepted');
const confirmBtn = document.getElementById('disclaimerConfirmBtn');
- if (!overlay || !checkbox || !confirmBtn) return;
+ if (!overlay || !checkbox || !confirmBtn) {
+ maybeShowWhatsNew();
+ return;
+ }
checkbox.addEventListener('change', () => {
confirmBtn.disabled = !checkbox.checked;
@@ -1587,6 +1727,7 @@ document.addEventListener('DOMContentLoaded', () => {
confirmBtn.addEventListener('click', () => {
if (!checkbox.checked) return;
overlay.classList.add('is-hidden');
+ maybeShowWhatsNew();
});
});
diff --git a/templates/user_manual.html b/templates/user_manual.html
index c85576e..697be68 100644
--- a/templates/user_manual.html
+++ b/templates/user_manual.html
@@ -3,8 +3,8 @@
-
- RollCalc User Manual
+
+ RollCalc {{ document_title }}