From 8903452e9618cfb3bd1b5500c891590242069699 Mon Sep 17 00:00:00 2001 From: Martin Tazl Date: Tue, 28 Jul 2026 12:15:38 +0200 Subject: [PATCH] feat: integrate user manual into help section --- PROJECT_KNOWLEDGE.md | 27 +++++++ README.md | 46 ++++++++++- app.py | 142 ++++++++++++++++++++++++++++++++- docs/{ => de}/user_manual.md | 0 requirements.txt | 1 + templates/help_index.html | 73 +++++++++++++++++ templates/roll_calculator.html | 14 +++- templates/user_manual.html | 110 +++++++++++++++++++++++++ 8 files changed, 409 insertions(+), 4 deletions(-) rename docs/{ => de}/user_manual.md (100%) create mode 100644 templates/help_index.html create mode 100644 templates/user_manual.html diff --git a/PROJECT_KNOWLEDGE.md b/PROJECT_KNOWLEDGE.md index d3dc7a2..411f7e3 100644 --- a/PROJECT_KNOWLEDGE.md +++ b/PROJECT_KNOWLEDGE.md @@ -16,6 +16,9 @@ Backend: - HTTP Basic Auth is implemented with `Flask-HTTPAuth`. - Users are currently configured in `BETA_USERS` with Werkzeug password hashes. - `/` 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/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. - `/api/user` returns the authenticated user. @@ -50,6 +53,30 @@ If `build_info.json` is missing, malformed, or does not contain a usable value, The data is loaded centrally in `app.py` and made available to every template as `build_info`. +## Help and User Manual + +The in-app Help area keeps `docs/de/user_manual.md` as the leading German documentation source. RollCalc reads the Markdown file on each `/help/user-manual` request and renders it server-side with `markdown-it-py`. + +The documentation structure is prepared for language-specific manuals: + +```text +docs/ + de/ + user_manual.md + screenshots/ +``` + +Additional languages can follow the same structure later. The current public route still opens the German manual and no language switcher is implemented. + +Rendering rules: + +- Raw HTML from the Markdown source is disabled. +- Heading ids and the table of contents are generated from the Markdown content. +- Relative image references such as `screenshots/disclaimer.png` are rewritten to the authenticated manual screenshot route. +- Screenshots belong in the active language's `screenshots/` directory, currently `docs/de/screenshots/`; the screenshot route must remain limited to that directory. + +The Help pages use the same HTTP Basic Auth protection as the calculator. + ## Authentication RollCalc uses HTTP Basic Auth. User entries in `BETA_USERS` have this shape: diff --git a/README.md b/README.md index e010721..e5e0f94 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This README is intended for developers maintaining the project, not for end user - Python 3 - Flask 2.3.3 - Flask-HTTPAuth 4.8.0 +- markdown-it-py 3.0.0 for server-side Markdown rendering - Vanilla HTML, CSS, and JavaScript - JSON files for product data and forklift/load rules @@ -58,7 +59,9 @@ The app uses HTTP Basic Auth. Current users are defined in `BETA_USERS` in `app. ├── fix_article_data.py ├── service-worker.js ├── templates/ -│ └── roll_calculator.html +│ ├── help_index.html +│ ├── roll_calculator.html +│ └── user_manual.html ├── static/ │ ├── article-data.json │ ├── config.json @@ -71,6 +74,9 @@ The app uses HTTP Basic Auth. Current users are defined in `BETA_USERS` in `app. │ ├── rollcalc_stddev_ranges.js │ └── rollcalc_stddev_integration.js └── docs/ + ├── de/ + │ ├── screenshots/ + │ └── user_manual.md ├── DEPLOYMENT_GUIDE.md ├── QOL_UPDATE_SUMMARY.md ├── STDDEV_IMPLEMENTATION_CHECKLIST.md @@ -86,6 +92,9 @@ Implemented routes: | Route | Methods | Auth | Purpose | | --- | --- | --- | --- | | `/` | `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/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. | | `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. | @@ -109,6 +118,41 @@ Do not commit real passwords or print them in logs. Access logging is handled by `log_access()`, which reads `access_log.json`, appends a record, and writes the whole file back. +## Help and User Manual + +RollCalc exposes an authenticated Help area at: + +```text +/help +``` + +The user manual is available at: + +```text +/help/user-manual +``` + +`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. + +The documentation directory is prepared for additional languages: + +```text +docs/ + de/ + user_manual.md + screenshots/ +``` + +The public route `/help/user-manual` currently opens the German manual. No language switcher is implemented yet. + +Screenshots referenced from the manual as `screenshots/filename.png` should be placed in: + +```text +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. + ## Frontend Entry Point The active UI is `templates/roll_calculator.html`. diff --git a/app.py b/app.py index 1c250a6..b40267e 100644 --- a/app.py +++ b/app.py @@ -3,13 +3,26 @@ Naue Roll Calculator - Beta Flask app with HTTP Basic Authentication """ -from flask import Flask, render_template, request, send_file, send_from_directory, jsonify +from flask import ( + Flask, + abort, + jsonify, + render_template, + request, + send_file, + send_from_directory, + url_for, +) from flask_httpauth import HTTPBasicAuth +from markdown_it import MarkdownIt +from markupsafe import Markup from werkzeug.security import check_password_hash from functools import wraps import os from datetime import datetime import json +import re +import unicodedata app = Flask(__name__) auth = HTTPBasicAuth() @@ -43,6 +56,9 @@ BETA_USERS = { # Logging für Auditing LOG_FILE = "access_log.json" 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" UNKNOWN_BUILD_INFO = { "version": "unknown", "branch": "unknown", @@ -120,6 +136,83 @@ def log_access(username, endpoint, method, status=200): # ROUTES # ============================================================================ +def slugify_heading(text): + """Create stable, URL-friendly heading ids for rendered Markdown.""" + normalized = unicodedata.normalize("NFKD", text) + ascii_text = normalized.encode("ascii", "ignore").decode("ascii") + slug = re.sub(r"[^a-zA-Z0-9]+", "-", ascii_text).strip("-").lower() + return slug or "section" + + +def build_manual_renderer(): + """Build a Markdown renderer with raw HTML disabled.""" + renderer = MarkdownIt("commonmark", {"html": False}) + renderer.enable("table") + return renderer + + +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) + + +def get_manual_screenshot_dir(language=DEFAULT_DOC_LANGUAGE): + """Return the screenshot directory for a language-specific manual.""" + return os.path.join(DOCS_DIR, language, "screenshots") + + +def rewrite_manual_asset_paths(markdown_text, language=DEFAULT_DOC_LANGUAGE): + """Route relative manual screenshot links through the protected docs route.""" + screenshot_url = url_for("manual_screenshot", filename="") + return markdown_text.replace("](screenshots/", f"]({screenshot_url}") + + +def add_heading_ids_and_toc(tokens): + """Attach ids to headings and create a compact table of contents.""" + toc = [] + used_slugs = {} + + for index, token in enumerate(tokens): + if token.type != "heading_open": + continue + + level = int(token.tag[1]) + inline_token = tokens[index + 1] if index + 1 < len(tokens) else None + title = ( + inline_token.content + if inline_token and inline_token.type == "inline" + else "" + ) + base_slug = slugify_heading(title) + count = used_slugs.get(base_slug, 0) + used_slugs[base_slug] = count + 1 + slug = base_slug if count == 0 else f"{base_slug}-{count + 1}" + token.attrSet("id", slug) + + if level <= 2: + toc.append({ + "level": level, + "title": title, + "id": slug + }) + + return toc + + +def render_user_manual(language=DEFAULT_DOC_LANGUAGE): + """Read and render the Markdown user manual into HTML and a generated TOC.""" + renderer = build_manual_renderer() + manual_path = get_manual_path(language) + + with open(manual_path, "r", encoding="utf-8") as manual_file: + markdown_text = manual_file.read() + + markdown_text = rewrite_manual_asset_paths(markdown_text, language) + tokens = renderer.parse(markdown_text) + toc = add_heading_ids_and_toc(tokens) + html = renderer.renderer.render(tokens, renderer.options, {}) + return Markup(html), toc + @app.route("/", methods=["GET"]) @auth.login_required def index(): @@ -127,6 +220,53 @@ def index(): log_access(auth.current_user(), "/", "GET") return render_template("roll_calculator.html") +@app.route("/help", methods=["GET"]) +@auth.login_required +def help_index(): + """Help landing page - requires authentication.""" + log_access(auth.current_user(), "/help", "GET") + return render_template("help_index.html") + +@app.route("/help/user-manual", methods=["GET"]) +@auth.login_required +def user_manual(): + """Render the Markdown user manual inside the RollCalc layout.""" + log_access(auth.current_user(), "/help/user-manual", "GET") + try: + manual_html, toc = render_user_manual(DEFAULT_DOC_LANGUAGE) + return render_template( + "user_manual.html", + manual_html=manual_html, + toc=toc, + render_error=None + ) + except Exception: + app.logger.exception("Could not render user manual") + return render_template( + "user_manual.html", + manual_html=None, + toc=[], + render_error=( + "The user manual 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): + """Serve user-manual screenshots from the selected language directory.""" + log_access( + auth.current_user(), + f"/help/user-manual/screenshots/{filename}", + "GET" + ) + try: + return send_from_directory(get_manual_screenshot_dir(language), filename) + except Exception: + app.logger.warning("Missing or inaccessible manual screenshot: %s", filename) + abort(404) + @app.route("/static/", methods=["GET"]) @auth.login_required def serve_static(filename): diff --git a/docs/user_manual.md b/docs/de/user_manual.md similarity index 100% rename from docs/user_manual.md rename to docs/de/user_manual.md diff --git a/requirements.txt b/requirements.txt index 03b41a2..74d9425 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ click==8.1.7 itsdangerous==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.3 +markdown-it-py==3.0.0 diff --git a/templates/help_index.html b/templates/help_index.html new file mode 100644 index 0000000..525938a --- /dev/null +++ b/templates/help_index.html @@ -0,0 +1,73 @@ + + + + + + + RollCalc Help + + + +
+
+
+

RollCalc Help

+

Documentation for roll calculation and transport capacity analysis

+
+ +
+
+ +
+

+ This area provides internal technical documentation for RollCalc. The documents are rendered from the maintained Markdown sources. +

+ +
+
+

User Manual

+

+ Workflow-oriented manual for roll calculation, handling evaluation, Load Optimizer usage, and interpretation of Capacity Summary and Capacity Analysis. +

+ Open User Manual +
+
+
+ +
+ RollCalc Beta © Naue + +
+ + diff --git a/templates/roll_calculator.html b/templates/roll_calculator.html index 72e33d2..7a7c53f 100644 --- a/templates/roll_calculator.html +++ b/templates/roll_calculator.html @@ -9,8 +9,11 @@ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #333; min-height: 100vh; } .header { background: linear-gradient(135deg, #1F5438 0%, #2BAC70 100%); color: #fff; padding: 20px 32px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } + .header-inner { max-width: 900px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 18px; } .header h1 { font-size: 24px; margin-bottom: 5px; } .header p { font-size: 13px; opacity: 0.9; } + .header-help-link { color: #fff; border: 1px solid rgba(255,255,255,0.58); border-radius: 5px; padding: 7px 13px; text-decoration: none; font-size: 13px; font-weight: 700; background: rgba(255,255,255,0.08); } + .header-help-link:hover { background: rgba(255,255,255,0.18); } .container { max-width: 900px; margin: 30px auto; padding: 0 16px 60px; } .card { background: #fff; border-radius: 10px; box-shadow: 0 2px 12px rgba(0,0,0,0.07); margin-bottom: 24px; overflow: hidden; } .card-header { background: #f0f4fa; padding: 16px 22px; border-bottom: 1px solid #e0e8f0; } @@ -448,6 +451,8 @@ .margin-color { background: repeating-linear-gradient(45deg,#fff7e0,#fff7e0 3px,#ffe8a0 3px,#ffe8a0 6px); border-color:#f0b429; } @media (max-width: 768px) { + .header { padding: 18px 16px; } + .header-inner { align-items: flex-start; } .lo-accordion-header h3 { font-size: 14px; } .lo-preset-buttons { flex-direction: column; } .lo-preset-btn { width: 100%; } @@ -492,8 +497,13 @@
-

Naue Roll Diameter Calculator

-

Roll Diameter • Product Length • Roll Weight • Load Optimizer

+
+
+

Naue Roll Diameter Calculator

+

Roll Diameter • Product Length • Roll Weight • Load Optimizer

+
+ Help +
diff --git a/templates/user_manual.html b/templates/user_manual.html new file mode 100644 index 0000000..c85576e --- /dev/null +++ b/templates/user_manual.html @@ -0,0 +1,110 @@ + + + + + + + RollCalc User Manual + + + +
+
+
+

User Manual

+

RollCalc technical user documentation

+
+ +
+
+ +
+ {% if render_error %} + + {% else %} +
+ +
+ {{ manual_html }} +
+
+ {% endif %} +
+ + + +