2026-07-27 11:11:32 +02:00
2026-09-24 13:58:55 +02:00
2026-07-01 08:06:50 +00:00
2026-07-07 16:50:10 +02:00
2026-07-07 12:20:09 +02:00
2026-07-27 11:11:32 +02:00
2026-06-30 13:55:05 +00:00
2026-09-24 13:58:55 +02:00
2026-07-07 09:32:07 +02:00

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/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, and transport analysis. The available PoC tools are get_article, calculate_roll_diameter, and analyze_transport_capacity.

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 product-length or target-length calculation tools, or extrapolation.

Tool calling semantics:

  • get_article accepts textual article_number values (including leading zeroes) and an optional article_name_hint.
  • calculate_roll_diameter uses roll_length_m for the material length on one roll. Diameter and thickness inputs are in mm; width_m is in m; area_weight_g_m2 is in g/m². include_roll_weight requests the optional kilogram result.
  • analyze_transport_capacity uses roll/core diameters in mm; roll width and transport dimensions in m; and roll/payload weights in kg. product_length_m is the material length represented by one roll, used only for reported square metres. The supplied roll_weight_kg is passed through unchanged. Callers may use a canonical transport_preset or provide custom transport dimensions and max_weight_kg.

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_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:

/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:

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:

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:

  • 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.
S
Description
Naue roll properties calculator
Readme
1.4 MiB
Languages
Python 73.3%
HTML 13.4%
JavaScript 13.2%