Files
RollCalcPython/README.md
T

26 KiB
Raw Blame History

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_article accepts textual article_number values (including leading zeroes) and an optional article_name_hint.
  • search_articles deterministically 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_weight calculates 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_length composes 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_length calculates 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_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². It requires a known positive core diameter; use calculate_material_weight when the core is unknown and only material weight is needed. On success, it returns transport_roll_inputs, the browser-equivalent transport bundle.
  • 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. For a chained calculation, pass every field from transport_roll_inputs unchanged. 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.
  • get_machine resolves a configured machine by stable ID, display name, or configured alias. Locations are descriptive metadata only.
  • check_production_feasibility evaluates all configured independent machine limits deterministically. Its V1 weight is material-only: it never estimates core, packaging, or gross weight. Callers pass average_diameter_mm and maximum_diameter_mm from calculate_roll_diameter; a nominal fit with a maximum-diameter exceedance is feasible_with_warnings, while a nominal exceedance is not_feasible. Missing configured roll inputs produce needs_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_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 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:

  • 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.

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:

  • 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.