From 1f6ecc7982d38ea9a901d76a08730a65ff1155b3 Mon Sep 17 00:00:00 2001 From: salman Date: Thu, 27 Aug 2026 19:24:27 +0200 Subject: [PATCH] Modernize scan workflow and web deployment --- README.md | 56 ++++++++++++++++------ TrimSP.html | 75 ++++++++++++++--------------- TrimSPlib.js | 108 ++++++++++++++++++++++++++++++++---------- ZGUI.css | 92 ++++++++++++++++++++++++++++++----- scripts/deploy-web.sh | 85 +++++++++++++++++++++++++++++++++ singleTrimSP.cgi | 82 ++++++++++++++++++++++++++++++++ 6 files changed, 410 insertions(+), 88 deletions(-) create mode 100755 scripts/deploy-web.sh create mode 100644 singleTrimSP.cgi diff --git a/README.md b/README.md index 527aa44..55469dd 100644 --- a/README.md +++ b/README.md @@ -228,24 +228,52 @@ which matches how the packaged app currently handles simulation input/output. ###### Deploying on a web server ###### -The browser version requires a server-side setup in addition to the static -HTML/JavaScript files. +The repository includes the browser assets, CGI runner, and a deployment +script for an Apache-style server. With the default settings, run: -In particular: +```bash +sudo ./scripts/deploy-web.sh +``` -* the browser build uses `TrimSPweb.js` rather than the Electron-specific - `TrimSPelec.js` -* the simulation backend must be available on the server -* a web-exposed helper/CGI layer is required to create input files, run the - backend, and return the generated output +The script builds the static Fortran engine and installs: -Notes: +```text +/var/www/html/TRIMSP-NL/ browser application +/var/www/cgi-bin/singleTrimSP.cgi CGI runner +/usr/local/bin/trimspNL TRIM.SP-NL engine +``` -* this repository does not currently include the CGI/backend helper used by - the online deployment -* a plain static web server is therefore not sufficient for running web - simulations from this checkout -* static hosting can still be used to test page loading only +Deployment paths and ownership can be changed through environment variables: + +| Variable | Default | Purpose | +| --- | --- | --- | +| `TRIMSP_WEB_TARGET` | `/var/www/html/TRIMSP-NL` | Browser asset directory | +| `TRIMSP_CGI_TARGET` | `/var/www/cgi-bin/singleTrimSP.cgi` | Installed CGI path | +| `TRIMSP_BIN_TARGET` | `/usr/local/bin/trimspNL` | Installed engine path | +| `TRIMSP_WEB_OWNER` | `apache` | Web file owner | +| `TRIMSP_WEB_GROUP` | `apache` | Web file group | +| `TRIMSP_BUILD_STATIC` | `1` | Build the static engine before deployment | +| `TRIMSP_BINARY_SOURCE` | `fortran/trimspNL-static` | Binary used when installing | + +For example, to deploy without rebuilding and use an existing binary: + +```bash +sudo TRIMSP_BUILD_STATIC=0 \ + TRIMSP_BINARY_SOURCE=/path/to/trimspNL \ + ./scripts/deploy-web.sh +``` + +The web server must execute the CGI through `/cgi-bin/singleTrimSP.cgi` and +serve simulation files written below filesystem `/tmp` through URL `/tmp/`. +Review that mapping carefully so it exposes only the intended simulation +artifacts. On SELinux systems, inspect denied operations with: + +```bash +ausearch -m avc -ts recent +``` + +The Apache account needs permission to execute the CGI and engine and to create +the per-run directories in `/tmp`. ### Contact ### diff --git a/TrimSP.html b/TrimSP.html index cfb02ce..83f5f14 100644 --- a/TrimSP.html +++ b/TrimSP.html @@ -111,16 +111,27 @@
- Enable scan on - - - - - - - - - - + + +
+
+ + + +
+ + +
Single simulation using the Layers settings.
+
diff --git a/TrimSPlib.js b/TrimSPlib.js index dcb902d..e6536be 100644 --- a/TrimSPlib.js +++ b/TrimSPlib.js @@ -3351,11 +3351,12 @@ function adjust_table() function adjust_scans() { - // Deal with scans checkbox and type var scanSeq = document.getElementById("scanSeq").checked; - var sender = document.getElementById("scanSeq"); var scanType = document.getElementById("scanType").value; var scanThreads = document.getElementById("scanThreads"); + var scanControls = document.getElementById("ScansControls"); + var scanRange = document.getElementById("ScansTable"); + var scanList = document.getElementById("ScansLine"); // Default to half the available local worker slots, rounded down, while // keeping at least one process. Web mode keeps the default at 1 because @@ -3366,6 +3367,10 @@ function adjust_scans() availableThreads = Math.max(1, getParallelJobCount()); } scanThreads.max = availableThreads; + scanThreads.disabled = amIWeb(); + scanThreads.title = amIWeb() + ? 'The web backend currently runs scan points sequentially.' + : `This computer provides up to ${availableThreads} worker slots.`; if (scanThreads.value === "" || Number(scanThreads.value) < 1) { scanThreads.value = Math.max(1, Math.floor(availableThreads / 2)); @@ -3374,23 +3379,85 @@ function adjust_scans() } } - // consider only if scans checkbox is checked - if (scanSeq) { - ShowHide(1,'ScansControls'); - // Which type of scan - if (scanType == "scanLoop") { - ShowHide(1,'ScansTable'); - ShowHide(0,'ScansLine'); + scanControls.hidden = !scanSeq; + if (!scanSeq) { + scanRange.hidden = true; + scanList.hidden = true; + } else if (scanType === "scanLoop") { + scanRange.hidden = false; + scanList.hidden = true; + } else { + scanRange.hidden = true; + scanList.hidden = false; + } + + updateScanSummary(); +} + +function updateScanSummary() +{ + var summary = document.getElementById("scanSummary"); + if (!summary) { return; } + + if (!document.getElementById("scanSeq").checked) { + summary.dataset.level = "idle"; + summary.textContent = "Single simulation using the Layers settings."; + return; + } + + var scanType = document.getElementById("scanType").value; + var parameterSelect = document.getElementById("comboScan"); + var parameter = parameterSelect.options[parameterSelect.selectedIndex].text; + var workers = Number(document.getElementById("scanThreads").value) || 1; + var pointCount = 0; + var detail = ""; + var warning = ""; + + if (scanType === "scanLoop") { + var from = Number(document.getElementById("scanFrom").value); + var step = Number(document.getElementById("scanStep").value); + var to = Number(document.getElementById("scanTo").value); + if (!Number.isFinite(from) || !Number.isFinite(step) || !Number.isFinite(to)) { + warning = "Enter numeric From, Step, and To values."; + } else if (step <= 0) { + warning = "Step must be greater than zero."; + } else if (to < from) { + warning = "To must be greater than or equal to From."; } else { - ShowHide(1,'ScansLine'); - ShowHide(0,'ScansTable'); + pointCount = Math.floor((to - from) / step) + 1; + detail = `${from} to ${to}, step ${step}`; } } else { - // Hide both - ShowHide(0,'ScansControls'); - ShowHide(0,'ScansTable'); - ShowHide(0,'ScansLine'); + var values = document.getElementById("scanList").value + .split(",") + .map(value => value.trim()) + .filter(value => value !== ""); + var depthValues = document.getElementById("scanListdz").value + .split(",") + .map(value => value.trim()) + .filter(value => value !== ""); + if (values.length === 0) { + warning = "Enter at least one scan value."; + } else if (values.some(value => !Number.isFinite(Number(value)))) { + warning = "Every scan value must be numeric."; + } else if (depthValues.length > 0 && depthValues.length !== values.length) { + warning = "Depth increments must match the number of scan values."; + } else { + pointCount = values.length; + detail = values.join(", "); + } } + + if (warning) { + summary.dataset.level = "warning"; + summary.textContent = warning; + return; + } + + summary.dataset.level = "ready"; + var pointLabel = pointCount === 1 ? "point" : "points"; + var workerLabel = workers === 1 ? "worker" : "workers"; + summary.textContent = `${parameter} | ${pointCount} ${pointLabel} | ${workers} ${workerLabel} | ${detail}`; } function ProjSmartDefaults() @@ -3419,17 +3486,6 @@ function ProjNumberLimit() } -function ShowHide(onoff,item) { -// this function can switch item to visible/hidden onoff=1/0 - if(onoff){ -// document.getElementById(item).style.display = 'block'; - document.getElementById(item).style.visibility = "visible"; - } else { -// document.getElementById(item).style.display = 'none'; - document.getElementById(item).style.visibility = "hidden"; - } -} - function prep_cfg(toggle) { // This function collects various values and returns // toggle=1 : returns object with all values diff --git a/ZGUI.css b/ZGUI.css index b970e28..25fe291 100644 --- a/ZGUI.css +++ b/ZGUI.css @@ -339,23 +339,90 @@ input[type="button"]:hover { background: #f5f8f5; } -#ScansControls, -#ScansLine, -#ScansTable { - margin-top: 12px; +.scan-panel { + margin: 0 auto; + max-width: 900px; + padding: 12px 14px; } -#ScansControls table, -#ScansTable { +.scan-heading { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 12px; +} + +.scan-heading h2 { + color: var(--trim-deep); + font-family: Georgia, "Times New Roman", serif; + font-size: 1.08rem; + margin: 0; +} + +.scan-enable { + align-items: center; + background: var(--trim-panel-soft); + border: 1px solid var(--trim-border); + border-radius: 999px; + cursor: pointer; + display: flex; + font-size: 0.8rem; + font-weight: 700; + gap: 7px; + padding: 6px 10px; +} + +.scan-enable input { + accent-color: var(--trim-accent); + height: 16px; + margin: 0; + width: 16px; +} + +.scan-controls-grid { + display: grid; + gap: 9px; + grid-template-columns: minmax(160px, 0.8fr) minmax(240px, 1.4fr) minmax(100px, 0.5fr); +} + +.scan-values-panel { background: var(--trim-panel-soft); border: 1px solid var(--trim-border); border-radius: 8px; - padding: 8px; + display: grid; + gap: 9px; + margin-top: 10px; + padding: 10px; } -#ScansControls td, -#ScansTable td { - padding: 6px; +.scan-range-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.scan-list-grid { + grid-template-columns: minmax(260px, 1.5fr) minmax(220px, 1fr); +} + +.scan-summary { + background: #eef4f4; + border-left: 3px solid var(--trim-focus); + border-radius: 5px; + color: #31515a; + font-size: 0.8rem; + margin-top: 11px; + min-height: 31px; + padding: 7px 10px; +} + +.scan-summary[data-level="idle"] { + border-left-color: #91a3a7; + color: var(--trim-muted); +} + +.scan-summary[data-level="warning"] { + background: #fff1c9; + border-left-color: #c79824; + color: #674c0a; } #Plots > table td { @@ -566,7 +633,10 @@ input[type="button"]:hover { .output-settings, .layers-workspace, - .projectile-grid { + .projectile-grid, + .scan-controls-grid, + .scan-range-grid, + .scan-list-grid { grid-template-columns: 1fr; } diff --git a/scripts/deploy-web.sh b/scripts/deploy-web.sh new file mode 100755 index 0000000..41dfc39 --- /dev/null +++ b/scripts/deploy-web.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Zaher Salman +# SPDX-License-Identifier: GPL-2.0-only + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +web_target="${TRIMSP_WEB_TARGET:-/var/www/html/TRIMSP-NL}" +cgi_target="${TRIMSP_CGI_TARGET:-/var/www/cgi-bin/singleTrimSP.cgi}" +trimsp_target="${TRIMSP_BIN_TARGET:-/usr/local/bin/trimspNL}" +web_owner="${TRIMSP_WEB_OWNER:-apache}" +web_group="${TRIMSP_WEB_GROUP:-apache}" +build_static="${TRIMSP_BUILD_STATIC:-1}" +binary_source="${TRIMSP_BINARY_SOURCE:-$project_root/fortran/trimspNL-static}" +plotly_source="$project_root/node_modules/plotly.js-dist/plotly.js" + +if [[ "$build_static" == "1" ]]; then + make -C "$project_root/fortran" trimspNL-static +elif [[ "$build_static" != "0" ]]; then + printf 'TRIMSP_BUILD_STATIC must be 0 or 1, not %s\n' "$build_static" >&2 + exit 2 +fi + +if [[ ! -f "$binary_source" ]]; then + printf 'TRIM.SP-NL binary not found: %s\n' "$binary_source" >&2 + printf 'Build it first or set TRIMSP_BINARY_SOURCE.\n' >&2 + exit 1 +fi + +if [[ ! -f "$plotly_source" ]]; then + printf 'Plotly browser bundle not found: %s\n' "$plotly_source" >&2 + printf 'Run npm install before deploying.\n' >&2 + exit 1 +fi + +perl -c "$project_root/singleTrimSP.cgi" >/dev/null + +# Install only files used by the browser application. Electron-specific code, +# package metadata, tests, and Fortran sources stay outside the document root. +install -d -m 0755 "$web_target" "$(dirname "$cgi_target")" "$(dirname "$trimsp_target")" +install -m 0644 \ + "$project_root/TrimSP.html" \ + "$project_root/TrimSPlib.js" \ + "$project_root/TrimSPweb.js" \ + "$project_root/myplots.js" \ + "$project_root/ZGUI.css" \ + "$project_root/plotly.css" \ + "$project_root/icon.png" \ + "$web_target/" +install -m 0644 "$plotly_source" "$web_target/plotly.js" +ln -sfn TrimSP.html "$web_target/index.html" + +install -m 0755 "$binary_source" "$trimsp_target" + +# Keep the repository CGI generic while embedding the selected installed +# binary path in the deployed copy. +cgi_stage="$(mktemp)" +trap 'rm -f "$cgi_stage"' EXIT +escaped_trimsp_target="${trimsp_target//\\/\\\\}" +escaped_trimsp_target="${escaped_trimsp_target//&/\\&}" +escaped_trimsp_target="${escaped_trimsp_target//|/\\|}" +sed "s|^\\\$trimbin=.*|\$trimbin=\"timeout 300 $escaped_trimsp_target\";|" \ + "$project_root/singleTrimSP.cgi" > "$cgi_stage" +install -m 0755 "$cgi_stage" "$cgi_target" + +if getent passwd "$web_owner" >/dev/null 2>&1 && getent group "$web_group" >/dev/null 2>&1; then + chown -R "$web_owner:$web_group" "$web_target" + chown "$web_owner:$web_group" "$cgi_target" +else + printf 'Warning: owner/group %s:%s does not exist; ownership was not changed.\n' \ + "$web_owner" "$web_group" >&2 +fi + +if command -v restorecon >/dev/null 2>&1; then + restorecon -RF "$web_target" "$cgi_target" "$trimsp_target" || true +fi + +printf 'Installed TRIMSP-NL web assets in %s\n' "$web_target" +printf 'Installed TRIMSP-NL CGI runner at %s\n' "$cgi_target" +printf 'Installed TRIM.SP-NL engine at %s\n' "$trimsp_target" +printf '\nThe web server must provide:\n' +printf ' /cgi-bin/singleTrimSP.cgi -> %s\n' "$cgi_target" +printf ' /tmp/ -> /tmp/ (simulation results and archives)\n' +printf 'For SELinux deployments, verify CGI execution and write access with:\n' +printf ' ausearch -m avc -ts recent\n' diff --git a/singleTrimSP.cgi b/singleTrimSP.cgi new file mode 100644 index 0000000..a33042c --- /dev/null +++ b/singleTrimSP.cgi @@ -0,0 +1,82 @@ +#!/usr/bin/perl +# +# Copyright Zaher Salman 2023-. +# zaher.salman@psi.ch + +######## Relevant subdirectories and urls ######## +# May need modification on different servers +$trimbin="timeout 300 /usr/local/bin/trimspNL"; +$workroot="/tmp"; + +# Nothing need to be changed from here on +####################################################################### +######## Main body of code ######## +use CGI; + +my $in = new CGI; +# Collect parameters and their values from CGI +my ($name,$value); +$content = ""; +my $filename = ""; +my $rngType = "pcg32"; +my $lcount = 0; +foreach $name ($in->param) { + $value=$in->param($name); + if ($name eq "fn") { + # This is the file name + $filename = $value; + @prefix = split(/\//,$filename); + $randName = $prefix[2]; + $FILENAME = $prefix[3]; + } elsif ($name eq "rngType") { + # RNG backend passed as the optional second trimspNL argument. + if ($value =~ /^(ranlux|pcg32|xoshiro|xoshiro256)$/) { + $rngType = $value; + } + } elsif ( index($name, "line") == 0) { + # This is a line start counting + #if ($lcount <=14) { + # Limit number of lines (15) to avoid abuse + $content = $content.$value."\n"; + #} + $lcount++; + } +} +$workPath = $workroot."/".$randName; +if (-d $workPath) { +# Directory exists, do nothing +} else { + $cmd="mkdir ".$workPath; + system($cmd); +} +$filename = $workPath."/".$FILENAME; +$fname = $filename; +$fname =~ s/\.inp$//; +$runBase = $FILENAME; +$runBase =~ s/\.inp$//; +open (OUTF,q{>}, $filename); +print OUTF ($content); +close(OUTF); + +print ("Content-type: text/ascii \n\n"); +if (index($filename,"_Seq_Results.dat") != -1) { + # This is the final call of a scan + $cmd = "cd $workPath; rm -f edist;"; + $cmd = $cmd."tar -cvzf ../$randName.tgz *; cd ..; rm -rf $randName;"; +} elsif (index($filename,"TrimSP.cfg") != -1) { + # Save config file only + $cmd = "cd $workPath;"; +} else { + # This is a call to simulate one step in a scan + $cmd = "cd $workPath; $trimbin $runBase $rngType >> /tmp/debug;"; +} +system($cmd); +#print "Written file $filename\n"; +#print $content; +print "command $cmd\n"; +foreach $name ($in->param) { + $value=$in->param($name); + print $name,"=",$value,"\n"; +} + +exit(0);