505 lines
21 KiB
Markdown
505 lines
21 KiB
Markdown
# RollCalcPython
|
||
|
||
Flask-based internal roll diameter calculator for Naue roll products. Direct roll-diameter calculations are provided by a deterministic Python domain service and consumed by the authenticated browser UI and PDF path.
|
||
|
||
This README is intended for developers maintaining the project, not for end users.
|
||
|
||
## Runtime Stack
|
||
|
||
- Python 3
|
||
- Flask 2.3.3
|
||
- Flask-HTTPAuth 4.8.0
|
||
- Vanilla HTML, CSS, and JavaScript
|
||
- JSON files for product data and forklift/load rules
|
||
- Dependency-free server-side PDF report rendering
|
||
- Direct standard-library HTTP integration with a local Ollama service
|
||
|
||
Pinned Python dependencies are defined in `requirements.txt`.
|
||
|
||
## Local Setup
|
||
|
||
Create and activate a virtual environment:
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
Start with the hardcoded settings in `app.py`:
|
||
|
||
```bash
|
||
python app.py
|
||
```
|
||
|
||
By default this attempts to bind to:
|
||
|
||
```text
|
||
http://localhost:5000
|
||
```
|
||
|
||
If port `5000` is already occupied, start through Flask's CLI without changing files:
|
||
|
||
```bash
|
||
flask --app app run --host 127.0.0.1 --port 5001
|
||
```
|
||
|
||
The app uses HTTP Basic Auth. Current users are defined in `BETA_USERS` in `app.py` with Werkzeug password hashes.
|
||
|
||
## Project Layout
|
||
|
||
```text
|
||
.
|
||
├── app.py
|
||
├── conversation_service.py
|
||
├── core_presets.py
|
||
├── ollama_nlu.py
|
||
├── pdf_report.py
|
||
├── roll_calculation.py
|
||
├── transport_calculation.py
|
||
├── rollcalc_chat.py
|
||
├── integrations/
|
||
│ └── openwebui/
|
||
│ ├── README.md
|
||
│ └── rollcalc_pipe.py
|
||
├── requirements.txt
|
||
├── README.md
|
||
├── build_info.json
|
||
├── access_log.json
|
||
├── config.json
|
||
├── article-data_.json
|
||
├── fix_article_data.py
|
||
├── service-worker.js
|
||
├── templates/
|
||
│ └── roll_calculator.html
|
||
├── tests/
|
||
│ └── test_pdf_report.py
|
||
├── static/
|
||
│ ├── article-data.json
|
||
│ ├── config.json
|
||
│ ├── service-worker.js
|
||
│ ├── stddev_calculator.js
|
||
│ ├── direct_calc_handler.js
|
||
│ ├── rollcalc_v14.js
|
||
│ ├── rollcalc_improvements.js
|
||
│ ├── rollcalc-improvements.js
|
||
│ ├── rollcalc_stddev_ranges.js
|
||
│ └── rollcalc_stddev_integration.js
|
||
└── docs/
|
||
├── DEPLOYMENT_GUIDE.md
|
||
├── QOL_UPDATE_SUMMARY.md
|
||
├── STDDEV_IMPLEMENTATION_CHECKLIST.md
|
||
└── STDDEV_RANGES_DOCS.md
|
||
```
|
||
|
||
## Backend
|
||
|
||
`app.py` owns the Flask application and authentication.
|
||
|
||
Implemented routes:
|
||
|
||
| Route | Methods | Auth | Purpose |
|
||
| --- | --- | --- | --- |
|
||
| `/` | `GET` | Basic Auth | Renders `templates/roll_calculator.html`. |
|
||
| `/static/<path:filename>` | `GET` | Basic Auth | Intended protected static-file serving from `static/`. |
|
||
| `/api/health` | `GET` | Basic Auth | Returns app health and version. |
|
||
| `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. |
|
||
| `/api/calculations/roll` | `POST` | Basic Auth | Resolves, validates, and executes a direct roll-diameter request. |
|
||
| `/api/calculations/roll/modify` | `POST` | Basic Auth | Applies explicit changes to structured calculation state and recalculates. |
|
||
| `/api/calculations/transport` | `POST` | Basic Auth | Executes the shared transport-capacity analysis. |
|
||
| `/api/reports/roll-calculation.pdf` | `POST` | Basic Auth | Recalculates a request authoritatively and returns a one-page PDF. |
|
||
| `/api/conversations` | `POST` | Basic Auth | Creates an in-memory conversation session. |
|
||
| `/api/conversations/<id>/messages` | `POST` | Basic Auth | Interprets one utterance and executes the deterministic workflow. |
|
||
| `/api/conversations/reports/<id>.pdf` | `GET` | Basic Auth | Recalculates a stored input-only request and returns its PDF. |
|
||
|
||
Authentication is implemented with `Flask-HTTPAuth`. The current code checks `BETA_USERS` with `werkzeug.security.check_password_hash`; plaintext passwords are not stored in the application.
|
||
|
||
Generate a password hash or user entry with:
|
||
|
||
```bash
|
||
python scripts/manage_users.py username
|
||
python scripts/manage_users.py username --json
|
||
```
|
||
|
||
For non-interactive local maintenance only:
|
||
|
||
```bash
|
||
python scripts/manage_users.py username --password 'new-password'
|
||
```
|
||
|
||
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.
|
||
|
||
## Local Ollama NLU Adapter
|
||
|
||
The local LLM is an NLU adapter only. Engineering calculations remain deterministic.
|
||
|
||
`ollama_nlu.py` calls Ollama's `/api/chat` endpoint directly with the Python standard library. The default configuration is:
|
||
|
||
```text
|
||
model: qwen3.5:35B-A3B
|
||
base URL: http://127.0.0.1:11434
|
||
temperature: 0.0
|
||
think: false
|
||
timeout: 45 seconds
|
||
stream: false
|
||
format: strict JSON schema
|
||
```
|
||
|
||
Configuration can be overridden without editing code:
|
||
|
||
```bash
|
||
export ROLLCALC_OLLAMA_URL=http://127.0.0.1:11434
|
||
export ROLLCALC_OLLAMA_MODEL=qwen3.5:35B-A3B
|
||
export ROLLCALC_OLLAMA_TIMEOUT_SECONDS=45
|
||
export ROLLCALC_OLLAMA_TEMPERATURE=0
|
||
```
|
||
|
||
The model receives the user utterance and, after a successful calculation, a minimal summary of the current input state: resolved article reference, roll length, width, core, and weight-request flag. It does not receive formulas, the article/master dataset, calculated diameters or weights, warnings, or PDF data.
|
||
|
||
The accepted model output has exactly one of these shapes:
|
||
|
||
```json
|
||
{
|
||
"intent": "new_calculation",
|
||
"article_number": "180205",
|
||
"article_name_hint": "Bentofix NSP 4900",
|
||
"roll_length_m": 65.0,
|
||
"width_m": null,
|
||
"core_type": null,
|
||
"core_diameter_mm": null,
|
||
"include_roll_weight": false
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"intent": "modify_calculation",
|
||
"changes": {
|
||
"roll_length_m": 80.0,
|
||
"include_roll_weight": true
|
||
}
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"intent": "unsupported"
|
||
}
|
||
```
|
||
|
||
Every optional field must be present in `new_calculation` and use `null` when the user did not supply it. A modification supports one or more explicitly enumerated input fields (`roll_length_m`, `width_m`, `core_type`, `core_diameter_mm`, and `include_roll_weight`, plus the existing article identifiers). Unknown fields, invalid types, markdown-wrapped JSON, and calculated diameter/weight/warning fields are rejected by application validation even if Ollama returns them. All requested changes are validated and applied atomically; an unresolved clarification retains the complete structured patch until it can be applied.
|
||
|
||
### Conversation State and Reports
|
||
|
||
`conversation_service.py` owns the current `CalculationState`, last deterministic result, and typed pending clarification. It never reconstructs technical state from chat history. When a successful state exists, an identifier-free `new_calculation` interpretation containing only explicit modification-like values is narrowly converted into a state modification; omitted fields are inherited, while an explicit different article or explicit “new calculation” phrase starts fresh. Successful calculations create an unguessable temporary report ID that stores only the validated calculation request. PDF download recalculates that request through `calculate_roll()` and uses the existing renderer.
|
||
|
||
The store is an MVP process-local memory store:
|
||
|
||
- sessions and report references disappear on restart;
|
||
- state is not shared between multiple WSGI processes;
|
||
- no expiration, persistence, or cross-process locking is implemented;
|
||
- successful report references remain until process restart.
|
||
|
||
### OpenWebUI Browser Adapter
|
||
|
||
`integrations/openwebui/rollcalc_pipe.py` is a small OpenWebUI Pipe Function.
|
||
It registers **RollCalc Assistant** as a selectable model and forwards each user
|
||
message to the existing conversation API. It neither calls a second model nor
|
||
contains article, formula, warning, clarification, state-mutation, or PDF
|
||
logic.
|
||
|
||
The Pipe keeps a process-local mapping from `(OpenWebUI user ID, chat ID)` to a
|
||
RollCalc `conversation_id`. Only the API's deterministic `message` is displayed.
|
||
An API report path is converted into a Markdown download link using a separately
|
||
configured browser-reachable RollCalc URL.
|
||
|
||
Required configuration:
|
||
|
||
```bash
|
||
export ROLLCALC_API_BASE_URL=http://rollcalc.internal:5000
|
||
export ROLLCALC_PUBLIC_BASE_URL=https://rollcalc.example.internal
|
||
export ROLLCALC_USERNAME=service-user
|
||
export ROLLCALC_PASSWORD='set-outside-the-repository'
|
||
```
|
||
|
||
The API credentials are server-side only and are never embedded in report URLs.
|
||
The browser must authenticate separately against RollCalc's existing Basic-Auth
|
||
challenge. Installation, network-topology checks, Valve configuration, demo
|
||
steps, and MVP limitations are documented in
|
||
`integrations/openwebui/README.md`.
|
||
|
||
### Terminal Demo
|
||
|
||
Ensure Ollama is running and the configured model is installed, then run:
|
||
|
||
```bash
|
||
python -m rollcalc_chat
|
||
```
|
||
|
||
For validation output containing raw model JSON, validated interpretation, deterministic outcome, and latency:
|
||
|
||
```bash
|
||
python -m rollcalc_chat --debug
|
||
```
|
||
|
||
Optional real-model/manual article-name matrix (run the prompts in one
|
||
`python -m rollcalc_chat --debug` session):
|
||
|
||
| Prompt | Expected deterministic outcome |
|
||
| --- | --- |
|
||
| `Berechne Artikel 146900 mit 50 m und einem 150-mm-PVC-Kern.` | Exact article number `146900` resolves. |
|
||
| `Berechne Stex R 1801, 5,80 x 50 m mit 50 m und einem 150-mm-PVC-Kern.` | The normalized full name resolves to `146900`. |
|
||
| `Berechne stex r 1801; 5.80 × 50 m mit 50 m und einem 150-mm-PVC-Kern.` | Case and safe punctuation normalization still resolve to `146900`. |
|
||
| `Berechne Stex R 1801 mit 50 m und einem 150-mm-PVC-Kern.` | Clarification lists `146900` (5,80 x 50 m) and `146910` (6,00 x 50 m). |
|
||
| `146900` | Resolves the pending article clarification without another Ollama call. |
|
||
| `Berechne Artikel 180205, Stex R 1801, mit 40 m und einem 150-mm-PVC-Kern.` | Deterministic `article_conflict`; neither identifier is silently preferred. |
|
||
| `Berechne Nicht vorhandenes Produkt XYZ mit 40 m und einem 150-mm-PVC-Kern.` | Concise deterministic article-not-found response. |
|
||
|
||
The model may extract an article number or name, but all resolution and every
|
||
displayed candidate come from `static/article-data.json`.
|
||
|
||
The initial scope supports new direct roll-diameter calculations, multi-field modifications, and clarification replies. Product-length mode, target-diameter mode, extrapolation, load optimization, general questions, and arbitrary tool execution return `unsupported`.
|
||
|
||
The conversational adapter does not reuse the browser's visible 150 mm core default. Current article data contains no usable core mapping, so a request without an explicit known core or core diameter asks for clarification. Width remains optional for diameter-only calculations and becomes required only when roll weight is explicitly requested.
|
||
|
||
Normal tests mock Ollama. To opt into the real-model smoke test explicitly:
|
||
|
||
```bash
|
||
ROLLCALC_OLLAMA_MODEL=qwen3.5:35B-A3B \
|
||
ROLLCALC_RUN_OLLAMA_SMOKE=1 python -m unittest discover \
|
||
-s tests -p 'test_ollama_real_smoke.py' -v
|
||
```
|
||
|
||
## Frontend Entry Point
|
||
|
||
The active UI is `templates/roll_calculator.html`.
|
||
|
||
The template contains:
|
||
|
||
- Page layout and all main CSS.
|
||
- A disclaimer modal shown after Basic Auth login and before calculator use.
|
||
- Footer build metadata from `build_info`.
|
||
- Global state:
|
||
- `window.ARTICLE_DATA`
|
||
- `window.APP_CONFIG`
|
||
- `window.CURRENT_ROLL_REQUEST`
|
||
- `window.CURRENT_ROLL_RESULT`
|
||
- Product/article data loading from `/static/article-data.json`.
|
||
- Direct roll-diameter request/response rendering.
|
||
- Extrapolation calculations.
|
||
- Forklift/weight warnings.
|
||
- Load optimizer UI and calculations.
|
||
|
||
Most of the currently active JavaScript is inline in this template. Several files under `static/` appear to be older, alternative, or integration-oriented modules and should be checked before assuming they are active.
|
||
|
||
## Main Functional Areas
|
||
|
||
### Disclaimer Gate
|
||
|
||
The calculator page displays a modal disclaimer before use. The modal:
|
||
|
||
- Shows German and English disclaimer text.
|
||
- Requires a checkbox confirmation.
|
||
- Keeps the confirmation button disabled until checked.
|
||
- Hides the overlay after confirmation.
|
||
|
||
This is implemented in `templates/roll_calculator.html` and is client-side only.
|
||
|
||
### Article Data Loading
|
||
|
||
`templates/roll_calculator.html` fetches:
|
||
|
||
```text
|
||
/static/article-data.json
|
||
```
|
||
|
||
The loaded array is assigned to `window.ARTICLE_DATA` and used to populate article datalists for direct calculation and extrapolation.
|
||
|
||
Known article fields used by the UI include:
|
||
|
||
- `nr`
|
||
- `name`
|
||
- `thickness`
|
||
- `thickness_stddev`
|
||
- `area_weight`
|
||
- `area_weight_stddev`
|
||
- `core_type`
|
||
|
||
The active `static/article-data.json` currently contains product records with additional min/max/count statistics.
|
||
|
||
### Direct Calculation
|
||
|
||
The direct calculation tab supports multiple modes via mode buttons:
|
||
|
||
- Roll diameter from core diameter, material thickness, and product length.
|
||
- Product length from core diameter, material thickness, and roll diameter.
|
||
- Product length for a target diameter.
|
||
|
||
The direct roll-diameter mode posts raw decision inputs to `/api/calculations/roll`. The authoritative implementation is `calculate_roll()` in `roll_calculation.py`; the template only renders its result.
|
||
|
||
The four core preset buttons are rendered from the canonical ordered `CORE_PRESETS` tuple in `core_presets.py`. Headless and conversational core resolution uses the same tuple and its finite German/English aliases. Generic steel and PVC family names remain ambiguous because the current UI contains two presets for each family.
|
||
|
||
The nominal formula implemented there is:
|
||
|
||
```text
|
||
D = sqrt(d^2 + (4 * L * 1000 * t) / pi)
|
||
```
|
||
|
||
Where:
|
||
|
||
- `D` is roll diameter in mm.
|
||
- `d` is core diameter in mm.
|
||
- `L` is product length in m.
|
||
- `t` is material thickness in mm.
|
||
|
||
If a tolerance/stddev value is present, the UI also displays a `-2σ` and `+2σ` diameter range.
|
||
|
||
Optional roll weight is calculated by the same service when requested and when roll width, area weight, and length are available:
|
||
|
||
```text
|
||
weight_kg = area_weight_g_m2 * length_m * width_m / 1000
|
||
```
|
||
|
||
In the browser, weight remains optional: the direct-diameter request asks for a weight only when both width and area weight are present. A future machine client can request it explicitly with `include_roll_weight`; missing required weight inputs then produce structured clarification.
|
||
|
||
### PDF Calculation Report
|
||
|
||
After a valid direct roll-diameter calculation, the result card enables **Download PDF**. The browser posts the original `CalculationRequest`, not calculated result values. The server calls the same `calculate_roll()` service, adapts that authoritative `CalculationResult`, and renders it. Caller-supplied diameters, weights, and warnings are rejected as request fields.
|
||
|
||
The A4 report contains:
|
||
|
||
- article/reference data when present;
|
||
- all material calculation inputs, including category/site when selected;
|
||
- minimum, average, and maximum diameter plus the effective calculated roll length, using the UI's values and rounding;
|
||
- optional calculated roll weight;
|
||
- applicable forklift warnings or notes;
|
||
- generation time and build metadata.
|
||
|
||
PDF rendering is implemented in `pdf_report.py` using native PDF page and content primitives. It requires no browser installation and no additional Python package. The repeated diagonal `INTERNAL USE ONLY` marks are painted into the page content stream. They are a visible internal-use deterrent, not DRM or a cryptographically tamper-proof control.
|
||
|
||
The filename includes the article reference and local generation time as `roll-calculation_<article>_YYYY-MM-DD_HHMMSS.pdf`. The button is invalidated when a calculation input changes and is currently enabled for the normal direct roll-diameter mode.
|
||
|
||
### Extrapolation
|
||
|
||
The extrapolation tab estimates a new roll diameter from a known roll diameter/length pair and a new target length:
|
||
|
||
```text
|
||
D1 = sqrt(d^2 + (L1 / L0) * (D0^2 - d^2))
|
||
```
|
||
|
||
Where:
|
||
|
||
- `d` is core diameter.
|
||
- `D0` is measured/current diameter.
|
||
- `L0` is measured/current length.
|
||
- `L1` is target length.
|
||
- `D1` is the calculated new diameter.
|
||
|
||
### Forklift Check
|
||
|
||
For the authoritative direct roll-diameter flow, `roll_calculation.py` evaluates the current heavy-roll thresholds and returns structured warnings/notes for the UI and PDF. The legacy browser `checkForklift()` remains in use only by the other client-side direct modes.
|
||
|
||
The current inline config targets the `bentofix` category and includes:
|
||
|
||
- Warning threshold: `1700 kg`
|
||
- Hard limit: `2750 kg`
|
||
- Minimum core outer diameter requirement: `170 mm`
|
||
|
||
There is also a richer `static/config.json` with localized messages and more detailed requirements.
|
||
|
||
### Load Optimizer
|
||
|
||
`transport_calculation.py` is the authoritative capacity calculation. It owns
|
||
the active transport presets and `analyze_transport()`. The browser collects
|
||
inputs, calls `POST /api/calculations/transport`, renders the returned analysis,
|
||
and draws the SVG front view; it does not calculate capacity itself.
|
||
|
||
It uses the displayed roll data from the Direct Calculation tab and estimates loading capacity for transport presets or custom dimensions. In particular, the direct tab writes the roll weight with one decimal place before the optimizer consumes it; this behavior is intentionally preserved.
|
||
|
||
Preset transport units include:
|
||
|
||
- 20ft Container
|
||
- 40ft Container
|
||
- 40ft High Cube
|
||
- LKW Sattelzug
|
||
- LKW Tandem
|
||
|
||
The optimizer calculates limiting scenarios by:
|
||
|
||
- Weight
|
||
- Volume
|
||
- Geometry
|
||
|
||
It also renders a side-view SVG visualization of the loading arrangement.
|
||
|
||
### Standard Deviation Modules
|
||
|
||
There are standalone/static modules for standard deviation logic:
|
||
|
||
- `static/stddev_calculator.js`
|
||
- `static/rollcalc_stddev_ranges.js`
|
||
- `static/rollcalc_stddev_integration.js`
|
||
|
||
These provide or describe range calculations for material thickness, area weight, roll diameter, and roll weight. Check actual script inclusion before treating them as active in production, because the current template already contains inline stddev/tolerance behavior.
|
||
|
||
### Admin UI
|
||
|
||
RollCalc no longer contains an admin UI or admin API. Login/access logging remains in the backend, but logs are not exposed through a RollCalc admin screen.
|
||
|
||
Administration and maintenance of `article-data.json` is planned for a separate application. RollCalc should continue to consume `static/article-data.json` read-only.
|
||
|
||
## Static Assets and Data Files
|
||
|
||
### `static/article-data.json`
|
||
|
||
Primary product/article dataset used by the active calculator UI.
|
||
|
||
### `static/config.json`
|
||
|
||
JSON configuration for forklift/heavy-roll rules. The active template also contains an inline `window.APP_CONFIG`, so developers should verify which config source is authoritative before changing rule behavior.
|
||
|
||
### `access_log.json`
|
||
|
||
JSON audit log written by `app.py`.
|
||
|
||
Important implementation detail: each logged access reads and rewrites the entire JSON file. This is simple but not concurrency-safe and can become inefficient as the file grows.
|
||
|
||
### `build_info.json`
|
||
|
||
Deployment/build metadata displayed in the site footer.
|
||
|
||
Expected fields:
|
||
|
||
- `version`
|
||
- `branch`
|
||
- `commit`
|
||
- `timestamp`
|
||
|
||
`app.py` loads this file centrally at startup and exposes it to all templates as `build_info`. If the file is missing, invalid, or a field is empty, the affected values fall back to `unknown` and the website continues to work.
|
||
|
||
### `fix_article_data.py`
|
||
|
||
Utility script that updates relative frontend fetch/register paths to Flask-style `/static/...` paths and checks that key static files exist.
|
||
|
||
## Development Notes
|
||
|
||
- The codebase is currently closer to a single-page static calculator wrapped by Flask than to a conventional Flask MVC app.
|
||
- The active calculator logic is concentrated in `templates/roll_calculator.html`.
|
||
- There are duplicated or legacy-looking files with similar names. Before editing JavaScript under `static/`, confirm it is actually referenced by the active template.
|
||
- The documentation under `docs/` contains deployment and feature notes, but some filenames and route assumptions may not match the current app exactly.
|
||
- The Flask dev server is used for local development only. Production should use a WSGI server.
|
||
- Use `scripts/manage_users.py` to create password hashes when adding or rotating Basic Auth users.
|
||
|
||
## Known Risks and Maintenance Items
|
||
|
||
- Password hashes are currently configured in `app.py`; user configuration should eventually move to environment variables, a protected config file, or a secrets manager.
|
||
- `access_log.json` is not safe for concurrent writes.
|
||
- `access_log.json` grows without rotation or retention limits.
|
||
- The custom `/static/<path:filename>` route is intended to protect static files, but Flask also creates a default static route unless disabled. Verify effective route behavior before relying on static-file protection.
|
||
- RollCalc no longer includes an admin UI/API; article data administration belongs in a separate application.
|
||
- Calculation, mutation, UI-integration, PDF-model, and endpoint tests use Python's standard-library `unittest`; endpoint tests require the normal dependencies from `requirements.txt` to be installed.
|
||
- The main template is large and mixes layout, styling, data loading, calculations, and UI behavior.
|
||
- The disclaimer confirmation is client-side only and is not persisted or audited server-side.
|