feat: add in-app release notes and what's new dialog
This commit is contained in:
@@ -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/<path:filename>` serves German manual screenshots from `docs/de/screenshots/`.
|
||||
- `/static/<path:filename>` 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:
|
||||
|
||||
@@ -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/<path:filename>` | `GET` | Basic Auth | Serves German user-manual screenshots from `docs/de/screenshots/` only. |
|
||||
| `/static/<path:filename>` | `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`.
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"items": [
|
||||
"User Manual integrated",
|
||||
"Capacity Analysis improved",
|
||||
"Governing Constraint clarified",
|
||||
"Remaining Capacity added"
|
||||
]
|
||||
}
|
||||
@@ -57,6 +57,13 @@
|
||||
</p>
|
||||
<a href="{{ url_for('user_manual') }}">Open User Manual</a>
|
||||
</article>
|
||||
<article class="help-card">
|
||||
<h2>Recent Changes</h2>
|
||||
<p>
|
||||
Compact release notes for relevant user-facing changes in RollCalc.
|
||||
</p>
|
||||
<a href="{{ url_for('recent_changes') }}">Open Recent Changes</a>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="whats-new-overlay is-hidden" id="whatsNewOverlay" role="dialog" aria-modal="true" aria-labelledby="whatsNewTitle">
|
||||
<div class="whats-new-dialog">
|
||||
<div class="whats-new-header">
|
||||
<h2 id="whatsNewTitle">What’s New</h2>
|
||||
<div class="whats-new-version">Version: <span id="whatsNewVersion">–</span></div>
|
||||
</div>
|
||||
<div class="whats-new-content">
|
||||
<ul id="whatsNewList"></ul>
|
||||
</div>
|
||||
<div class="whats-new-actions">
|
||||
<button class="whats-new-btn whats-new-btn-secondary" id="whatsNewOpenChangesBtn" type="button">Open Recent Changes</button>
|
||||
<button class="whats-new-btn whats-new-btn-primary" id="whatsNewContinueBtn" type="button">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<div class="header-inner">
|
||||
<div>
|
||||
<h1>Naue Roll Diameter Calculator</h1>
|
||||
<p>Roll Diameter • Product Length • Roll Weight • Load Optimizer</p>
|
||||
</div>
|
||||
<a class="header-help-link" href="{{ url_for('help_index') }}">Help</a>
|
||||
<a class="header-help-link" href="{{ url_for('user_manual') }}">User Manual</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="RollCalc User Manual">
|
||||
<title>RollCalc User Manual</title>
|
||||
<meta name="description" content="RollCalc {{ document_title }}">
|
||||
<title>RollCalc {{ document_title }}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { scroll-behavior: smooth; }
|
||||
@@ -62,8 +62,8 @@
|
||||
<header class="help-header">
|
||||
<div class="help-header-inner">
|
||||
<div>
|
||||
<h1>User Manual</h1>
|
||||
<p>RollCalc technical user documentation</p>
|
||||
<h1>{{ document_title }}</h1>
|
||||
<p>{{ document_subtitle }}</p>
|
||||
</div>
|
||||
<nav class="help-nav" aria-label="Help navigation">
|
||||
<a href="{{ url_for('index') }}">Calculator</a>
|
||||
@@ -75,7 +75,7 @@
|
||||
<main class="manual-shell">
|
||||
{% if render_error %}
|
||||
<section class="manual-error" role="alert">
|
||||
<strong>User Manual unavailable</strong>
|
||||
<strong>{{ error_title }}</strong>
|
||||
<p>{{ render_error }}</p>
|
||||
</section>
|
||||
{% else %}
|
||||
|
||||
Reference in New Issue
Block a user