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:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Start with the hardcoded settings in app.py:
python app.py
By default this attempts to bind to:
http://localhost:5000
If port 5000 is already occupied, start through Flask's CLI without changing files:
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
.
├── 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/calculations/production-feasibility |
POST |
Basic Auth | Checks configured production-machine constraints. |
/api/calculations/machine-max-product-length |
POST |
Basic Auth | Calculates deterministic machine-aware maximum product lengths. |
/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.
MCP PoC
mcp_server.py provides a local stdio-only Model Context Protocol server. It is
a thin adapter over the same domain services used by Flask: article lookup,
direct roll calculation, product-length calculation, material-weight calculation,
transport analysis, and configured production-machine feasibility.
The available PoC tools are get_article, search_articles,
calculate_material_weight, calculate_product_length,
calculate_roll_diameter, analyze_transport_capacity, get_machine,
check_production_feasibility, and calculate_machine_max_product_length.
Install the pinned dependencies, including mcp==1.26.0, in the existing
environment, then start it with:
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python mcp_server.py
The PoC has no MCP resources, HTTP/SSE transport, remote authentication, or dedicated target-length calculation tool or extrapolation support.
Tool calling semantics:
get_articleaccepts textualarticle_numbervalues (including leading zeroes) and an optionalarticle_name_hint.search_articlesdeterministically returns master-data candidates for a product name or designation. Its ordering is retrieval relevance, not a product recommendation: when multiple candidates remain, callers must ask the user to select an article number rather than choosing one.calculate_material_weightcalculates material-only weight from roll length, width, and area weight. It requires no core diameter, does not infer one, and cannot return a roll diameter or a core-inclusive total weight.calculate_machine_max_product_lengthcomposes configured machine limits with the shared calculation domain. Resolve an article first, then pass its properties unchanged. Its nominal maximum uses the nominal diameter limit; its conservative/no-warning maximum uses RollCalc's existing maximum-diameter thickness variation. Both also respect configured material-only maximum roll weight, and the domain returns the governing constraint(s), unrounded. Core and width incompatibility cannot be corrected by shortening and is returned structurally. No manufacturing increment, core/packaging/gross weight, or conditional production rule is applied in V1.1. Its successful MCP response is a compact projection: machine identity, configured maximum diameter/material-weight limits, production maxima, material weights, and warnings. Flask and other domain consumers retain the full diagnostic result.calculate_product_lengthcalculates required roll length from a target outer diameter, core diameter, and material thickness. Its optional thickness stddev returns minimum/average/maximum length ranges; optional width and area weight return material-weight ranges. It does not infer missing article or material inputs.calculate_roll_diameterusesroll_length_mfor the material length on one roll. Diameter and thickness inputs are in mm;width_mis in m;area_weight_g_m2is in g/m². It requires a known positive core diameter; usecalculate_material_weightwhen the core is unknown and only material weight is needed. On success, it returnstransport_roll_inputs, the browser-equivalent transport bundle.analyze_transport_capacityuses roll/core diameters in mm; roll width and transport dimensions in m; and roll/payload weights in kg.product_length_mis the material length represented by one roll, used only for reported square metres. For a chained calculation, pass every field fromtransport_roll_inputsunchanged. The suppliedroll_weight_kgis passed through unchanged. Callers may use a canonicaltransport_presetor provide custom transport dimensions andmax_weight_kg.get_machineresolves a configured machine by stable ID, display name, or configured alias. Locations are descriptive metadata only.check_production_feasibilityevaluates all configured independent machine limits deterministically. Its V1 weight is material-only: it never estimates core, packaging, or gross weight. Callers passaverage_diameter_mmandmaximum_diameter_mmfromcalculate_roll_diameter; a nominal fit with a maximum-diameter exceedance isfeasible_with_warnings, while a nominal exceedance isnot_feasible. Missing configured roll inputs produceneeds_clarification; line speed is retained in configuration but outside the initial roll-feasibility scope.
Generate a password hash or user entry with:
python scripts/manage_users.py username
python scripts/manage_users.py username --json
For non-interactive local maintenance only:
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:
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:
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:
{
"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
}
{
"intent": "modify_calculation",
"changes": {
"roll_length_m": 80.0,
"include_roll_weight": true
}
}
{
"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:
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:
python -m rollcalc_chat
For validation output containing raw model JSON, validated interpretation, deterministic outcome, and latency:
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:
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_DATAwindow.APP_CONFIGwindow.CURRENT_ROLL_REQUESTwindow.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:
/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:
nrnamethicknessthickness_stddevarea_weightarea_weight_stddevcore_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 Product Length mode posts to /api/calculations/product-length. The
authoritative implementations are calculate_roll() and
calculate_product_length() in roll_calculation.py; the template only renders
their results.
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:
D = sqrt(d^2 + (4 * L * 1000 * t) / pi)
Where:
Dis roll diameter in mm.dis core diameter in mm.Lis product length in m.tis 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:
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:
D1 = sqrt(d^2 + (L1 / L0) * (D0^2 - d^2))
Where:
dis core diameter.D0is measured/current diameter.L0is measured/current length.L1is target length.D1is 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.jsstatic/rollcalc_stddev_ranges.jsstatic/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.
config/machines.yaml
Runtime configuration for production-machine constraints, loaded with
yaml.safe_load and strict schema validation by machine_constraints.py.
It has schema_version: 1, stable IDs, names, aliases, locations, and explicit
unit-bearing constraint keys. null line-speed constraints mean unknown/not
configured; they do not mean zero or unlimited capability. The CSV in
data/source/LineLimitations_RollCalc.csv remains the source/reference
document. The browser currently has no production-machine control; the shared
Flask API and MCP tools expose the capability for later UI work.
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:
versionbranchcommittimestamp
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.pyto 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.jsonis not safe for concurrent writes.access_log.jsongrows 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 fromrequirements.txtto 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.