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 @@ + +

Naue Roll Diameter Calculator

Roll Diameter • Product Length • Roll Weight • Load Optimizer

- Help + User Manual
@@ -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 }}