Compare commits
5
Commits
8f64760add
..
v0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe267b8766 | ||
|
|
c2e4f64d82 | ||
|
|
7a27fb85c4 | ||
|
|
4670666421 | ||
|
|
dcca3fa042 |
+20
-2
@@ -14,7 +14,7 @@ Backend:
|
|||||||
|
|
||||||
- `app.py` creates the Flask app.
|
- `app.py` creates the Flask app.
|
||||||
- HTTP Basic Auth is implemented with `Flask-HTTPAuth`.
|
- HTTP Basic Auth is implemented with `Flask-HTTPAuth`.
|
||||||
- Users are currently hardcoded in `BETA_USERS`.
|
- Users are currently configured in `BETA_USERS` with Werkzeug password hashes.
|
||||||
- `/` renders the active calculator template.
|
- `/` renders the active calculator template.
|
||||||
- `/static/<path:filename>` is intended to serve static files behind Basic Auth.
|
- `/static/<path:filename>` is intended to serve static files behind Basic Auth.
|
||||||
- `/api/health` returns health/version information.
|
- `/api/health` returns health/version information.
|
||||||
@@ -50,6 +50,24 @@ If `build_info.json` is missing, malformed, or does not contain a usable value,
|
|||||||
|
|
||||||
The data is loaded centrally in `app.py` and made available to every template as `build_info`.
|
The data is loaded centrally in `app.py` and made available to every template as `build_info`.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
RollCalc uses HTTP Basic Auth. User entries in `BETA_USERS` have this shape:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"username": {
|
||||||
|
"password_hash": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Password verification uses `werkzeug.security.check_password_hash`. New hashes or config snippets can be generated with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/manage_users.py username
|
||||||
|
```
|
||||||
|
|
||||||
|
Passwords and hashes must not be logged.
|
||||||
|
|
||||||
## Roll Geometry
|
## Roll Geometry
|
||||||
|
|
||||||
Calculations assume an ideal cylindrical winding.
|
Calculations assume an ideal cylindrical winding.
|
||||||
@@ -210,7 +228,7 @@ Login/access logging remains part of RollCalc, but log viewing and article-data
|
|||||||
|
|
||||||
## Known Technical Risks
|
## Known Technical Risks
|
||||||
|
|
||||||
- Hardcoded plaintext credentials in `app.py`.
|
- Password hashes are currently configured in `app.py`; this should eventually move to a protected external configuration or secrets mechanism.
|
||||||
- Basic Auth only; no sessions or role framework beyond the user dictionary.
|
- Basic Auth only; no sessions or role framework beyond the user dictionary.
|
||||||
- `access_log.json` is rewritten on every logged request and is not concurrency-safe.
|
- `access_log.json` is rewritten on every logged request and is not concurrency-safe.
|
||||||
- No log rotation or retention policy is implemented.
|
- No log rotation or retention policy is implemented.
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ If port `5000` is already occupied, start through Flask's CLI without changing f
|
|||||||
flask --app app run --host 127.0.0.1 --port 5001
|
flask --app app run --host 127.0.0.1 --port 5001
|
||||||
```
|
```
|
||||||
|
|
||||||
The app uses HTTP Basic Auth. Current credentials are defined in `BETA_USERS` in `app.py`.
|
The app uses HTTP Basic Auth. Current users are defined in `BETA_USERS` in `app.py` with Werkzeug password hashes.
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
@@ -90,7 +90,22 @@ Implemented routes:
|
|||||||
| `/api/health` | `GET` | Basic Auth | Returns app health and version. |
|
| `/api/health` | `GET` | Basic Auth | Returns app health and version. |
|
||||||
| `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. |
|
| `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. |
|
||||||
|
|
||||||
Authentication is implemented with `Flask-HTTPAuth`. The current code checks `BETA_USERS` and performs direct plaintext string comparison.
|
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.
|
Access logging is handled by `log_access()`, which reads `access_log.json`, appends a record, and writes the whole file back.
|
||||||
|
|
||||||
@@ -287,11 +302,11 @@ Utility script that updates relative frontend fetch/register paths to Flask-styl
|
|||||||
- There are duplicated or legacy-looking files with similar names. Before editing JavaScript under `static/`, confirm it is actually referenced by the active template.
|
- 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 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.
|
- 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
|
## Known Risks and Maintenance Items
|
||||||
|
|
||||||
- Credentials are hardcoded in `app.py` and should be moved to environment variables or a secrets manager.
|
- Password hashes are currently configured in `app.py`; user configuration should eventually move to environment variables, a protected config file, or a secrets manager.
|
||||||
- Passwords are stored in plaintext and compared directly.
|
|
||||||
- `access_log.json` is not safe for concurrent writes.
|
- `access_log.json` is not safe for concurrent writes.
|
||||||
- `access_log.json` grows without rotation or retention limits.
|
- `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.
|
- 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.
|
||||||
|
|||||||
@@ -628,5 +628,40 @@
|
|||||||
"endpoint": "/",
|
"endpoint": "/",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"status": 200
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-07T16:13:42.026433",
|
||||||
|
"username": "mtazl",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-07T16:19:41.488752",
|
||||||
|
"username": "mtazl",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-07T16:34:58.280178",
|
||||||
|
"username": "mtazl",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-07T16:36:56.896657",
|
||||||
|
"username": "mtazl",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-07T16:41:46.568545",
|
||||||
|
"username": "mtazl",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -5,6 +5,7 @@ Flask app with HTTP Basic Authentication
|
|||||||
|
|
||||||
from flask import Flask, render_template, request, send_file, send_from_directory, jsonify
|
from flask import Flask, render_template, request, send_file, send_from_directory, jsonify
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -19,12 +20,24 @@ auth = HTTPBasicAuth()
|
|||||||
|
|
||||||
# Benutzer für Beta-Phase (in Produktion aus env-Variablen laden!)
|
# Benutzer für Beta-Phase (in Produktion aus env-Variablen laden!)
|
||||||
BETA_USERS = {
|
BETA_USERS = {
|
||||||
"beta": "rollcalc_beta_2026", # ⚠️ ÄNDERN!
|
"beta": {
|
||||||
"naue": "naue_access_2026", # ⚠️ ÄNDERN!
|
"password_hash": "pbkdf2:sha256:600000$HNtF3VdZKdmtg8Vw$8f976a99a457c5e924916dc6f735dd631042c8c126af8d4bda9abcd7532d904f"
|
||||||
"cniehues": "beta_test_2026", # ⚠️ ÄNDERN!
|
},
|
||||||
"lvollmert": "Geheim!", # ⚠️ ÄNDERN!
|
"naue": {
|
||||||
"mtazl": "rollcalc", # ⚠️ ÄNDERN!
|
"password_hash": "pbkdf2:sha256:600000$RVPiW2mYXJJIp3d7$0f838b8619d386e2da57e60ae8ccb944bb0f9f63d81ce3d174ec3034b0abcd19"
|
||||||
"controlling": "beta2026" # ⚠️ ÄNDERN!
|
},
|
||||||
|
"cniehues": {
|
||||||
|
"password_hash": "pbkdf2:sha256:600000$iNmhakf34xk26xrz$ae78846461370c52b7f56fad5ac0ae8e33860d6f00075ac78e3a5b8c31d51a3d"
|
||||||
|
},
|
||||||
|
"lvollmert": {
|
||||||
|
"password_hash": "pbkdf2:sha256:600000$8FGop5ymmHpuIqfK$8ba223e20c514cf6bc1297435a2e7e2be81668951297f0e01ad6a931718ad7de"
|
||||||
|
},
|
||||||
|
"mtazl": {
|
||||||
|
"password_hash": "pbkdf2:sha256:600000$H19skoalhWxlLnY5$d30bfeae38470b19900648fd110978b3677607c3a161d663a7e5d5c2167a3710"
|
||||||
|
},
|
||||||
|
"controlling": {
|
||||||
|
"password_hash": "pbkdf2:sha256:600000$ksj86lrKz6nnSpXz$5161e0b5c76bd72d6b2cd04866ef49e69f463f474d7e47075171894bf3366f29"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Logging für Auditing
|
# Logging für Auditing
|
||||||
@@ -74,7 +87,12 @@ def inject_build_info():
|
|||||||
@auth.verify_password
|
@auth.verify_password
|
||||||
def verify_password(username, password):
|
def verify_password(username, password):
|
||||||
"""Verify HTTP Basic Auth credentials"""
|
"""Verify HTTP Basic Auth credentials"""
|
||||||
if username in BETA_USERS and BETA_USERS[username] == password:
|
user_config = BETA_USERS.get(username)
|
||||||
|
if not user_config:
|
||||||
|
return None
|
||||||
|
|
||||||
|
password_hash = user_config.get("password_hash")
|
||||||
|
if password_hash and check_password_hash(password_hash, password):
|
||||||
return username
|
return username
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "0.4",
|
"version": "v0.4-dev",
|
||||||
"branch": "develop/v0.4",
|
"branch": "develop/v0.4",
|
||||||
"commit": "ffa3481",
|
"commit": "7a27fb8",
|
||||||
"timestamp": "2026-07-07T10:26:13Z"
|
"timestamp": "2026-07-07T16:46:32+02:00"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Create password hashes or user config snippets for RollCalc Basic Auth users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import json
|
||||||
|
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate RollCalc password hashes or user config entries."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"username",
|
||||||
|
nargs="?",
|
||||||
|
help="Optional username for a generated user config snippet.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password",
|
||||||
|
help="Password to hash. Omit to enter it securely via prompt.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output a JSON object for the user entry.",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = build_parser().parse_args()
|
||||||
|
password = args.password
|
||||||
|
|
||||||
|
if password is None:
|
||||||
|
password = getpass.getpass("Password: ")
|
||||||
|
|
||||||
|
password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
if args.username:
|
||||||
|
entry = {args.username: {"password_hash": password_hash}}
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(entry, indent=2))
|
||||||
|
else:
|
||||||
|
print(f'"{args.username}": {{')
|
||||||
|
print(f' "password_hash": "{password_hash}"')
|
||||||
|
print("}")
|
||||||
|
else:
|
||||||
|
print(password_hash)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -48,6 +48,9 @@
|
|||||||
.result-label { font-size: 12px; color: #1F5438; font-weight: 600; text-transform: uppercase; }
|
.result-label { font-size: 12px; color: #1F5438; font-weight: 600; text-transform: uppercase; }
|
||||||
.result-value { font-size: 28px; font-weight: 700; color: #1F5438; line-height: 1.2; }
|
.result-value { font-size: 28px; font-weight: 700; color: #1F5438; line-height: 1.2; }
|
||||||
.result-range { font-size: 12px; color: #1F5438; margin-top: 4px; font-weight: 500; }
|
.result-range { font-size: 12px; color: #1F5438; margin-top: 4px; font-weight: 500; }
|
||||||
|
.result-range div { margin-top: 2px; }
|
||||||
|
.result-range .range-warning { color: #9a6700; font-weight: 700; }
|
||||||
|
.result-range .range-ok { color: #1F5438; font-weight: 700; }
|
||||||
.forklift-check { border-radius: 7px; padding: 14px 16px; margin-top: 14px; font-size: 13px; line-height: 1.6; display: none; }
|
.forklift-check { border-radius: 7px; padding: 14px 16px; margin-top: 14px; font-size: 13px; line-height: 1.6; display: none; }
|
||||||
.forklift-check.show { display: block; }
|
.forklift-check.show { display: block; }
|
||||||
.forklift-check.fc-ok { background: #eafaf1; border: 1.5px solid #28a745; }
|
.forklift-check.fc-ok { background: #eafaf1; border: 1.5px solid #28a745; }
|
||||||
@@ -572,6 +575,27 @@
|
|||||||
<span class="unit-label">kg</span>
|
<span class="unit-label">kg</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Max. Roll Diameter</label>
|
||||||
|
<div class="input-with-unit">
|
||||||
|
<input type="number" id="d-target-di" placeholder="mm" min="0" step="1">
|
||||||
|
<span class="unit-label">mm</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Max. Roll Weight</label>
|
||||||
|
<div class="input-with-unit">
|
||||||
|
<input type="number" id="d-max-weight" placeholder="kg" min="0" step="1">
|
||||||
|
<span class="unit-label">kg</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Minimum Product Length</label>
|
||||||
|
<div class="input-with-unit">
|
||||||
|
<input type="number" id="d-min-le" placeholder="m" min="0" step="0.1">
|
||||||
|
<span class="unit-label">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -993,21 +1017,69 @@ document.getElementById('d-calc').addEventListener('click', () => {
|
|||||||
resRange.textContent = '';
|
resRange.textContent = '';
|
||||||
document.getElementById('d-le').value = L_calc.toFixed(2);
|
document.getElementById('d-le').value = L_calc.toFixed(2);
|
||||||
} else if (mode === 'target') {
|
} else if (mode === 'target') {
|
||||||
if (!d || !t) { alert('Please enter d and t.'); return; }
|
const wi = parseFloat(document.getElementById('d-wi').value) || 0;
|
||||||
const D_target = D || 800;
|
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
||||||
const L_calc = (Math.PI / (4 * t)) * (D_target*D_target - d*d) / 1000;
|
const maxWeight = parseFloat(document.getElementById('d-max-weight').value) || 0;
|
||||||
resLabel.textContent = 'Length for Target Diameter ' + D_target + ' mm';
|
const minLength = parseFloat(document.getElementById('d-min-le').value) || 0;
|
||||||
resVal.textContent = L_calc.toFixed(2) + ' m';
|
const D_target = parseFloat(document.getElementById('d-target-di').value) || 0;
|
||||||
resRange.textContent = '';
|
|
||||||
document.getElementById('d-le').value = L_calc.toFixed(2);
|
if (!d || !t || !D_target || !wi || !aw || !maxWeight) {
|
||||||
|
alert('Please enter d, t, max. roll diameter, roll width, area weight, and max. roll weight.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (D_target <= d) {
|
||||||
|
alert('Max. roll diameter must be larger than core diameter.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const L_diameter = (Math.PI / (4 * t)) * (D_target*D_target - d*d) / 1000;
|
||||||
|
const L_weight = maxWeight * 1000 / (aw * wi);
|
||||||
|
const L_allowed = Math.min(L_diameter, L_weight);
|
||||||
|
const D_result = Math.sqrt(d*d + (4*L_allowed*1000*t)/Math.PI);
|
||||||
|
const W_result = aw * L_allowed * wi / 1000;
|
||||||
|
const diameterUtil = D_result / D_target * 100;
|
||||||
|
const weightUtil = W_result / maxWeight * 100;
|
||||||
|
const lengthDiff = minLength > 0 ? L_allowed - minLength : null;
|
||||||
|
const epsilon = 0.0001;
|
||||||
|
let limitingFactor = 'Diameter + Weight';
|
||||||
|
|
||||||
|
if (L_diameter < L_weight - epsilon) {
|
||||||
|
limitingFactor = 'Diameter';
|
||||||
|
} else if (L_weight < L_diameter - epsilon) {
|
||||||
|
limitingFactor = 'Weight';
|
||||||
|
}
|
||||||
|
|
||||||
|
resLabel.textContent = 'Maximum Allowed Product Length';
|
||||||
|
resVal.textContent = L_allowed.toFixed(2) + ' m';
|
||||||
|
|
||||||
|
const minLengthText = minLength > 0 ? minLength.toFixed(2) + ' m' : 'not set';
|
||||||
|
const diffText = lengthDiff !== null ? lengthDiff.toFixed(2) + ' m' : 'n/a';
|
||||||
|
let minStatus = '<div>Minimum length not set</div>';
|
||||||
|
if (minLength > 0 && L_allowed < minLength) {
|
||||||
|
minStatus = '<div class="range-warning">Warning: maximum allowed length is below the minimum product length.</div>';
|
||||||
|
} else if (minLength > 0) {
|
||||||
|
minStatus = '<div class="range-ok">Minimum length OK</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
resRange.innerHTML =
|
||||||
|
'<div>Resulting Roll Diameter: ' + D_result.toFixed(1) + ' mm</div>' +
|
||||||
|
'<div>Resulting weight: ' + W_result.toFixed(1) + ' kg</div>' +
|
||||||
|
'<div>Minimum length: ' + minLengthText + '</div>' +
|
||||||
|
'<div>Difference: ' + diffText + '</div>' +
|
||||||
|
'<div>Limiting factor: ' + limitingFactor + '</div>' +
|
||||||
|
'<div>Utilization: Diameter ' + diameterUtil.toFixed(1) + '% | Weight ' + weightUtil.toFixed(1) + '%</div>' +
|
||||||
|
minStatus;
|
||||||
|
|
||||||
|
document.getElementById('d-le').value = L_allowed.toFixed(2);
|
||||||
|
document.getElementById('d-wo').value = W_result.toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wi = parseFloat(document.getElementById('d-wi').value) || 0;
|
const wi = parseFloat(document.getElementById('d-wi').value) || 0;
|
||||||
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
||||||
const L_final = parseFloat(document.getElementById('d-le').value) || 0;
|
const L_final = parseFloat(document.getElementById('d-le').value) || 0;
|
||||||
if (wi > 0 && aw > 0 && L_final > 0) {
|
if (wi > 0 && aw > 0 && L_final > 0) {
|
||||||
const weight = (aw * L_final * wi / 1000).toFixed(1);
|
const weight = aw * L_final * wi / 1000;
|
||||||
document.getElementById('d-wo').value = weight;
|
document.getElementById('d-wo').value = weight.toFixed(1);
|
||||||
checkForklift(weight, d, category);
|
checkForklift(weight, d, category);
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('forkliftCheck').classList.remove('show');
|
document.getElementById('forkliftCheck').classList.remove('show');
|
||||||
@@ -1017,7 +1089,7 @@ document.getElementById('d-calc').addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('d-reset').addEventListener('click', () => {
|
document.getElementById('d-reset').addEventListener('click', () => {
|
||||||
['d-co', 'd-th', 'd-tol', 'd-le', 'd-di', 'd-wi', 'd-aw', 'd-wo'].forEach(id => {
|
['d-co', 'd-th', 'd-tol', 'd-le', 'd-di', 'd-wi', 'd-aw', 'd-wo', 'd-target-di', 'd-max-weight', 'd-min-le'].forEach(id => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.value = id === 'd-co' ? '150' : '';
|
if (el) el.value = id === 'd-co' ? '150' : '';
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user