diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..c4dd004 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Deploy static site to GitHub Pages + +on: + push: + branches: + - pyodide # Triggers the workflow on pushes to the pyodide branch + workflow_dispatch: # Allows manual triggering from the GitHub UI + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout your repository + uses: actions/checkout@v4 + # Add steps to build your static site (e.g., npm run build) + - name: Run deployment script + run: | + export PYODIDE_VERSION=0.29.3 + export TARGET_DIR=dist + ./deploy_calculator.sh + - name: Upload artifact + # The contents of the 'build' directory will be uploaded as an artifact + uses: actions/upload-pages-artifact@v4 + with: + path: 'dist/' # Change this to your build output directory (e.g., public, dist) + + deploy: + # Add a dependency to the build job + needs: build + runs-on: ubuntu-latest + permissions: + pages: write # Grants the GITHUB_TOKEN the necessary permissions to deploy to GitHub Pages + id-token: write # Required for OIDC authentication + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 # This action handles the deployment diff --git a/.gitignore b/.gitignore index a0eb841..02a01ea 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ endf/MT.DAT endf/PLOT.CHR endf/PLOT.SYM endf/*.out + +# pyodide build cruft +activation/pyodide +activation/periodictable_wheel_name.txt diff --git a/README.md b/README.md index d78a1a9..3f13508 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,8 @@ available on [github](https://github.com/pkienzle/periodictable). Installation ============ -The activation web frontend is in the activation subdirectory and the cgi -backend is in cgi-bin. Update the server with something like: - - sudo cp -rp activation/* /var/www/resources/activation - sudo cp -p cgi-bin/nact.py /var/www/cgi-bin - -The web page uses the date of activate/index.html to show the last -modification date on the program, so be sure to preserve attributes in copy. +The activation web frontend is in the activation subdirectory. +The backend API is in the cgi-bin folder (nact.py) Be sure the web server is configured to use python 3, with the periodictable package updated to the latest version: @@ -33,7 +27,8 @@ of updates this will also set the last modification date on index.html. For testing you can run the server from the repository: - python server.py [host | host:port] + pip install flask + python flask_server.py [host | host:port] Additional files: @@ -41,7 +36,7 @@ Additional files: needed unless you wish to update the graphs, for example, when new versions of the endf database are released. -* server.py is used to run a test server for debugging the web application, or +* flask_server.py is used to run a test server for debugging the web application, or showing potential new features to users. See the help inside the file for details on running the server. @@ -50,6 +45,19 @@ Additional files: * cgi-bin/hello.py is a minimal test script for python cgi. +Pyodide implementation +====================== +You can make a serverless install using pyodide to run the backend API. The `deploy_calculator.sh` +script will install to `/var/html/resources/activation/index.html`. + +To test the pyodide version before deploying, install into a temporary directory: +```sh +TARGET_DIR=/tmp/pt bash deploy_calculator.sh +(cd /tmp/pt && python -m http.server) +``` +You can then navigate to http://localhost:8000/index.html to view the application. + + Backend interface ================= @@ -84,6 +92,25 @@ request = { } ``` +```python +python_request = { + 'calculate': "all", # target is "scattering" or "activation" or "all" + 'sample': 'Co', # Material + 'flux': '100000', # Thermal flux + 'Cd': '0', # Cd ratio + 'fast': '0', # Thermal/fast ratio + 'mass': '0', # Mass + 'exposure': '1', # Time on beam + 'rest': ["0","1","24","360"], # Time off beam + 'density': '0', # Density + 'thickness': '1', # Thickness + 'wavelength': '1', # Source neutrons + 'xray': 'Cu Ka', # Source Xrays + 'decay': '0.001', # target for "Time to decay below" + 'abundance': 'IAEA' # natural abundance tables (IAEA or NIST) +} +``` + The response is a JSON object with the following fields ```javascript response = { @@ -148,119 +175,125 @@ Example ------- ```sh -$ curl -s -d "sample=Co" -X POST https://www.ncnr.nist.gov/cgi-bin/nact.py | python -m json.tool +$ curl -s -d '{"sample": "Co"}' -H "Content-Type: application/json" -X POST http://localhost:8008/api/calculate | python -m json.tool { - "sample": { - "name": "Co", - "density": 8.9, - "natural_density": 8.9, - "thickness": 1.0, - "mass": 1.0, - "formula": "Co" - }, "activation": { - "flux": 100000.0, - "decay_level": 0.001, - "total": [ - 0.5088248094656863, - 0.009706843055148993, - 1.550197781340068e-05, - 1.5423998799711213e-05 - ], - "rest": [ - 0, - 1, - 24, - 360 - ], + "Cd": 0.0, "activity": [ { - "reaction": "act", - "product": "Co-60", - "halflife": "5.272 y", - "comments": "s for 10m isomer added to ground state", + "comments": "", + "halflife": "10.5 m", + "isotope": "Co-59", "levels": [ - 1.550756280503848e-05, - 1.5507330056885684e-05, - 1.5501977813400642e-05, - 1.5423998799711213e-05 + 0.5087467869376632, + 0.009690144997026036, + 2.6447704770864502e-42, + 0.0 ], - "isotope": "Co-59" + "product": "Co-60m+", + "reaction": "act" }, { - "reaction": "act", - "product": "Co-60m+", - "halflife": "10.5 m", - "comments": "", + "comments": "Co-61 prod from Co-60m only", + "halflife": "1.65 h", + "isotope": "Co-59", "levels": [ - 0.5088093019028804, - 0.009691335725091536, - 2.6450954673146495e-42, - 0.0 + 7.305869373119584e-16, + 4.799870068457319e-16, + 3.0552994658480865e-20, + 1.52897407497221e-81 ], - "isotope": "Co-59" + "product": "Co-61", + "reaction": "2n" }, { - "reaction": "2n", - "product": "Co-61", - "halflife": "1.65 h", - "comments": "Co-61 prod from Co-60m only", + "comments": "s for 10m isomer added to ground state", + "halflife": "5.272 y", + "isotope": "Co-59", "levels": [ - 7.306767120646388e-16, - 4.800459878001244e-16, - 3.055674902007567e-20, - 1.5291619557875176e-81 + 1.5505657464889658e-05, + 1.5505424745333514e-05, + 1.5500073159453058e-05, + 1.5422103726672467e-05 ], - "isotope": "Co-59" + "product": "Co-60", + "reaction": "act" }, { - "reaction": "2n", - "product": "Co-61", - "halflife": "1.65 h", "comments": "Co-61 prod assuming all Co-60m has decayed to Co-60", + "halflife": "1.65 h", + "isotope": "Co-59", "levels": [ - 1.3649499897275873e-16, - 8.967560554449202e-17, - 5.7081926346341585e-21, - 2.8565705754412276e-82 + 1.3647822848511002e-16, + 8.966458753177e-17, + 5.707491296308241e-21, + 2.8562196022780084e-82 ], - "isotope": "Co-59" + "product": "Co-61", + "reaction": "2n" } ], + "decay_level": 0.001, + "decay_time": 1.5773360047132599, "exposure": 1.0, - "decay_time": 1.5773675158317233, "fast": 0.0, - "Cd": 0.0 + "flux": 100000.0, + "rest": [ + 0, + 1, + 24, + 360 + ], + "total": [ + 0.508762292595129, + 0.00970565042177194, + 1.5500073159453095e-05, + 1.5422103726672467e-05 + ] + }, + "sample": { + "density": 8.9, + "formula": "Co", + "formula_latex": "Co", + "mass": 1.0, + "name": "Co", + "natural_density": 8.9, + "thickness": 1.0 }, "scattering": { - "sld": { - "real": 2.2645416201426363, - "imag": 0.009403091502484154, - "incoh": 5.632988294016107 + "contrast_match": { + "D2O_fraction": 0.4066002243307043, + "sld": 2.2645414633790257 }, - "xs": { - "coh": 0.07085810248318081, - "abs": 1.8806183004968307, - "incoh": 0.43843639843243204 - }, - "penetration": 0.4184253079898973, "neutron": { - "wavelength": 1.0, - "energy": 81.80420235572412, - "velocity": 3956.0339760560055 + "energy": 81.80421023488275, + "velocity": 3956.0340061039888, + "wavelength": 1.0 }, - "transmission": 9.163767420488476 + "penetration": 0.4184253369555237, + "sld": { + "imag": 0.009403090851552168, + "incoh": 5.632980055822083, + "real": 2.2645414633790257 + }, + "transmission": 9.16376893656492, + "xs": { + "abs": 1.8806181703104334, + "coh": 0.07085931929382916, + "incoh": 0.4384351463657107 + } }, + "success": true, + "version": "2.0.2", "xray_scattering": { - "xray": { - "wavelength": 1.5418, - "energy": 8.041522080237366 - }, "sld": { - "real": 63.020248367719645, - "imag": 9.14097379704212 + "imag": 9.140742563282087, + "real": 63.02025244915057 + }, + "xray": { + "energy": 8.041522793695698, + "wavelength": 1.5418 } - }, - "success": true + } } ``` diff --git a/activation/api_fetch.js b/activation/api_fetch.js new file mode 100644 index 0000000..12b8368 --- /dev/null +++ b/activation/api_fetch.js @@ -0,0 +1,25 @@ +class FetchAPI { + response_callback = null; + url = null; + + initialize (response_callback, ready_callback=() => {}, url="/api/calculate") { + this.response_callback = response_callback; + this.url = url; + ready_callback(); + } + + submit(data) { + fetch(this.url, { + method: "POST", + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(json => this.response_callback(json)) + .catch(error => this.response_callback({'success':false,'detail':{'fetch error':error}})); + } +} + +const API = new FetchAPI(); \ No newline at end of file diff --git a/activation/api_webworker.js b/activation/api_webworker.js new file mode 100644 index 0000000..61e469c --- /dev/null +++ b/activation/api_webworker.js @@ -0,0 +1,29 @@ +class WebWorkerAPI { + response_callback = null; + ready_callback = null; + url = null; + pyodideWorker = null; + + initialize (response_callback, ready_callback=() => {}, url="./webworker.js") { + this.response_callback = response_callback; + this.ready_callback = ready_callback; + this.url = url; + this.pyodideWorker = new Worker(this.url, {type: "module"}); + this.pyodideWorker.onmessage = (event) => { + if ('worker_ready' in event.data) { + this.ready_callback(); + } + else { + this.response_callback(event.data); + } + } + } + + submit(data) { + this.pyodideWorker.postMessage({ + data: data + }); + } +} + +const API = new WebWorkerAPI(); diff --git a/activation/calculator_main.js b/activation/calculator_main.js new file mode 100644 index 0000000..051c59b --- /dev/null +++ b/activation/calculator_main.js @@ -0,0 +1,452 @@ +// API comes in from webworker_api.js or form_api.js above +// JQuery ($) is loaded in a script tag as well + +// standard year for activation calculations is 365, not 365.2425 +const HOURS_PER_YEAR = 365*24; // = 365.2425*24; +const ELEMENTS = {"n": 0, "H": 1, "He": 2, "Li": 3, "Be": 4, "B": 5, "C": 6, "N": 7, "O": 8, "F": 9, "Ne": 10, "Na": 11, "Mg": 12, "Al": 13, "Si": 14, "P": 15, "S": 16, "Cl": 17, "Ar": 18, "K": 19, "Ca": 20, "Sc": 21, "Ti": 22, "V": 23, "Cr": 24, "Mn": 25, "Fe": 26, "Co": 27, "Ni": 28, "Cu": 29, "Zn": 30, "Ga": 31, "Ge": 32, "As": 33, "Se": 34, "Br": 35, "Kr": 36, "Rb": 37, "Sr": 38, "Y": 39, "Zr": 40, "Nb": 41, "Mo": 42, "Tc": 43, "Ru": 44, "Rh": 45, "Pd": 46, "Ag": 47, "Cd": 48, "In": 49, "Sn": 50, "Sb": 51, "Te": 52, "I": 53, "Xe": 54, "Cs": 55, "Ba": 56, "La": 57, "Ce": 58, "Pr": 59, "Nd": 60, "Pm": 61, "Sm": 62, "Eu": 63, "Gd": 64, "Tb": 65, "Dy": 66, "Ho": 67, "Er": 68, "Tm": 69, "Yb": 70, "Lu": 71, "Hf": 72, "Ta": 73, "W": 74, "Re": 75, "Os": 76, "Ir": 77, "Pt": 78, "Au": 79, "Hg": 80, "Tl": 81, "Pb": 82, "Bi": 83, "Po": 84, "At": 85, "Rn": 86, "Fr": 87, "Ra": 88, "Ac": 89, "Th": 90, "Pa": 91, "U": 92, "Np": 93, "Pu": 94, "Am": 95, "Cm": 96, "Bk": 97, "Cf": 98, "Es": 99, "Fm": 100, "Md": 101, "No": 102, "Lr": 103, "Rf": 104, "Db": 105, "Sg": 106, "Bh": 107, "Hs": 108, "Mt": 109, "Ds": 110, "Rg": 111, "Cn": 112, "Nh": 113, "Fl": 114, "Mc": 115, "Lv": 116, "Ts": 117, "Og": 118}; +let ACTIVE_BUTTON = "activation"; +// Parse environment variables "index.html?cutoff=...&abundance=..." +function getURLParameter(name) { + var value = decodeURIComponent( + (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1] + ); + return value; +} +// From Doron Grinzaig (2017-11-21) +// https://stackoverflow.com/questions/14129953/how-to-encode-a-string-in-javascript-for-displaying-in-html +function sanitize(html) { + return $('
').text(html).html(); +} +function format_activation_value(v, cutoff) { + return (v= 1000.) return v.toFixed(0)+' g'; + else if (v >= 1.) return v.toFixed(3)+' g'; + else if (v >= 1e-3) return (1e3*v).toFixed(3)+' mg'; + else if (v >= 1e-6) return (1e6*v).toFixed(3)+' ug'; + else if (v >= 1e-9) return (1e9*v).toFixed(3)+' ng'; + else return v.toExponential(4)+' ng'; +} +function format_time(v) { + if (v >= HOURS_PER_YEAR) return nice_number(v/HOURS_PER_YEAR)+' yrs'; + else if (v >= 48 || v == 24) return nice_number(v/24)+' days'; + else if (v >= 2 || v == 1) return nice_number(v)+' hrs'; + else if (v >= 2/60. | v == 1/60.) return nice_number(v*60)+' min'; + else return (v*3600).toPrecision(3)+' sec'; +} +function parse_half_life(s) { + var y = HOURS_PER_YEAR*3600; + var units = {Gy: 1e9*y, My: 1e6*y, ky: 100*y, y: y, d: 24*3600, h: 3600, m: 60, s: 1}; + var a = s.split(" "); + var value_str = a[0], units_str = a[1]; + return parseFloat(value_str) * units[units_str]; +} +function parse_isotope(s) { + var a = s.split("-"); + var element = a[0], isotope = a[1]; + return ELEMENTS[element] + parseInt(isotope)/1000; +} +$.tablesorter.addParser({ + id: 'isotope', + is: function (s) { return /^ *[A-Z][a-z]?-[0-9]+[sm+]* *$/.test(s); }, + format: function (s) { return parse_isotope(s); }, + type: 'numeric' +}); +$.tablesorter.addParser({ + id: 'half-life', + is: function (s) { return /^ *[0-9.+-eE]+ *[kMG]?[smhdy] *$/.test(s); }, + format: function (s) { return parse_half_life(s); }, + type: 'numeric' +}); +$.tablesorter.addParser({ + id: 'float', + is: function (s) { return /^ *[0-9.+-eE]+ *$/.test(s); }, + format: function (s) { return s == '---' ? 0. : parseFloat(s); }, + type: 'numeric' +}); + +function parse_url_parameters() { + // Parse URL parameters. + var cutoff_par = getURLParameter('cutoff'); + if (cutoff_par==='null') CUTOFF = 0.0005; + else CUTOFF = Number(cutoff_par); + if (isNaN(CUTOFF)) { // IE doesn't support Number.isNaN + show_error("

cutoff="+sanitize(cutoff_par)+" in URL should be floating point μCi; default is 0.0005.

"); + CUTOFF = 0.0005; + } + CUTOFF_Bq = CUTOFF*37000; + + var decay_par = getURLParameter('decay'); + if (decay_par==='null') DECAY_LEVEL = CUTOFF>0?CUTOFF:0.0005; + else DECAY_LEVEL = Number(decay_par) + if (isNaN(DECAY_LEVEL)) { // IE doesn't support Number.isNaN + show_error("

decay="+sanitize(decay_par)+" in URL should be floating point μCi; default is cutoff.

"); + DECAY_LEVEL = CUTOFF>0?CUTOFF:0.0005; + } + + var abundance_par = getURLParameter('abundance'); + if (abundance_par==='null') ABUNDANCE = "IAEA"; + // CRUFT: periodictable no longer uses NIST 2001 data for abundance + else if (abundance_par==='NIST') ABUNDANCE = "IUPAC"; + else ABUNDANCE = abundance_par; + if (ABUNDANCE!=="IUPAC" && ABUNDANCE!=="IAEA") { + show_error("

abundance="+sanitize(abundance_par)+" in URL should be IUPAC or IAEA; default is IAEA

"); + ABUNDANCE = "IAEA"; + } +} + +function enable_forms() { + // Enable forms; they are disabled by default so that noscript users + // can't do anything. + $('.container').removeClass("disabled"); + $('#id_chemical_formula').removeAttr("disabled"); + $('#id_submit_scat').removeAttr("disabled"); + $('#id_submit_act').removeAttr("disabled"); + $('a').each(function(){ this.tabIndex = -1; }); + $('.help').each(function(){ $(this).append("
"); }); + $('input[type=text]').keydown(function(e) { + if (e.keyCode == 13) { + e.preventDefault(); + submit_query(ACTIVE_BUTTON); + } + }); + $('input[type=text]').focus(function(e) { + var panel = $(this).closest('.panel')[0].id; + if (panel == "scattering-panel") { + ACTIVE_BUTTON = "scattering"; + } else if (panel == "activation-panel") { + ACTIVE_BUTTON = "activation"; + } + } ); + + //$("#id-activationForm").submit(submit_query); + $("#id-activationForm").submit(function(event) {event.preventDefault();}); + $('#id_submit_scat').click(function() { submit_query('scattering'); }); + $('#id_submit_act').click(function() { submit_query('activation'); }); +} + +function submit_query(target){ + ACTIVE_BUTTON = target; + + // activate the loader spinner: + document.querySelector("div.loader").classList.remove("not-started"); + + const form = document.getElementById("id-activationForm"); + const formData = new FormData(form); + const initial_data = Object.fromEntries(formData.entries()); + const { time_off, ...data } = initial_data; // destructuring assignment to remove time_off from data + + // populate rest array with time_off from initial data + data.rest = ["0","1","24","360",time_off]; + + const query = { + calculate: target, + decay: DECAY_LEVEL, + abundance: ABUNDANCE, + ...data + }; + API.submit(query); +} + +function cssrule(class_name){ + var ss = document.styleSheets; + for (var i=0; i= cutoff); + columns.push(row.levels[j]) + } + if (active) data.rows.push(columns); + } + + var activity_header, activity_button; + if (cutoff>0) { + activity_header = 'Activity (μCi) above ' + cutoff.toExponential(4) + ' μCi'; + activity_button = ''; + } else if (CUTOFF>0) { + activity_header = 'Activity (μCi)'; + activity_button = ''; + } else { + activity_header = 'Activity (μCi)'; + activity_button = ''; + } + //add headers to table + content += '\n \n'; + content += ' \n'; + content += ' '; + for (var i in data.headers) { + content += ''; + } + content += '\n \n \n' + var some_unusual = false; + for (var i in data.rows) { + var row_style = ''; + if (data.rows[i][1] === 'b') { + some_unusual = true; + row_style = 'activity_unusual'; + } else if (data.rows[i][2].slice(-1) === 't') { + some_unusual = true; + row_style = 'activity_unusual'; + //} else if (data.rows[i][2].slice(-1) === 's') { + // row_style = 'activity_normal'; + } else { + row_style = 'activity_normal'; + } + content += ` `; + for (var j=0; j =4) { + content += ''; + } else { + content += ''; + } + } + content += '\n'; + } + content += ' \n \n '; + for (var j=0; j < act.total.length; j++) { + content += ''; + } + content += '\n'; + if (some_unusual) { + content += ' \n'; + } + content += ' \n
'+activity_header+activity_button+'
' + data.headers[i] + '
'+format_activation_value(data.rows[i][j], cutoff)+'' + data.rows[i][j] + '
total activity'+format_activation_value(act.total[j], cutoff)+'
b reaction activity is slightly underestimated; it does not include decay of transients after removal from beam.
\n'; + return content; +} +function show_error(error_html) { + var content = '

\n'; + content += error_html; + content += '
\n'; + $(content).prependTo('div[id=results]'); +} +function process_response(ldata) { + //console.log(ldata); + if (typeof(ldata)==="string") ldata=$.parseJSON(ldata); + if (!ldata.success) { + var error_html = ''; + var keys = Object.keys(ldata.detail); + keys.sort(); + for (i = keys.length-1; i >= 0; i--) { + error_html += '
Error ' +sanitize(keys[i])+': '+sanitize(ldata.detail[keys[i]])+'
\n'; + } + show_error(error_html); + return; + } + + var act = ldata.activation; + var sample = ldata.sample; + var scat = ldata.scattering; + var xscat = ldata.xray_scattering; + var content = '

\n'; + + if (typeof act == 'undefined') { + // do nothing if not returned + } else if ("error" in act) { + // Restricted title + content += '

activation calculation failed with

\n'+sanitize(act.error)+'
\n'; + } else { + // Full title + content += '

Activation of ' + sample.name + ' after ' + format_time(act.exposure)+' at '+act.flux.toPrecision(3)+' n/cm2/s

\n'; + + // Disclaimer + content += '

Estimated activation only. All samples must be evaluated by NIST Health Physics to determine if and how the sample can be removed from the NCNR.

'; + + // Atoms + //var formula = sample.formula; + var formula = sanitize(sample.formula_latex).replace(/\$_{([^}]*)}\$/g, '$1'); + content += '

Sample in beam: ' + format_mass(sample.mass) + ' of ' + formula + '\n'; + + // Rabbit parameters + if (act.Cd > 0 || act.fast > 0) { + content += '
Rabbit system: Cd ratio = ' + act.Cd + ', thermal/fast ratio = ' + act.fast + '\n' + } + + // Decay time + if (act.decay_time > 0) { + content += '
Time to decay below '+act.decay_level.toExponential(4)+' μCi is '+format_time(act.decay_time)+'.\n'; + } + content += '

\n' + + // Activation table + if (CUTOFF == 0.) { + content += activation_table(act, 0.); + } else { + content += '
\n' + activation_table(act, CUTOFF) + '
\n'; + content += '
\n' + activation_table(act, 0.) + '
\n'; + } + } + + + if (typeof scat == 'undefined') { + // do nothing if not returned + } else if ("error" in scat) { + content += '

neutron scattering calculation failed with

\n'+sanitize(scat.error)+'
\n'; + } else if ("error" in xscat) { + content += '

X-ray scattering calculation failed with

\n'+sanitize(xscat.error)+'
\n'; + } else { + // Drop coherent cross section from penetration depth calculation. + // An alternative formulation, based on total scattering b, would be + // penetration = 1/(1/scat.penetration - scat.xs.coh), which perhaps + // takes diffuse coherent scattering into account [Glinka 2011]. + var penetration = 1 /(scat.xs.abs+scat.xs.incoh); + var transmission = 100*Math.exp(-sample.thickness/penetration); + var flux_str = $('input:text[id=id_flux]').val(); + var transmitted_flux = parseFloat(flux_str)*transmission/100.0; + + // Mass density + var density_str = $('input:text[id=id_density]').val(); + content += "

Scattering from " + sanitize(sample.name) + "

"; + + // Source neutrons + content += "

Source neutrons: " + + scat.neutron.wavelength.toFixed(3) + " Å" + + " = "+scat.neutron.energy.toFixed(2) + " meV" + + " = "+scat.neutron.velocity.toFixed(0) + " m/s
"; + content += "Source X-rays: " + + xscat.xray.wavelength.toFixed(3) + " Å" + + " = " + xscat.xray.energy.toFixed(3) + " keV
"; + // Atoms + //var formula = sample.formula; + var formula = sanitize(sample.formula_latex).replace(/\$_{([^}]*)}\$/g, '$1'); + content += "Sample in beam: " + formula + " at " + sample.density.toFixed(2) + " g/cm3"; + if (density_str.indexOf(':') !== -1) content += " from lattice "+sanitize(density_str); + content += "

"; + + // Neutron scattering + content += '\n '; + content += ''; + content += ''; + content += ''; + content += ''; + content += '\n '; + content += ''; + content += "'; + content += ''; + content += "'; + content += '\n '; + content += ''; + content += "'; + content += ''; + content += "'; + content += '\n '; + content += ''; + content += ''; + content += ''; + content += ''; + content += '\n '; + content += '\n
1/e penetration depth
(cm)
Scattering length density
(10-62)
Scattering cross section
(1/cm)
X-ray SLD
(10-62)
abs'+(1/scat.xs.abs).toFixed(3)+'real"+scat.sld.real.toFixed(3)+'coh'+scat.xs.coh.toFixed(3)+'real"+xscat.sld.real.toFixed(3)+'
abs+incoh'+(1/(scat.xs.incoh+scat.xs.abs)).toFixed(3)+'imag"+(-scat.sld.imag).toFixed(3)+'abs'+scat.xs.abs.toFixed(3)+'imag"+(-xscat.sld.imag).toFixed(3)+'
abs+incoh+coh'+scat.penetration.toFixed(3)+'incoh'+scat.sld.incoh.toFixed(3)+'incoh'+scat.xs.incoh.toFixed(3)+''+''+'
'; + + // Neutron transmission + content += '

Neutron transmission is ' + +(transmission>=2||transmitted_flux2/s' + +' for a '+sanitize(flux_str)+' n/cm2/s beam.\n'; + + // Contrast match + if (scat.contrast_match.D2O_fraction === null) { + content += "

"; + } else { + content += '
Contrast match point: '; + if (scat.contrast_match.D2O_fraction < 0) { + content += '< 0% D2O

\n'; + } else if (scat.contrast_match.D2O_fraction > 1) { + content += '> 100% D2O

\n'; + } else { + console.log(scat.sld, scat); + content += (scat.contrast_match.D2O_fraction*100).toFixed(1) + + '% D2O by volume (real SLD = ' + + scat.contrast_match.sld.toFixed(3) + + '×10-62)

\n'; + } + } + } + + + // End of div + content += '
\n'; + + var $new = $(content).prependTo('div[id=results]'); //add to the dom + // Start table unsorted so that b reactions follow transients as they + // do in the activation table. Note that there is no way to get back + // the original order. + $new.find("table").tablesorter(); + scroll_to_result(); +} + +function scroll_to_result(){ + var opt = {behavior: "smooth", block: "nearest"}; + document.getElementById("results").firstChild.scrollIntoView(opt); +} + +// Dynamic help support. Clicking an input field jumps to the +// corresponding section of the help. Input "gork" uses: +// +// and the corresponding help section uses: +//

Gork Section Title

...
+function help_jump(section_name){ + var section = document.getElementById(section_name); + var frame = document.getElementById('help-frame'); + if (!!section) { + frame.scrollTop = section.offsetTop; // - frame.offsetTop; + } +} + +function setup_help() { + // $('#help-frame').height($('#activation-frame').height()); + $('input').focusin(function(){help_jump("help_"+this.id.slice(3))}); + $('.cite').click(function(event){ + event.preventDefault(); + help_jump(this.getAttribute('href').substring(1)) + }); +} + +function ready_callback() { + document.querySelector("div.loader").classList.add("finished"); +} + + +$(document).ready(function () { + API.initialize(process_response, ready_callback); + parse_url_parameters(); + enable_forms(); + setup_help(); + // var timestamp = (new Date(Date.parse(document.lastModified))).toLocaleDateString(); + // document.getElementById("lastmod-timestamp").innerHTML = timestamp; +}); diff --git a/activation/css/footer.css b/activation/css/footer.css new file mode 100644 index 0000000..807fb95 --- /dev/null +++ b/activation/css/footer.css @@ -0,0 +1,10 @@ +.footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; +} + +.footer .contacts { + flex: 1; +} diff --git a/activation/css/loading-spinner.css b/activation/css/loading-spinner.css new file mode 100644 index 0000000..235b010 --- /dev/null +++ b/activation/css/loading-spinner.css @@ -0,0 +1,30 @@ +.loader .spinner { + border: 4px solid #f3f3f3; /* Light grey background */ + border-top: 4px solid #3498db; /* Blue "spinning" part */ + border-radius: 50%; + width: 30px; + height: 30px; + animation: spin 1s linear infinite; +} + +.loader.finished { + display: none; +} + +.loader { + display: flex; + align-items: center; + gap: 10px; + margin: 0 auto; + width: fit-content; + padding: 1em; +} + +.loader.not-started { + display: none; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/activation/css/main.css b/activation/css/main.css new file mode 100644 index 0000000..b139fea --- /dev/null +++ b/activation/css/main.css @@ -0,0 +1,186 @@ +html, body { + height: 100%; + margin: 0; + padding: 0; +} +@media print +{ + html, body { font-size: 10pt !important; height: unset; } + table, caption, th, td { + font-size: 10pt !important; + /* border: 0.1pt solid lightgray !important; */ + border: none !important; + } + .no-print { display: none !important; height: 0; } + .no-print *{ display: none !important; height: 0; } + .print-only { display: block !important; } + .print-only *{ display: block !important; } +} +.print-only { display: none; } +.print-only *{ display: none; } + +p { margin-bottom: 0; } +.footer p { margin-top: 0; } + +/* body { font-size: 0.9em; } */ +.disabled { opacity: 0.3; } +label { font-weight: inherit; } +.controls input { width: 100%; } +table { margin: 0; padding: 0; border-spacing: 0; border-color: lightgray; } +caption, td, th { padding: 0 0.5em 0 0.5em; } +th { vertical-align: baseline; } +tbody tr:nth-child(2n) th, tbody tr.even th, +tbody tr:nth-child(2n) td, tbody tr.even td, +ul li:nth-child(2n), ol li:nth-child(2n) { background: inherit; /* background: none repeat scroll 0 0 #F3F3F9; */ } +table.tablesorter thead tr .headerSortable { + padding-right: 2em; /* space for sort arrows */ + cursor: pointer; +} +table.tablesorter thead tr .headerSortable::after { + content: " \25B4\25BE"; /* ▴▾ */ + opacity: 0.4; +} +table.tablesorter thead tr .headerSortable.headerSortUp::after { + content: " \25B2"; /* ▲ */ + opacity: 1; +} +table.tablesorter thead tr .headerSortable.headerSortDown::after { + content: " \25BC"; /* ▼ */ + opacity: 1; +} + +/* output markup */ +.error { color: red; } +p.disclaimer { color: darkred; font-size:1.2em; } +.activity_unusual { background-color:wheat; } +/* .activity_normal { background-color: lightred; } */ + +#id_chemical_formula { width: 30em; } + +div.cutoff { display: block; } +div.nocutoff { display: none; } +button.activity_button { float: right; } + +/* #wrapper { + height: 100%; + display: flex; + flex-direction: column; +} */ + +/* frame layout */ +#content-frame { + display: flex; + flex-direction: row; + width: 100%; +} + +#activation-frame { + width:30em; +} + +#help-frame { + position: relative; + flex: 1; + /* min-width:10em; max-width:40em; */ + overflow-y:scroll; +} + +#help-wrapper { + position: absolute; + padding-left:1em; + padding-right: 1em; + margin-right:0em; +} + +#help-wrapper .word-break { + word-break: break-all; +} + +#results-frame { + overflow: auto; + flex: 1; + border-top:#005ea2 1px solid; + border-bottom:#005ea2 1px solid; +} + +div.control-group { margin-left: 0.5em; vertical-align: bottom; } +/* div.control-group { height: 3ex; width: 15em; } */ +/* .help starts display:none so that size is initially zero. */ +.help { display:block; } +/*.help dl { margin-left: 30em; } +.help ol { margin-left: 30em; } +.help p { margin-left: 30em; } +*/ +.help h3 { margin-top: 0.25em; } +.help dd { margin-left: 1em; } +.help dt { margin-left: 0em; padding: 0.3em; font-weight: bold; } +.help ol { list-style-position: inside; padding-left: 0em; } +.help li { margin-left: 0em; margin-bottom: 1ex; } +.help div.bold { font-weight: bold; display: inline; } +.help div.entrytext { + display:inline; + font-weight:bold; + font-style:oblique; +} +.help div.example:before { content:"Example: "; } + +/* fading horizontal rule +modified from http://konigi.com/tools/css-techniques-horizontal-rules +*/ +.help hr { + width: 100%; + height: 1px; + margin: 2.4em 0; + border: none; + background: #ddd; + background-image: -webkit-gradient( + linear, + left bottom, + right bottom, + color-stop(0, rgb(255,255,255)), + color-stop(0.1, rgb(221,221,221)), + color-stop(0.9, rgb(221,221,221)), + color-stop(1, rgb(255,255,255)) + ); + background-image: -moz-linear-gradient( + left center, + rgb(255,255,255) 0%, + rgb(221,221,221) 10%, + rgb(221,221,221) 90%, + rgb(255,255,255) 100% + ); +} + +/* dictionary type: value on same line */ +dl.inline { + display: -ms-grid; -ms-grid-template-columns: max-content auto; + display: grid; grid-template-columns: max-content auto; +} +dl.inline dt { + -ms-grid-column-start: 1; + grid-column-start: 1; + padding: 0.1em; +} +dl.inline dd { + -ms-grid-column-start: 2; + grid-column-start: 2; + padding: 0.1em; margin-left: 0.5em; +} + +.panel { border-radius:0.6em; border: 2pt solid gray; margin: 0.5em; padding: 0.5em; } +.panel h3 { margin-top: -1em; margin-bottom: 0.3em; font-size: 1em; } +.panel h3 .text { background: white; margin-left: 1em; padding: 0 0.3em 0 0.3em; } +.panel { position: relative; } +.panel .btn { position:absolute; right: 1em; top: 0.75em;} +/* .panel .btn { position:absolute; top:1em; right:1em; } */ + +/* Keep constant baseline spacing even with superscript/subscript */ +sup, sub { + height: 0; + line-height: 1; + vertical-align: baseline; + _vertical-align: bottom; + position: relative; +} +sup { bottom: 1ex; } +sub { top: .5ex; } diff --git a/activation/css/nist-header.css b/activation/css/nist-header.css new file mode 100644 index 0000000..d75f88d --- /dev/null +++ b/activation/css/nist-header.css @@ -0,0 +1,56 @@ +@font-face { + font-family:"Source Sans Pro Web"; + font-style:normal; + font-weight:400; + font-display:fallback; + src: url(https://www.nist.gov/libraries/nist-component-library/dist/fonts/source-sans-pro/sourcesanspro-bold-webfont.woff2) format("woff2"), + url(https://www.nist.gov/libraries/nist-component-library/dist/fonts/source-sans-pro/sourcesanspro-regular-webfont.woff) format("woff"), + url(https://www.nist.gov/libraries/nist-component-library/dist/fonts/source-sans-pro/sourcesanspro-regular-webfont.ttf) format("truetype"); +} + +.ncnr-nist-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background-color: #f5f5f5; + border-bottom: 1px solid #ddd; + background: url(https://www.nist.gov/libraries/nist-component-library/dist/img/pattern/bg_pattern.png) #006dbc; + position: relative; +} + +.ncnr-nist-header::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-image: linear-gradient(to right,transparent,#005ea2); +} + +.ncnr-nist-header h2 { + margin: 0; + font-family: Source Sans Pro Web; + font-size: 1.86rem; + line-height: 1.1; + text-decoration: none; + padding-left: 1rem; + text-transform: uppercase; + font-weight: 700; +} + +.ncnr-nist-header a { + text-decoration: none; + color: #FFF; + z-index: 2; + position: relative; +} + +.ncnr-nist-header img { + position: relative; + padding-right: 2rem; + max-width: 100px; + height: auto; + z-index: 2; +} diff --git a/activation/get_pyodide.sh b/activation/get_pyodide.sh new file mode 100755 index 0000000..dc9e539 --- /dev/null +++ b/activation/get_pyodide.sh @@ -0,0 +1,30 @@ +# !/bin/bash + +# Run in the activation directory (where this script lives) +cd "$(dirname "$0")" + +# Default version of pyodide to download if not set via environment variable +PYODIDE_VERSION=${PYODIDE_VERSION:-0.29.3} + +rm -rf pyodide +mkdir pyodide + +# Get the core pyodide files first (this is smaller and faster to download than the full version) +curl -L https://github.com/pyodide/pyodide/releases/download/${PYODIDE_VERSION}/pyodide-core-${PYODIDE_VERSION}.tar.bz2 -o pyodide.tar.bz2 + tar -xjf pyodide.tar.bz2 -C ./pyodide --strip-components=1 + +# Download the full version to get the specific wheels we need (numpy, pytz, micropip) +curl -L https://github.com/pyodide/pyodide/releases/download/${PYODIDE_VERSION}/pyodide-${PYODIDE_VERSION}.tar.bz2 -o pyodide_full.tar.bz2 + +# Extract only the core runtime + our specific wheels +TAR_OPTIONS="--wildcards" tar -xjf pyodide_full.tar.bz2 "*micropip-*.whl" "*numpy-*.whl" "*pytz-*.whl" "*pyparsing-*.whl" + +# Download the latest periodictable wheel from PyPI +pip3 download periodictable --no-deps --only-binary :all: -d ./pyodide/ + +# Write out the full wheel file name to a text file for use in the workflow +ls pyodide/periodictable-*.whl > periodictable_wheel_name.txt + +# Cleanup +rm -f pyodide.tar.bz2 +rm -f pyodide_full.tar.bz2 diff --git a/activation/index.html b/activation/index_template.html similarity index 52% rename from activation/index.html rename to activation/index_template.html index 0a1bfea..b661a5d 100644 --- a/activation/index.html +++ b/activation/index_template.html @@ -4,733 +4,17 @@ Neutron Activation and Scattering Calculator + + + + - - - - - - - - - + + + @@ -760,7 +44,7 @@

Material

-
+
@@ -789,7 +73,7 @@

Neutron Activation

-
+
@@ -800,7 +84,7 @@

Neutron Activation

- + @@ -811,7 +95,7 @@

Neutron Activation

-
+
@@ -1172,7 +456,7 @@

Material mass

-
+

Exposure

Units: h m s d w y

@@ -1453,6 +737,7 @@

History

+
Initializing calculator...
@@ -1464,7 +749,7 @@
-
Using periodictable v2.0.0
+
Using periodictable {{ periodictable_version }}
diff --git a/activation/webworker.js b/activation/webworker.js new file mode 100644 index 0000000..b26812c --- /dev/null +++ b/activation/webworker.js @@ -0,0 +1,60 @@ +// webworker.js + +import { loadPyodide } from "./pyodide/pyodide.mjs"; +// import { loadPyodide } from "./pyodide/pyodide.mjs"; + +async function loadPyodideAndPackages() { + self.pyodide = await loadPyodide(); + await self.pyodide.loadPackage(["numpy", "pytz", "micropip"]); + + // get the periodictable wheel name from the special file: + const response = await fetch("./periodictable_wheel_name.txt"); + const wheelName = (await response.text()).trim(); + await self.pyodide.runPythonAsync(` + import micropip + await micropip.install("./${wheelName}") + import periodictable + print(periodictable.__version__) + `) + + // Downloading a single file + await pyodide.runPythonAsync(` + from pyodide.http import pyfetch + response = await pyfetch("./nact.py") + with open("nact.py", "wb") as f: + f.write(await response.bytes()) + import nact + import json + + `) +} +let pyodideReadyPromise = loadPyodideAndPackages(); +pyodideReadyPromise.then(() => self.postMessage({worker_ready: true})); + +self.onmessage = async (event) => { + // make sure loading is done + await pyodideReadyPromise; + // Now is the easy part, the one that is similar to working in the main thread: + try { + const json_data = JSON.stringify(event.data.data); + let python = ` + request = json.loads('${json_data}') + try: + response = nact.api_call(request) + except Exception: + response = { + 'success': False, + 'version': periodictable.__version__, + 'detail': {'query': error()}, + 'error': 'unexpected exception', + } + json.dumps(response) + `; + //console.log('python:', python); + let results = await self.pyodide.runPythonAsync(python); + let ldata = JSON.parse(results); + self.postMessage(ldata); + } catch (error) { + self.postMessage({ success: false, detail: {error: error.message }}); + } +}; \ No newline at end of file diff --git a/cgi-bin/nact.py b/cgi-bin/nact.py index 24cc2f8..2c9d821 100755 --- a/cgi-bin/nact.py +++ b/cgi-bin/nact.py @@ -8,7 +8,6 @@ from __future__ import print_function import sys -import cgi import re import json from math import exp @@ -16,11 +15,7 @@ from datetime import datetime, timedelta from calendar import monthrange -# CRUFT: python 2 doesn't have html.escape -try: - from html import escape -except ImportError: - from cgi import escape +from html import escape from pytz import timezone, utc @@ -253,41 +248,38 @@ def parse_date(datestring, default_timezone=default_timezone): dt = utc.localize(dt) - timedelta(0, offset) return dt -def cgi_call(): - form = cgi.FieldStorage() - #print(form, file=sys.stderr) - #print >>sys.stderr, "sample",form.getfirst('sample') - #print >>sys.stderr, "mass",form.getfirst('mass') - +def api_call(form: dict): # Parse inputs errors = {} - calculate = form.getfirst('calculate', 'all') + calculate = form.get('calculate', 'all') if calculate not in ('scattering', 'activation', 'all'): errors['calculate'] = "calculate should be one of 'scattering', 'activation' or 'all'" try: - sample = form.getfirst('sample') + sample = form.get('sample') chem = formula(sample) except Exception: errors['sample'] = error() try: - fluence = float(form.getfirst('flux', 100000)) + fluence = float(form.get('flux', 100000)) except Exception: errors['flux'] = error() try: - fast_ratio = float(form.getfirst('fast', '0')) + fast_ratio = float(form.get('fast', '0')) except Exception: errors['fast'] = error() try: - Cd_ratio = float(form.getfirst('Cd', '0')) + Cd_ratio = float(form.get('Cd', '0')) except Exception: errors['Cd'] = error() try: - exposure = parse_hours(form.getfirst('exposure', '1')) + exposure = parse_hours(form.get('exposure', '1')) except Exception: errors['exposure'] = error() try: - mass_str = form.getfirst('mass', '0') - if mass_str.endswith('kg'): + mass_str = form.get('mass', '0') + if not mass_str: + mass = 0 + elif mass_str.endswith('kg'): mass = 1000*float(mass_str[:-2]) elif mass_str.endswith('mg'): mass = 0.001*float(mass_str[:-2]) @@ -300,26 +292,26 @@ def cgi_call(): except Exception: errors['mass'] = error() try: - density_type, density_value = parse_density(form.getfirst('density', '0')) + density_type, density_value = parse_density(form.get('density', '0')) except Exception: errors['density'] = error() try: - #print >>sys.stderr,form.getlist('rest[]') - rest_times = [parse_rest(v) for v in form.getlist('rest[]')] + rest_times = [parse_rest(v) for v in form.get('rest', [])] if not rest_times: rest_times = [0, 1, 24, 360] except Exception: errors['rest'] = error() try: - decay_level = float(form.getfirst('decay', '0.001')) + decay_level = float(form.get('decay', '0.001')) except Exception: errors['decay'] = error() try: - thickness = float(form.getfirst('thickness', '1')) + thickness = float(form.get('thickness', '1')) except Exception: errors['thickness'] = error() try: - wavelength_str = form.getfirst('wavelength', '1').strip() + # Ensure we cast to string so we can safely .strip() + wavelength_str = str(form.get('wavelength', '1')).strip() if wavelength_str.endswith('meV'): wavelength = nsf.neutron_wavelength(float(wavelength_str[:-3])) elif wavelength_str.endswith('m/s'): @@ -328,11 +320,10 @@ def cgi_call(): wavelength = float(wavelength_str[:-3]) else: wavelength = float(wavelength_str) - #print >>sys.stderr,wavelength_str except Exception: errors['wavelength'] = error() try: - xray_source = form.getfirst('xray', 'Cu Ka').strip() + xray_source = str(form.get('xray', 'Cu Ka')).strip() if xray_source.endswith('Ka'): xray_wavelength = elements.symbol(xray_source[:-2].strip()).K_alpha elif xray_source.endswith('keV'): @@ -343,14 +334,12 @@ def cgi_call(): xray_wavelength = elements.symbol(xray_source).K_alpha else: xray_wavelength = float(xray_source) - #print >>sys.stderr,"xray",xray_source,xray_wavelength except Exception: errors['xray'] = error() try: - abundance_source = form.getfirst('abundance', 'IAEA') + abundance_source = form.get('abundance', 'IAEA') if abundance_source == "IUPAC": abundance = activation.table_abundance - # CRUFT: periodictable no longer uses NIST 2001 data for abundance elif abundance_source == "NIST": abundance = activation.table_abundance elif abundance_source == "IAEA": @@ -490,10 +479,33 @@ def cgi_call(): return result +def fieldstorage_to_dict(form: 'cgi.FieldStorage') -> dict: + """ + special handling for "rest", which is a list of times. + """ + result = {} + if form is None: + return result + + for key in form.keys(): + values = form.getlist(key) + if len(values) == 1: + value = values[0] + if key == "rest": + result[key] = value.split(",") + else: + result[key] = value + + return result if __name__ == "__main__": + import cgi try: - response = cgi_call() + form = cgi.FieldStorage() + + # Convert to dictionary + form_dict = fieldstorage_to_dict(form) + response = api_call(form_dict) except Exception: response = { 'success': False, diff --git a/deploy_calculator.sh b/deploy_calculator.sh new file mode 100755 index 0000000..4b97ea3 --- /dev/null +++ b/deploy_calculator.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +TARGET_DIR=${TARGET_DIR:-/var/www/html/resources/activation} + +# First, get the latest pyodide files and the specific periodictable wheel we need: +./activation/get_pyodide.sh + +# Now copy all the necessary files to the target directory for deployment: +mkdir -p $TARGET_DIR +cp activation/jquery* $TARGET_DIR/ +cp activation/*.js $TARGET_DIR/ +cp activation/webworker.js $TARGET_DIR/ +cp activation/favicon.ico $TARGET_DIR/ +cp activation/periodictable_wheel_name.txt $TARGET_DIR/ +cp cgi-bin/nact.py $TARGET_DIR/ +cp -r activation/pyodide $TARGET_DIR/pyodide +cp -r activation/css $TARGET_DIR/ + +# Get the version of periodictable from the wheel file name and write it to a text file for use in the workflow +PERIODICTABLE_VERSION=$(python -c "import periodictable; print(periodictable.__version__)") + +# Write replacements in template +API_SUB="s@{{ api_script }}@api_webworker.js@g" +VER_SUB="s@{{ periodictable_version }}@$PERIODICTABLE_VERSION@g" +sed -e "$API_SUB;$VER_SUB" activation/index_template.html > "$TARGET_DIR/index.html" diff --git a/flask_server.py b/flask_server.py new file mode 100644 index 0000000..5710ed3 --- /dev/null +++ b/flask_server.py @@ -0,0 +1,70 @@ +from pathlib import Path +import re +import sys +from flask import abort, Flask, request, jsonify, make_response, render_template, send_from_directory + +# Import your refactored script (ensure it's named nact.py in the same folder) +sys.path.append(str(Path(__file__).parent / 'cgi-bin')) +import nact + +app = Flask(__name__, template_folder="activation") + + +@app.route('/api/calculate', methods=['POST']) +def calculate(): + """ + Handles the API request, converts the input into a standard dict, + and passes it to nact.cgi_call(). + """ + + try: + data = request.get_json() + result = nact.api_call(data) + return jsonify(result) + except Exception as e: + return jsonify({ + 'success': False, + 'error': 'Server error', + 'detail': str(e) + }), 500 + + +@app.route('/') +@app.route('/index.html') +def serve_index(): + """ + Reads the index.html file, performs a regex substitution on a script tag, + and returns the modified HTML. + """ + return render_template( + 'index_template.html', + api_script='api_fetch.js', + periodictable_version=nact.periodictable.__version__ + ) + + +@app.route('/') +def serve_static_files(filename): + """ + Serves any other file requested at the root level from the 'activation' folder. + """ + # Prevent this route from accidentally serving the unmodified index.html + if filename == 'index.html': + # You can either call serve_index() directly or return a 404 + return serve_index() + + # Securely send the file from the 'activation' directory + try: + return send_from_directory('activation', filename) + except FileNotFoundError: + abort(404) + + +if __name__ == '__main__': + if len(sys.argv) > 1: + host, *rest = sys.argv[1].split(':', 1) + port = int(rest[0]) if rest else 8008 + else: + host, port = "localhost", 8008 + # Run the server in debug mode on port 5000 + app.run(debug=True, host=host, port=port) \ No newline at end of file diff --git a/server.py b/server.py deleted file mode 100755 index 234575a..0000000 --- a/server.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -""" -Simple server for running the neutron activation calculator. - -This is intended for testing. It has not been assessed for security and is -not recommended for a public facing server. The activation calculator -expects a cgi interface, which should be provided by the web infrastructure -(apache, nginx, etc.) that you are using on your production server. - -Usage: python server.py [host | host:port] - -Default is localhost:8008 -""" - -from __future__ import print_function - -import sys -import os -try: - from http.server import HTTPServer, CGIHTTPRequestHandler - from socketserver import ThreadingMixIn -except ImportError: - from BaseHTTPServer import HTTPServer - from SocketServer import ThreadingMixIn - from CGIHTTPServer import CGIHTTPRequestHandler -import cgitb -cgitb.enable() ## This line enables CGI error reporting - -class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): - """Very simple threaded server""" - allow_reuse_address = True - request_queue_size = 50 - -server = ThreadedHTTPServer -handler = CGIHTTPRequestHandler -handler.cgi_directories = ["/cgi-bin"] - -if len(sys.argv) > 1: - host, *rest = sys.argv[1].split(':', 1) - port = int(rest[0]) if rest else 8008 -else: - host, port = "", 8008 -print(f"serving on http://{host}:{port}/activation/") -httpd = server((host, port), handler) -httpd.serve_forever()