Compare commits
8
Commits
main
...
216f53c641
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
216f53c641 | ||
|
|
8903452e96 | ||
|
|
41dbb6297c | ||
|
|
1ac9b08233 | ||
|
|
1827c6de6c | ||
|
|
4f4bdf8d72 | ||
|
|
ea23f3f35d | ||
|
|
30ed0bb79b |
@@ -32,3 +32,6 @@ node_modules/
|
|||||||
# Backup files
|
# Backup files
|
||||||
*.bak
|
*.bak
|
||||||
*.backup
|
*.backup
|
||||||
|
|
||||||
|
# runtime-generated files
|
||||||
|
access_log.json
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ Backend:
|
|||||||
- HTTP Basic Auth is implemented with `Flask-HTTPAuth`.
|
- HTTP Basic Auth is implemented with `Flask-HTTPAuth`.
|
||||||
- Users are currently configured in `BETA_USERS` with Werkzeug password hashes.
|
- Users are currently configured in `BETA_USERS` with Werkzeug password hashes.
|
||||||
- `/` renders the active calculator template.
|
- `/` 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.
|
- `/static/<path:filename>` is intended to serve static files behind Basic Auth.
|
||||||
- `/api/health` returns health/version information.
|
- `/api/health` returns health/version information.
|
||||||
- `/api/user` returns the authenticated user.
|
- `/api/user` returns the authenticated user.
|
||||||
@@ -50,6 +54,54 @@ 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`.
|
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.
|
||||||
|
|
||||||
|
### 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
|
## Authentication
|
||||||
|
|
||||||
RollCalc uses HTTP Basic Auth. User entries in `BETA_USERS` have this shape:
|
RollCalc uses HTTP Basic Auth. User entries in `BETA_USERS` have this shape:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ This README is intended for developers maintaining the project, not for end user
|
|||||||
- Python 3
|
- Python 3
|
||||||
- Flask 2.3.3
|
- Flask 2.3.3
|
||||||
- Flask-HTTPAuth 4.8.0
|
- Flask-HTTPAuth 4.8.0
|
||||||
|
- markdown-it-py 3.0.0 for server-side Markdown rendering
|
||||||
- Vanilla HTML, CSS, and JavaScript
|
- Vanilla HTML, CSS, and JavaScript
|
||||||
- JSON files for product data and forklift/load rules
|
- 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
|
├── fix_article_data.py
|
||||||
├── service-worker.js
|
├── service-worker.js
|
||||||
├── templates/
|
├── templates/
|
||||||
│ └── roll_calculator.html
|
│ ├── help_index.html
|
||||||
|
│ ├── roll_calculator.html
|
||||||
|
│ └── user_manual.html
|
||||||
├── static/
|
├── static/
|
||||||
│ ├── article-data.json
|
│ ├── article-data.json
|
||||||
│ ├── config.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_ranges.js
|
||||||
│ └── rollcalc_stddev_integration.js
|
│ └── rollcalc_stddev_integration.js
|
||||||
└── docs/
|
└── docs/
|
||||||
|
├── de/
|
||||||
|
│ ├── screenshots/
|
||||||
|
│ └── user_manual.md
|
||||||
├── DEPLOYMENT_GUIDE.md
|
├── DEPLOYMENT_GUIDE.md
|
||||||
├── QOL_UPDATE_SUMMARY.md
|
├── QOL_UPDATE_SUMMARY.md
|
||||||
├── STDDEV_IMPLEMENTATION_CHECKLIST.md
|
├── STDDEV_IMPLEMENTATION_CHECKLIST.md
|
||||||
@@ -86,6 +92,10 @@ Implemented routes:
|
|||||||
| Route | Methods | Auth | Purpose |
|
| Route | Methods | Auth | Purpose |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `/` | `GET` | Basic Auth | Renders `templates/roll_calculator.html`. |
|
| `/` | `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/`. |
|
| `/static/<path:filename>` | `GET` | Basic Auth | Intended protected static-file serving from `static/`. |
|
||||||
| `/api/health` | `GET` | Basic Auth | Returns app health and version. |
|
| `/api/health` | `GET` | Basic Auth | Returns app health and version. |
|
||||||
| `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. |
|
| `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. |
|
||||||
@@ -109,6 +119,67 @@ 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.
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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
|
## Frontend Entry Point
|
||||||
|
|
||||||
The active UI is `templates/roll_calculator.html`.
|
The active UI is `templates/roll_calculator.html`.
|
||||||
|
|||||||
-667
@@ -1,667 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:06:19.854940",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:06:58.223722",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:07:05.902636",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:07:51.345366",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:09:37.481260",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:14:02.867188",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:28:08.386403",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:29:06.541184",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:30:47.979071",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:46:05.447815",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T13:46:19.317688",
|
|
||||||
"username": "beta",
|
|
||||||
"endpoint": "/admin",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 403
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:42:29.477028",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:43:31.374462",
|
|
||||||
"username": "admin",
|
|
||||||
"endpoint": "/admin",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:43:31.411860",
|
|
||||||
"username": "admin",
|
|
||||||
"endpoint": "/api/admin/stats",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:43:31.562482",
|
|
||||||
"username": "admin",
|
|
||||||
"endpoint": "/api/admin/logs",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:44:01.420524",
|
|
||||||
"username": "admin",
|
|
||||||
"endpoint": "/api/admin/stats",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:44:01.438789",
|
|
||||||
"username": "admin",
|
|
||||||
"endpoint": "/api/admin/logs",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:44:13.773466",
|
|
||||||
"username": "admin",
|
|
||||||
"endpoint": "/admin",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:44:13.817782",
|
|
||||||
"username": "admin",
|
|
||||||
"endpoint": "/api/admin/stats",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-05-28T14:44:13.894128",
|
|
||||||
"username": "admin",
|
|
||||||
"endpoint": "/api/admin/logs",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-06-03T05:20:48.808775",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:31:48.451519",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:31:48.676701",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:31:54.481445",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:31:58.011356",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:32:48.747837",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:33:07.753648",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:42:11.820817",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:43:37.598978",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T07:47:39.689201",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T08:25:47.585793",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T09:32:59.419063",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T09:35:05.236655",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T09:40:47.890997",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T09:41:02.461403",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T09:42:11.914989",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T09:42:57.125161",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T09:54:34.067541",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T10:02:39.927663",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:00:50.808697",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:09:28.615255",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:09:40.011779",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:09:53.681061",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:10:06.334747",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:17:18.862106",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:44:00.451943",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:44:20.277301",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:49:56.333142",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:50:11.692907",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:56:07.569410",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:59:03.417506",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T12:59:21.102453",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:04:52.131022",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:05:09.915771",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:09:55.899370",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:10:20.387853",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:10:20.736005",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:12:48.924277",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:15:13.236624",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:17:32.470202",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:23:23.613251",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:23:24.369105",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:26:48.783516",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:47:07.888072",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:52:38.653855",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:53:02.662397",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T13:57:19.316896",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T14:05:28.671663",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T14:06:26.413418",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T14:06:42.715163",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T14:12:15.337499",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T14:12:19.510992",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-01T14:13:17.018760",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-02T11:28:30.320494",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-02T11:28:46.119436",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-02T11:36:55.439798",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-02T11:41:31.868950",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-02T16:07:56.334163",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-02T16:08:38.097844",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-03T08:39:44.284544",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-03T08:41:57.673023",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-03T08:42:19.877155",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-06T10:34:02.710459",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-06T13:14:58.113794",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-06T13:15:20.071507",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-06T13:15:32.538576",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T11:44:53.524315",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T11:47:36.702455",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T12:18:47.866971",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T14:46:22.335316",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T16:13:42.026433",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T16:19:41.488752",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T16:34:58.280178",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T16:36:56.896657",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2026-07-07T16:41:46.568545",
|
|
||||||
"username": "mtazl",
|
|
||||||
"endpoint": "/",
|
|
||||||
"method": "GET",
|
|
||||||
"status": 200
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -3,13 +3,27 @@ Naue Roll Calculator - Beta
|
|||||||
Flask app with HTTP Basic Authentication
|
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 flask_httpauth import HTTPBasicAuth
|
||||||
|
from markdown_it import MarkdownIt
|
||||||
|
from markupsafe import Markup
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
auth = HTTPBasicAuth()
|
auth = HTTPBasicAuth()
|
||||||
@@ -43,6 +57,11 @@ BETA_USERS = {
|
|||||||
# Logging für Auditing
|
# Logging für Auditing
|
||||||
LOG_FILE = "access_log.json"
|
LOG_FILE = "access_log.json"
|
||||||
BUILD_INFO_FILE = "build_info.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"
|
||||||
|
RECENT_CHANGES_FILENAME = "recent_changes.md"
|
||||||
|
WHATS_NEW_SUMMARY_FILE = os.path.join(app.root_path, "static", "recent_changes.json")
|
||||||
UNKNOWN_BUILD_INFO = {
|
UNKNOWN_BUILD_INFO = {
|
||||||
"version": "unknown",
|
"version": "unknown",
|
||||||
"branch": "unknown",
|
"branch": "unknown",
|
||||||
@@ -75,10 +94,36 @@ def load_build_info():
|
|||||||
|
|
||||||
BUILD_INFO = 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
|
@app.context_processor
|
||||||
def inject_build_info():
|
def inject_build_info():
|
||||||
"""Make build metadata available in all templates."""
|
"""Make build metadata and release-note summary available in templates."""
|
||||||
return {"build_info": BUILD_INFO}
|
return {
|
||||||
|
"build_info": BUILD_INFO,
|
||||||
|
"whats_new_summary": WHATS_NEW_SUMMARY
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# AUTHENTICATION
|
# AUTHENTICATION
|
||||||
@@ -116,10 +161,119 @@ def log_access(username, endpoint, method, status=200):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Logging Error] {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
|
# 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_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 get_document_path(USER_MANUAL_FILENAME, language)
|
||||||
|
|
||||||
|
|
||||||
|
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_markdown_document(filename, language=DEFAULT_DOC_LANGUAGE):
|
||||||
|
"""Read and render a Markdown help document into HTML and a generated TOC."""
|
||||||
|
renderer = build_manual_renderer()
|
||||||
|
document_path = get_document_path(filename, language)
|
||||||
|
|
||||||
|
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)
|
||||||
|
toc = add_heading_ids_and_toc(tokens)
|
||||||
|
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"])
|
@app.route("/", methods=["GET"])
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
def index():
|
def index():
|
||||||
@@ -127,6 +281,92 @@ def index():
|
|||||||
log_access(auth.current_user(), "/", "GET")
|
log_access(auth.current_user(), "/", "GET")
|
||||||
return render_template("roll_calculator.html")
|
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",
|
||||||
|
document_title="User Manual",
|
||||||
|
document_subtitle="RollCalc technical user documentation",
|
||||||
|
error_title="User Manual unavailable",
|
||||||
|
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",
|
||||||
|
document_title="User Manual",
|
||||||
|
document_subtitle="RollCalc technical user documentation",
|
||||||
|
error_title="User Manual unavailable",
|
||||||
|
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/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):
|
||||||
|
"""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/<path:filename>", methods=["GET"])
|
@app.route("/static/<path:filename>", methods=["GET"])
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
def serve_static(filename):
|
def serve_static(filename):
|
||||||
@@ -177,21 +417,22 @@ def internal_error(e):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
os.makedirs("templates", exist_ok=True)
|
os.makedirs("templates", exist_ok=True)
|
||||||
os.makedirs("static", 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 ║
|
║ Naue Roll Calculator - V14 + QoL Update ║
|
||||||
║ HTTP Basic Auth Enabled ║
|
║ HTTP Basic Auth Enabled ║
|
||||||
║ ║
|
║ ║
|
||||||
║ ⚠️ BEFORE PRODUCTION: Update passwords in app.py ║
|
║ ⚠️ BEFORE PRODUCTION: Update passwords in app.py ║
|
||||||
║ ║
|
║ ║
|
||||||
║ Starten auf: http://localhost:5000 ║
|
║ Starten auf: http://localhost:{port:<5} ║
|
||||||
╚═══════════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════════╝
|
||||||
""")
|
""")
|
||||||
|
|
||||||
app.run(
|
app.run(
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=5000,
|
port=port,
|
||||||
debug=False,
|
debug=False,
|
||||||
use_reloader=False
|
use_reloader=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Documentation Guidelines
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
The primary documentation language is German.
|
||||||
|
|
||||||
|
An English version will be maintained as a translation of the German master document.
|
||||||
|
|
||||||
|
## UI terminology
|
||||||
|
|
||||||
|
All user interface terms remain in English.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Direct Calculation
|
||||||
|
- Extrapolation
|
||||||
|
- Roll Diameter
|
||||||
|
- Product Length
|
||||||
|
- Capacity Summary
|
||||||
|
- Capacity Analysis
|
||||||
|
- Governing Constraint
|
||||||
|
|
||||||
|
Do not translate UI labels.
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
|
- Technical user manual
|
||||||
|
- Workflow-oriented
|
||||||
|
- Explain why, not only how
|
||||||
|
- Engineering language
|
||||||
|
- No marketing language
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Write German chapter.
|
||||||
|
2. Technical review.
|
||||||
|
3. Add screenshots.
|
||||||
|
4. Approve.
|
||||||
|
5. Translate to English.
|
||||||
@@ -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.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,3 +5,4 @@ click==8.1.7
|
|||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
MarkupSafe==2.1.3
|
MarkupSafe==2.1.3
|
||||||
|
markdown-it-py==3.0.0
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
"User Manual integrated",
|
||||||
|
"Capacity Analysis improved",
|
||||||
|
"Governing Constraint clarified",
|
||||||
|
"Remaining Capacity added"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="RollCalc Help">
|
||||||
|
<title>RollCalc Help</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #2f3437; min-height: 100vh; }
|
||||||
|
.help-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); }
|
||||||
|
.help-header-inner { max-width: 980px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
||||||
|
.help-header h1 { font-size: 24px; margin-bottom: 5px; }
|
||||||
|
.help-header p { font-size: 13px; opacity: 0.9; }
|
||||||
|
.help-nav { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.help-nav a { 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); }
|
||||||
|
.help-nav a:hover { background: rgba(255,255,255,0.18); }
|
||||||
|
.help-container { max-width: 980px; margin: 30px auto; padding: 0 16px 60px; }
|
||||||
|
.help-intro { margin-bottom: 22px; line-height: 1.55; color: #4b555a; }
|
||||||
|
.help-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px; }
|
||||||
|
.help-card { background: #fff; border: 1px solid #dfe7e2; border-radius: 8px; padding: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
|
||||||
|
.help-card h2 { color: #1F5438; font-size: 18px; margin-bottom: 10px; }
|
||||||
|
.help-card p { color: #4f5b60; font-size: 14px; line-height: 1.55; margin-bottom: 18px; }
|
||||||
|
.help-card a { display: inline-block; background: #2BAC70; color: #fff; text-decoration: none; border-radius: 6px; padding: 10px 16px; font-size: 14px; font-weight: 700; }
|
||||||
|
.help-card a:hover { background: #1F5438; }
|
||||||
|
.footer { text-align: center; font-size: 11px; color: #aaa; padding: 18px; }
|
||||||
|
.footer-build-info { margin-top: 6px; color: #bbb; }
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.help-header { padding: 18px 16px; }
|
||||||
|
.help-header-inner { align-items: flex-start; flex-direction: column; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="help-header">
|
||||||
|
<div class="help-header-inner">
|
||||||
|
<div>
|
||||||
|
<h1>RollCalc Help</h1>
|
||||||
|
<p>Documentation for roll calculation and transport capacity analysis</p>
|
||||||
|
</div>
|
||||||
|
<nav class="help-nav" aria-label="Help navigation">
|
||||||
|
<a href="{{ url_for('index') }}">Calculator</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="help-container">
|
||||||
|
<p class="help-intro">
|
||||||
|
This area provides internal technical documentation for RollCalc. The documents are rendered from the maintained Markdown sources.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section class="help-grid" aria-label="Available help documents">
|
||||||
|
<article class="help-card">
|
||||||
|
<h2>User Manual</h2>
|
||||||
|
<p>
|
||||||
|
Workflow-oriented manual for roll calculation, handling evaluation, Load Optimizer usage, and interpretation of Capacity Summary and Capacity Analysis.
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
RollCalc Beta © Naue
|
||||||
|
<div class="footer-build-info">
|
||||||
|
Version {{ build_info.version }} |
|
||||||
|
Branch {{ build_info.branch }} |
|
||||||
|
Commit {{ build_info.commit }} |
|
||||||
|
Built {{ build_info.timestamp }}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+242
-16
@@ -9,8 +9,11 @@
|
|||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #333; min-height: 100vh; }
|
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 { 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 h1 { font-size: 24px; margin-bottom: 5px; }
|
||||||
.header p { font-size: 13px; opacity: 0.9; }
|
.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; }
|
.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 { 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; }
|
.card-header { background: #f0f4fa; padding: 16px 22px; border-bottom: 1px solid #e0e8f0; }
|
||||||
@@ -143,6 +146,59 @@
|
|||||||
background: #b8c7bf;
|
background: #b8c7bf;
|
||||||
cursor: not-allowed;
|
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
|
LOAD OPTIMIZER STYLES
|
||||||
@@ -289,6 +345,23 @@
|
|||||||
border-left: 4px solid #2BAC70;
|
border-left: 4px solid #2BAC70;
|
||||||
}
|
}
|
||||||
.lo-results-summary h4 { margin-top: 0; color: #333; font-size: 14px; margin-bottom: 12px; }
|
.lo-results-summary h4 { margin-top: 0; color: #333; font-size: 14px; margin-bottom: 12px; }
|
||||||
|
.lo-summary-section {
|
||||||
|
padding-top: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
border-top: 1px solid #e6ece9;
|
||||||
|
}
|
||||||
|
.lo-summary-section:first-of-type {
|
||||||
|
padding-top: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
.lo-summary-section-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1F5438;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
.lo-summary-item {
|
.lo-summary-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -297,9 +370,23 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.lo-summary-item:last-child { margin-bottom: 0; }
|
.lo-summary-item:last-child { margin-bottom: 0; }
|
||||||
|
.lo-summary-item.lo-governing-summary {
|
||||||
|
background: #eef8f2;
|
||||||
|
border-left: 3px solid #2BAC70;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
.lo-value-highlight { color: #2BAC70; font-weight: bold; font-size: 16px; }
|
.lo-value-highlight { color: #2BAC70; font-weight: bold; font-size: 16px; }
|
||||||
.lo-value-number { color: #1F5438; font-weight: bold; }
|
.lo-value-number { color: #1F5438; font-weight: bold; }
|
||||||
.lo-util-group { display: flex; flex-direction: column; gap: 4px; text-align: right; }
|
.lo-util-group { display: flex; flex-direction: column; gap: 4px; text-align: right; }
|
||||||
|
.lo-reserve-note {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-align: right;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
.lo-results-table {
|
.lo-results-table {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -315,10 +402,15 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
border-bottom: 2px solid #2BAC70;
|
border-bottom: 2px solid #2BAC70;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
.lo-results-table td { padding: 8px; border-bottom: 1px solid #e0e0e0; }
|
.lo-results-table td { padding: 8px; border-bottom: 1px solid #e0e0e0; }
|
||||||
.lo-results-table tbody tr:hover { background: #f9f9f9; }
|
.lo-results-table tbody tr:hover { background: #f9f9f9; }
|
||||||
.lo-results-table tbody tr.lo-recommended { background: #fffacd; font-weight: bold; }
|
.lo-results-table tbody tr.lo-governing {
|
||||||
|
background: #eef8f2;
|
||||||
|
font-weight: 600;
|
||||||
|
border-left: 3px solid #2BAC70;
|
||||||
|
}
|
||||||
.lo-warning {
|
.lo-warning {
|
||||||
background: #fff8e1;
|
background: #fff8e1;
|
||||||
border: 1px solid #f0b429;
|
border: 1px solid #f0b429;
|
||||||
@@ -396,6 +488,11 @@
|
|||||||
}
|
}
|
||||||
.lo-sideview-section.show { display: block; }
|
.lo-sideview-section.show { display: block; }
|
||||||
.lo-sideview-section h4 { margin: 0 0 12px 0; font-size: 14px; color: #333; }
|
.lo-sideview-section h4 { margin: 0 0 12px 0; font-size: 14px; color: #333; }
|
||||||
|
.lo-sideview-context {
|
||||||
|
margin: -6px 0 12px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
.lo-sideview-wrap {
|
.lo-sideview-wrap {
|
||||||
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden;
|
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -407,6 +504,10 @@
|
|||||||
.margin-color { background: repeating-linear-gradient(45deg,#fff7e0,#fff7e0 3px,#ffe8a0 3px,#ffe8a0 6px); border-color:#f0b429; }
|
.margin-color { background: repeating-linear-gradient(45deg,#fff7e0,#fff7e0 3px,#ffe8a0 3px,#ffe8a0 6px); border-color:#f0b429; }
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@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-accordion-header h3 { font-size: 14px; }
|
||||||
.lo-preset-buttons { flex-direction: column; }
|
.lo-preset-buttons { flex-direction: column; }
|
||||||
.lo-preset-btn { width: 100%; }
|
.lo-preset-btn { width: 100%; }
|
||||||
@@ -450,9 +551,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
|
<div class="header-inner">
|
||||||
|
<div>
|
||||||
<h1>Naue Roll Diameter Calculator</h1>
|
<h1>Naue Roll Diameter Calculator</h1>
|
||||||
<p>Roll Diameter • Product Length • Roll Weight • Load Optimizer</p>
|
<p>Roll Diameter • Product Length • Roll Weight • Load Optimizer</p>
|
||||||
|
</div>
|
||||||
|
<a class="header-help-link" href="{{ url_for('user_manual') }}">User Manual</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -807,11 +929,17 @@
|
|||||||
<div class="lo-results" id="resultsContainer">
|
<div class="lo-results" id="resultsContainer">
|
||||||
|
|
||||||
<div class="lo-results-summary">
|
<div class="lo-results-summary">
|
||||||
<h4>Results:</h4>
|
<h4>Capacity Summary:</h4>
|
||||||
<div class="lo-summary-item">
|
<div class="lo-summary-section">
|
||||||
<span><strong>Limiting Factor:</strong></span>
|
<div class="lo-summary-section-title">Governing Constraint</div>
|
||||||
|
<div class="lo-summary-item lo-governing-summary">
|
||||||
|
<span><strong>Constraint:</strong></span>
|
||||||
<span id="limitingFactorValue" class="lo-value-highlight">–</span>
|
<span id="limitingFactorValue" class="lo-value-highlight">–</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lo-summary-section">
|
||||||
|
<div class="lo-summary-section-title">Transport Result</div>
|
||||||
<div class="lo-summary-item">
|
<div class="lo-summary-item">
|
||||||
<span><strong>Total Rolls:</strong></span>
|
<span><strong>Total Rolls:</strong></span>
|
||||||
<div style="text-align:right;">
|
<div style="text-align:right;">
|
||||||
@@ -822,23 +950,37 @@
|
|||||||
<div class="lo-summary-item">
|
<div class="lo-summary-item">
|
||||||
<span><strong>Utilization:</strong></span>
|
<span><strong>Utilization:</strong></span>
|
||||||
<div class="lo-util-group">
|
<div class="lo-util-group">
|
||||||
<div>Weight: <span id="weightUtilValue" class="lo-value-number">–</span> %</div>
|
<div>Weight Utilization: <span id="weightUtilValue" class="lo-value-number">–</span> %</div>
|
||||||
<div>Volume: <span id="volumeUtilValue" class="lo-value-number">–</span> %</div>
|
<div>Volume Utilization: <span id="volumeUtilValue" class="lo-value-number">–</span> %</div>
|
||||||
<div>Product: <span id="productM2Value" class="lo-value-number">–</span> m²</div>
|
<div>Transported Product Area: <span id="productM2Value" class="lo-value-number">–</span> m²</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lo-summary-section">
|
||||||
|
<div class="lo-summary-section-title">Remaining Capacity</div>
|
||||||
|
<div class="lo-summary-item">
|
||||||
|
<span><strong>Remaining Capacity:</strong></span>
|
||||||
|
<div class="lo-util-group">
|
||||||
|
<div>Remaining Payload: <span id="remainingPayloadValue" class="lo-value-number">–</span> kg</div>
|
||||||
|
<div>Unused Payload: <span id="unusedPayloadValue" class="lo-value-number">–</span> %</div>
|
||||||
|
<div>Unused Volume: <span id="unusedVolumeValue" class="lo-value-number">–</span> %</div>
|
||||||
|
<div id="capacityInterpretationValue" class="lo-reserve-note"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lo-results-table">
|
<div class="lo-results-table">
|
||||||
<h4>Comparison Table:</h4>
|
<h4>Capacity Analysis:</h4>
|
||||||
<table id="comparisonTable">
|
<table id="comparisonTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Scenario</th>
|
<th>Constraint</th>
|
||||||
<th>Rolls</th>
|
<th>Max Rolls</th>
|
||||||
<th>kg</th>
|
<th>Total Weight</th>
|
||||||
<th>m²</th>
|
<th>Transported Area [m²]</th>
|
||||||
<th>Util. %</th>
|
<th>Payload Util. %</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
@@ -847,6 +989,7 @@
|
|||||||
|
|
||||||
<div class="lo-sideview-section" id="sideViewSection">
|
<div class="lo-sideview-section" id="sideViewSection">
|
||||||
<h4>🔍 Front View – Cross-Section</h4>
|
<h4>🔍 Front View – Cross-Section</h4>
|
||||||
|
<p class="lo-sideview-context">Cross-section of the actual loading configuration.</p>
|
||||||
<div class="lo-sideview-wrap">
|
<div class="lo-sideview-wrap">
|
||||||
<svg id="sideViewSVG" width="100%" viewBox="0 0 800 300" xmlns="http://www.w3.org/2000/svg">
|
<svg id="sideViewSVG" width="100%" viewBox="0 0 800 300" xmlns="http://www.w3.org/2000/svg">
|
||||||
<text x="400" y="130" text-anchor="middle" fill="#aaa" font-size="14">Run a calculation to see the side view.</text>
|
<text x="400" y="130" text-anchor="middle" fill="#aaa" font-size="14">Run a calculation to see the side view.</text>
|
||||||
@@ -1350,6 +1493,14 @@ class LoadOptimizerUI {
|
|||||||
|
|
||||||
showResults(res, maxW) {
|
showResults(res, maxW) {
|
||||||
const m2PerRoll = this.optimizer.state.m2PerRoll || 0;
|
const m2PerRoll = this.optimizer.state.m2PerRoll || 0;
|
||||||
|
const remainingPayload = Math.max(0, maxW - res.finalWeight);
|
||||||
|
const unusedPayload = Math.max(0, 100 - res.utilizationWeight);
|
||||||
|
const unusedVolume = Math.max(0, 100 - res.utilizationVolume);
|
||||||
|
const interpretations = {
|
||||||
|
geometry: 'Geometry prevents loading additional rolls although payload capacity remains available.',
|
||||||
|
weight: 'Payload limit is reached before the available geometry or volume is fully used.',
|
||||||
|
volume: 'Volume capacity is reached before payload or geometric capacity is fully used.'
|
||||||
|
};
|
||||||
document.getElementById('limitingFactorValue').textContent = res.limiting.name;
|
document.getElementById('limitingFactorValue').textContent = res.limiting.name;
|
||||||
document.getElementById('totalRollsValue').textContent = res.finalRolls;
|
document.getElementById('totalRollsValue').textContent = res.finalRolls;
|
||||||
document.getElementById('rollsDetailValue').textContent =
|
document.getElementById('rollsDetailValue').textContent =
|
||||||
@@ -1357,16 +1508,21 @@ class LoadOptimizerUI {
|
|||||||
document.getElementById('weightUtilValue').textContent = this.optimizer.fmt(res.utilizationWeight);
|
document.getElementById('weightUtilValue').textContent = this.optimizer.fmt(res.utilizationWeight);
|
||||||
document.getElementById('volumeUtilValue').textContent = this.optimizer.fmt(res.utilizationVolume);
|
document.getElementById('volumeUtilValue').textContent = this.optimizer.fmt(res.utilizationVolume);
|
||||||
document.getElementById('productM2Value').textContent = this.optimizer.fmt(res.finalM2, 0);
|
document.getElementById('productM2Value').textContent = this.optimizer.fmt(res.finalM2, 0);
|
||||||
|
document.getElementById('remainingPayloadValue').textContent = this.optimizer.fmt(remainingPayload, 0);
|
||||||
|
document.getElementById('unusedPayloadValue').textContent = this.optimizer.fmt(unusedPayload);
|
||||||
|
document.getElementById('unusedVolumeValue').textContent = this.optimizer.fmt(unusedVolume);
|
||||||
|
document.getElementById('capacityInterpretationValue').textContent =
|
||||||
|
interpretations[res.limiting.type] || '';
|
||||||
const tbody = document.querySelector('#comparisonTable tbody');
|
const tbody = document.querySelector('#comparisonTable tbody');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
res.scenarios.forEach(sc => {
|
res.scenarios.forEach(sc => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
const lim = (sc.type === res.limiting.type);
|
const lim = (sc.type === res.limiting.type);
|
||||||
if (lim) tr.classList.add('lo-recommended');
|
if (lim) tr.classList.add('lo-governing');
|
||||||
const util = maxW > 0 ? (sc.weight / maxW * 100).toFixed(1) : '–';
|
const util = maxW > 0 ? (sc.weight / maxW * 100).toFixed(1) : '–';
|
||||||
const scM2 = isFinite(sc.rolls) && m2PerRoll > 0 ? Math.round(sc.rolls * m2PerRoll) : '–';
|
const scM2 = isFinite(sc.rolls) && m2PerRoll > 0 ? Math.round(sc.rolls * m2PerRoll) : '–';
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
'<td>'+ sc.name +(lim?' ★':'')+'</td>'+
|
'<td>'+ sc.name +(lim?' → Governing Constraint':'')+'</td>'+
|
||||||
'<td>'+ (sc.rolls === Infinity ? '–' : sc.rolls) +'</td>'+
|
'<td>'+ (sc.rolls === Infinity ? '–' : sc.rolls) +'</td>'+
|
||||||
'<td>'+ (isFinite(sc.weight) ? Math.round(sc.weight) : '–') +'</td>'+
|
'<td>'+ (isFinite(sc.weight) ? Math.round(sc.weight) : '–') +'</td>'+
|
||||||
'<td>'+ scM2 +'</td>'+
|
'<td>'+ scM2 +'</td>'+
|
||||||
@@ -1488,12 +1644,81 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
window.loadOptimizerUI = new LoadOptimizerUI();
|
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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const overlay = document.getElementById('disclaimerOverlay');
|
const overlay = document.getElementById('disclaimerOverlay');
|
||||||
const checkbox = document.getElementById('disclaimerAccepted');
|
const checkbox = document.getElementById('disclaimerAccepted');
|
||||||
const confirmBtn = document.getElementById('disclaimerConfirmBtn');
|
const confirmBtn = document.getElementById('disclaimerConfirmBtn');
|
||||||
|
|
||||||
if (!overlay || !checkbox || !confirmBtn) return;
|
if (!overlay || !checkbox || !confirmBtn) {
|
||||||
|
maybeShowWhatsNew();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkbox.addEventListener('change', () => {
|
checkbox.addEventListener('change', () => {
|
||||||
confirmBtn.disabled = !checkbox.checked;
|
confirmBtn.disabled = !checkbox.checked;
|
||||||
@@ -1502,6 +1727,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
confirmBtn.addEventListener('click', () => {
|
confirmBtn.addEventListener('click', () => {
|
||||||
if (!checkbox.checked) return;
|
if (!checkbox.checked) return;
|
||||||
overlay.classList.add('is-hidden');
|
overlay.classList.add('is-hidden');
|
||||||
|
maybeShowWhatsNew();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<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; }
|
||||||
|
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #2f3437; min-height: 100vh; }
|
||||||
|
.help-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); }
|
||||||
|
.help-header-inner { max-width: 1180px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
||||||
|
.help-header h1 { font-size: 24px; margin-bottom: 5px; }
|
||||||
|
.help-header p { font-size: 13px; opacity: 0.9; }
|
||||||
|
.help-nav { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.help-nav a { 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); }
|
||||||
|
.help-nav a:hover { background: rgba(255,255,255,0.18); }
|
||||||
|
.manual-shell { max-width: 1180px; margin: 28px auto; padding: 0 16px 60px; }
|
||||||
|
.manual-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 24px; align-items: start; }
|
||||||
|
.manual-toc { position: sticky; top: 18px; background: #fff; border: 1px solid #dfe7e2; border-radius: 8px; padding: 16px; box-shadow: 0 2px 12px rgba(0,0,0,0.05); max-height: calc(100vh - 36px); overflow: auto; }
|
||||||
|
.manual-toc h2 { color: #1F5438; font-size: 14px; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0; }
|
||||||
|
.manual-toc ul { list-style: none; }
|
||||||
|
.manual-toc li { margin: 0 0 8px; }
|
||||||
|
.manual-toc a { color: #34443b; text-decoration: none; font-size: 13px; line-height: 1.35; }
|
||||||
|
.manual-toc a:hover { color: #1F5438; text-decoration: underline; }
|
||||||
|
.manual-toc .toc-level-2 { padding-left: 14px; }
|
||||||
|
.manual-content { background: #fff; border: 1px solid #dfe7e2; border-radius: 8px; padding: 34px 42px; box-shadow: 0 2px 12px rgba(0,0,0,0.05); line-height: 1.62; font-size: 15px; }
|
||||||
|
.manual-content h1, .manual-content h2, .manual-content h3, .manual-content h4 { color: #1F5438; line-height: 1.25; scroll-margin-top: 18px; }
|
||||||
|
.manual-content h1 { font-size: 30px; margin: 0 0 24px; padding-bottom: 14px; border-bottom: 2px solid #dfe7e2; }
|
||||||
|
.manual-content h2 { font-size: 22px; margin: 34px 0 14px; padding-top: 4px; }
|
||||||
|
.manual-content h3 { font-size: 17px; margin: 26px 0 10px; color: #244b39; }
|
||||||
|
.manual-content h4 { font-size: 15px; margin: 20px 0 8px; color: #244b39; }
|
||||||
|
.manual-content p { margin: 0 0 14px; }
|
||||||
|
.manual-content ul, .manual-content ol { margin: 0 0 16px 24px; }
|
||||||
|
.manual-content li { margin: 5px 0; }
|
||||||
|
.manual-content a { color: #1F5438; font-weight: 600; }
|
||||||
|
.manual-content code { background: #eef4f1; border: 1px solid #d7e4de; border-radius: 4px; padding: 1px 4px; font-family: Consolas, 'Courier New', monospace; font-size: 0.92em; }
|
||||||
|
.manual-content pre { background: #f5f7f6; border: 1px solid #dfe7e2; border-radius: 6px; padding: 14px; margin: 16px 0; overflow-x: auto; }
|
||||||
|
.manual-content pre code { background: transparent; border: 0; padding: 0; }
|
||||||
|
.manual-content blockquote { margin: 18px 0; padding: 14px 18px; border-left: 4px solid #2BAC70; background: #f1f8f5; color: #34443b; }
|
||||||
|
.manual-content blockquote p:last-child, .manual-content blockquote ul:last-child { margin-bottom: 0; }
|
||||||
|
.manual-content table { width: 100%; border-collapse: collapse; margin: 18px 0; display: block; overflow-x: auto; }
|
||||||
|
.manual-content th, .manual-content td { border: 1px solid #d7e4de; padding: 9px 11px; text-align: left; vertical-align: top; }
|
||||||
|
.manual-content th { background: #eef4f1; color: #1F5438; }
|
||||||
|
.manual-content img { max-width: 100%; height: auto; border: 1px solid #dfe7e2; border-radius: 6px; background: #f8fafa; display: block; margin: 18px 0 8px; }
|
||||||
|
.manual-error { background: #fff; border: 1px solid #e6c7c7; border-left: 4px solid #b84a4a; border-radius: 8px; padding: 20px; color: #5a2d2d; box-shadow: 0 2px 12px rgba(0,0,0,0.05); }
|
||||||
|
.footer { text-align: center; font-size: 11px; color: #aaa; padding: 18px; }
|
||||||
|
.footer-build-info { margin-top: 6px; color: #bbb; }
|
||||||
|
@media (max-width: 840px) {
|
||||||
|
.help-header { padding: 18px 16px; }
|
||||||
|
.help-header-inner { align-items: flex-start; flex-direction: column; }
|
||||||
|
.manual-layout { grid-template-columns: 1fr; }
|
||||||
|
.manual-toc { position: static; max-height: none; }
|
||||||
|
.manual-content { padding: 24px 20px; }
|
||||||
|
.manual-content h1 { font-size: 25px; }
|
||||||
|
.manual-content h2 { font-size: 20px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="help-header">
|
||||||
|
<div class="help-header-inner">
|
||||||
|
<div>
|
||||||
|
<h1>{{ document_title }}</h1>
|
||||||
|
<p>{{ document_subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<nav class="help-nav" aria-label="Help navigation">
|
||||||
|
<a href="{{ url_for('index') }}">Calculator</a>
|
||||||
|
<a href="{{ url_for('help_index') }}">Help</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="manual-shell">
|
||||||
|
{% if render_error %}
|
||||||
|
<section class="manual-error" role="alert">
|
||||||
|
<strong>{{ error_title }}</strong>
|
||||||
|
<p>{{ render_error }}</p>
|
||||||
|
</section>
|
||||||
|
{% else %}
|
||||||
|
<div class="manual-layout">
|
||||||
|
<nav class="manual-toc" aria-label="Table of contents">
|
||||||
|
<h2>Contents</h2>
|
||||||
|
<ul>
|
||||||
|
{% for entry in toc %}
|
||||||
|
<li class="toc-level-{{ entry.level }}">
|
||||||
|
<a href="#{{ entry.id }}">{{ entry.title }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<article class="manual-content">
|
||||||
|
{{ manual_html }}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
RollCalc Beta © Naue
|
||||||
|
<div class="footer-build-info">
|
||||||
|
Version {{ build_info.version }} |
|
||||||
|
Branch {{ build_info.branch }} |
|
||||||
|
Commit {{ build_info.commit }} |
|
||||||
|
Built {{ build_info.timestamp }}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user