diff --git a/.gitignore b/.gitignore
index 71cc7f3ae..4a7057074 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@ assets/demo-data/CLEWs.Demo.zip
Procfile
.env
+.runtime/
+WebAPP/app.log
dist/
build/
venv/
diff --git a/API/Classes/Base/Config.py b/API/Classes/Base/Config.py
index 49a889043..c5b254045 100644
--- a/API/Classes/Base/Config.py
+++ b/API/Classes/Base/Config.py
@@ -1,9 +1,10 @@
from pathlib import Path
import os
+import tempfile
from dotenv import load_dotenv
import platform
-# Central path validation utility (prevents path traversal)
+# Central path validation utility (prevents path traversal).
def validate_path(base_dir, user_input):
base_raw = os.fspath(base_dir)
user_raw = "" if user_input is None else os.fspath(user_input)
@@ -62,6 +63,10 @@ def validate_path(base_dir, user_input):
CLASS_FOLDER = WEBAPP_PATH / "Classes"
SOLVERs_FOLDER = WEBAPP_PATH / "SOLVERs"
EXTRACT_FOLDER = BASE_DIR
+PRIMARY_RUNTIME_DIR = BASE_DIR / ".runtime"
+PRIMARY_RUNTIME_LOG_DIR = PRIMARY_RUNTIME_DIR / "logs"
+TEMP_RUNTIME_LOG_DIR = Path(tempfile.gettempdir()) / "muiogo-runtime" / "logs"
+_RUNTIME_LOG_PATH = None
# Ensure DataStorage exists
DATA_STORAGE.mkdir(parents=True, exist_ok=True)
@@ -69,6 +74,27 @@ def validate_path(base_dir, user_input):
# Validate writability instead of forcing permissions
if not os.access(DATA_STORAGE, os.W_OK):
raise PermissionError(f"Data storage path is not writable: {DATA_STORAGE}")
+
+
+def get_runtime_log_path():
+ global _RUNTIME_LOG_PATH
+
+ if _RUNTIME_LOG_PATH is not None:
+ return _RUNTIME_LOG_PATH
+
+ for log_dir in (PRIMARY_RUNTIME_LOG_DIR, TEMP_RUNTIME_LOG_DIR):
+ try:
+ log_dir.mkdir(parents=True, exist_ok=True)
+ probe = log_dir / ".write_test"
+ with open(probe, "a", encoding="utf-8"):
+ pass
+ probe.unlink(missing_ok=True)
+ _RUNTIME_LOG_PATH = log_dir / "app.log"
+ return _RUNTIME_LOG_PATH
+ except OSError:
+ continue
+
+ raise PermissionError("No writable runtime log directory is available.")
#absolute paths
# OSEMOSYS_ROOT = os.path.abspath(os.getcwd())
# UPLOAD_FOLDER = Path(OSEMOSYS_ROOT, 'WebAPP')
diff --git a/API/Classes/Case/DataFileClass.py b/API/Classes/Case/DataFileClass.py
index bd5cc38b1..b3fc93190 100644
--- a/API/Classes/Case/DataFileClass.py
+++ b/API/Classes/Case/DataFileClass.py
@@ -792,17 +792,16 @@ def createCaseRun(self, caserunname, data):
try:
caseRunPath = Path(Config.DATA_STORAGE,self.case,'res', caserunname)
csvPath = Path(Config.DATA_STORAGE,self.case,'res', caserunname, 'csv')
- resDataPath = Path(Config.DATA_STORAGE,self.case,'view', 'resData.json')
if not os.path.exists(caseRunPath):
os.makedirs(caseRunPath)
os.makedirs(csvPath)
- if not os.path.exists(resDataPath):
- File.writeFile( data, resDataPath)
+ if not os.path.exists(self.resDataPath):
+ self.resData = {"osy-cases": [data]}
+ File.writeFile(self.resData, self.resDataPath)
else:
- resData = File.readFile(resDataPath)
- resData['osy-cases'].append(data)
- File.writeFile( resData, resDataPath)
+ self.resData['osy-cases'].append(data)
+ File.writeFile(self.resData, self.resDataPath)
response = {
"message": "You have created a case run!",
"status_code": "success"
@@ -822,8 +821,7 @@ def createCaseRun(self, caserunname, data):
def deleteScenarioCaseRuns(self, scenarioId):
try:
- resData = File.readFile(self.resDataPath)
- cases = resData['osy-cases']
+ cases = self.resData['osy-cases']
for cs in cases:
for sc in cs['Scenarios']:
@@ -831,7 +829,7 @@ def deleteScenarioCaseRuns(self, scenarioId):
cs['Scenarios'].remove(sc)
- File.writeFile(resData, self.resDataPath)
+ File.writeFile(self.resData, self.resDataPath)
response = {
"message": "You have deleted scenario from caseruns!",
"status_code": "success"
@@ -850,7 +848,6 @@ def updateCaseRun(self, caserunname, oldcaserunname, data):
caseRunPath = Path(Config.DATA_STORAGE,self.case,'res', oldcaserunname)
newcaseRunPath = Path(Config.DATA_STORAGE,self.case,'res', caserunname)
csvPath = Path(Config.DATA_STORAGE,self.case,'res', caserunname, 'csv')
- resDataPath = Path(Config.DATA_STORAGE,self.case,'view', 'resData.json')
if not os.path.exists(newcaseRunPath):
os.rename(caseRunPath, newcaseRunPath)
@@ -858,14 +855,12 @@ def updateCaseRun(self, caserunname, oldcaserunname, data):
if not os.path.exists(csvPath):
os.makedirs(csvPath)
- resData = File.readFile(resDataPath)
-
- resdata = resData['osy-cases']
+ resdata = self.resData['osy-cases']
for i, case in enumerate(resdata):
if case['Case'] == oldcaserunname:
- resData['osy-cases'][i] = data
+ self.resData['osy-cases'][i] = data
- File.writeFile( resData, resDataPath)
+ File.writeFile(self.resData, self.resDataPath)
response = {
"message": "You have updated a case run!",
"status_code": "success"
@@ -874,14 +869,12 @@ def updateCaseRun(self, caserunname, oldcaserunname, data):
if not os.path.exists(csvPath):
os.makedirs(csvPath)
- resData = File.readFile(resDataPath)
-
- resdata = resData['osy-cases']
+ resdata = self.resData['osy-cases']
for i, case in enumerate(resdata):
if case['Case'] == oldcaserunname:
- resData['osy-cases'][i] = data
+ self.resData['osy-cases'][i] = data
- File.writeFile( resData, resDataPath)
+ File.writeFile(self.resData, self.resDataPath)
response = {
"message": "You have updated a case run!",
"status_code": "success"
@@ -924,17 +917,15 @@ def deleteCaseResultsJSON(self, caserunname):
def deleteCaseRun(self, caserunname, resultsOnly):
try:
#caseRunPath = Path(Config.DATA_STORAGE,self.case,'res', caserunname)
- #resDataPath = Path(Config.DATA_STORAGE,self.case,'view', 'resData.json')
+ #self.resData = Path(Config.DATA_STORAGE,self.case,'view', 'resData.json')
if not resultsOnly:
- resData = File.readFile(self.resDataPath)
-
- for obj in resData['osy-cases']:
+ for obj in self.resData['osy-cases']:
if obj['Case'] == caserunname:
- resData['osy-cases'].remove(obj)
+ self.resData['osy-cases'].remove(obj)
- File.writeFile( resData, self.resDataPath)
+ File.writeFile(self.resData, self.resDataPath)
#delete from view folder
for group, array in self.VARIABLES.items():
@@ -970,45 +961,38 @@ def cleanUp(self):
# self.viewFolderPath = Path(Config.DATA_STORAGE,case,'view')
# folder_path = "C:/putanja/do/foldera"
- for caserunname in os.listdir( self.resultsPath):
- caserunname_path = os.path.join(self.resultsPath, caserunname)
- # Skip files such as .DS_Store that can appear on macOS.
- if not os.path.isdir(caserunname_path):
- continue
- for carerunData in os.listdir( caserunname_path):
- file_path = os.path.join(caserunname_path, carerunData)
- try:
- if os.path.isfile(file_path) or os.path.islink(file_path):
- os.remove(file_path)
- elif os.path.isdir(file_path):
- shutil.rmtree(file_path)
- except Exception as e:
- print(f"Greška pri brisanju {file_path}: {e}")
-
- for filename in os.listdir( self.viewFolderPath):
- if filename !='resData.json':
- file_path = os.path.join(self.viewFolderPath, filename)
- try:
- if os.path.isfile(file_path) or os.path.islink(file_path):
- os.remove(file_path)
- elif os.path.isdir(file_path):
- shutil.rmtree(file_path)
- except Exception as e:
- print(f"Greška pri brisanju {file_path}: {e}")
-
- #sad moramo napraviti defualt definitions file
- viewDefPath = Path(self.viewFolderPath, 'viewDefinitions.json')
- configPath = Path(Config.DATA_STORAGE, 'Variables.json')
- vars = File.readParamFile(configPath)
- viewDef = {}
- for group, lists in vars.items():
- for list in lists:
- viewDef[list['id']] = []
-
- viewData = {
- "osy-views": viewDef
- }
- File.writeFile( viewData, viewDefPath)
+ if os.path.exists(self.resultsPath) and os.path.isdir(self.resultsPath):
+ for caserunname in os.listdir(self.resultsPath):
+ caserunname_path = os.path.join(self.resultsPath, caserunname)
+ if not os.path.isdir(caserunname_path):
+ continue
+ for carerunData in os.listdir(caserunname_path):
+ file_path = os.path.join(caserunname_path, carerunData)
+ try:
+ if os.path.isfile(file_path) or os.path.islink(file_path):
+ os.remove(file_path)
+ elif os.path.isdir(file_path):
+ shutil.rmtree(file_path)
+ except Exception as e:
+ print(f"Greška pri brisanju {file_path}: {e}")
+
+ if os.path.exists(self.viewFolderPath) and os.path.isdir(self.viewFolderPath):
+ for filename in os.listdir(self.viewFolderPath):
+ if filename not in {'resData.json', 'viewDefinitions.json'}:
+ file_path = os.path.join(self.viewFolderPath, filename)
+ try:
+ if os.path.isfile(file_path) or os.path.islink(file_path):
+ os.remove(file_path)
+ elif os.path.isdir(file_path):
+ shutil.rmtree(file_path)
+ except Exception as e:
+ print(f"Greška pri brisanju {file_path}: {e}")
+
+ case_names = [c["Case"] for c in self.resData.get("osy-cases", [])]
+ for case in case_names:
+ case_path = Path(self.resultsPath, case)
+ if not case_path.exists():
+ case_path.mkdir(parents=True, exist_ok=True)
response = {
@@ -1073,9 +1057,8 @@ def readDataFile( self, caserunname ):
#f = open(self.dataFile, mode="r")
dataFilePath = Path(Config.DATA_STORAGE, self.case, 'res',caserunname,'data.txt')
if os.path.exists(dataFilePath):
- f = open(dataFilePath, mode="r", encoding='utf-8-sig')
- data = f.read()
- f.close
+ with open(dataFilePath, mode="r", encoding='utf-8-sig') as f:
+ data = f.read()
else:
data = None
diff --git a/API/Classes/Case/OsemosysClass.py b/API/Classes/Case/OsemosysClass.py
index 5ef3a5e53..93ba262fc 100644
--- a/API/Classes/Case/OsemosysClass.py
+++ b/API/Classes/Case/OsemosysClass.py
@@ -7,6 +7,7 @@
class Osemosys():
def __init__(self, case):
+ Config.validate_path(Config.DATA_STORAGE, case)
self.case = case
self.PARAMETERS = File.readParamFile(Path(Config.DATA_STORAGE, 'Parameters.json'))
self.VARIABLES = File.readParamFile(Path(Config.DATA_STORAGE, 'Variables.json'))
diff --git a/API/Routes/DataFile/DataFileRoute.py b/API/Routes/DataFile/DataFileRoute.py
index 2b7e98eff..a89b69e61 100644
--- a/API/Routes/DataFile/DataFileRoute.py
+++ b/API/Routes/DataFile/DataFileRoute.py
@@ -1,9 +1,11 @@
-from flask import Blueprint, jsonify, request, send_file, session
+from flask import Blueprint, Response, jsonify, request, send_file, session
from pathlib import Path
-import shutil, datetime, time, os
+import shutil, datetime, time, os, logging
from Classes.Case.DataFileClass import DataFile
from Classes.Base import Config
+logger = logging.getLogger(__name__)
+
datafile_api = Blueprint('DataFileRoute', __name__)
@datafile_api.route("/generateDataFile", methods=['POST'])
@@ -64,6 +66,7 @@ def deleteCaseRun():
if not casename:
return jsonify({'message': 'No model selected.', 'status_code': 'error'}), 400
+ Config.validate_path(Config.DATA_STORAGE, os.path.join(casename, 'res', caserunname or ''))
casePath = Path(Config.DATA_STORAGE, casename, 'res', caserunname)
if not resultsOnly:
shutil.rmtree(casePath)
@@ -141,6 +144,31 @@ def readDataFile():
return jsonify(response), 200
except(IOError):
return jsonify('No existing cases!'), 404
+
+@datafile_api.route("/readModelFile", methods=['GET'])
+def readModelFile():
+ model_path = Path(Config.SOLVERs_FOLDER, 'model.v.5.4.txt')
+ if not model_path.is_file():
+ return jsonify({'message': 'Model file not found.', 'status_code': 'error'}), 404
+
+ text = model_path.read_text(encoding="utf-8", errors="replace")
+ return Response(text, mimetype="text/plain; charset=utf-8")
+
+
+@datafile_api.route("/readLogFile", methods=['GET'])
+def readLogFile():
+ try:
+ log_path = Config.get_runtime_log_path()
+ except OSError:
+ return Response("Runtime logging is not available.\n", mimetype="text/plain; charset=utf-8")
+
+ if not log_path.is_file():
+ return Response("No runtime log available yet.\n", mimetype="text/plain; charset=utf-8")
+
+ text = log_path.read_text(encoding="utf-8", errors="replace")
+ if not text.strip():
+ return Response("No runtime log available yet.\n", mimetype="text/plain; charset=utf-8")
+ return Response(text, mimetype="text/plain; charset=utf-8")
@datafile_api.route("/validateInputs", methods=['POST'])
def validateInputs():
@@ -172,9 +200,12 @@ def downloadDataFile():
#path = "/Examples.pdf"
case = session.get('osycase', None)
caserunname = request.args.get('caserunname')
+ Config.validate_path(Config.DATA_STORAGE, os.path.join(case or '', 'res', caserunname or ''))
dataFile = Path(Config.DATA_STORAGE,case, 'res',caserunname, 'data.txt')
return send_file(dataFile.resolve(), as_attachment=True, max_age=0)
-
+
+ except PermissionError:
+ return jsonify({'message': 'Invalid path.', 'status_code': 'error'}), 400
except(IOError):
return jsonify('No existing cases!'), 404
@@ -183,9 +214,12 @@ def downloadFile():
try:
case = session.get('osycase', None)
file = request.args.get('file')
+ Config.validate_path(Config.DATA_STORAGE, os.path.join(case or '', 'res', 'csv', file or ''))
dataFile = Path(Config.DATA_STORAGE,case,'res','csv',file)
return send_file(dataFile.resolve(), as_attachment=True, max_age=0)
-
+
+ except PermissionError:
+ return jsonify({'message': 'Invalid path.', 'status_code': 'error'}), 400
except(IOError):
return jsonify('No existing cases!'), 404
@@ -195,9 +229,12 @@ def downloadCSVFile():
case = session.get('osycase', None)
file = request.args.get('file')
caserunname = request.args.get('caserunname')
+ Config.validate_path(Config.DATA_STORAGE, os.path.join(case or '', 'res', caserunname or '', 'csv', file or ''))
dataFile = Path(Config.DATA_STORAGE,case,'res',caserunname,'csv',file)
return send_file(dataFile.resolve(), as_attachment=True, max_age=0)
-
+
+ except PermissionError:
+ return jsonify({'message': 'Invalid path.', 'status_code': 'error'}), 400
except(IOError):
return jsonify('No existing cases!'), 404
@@ -206,9 +243,12 @@ def downloadResultsFile():
try:
case = session.get('osycase', None)
caserunname = request.args.get('caserunname')
+ Config.validate_path(Config.DATA_STORAGE, os.path.join(case or '', 'res', caserunname or ''))
dataFile = Path(Config.DATA_STORAGE,case, 'res', caserunname,'results.txt')
return send_file(dataFile.resolve(), as_attachment=True, max_age=0)
-
+
+ except PermissionError:
+ return jsonify({'message': 'Invalid path.', 'status_code': 'error'}), 400
except(IOError):
return jsonify('No existing cases!'), 404
@@ -218,8 +258,10 @@ def run():
casename = request.json['casename']
caserunname = request.json['caserunname']
solver = request.json['solver']
+ logger.info("Starting optimization process for model %s caserun %s", casename, caserunname)
txtFile = DataFile(casename)
- response = txtFile.run(solver, caserunname)
+ response = txtFile.run(solver, caserunname)
+ logger.info("Optimization finished for model %s caserun %s", casename, caserunname)
return jsonify(response), 200
# except Exception as ex:
# print(ex)
@@ -238,6 +280,7 @@ def batchRun():
if modelname != None:
txtFile = DataFile(modelname)
for caserun in cases:
+ logger.info("Generating data file for model %s caserun %s", modelname, caserun)
txtFile.generateDatafile(caserun)
response = txtFile.batchRun( 'CBC', cases)
@@ -254,8 +297,9 @@ def cleanUp():
if modelname != None:
model = DataFile(modelname)
- response = model.cleanUp()
+ logger.info("Cleaning up results for model %s", modelname)
+ response = model.cleanUp()
return jsonify(response), 200
except(IOError):
- return jsonify('Error!'), 404
\ No newline at end of file
+ return jsonify('Error!'), 404
diff --git a/API/app.py b/API/app.py
index 458aad034..3651fbfab 100644
--- a/API/app.py
+++ b/API/app.py
@@ -3,6 +3,8 @@
import os
import secrets
import sys
+import warnings
+from logging.handlers import TimedRotatingFileHandler
# Fail fast: unsupported Python hits cryptic pandas/numpy import errors without this.
SUPPORTED_PYTHON_MIN = (3, 10)
@@ -34,31 +36,52 @@
from Routes.Case.ViewDataRoute import viewdata_api
from Routes.DataFile.DataFileRoute import datafile_api
-#RADI
-# -------------------------
-# FIX: Make template/static paths independent of cwd
-# -------------------------
+def _configure_logging():
+ if getattr(_configure_logging, "_configured", False):
+ return getattr(_configure_logging, "_log_path", None)
-# This file is in: API/app.py
-# So project root is 1 level up
-BASE_DIR = Path(__file__).resolve().parents[1]
-WEBAPP_PATH = BASE_DIR / "WebAPP"
+ logger = logging.getLogger()
+ logger.setLevel(logging.INFO)
-template_dir = str(WEBAPP_PATH)
-static_dir = str(WEBAPP_PATH)
+ formatter = logging.Formatter("%(asctime)s [%(name)s] %(levelname)s: %(message)s")
-# template_dir = Config.WebAPP_PATH.resolve()
-# static_dir = Config.WebAPP_PATH.resolve()
+ console_handler = logging.StreamHandler()
+ console_handler.setFormatter(formatter)
+ console_handler._muiogo_handler = True
+ logger.addHandler(console_handler)
+
+ log_path = None
+ try:
+ log_path = Config.get_runtime_log_path()
+ file_handler = TimedRotatingFileHandler(
+ log_path, when="midnight", interval=1, backupCount=7, encoding="utf-8"
+ )
+ file_handler.setFormatter(formatter)
+ file_handler._muiogo_handler = True
+ logger.addHandler(file_handler)
+ except OSError as exc:
+ logger.warning("Runtime log file unavailable. Continuing with console logging only: %s", exc)
+
+ logging.captureWarnings(True)
+ warnings.simplefilter("default")
+
+ def log_exception(exc_type, exc_value, exc_traceback):
+ if issubclass(exc_type, KeyboardInterrupt):
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
+ return
+ logger.error("UNCAUGHT EXCEPTION", exc_info=(exc_type, exc_value, exc_traceback))
+
+ sys.excepthook = log_exception
+
+ _configure_logging._configured = True
+ _configure_logging._log_path = log_path
+ return log_path
-# template_dir = os.path.join(sys._MEIPASS, 'WebAPP')
-# static_dir = os.path.join(sys._MEIPASS, 'WebAPP')
-#gets absolute path
-# template_dir = Path('WebAPP').resolve()
-# static_dir = Path('../WebAPP').resolve()
+RUNTIME_LOG_PATH = _configure_logging()
-# template_dir = 'WebAPP'
-# static_dir = '../WebAPP'
+template_dir = str(Config.WEBAPP_PATH)
+static_dir = str(Config.WEBAPP_PATH)
app = Flask(__name__, static_url_path='', static_folder=static_dir, template_folder=template_dir)
@@ -66,7 +89,7 @@
secret_key = os.environ.get("MUIOGO_SECRET_KEY", "").strip()
if not secret_key:
secret_key = secrets.token_hex(32)
- logging.warning(
+ logging.getLogger(__name__).warning(
"MUIOGO_SECRET_KEY is not configured. Using a temporary in-memory key. "
"Run setup to create a persistent secret in .env."
)
@@ -136,7 +159,6 @@ def setSession():
session.pop('osycase', None)
return jsonify({"osycase": None}), 200
- from pathlib import Path
if not Path(Config.DATA_STORAGE, cs).is_dir():
return jsonify({'message': 'Case not found.', 'status_code': 'error'}), 404
session['osycase'] = cs
diff --git a/WebAPP/App/Controller/DataFile.js b/WebAPP/App/Controller/DataFile.js
index c9c11d35c..d34f2c7e5 100644
--- a/WebAPP/App/Controller/DataFile.js
+++ b/WebAPP/App/Controller/DataFile.js
@@ -19,11 +19,13 @@ export default class DataFile {
promise.push(genData);
const resData = Osemosys.getResultData(casename, 'resData.json');
promise.push(resData);
+ const modelFile = Osemosys.readModelFile();
+ promise.push(modelFile);
return Promise.all(promise);
})
.then(data => {
- let [casename, genData, resData] = data;
- let model = new Model(casename, genData, resData, "DataFile");
+ let [casename, genData, resData, modelFile] = data;
+ let model = new Model(casename, genData, resData, modelFile, "DataFile");
if (casename) {
this.initPage(model);
} else {
@@ -43,6 +45,7 @@ export default class DataFile {
//Navbar.initPage(model.casename, model.pageId);
Html.title(model.casename, model.title, "");
Html.renderCases(model.cases);
+ Html.renderModelFile(model.modelFile);
//potrebno je napraviti render svih scenarija (mozda je dodan novi scenario u medjuvremenu), on mora biti dodan u listu scenarija po case run samo sto nece biti aktivan
// Html.renderScOrder(model.scBycs[model.cs]);
//console.log('model.scenarios ',model.scenarios)
@@ -69,11 +72,13 @@ export default class DataFile {
promise.push(genData);
const resData = Osemosys.getResultData(casename, 'resData.json');
promise.push(resData);
+ const modelFile = Osemosys.readModelFile();
+ promise.push(modelFile);
return Promise.all(promise);
})
.then(data => {
- let [casename, genData, resData] = data;
- let model = new Model(casename, genData, resData, "DataFile");
+ let [casename, genData, resData, modelFile] = data;
+ let model = new Model(casename, genData, resData, modelFile, "DataFile");
$(".DataFile").hide();
$("#osy-DataFile").empty();
$("#osy-runOutput").empty();
@@ -95,6 +100,9 @@ export default class DataFile {
}
static initEvents(model) {
+ const renderLogText = (selector, primary, secondary = '') => {
+ Html.renderPreformatted(selector, `${primary || ''}${secondary || ''}`);
+ };
$("#casePicker").off('click');
$("#casePicker").on('click', '.selectCS', function (e) {
@@ -106,6 +114,21 @@ export default class DataFile {
Message.smallBoxInfo("Case selection", casename + " is selected!", 3000);
});
+ $("#osy-logFile").off('click');
+ $("#osy-logFile").on('click', function (event) {
+ Message.loaderStart('Generating log file!')
+ Osemosys.readLogFile()
+ .then(response => {
+ Message.loaderEnd();
+ Html.renderPreformatted('#osy-logFiletxt', response, 'No runtime log available yet.\n');
+ $("#osy-LogFileModal").modal("show");
+ })
+ .catch(error => {
+ Message.loaderEnd();
+ Message.bigBoxDanger('Error message', error, null);
+ })
+ });
+
$("#osy-btnScOrder").off('click');
$("#osy-btnScOrder").on('click', function (event) {
// console.log('model, ', model)
@@ -143,10 +166,54 @@ export default class DataFile {
});
+ function setAllCheckboxes(state) {
+ // Targetiramo checkboxove SAMO u #osy-scOrder (SC_0 ostaje netaknut jer je disabled i u #osy-sc0)
+ $('#osy-scOrder input[type="checkbox"]:not(:disabled)')
+ .prop('checked', state)
+ .trigger('change'); // ako imaš logiku na change eventu
+ }
+
+ function syncToggleAllButton() {
+ const $btn = $('#toggle-all');
+ const $boxes = $('#osy-scOrder input[type="checkbox"]:not(:disabled)');
+ const allChecked = $boxes.length > 0 && !$boxes.is(':not(:checked)');
+ const $icon = $btn.find('i.fa');
+
+ if (allChecked) {
+ $icon.removeClass('fa-square-o').addClass('fa-check-square-o');
+ $btn.find('span').text('Uncheck all');
+ } else {
+ $icon.removeClass('fa-check-square-o').addClass('fa-square-o');
+ $btn.find('span').text('Check all');
+ }
+ }
+
+ syncToggleAllButton();
+
+ $("#toggle-all").off('click');
+ $('#toggle-all').on('click', function (e) {
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ const $btn = $(this);
+ // Skup svih "normalnih" checkboxova (osim disabled i osim SC_0)
+ const $boxes = $('#osy-scOrder input[type="checkbox"]:not(:disabled)');
+ // Provjera: ima li ijedan nečekiran?
+ const anyUnchecked = $boxes.is(':not(:checked)');
+
+ // Ako ima nečekiranih → čekiraj sve, inače → poništi sve
+ setAllCheckboxes(anyUnchecked);
+
+ syncToggleAllButton();
+
+ });
+
+
$("#btnSaveOrder").off('click');
$("#btnSaveOrder").on('click', function (event) {
Message.clearMessages();
- Message.bigBoxSuccess('Sceanario order', 'You have updated scenarios order data!', 3000);
+ Message.bigBoxWarning('Scenario Order Updated', 'You have updated the order of scenarios. To apply your changes, please click Update Case.', 4000);
$('#osy-order').modal('toggle');
//nema potrebe da spasavmo scenario order jer se on ada nalazi u resData
@@ -410,11 +477,27 @@ export default class DataFile {
})
});
-
$("#osy-run").off('click');
$("#osy-run").on('click', function (event) {
Pace.restart();
Message.loaderStart('Optimization in process!')
+
+
+ // const logBox = document.getElementById("logBox");
+ // const eventSource = new EventSource("http://127.0.0.1:5002/stream_logs");
+
+ // eventSource.onmessage = function (e) {
+ // console.log('e.data ', e.data)
+ // logBox.innerHTML += e.data + "
";
+ // logBox.scrollTop = logBox.scrollHeight; // auto-scroll
+ // };
+
+
+
+
+
+ //////////////////////////////////////////////////////////////////////////////////////
+
//promijenjeno da radimo samo sa cBCsolverom
//let solver = $('input[name="solver"]:checked').val();
let solver = 'cbc';
@@ -430,9 +513,9 @@ export default class DataFile {
$(".batchOutput").hide();
$("#osy-batchOutput").empty();
$("#osy-runOutput").empty();
- $("#osy-runOutput").html('
' + response.cbc_message, response.cbc_stdmsg+ '
');
+ renderLogText('#osy-runOutput', response.cbc_message, response.cbc_stdmsg);
$("#osy-lpOutput").empty();
- $("#osy-lpOutput").html('' + response.glpk_message, response.glpk_stdmsg+ '
');
+ renderLogText('#osy-lpOutput', response.glpk_message, response.glpk_stdmsg);
Base.getResultCSV(model.casename, model.cs)
.then(csvs => {
Html.renderCSV(csvs, model.cs)
@@ -450,9 +533,9 @@ export default class DataFile {
$(".batchOutput").hide();
$("#osy-batchOutput").empty();
$("#osy-runOutput").empty();
- $("#osy-runOutput").html('' + response.cbc_message, response.cbc_stdmsg+ '
');
+ renderLogText('#osy-runOutput', response.cbc_message, response.cbc_stdmsg);
$("#osy-lpOutput").empty();
- $("#osy-lpOutput").html('' + response.glpk_message, response.glpk_stdmsg+ '
');
+ renderLogText('#osy-lpOutput', response.glpk_message, response.glpk_stdmsg);
Base.getResultCSV(model.casename, model.cs)
.then(csvs => {
Html.renderCSV(csvs, model.cs)
@@ -469,9 +552,9 @@ export default class DataFile {
$(".batchOutput").hide();
$("#osy-batchOutput").empty();
$("#osy-runOutput").empty();
- $("#osy-runOutput").html('' + response.cbc_message, response.cbc_stdmsg+ '
');
+ renderLogText('#osy-runOutput', response.cbc_message, response.cbc_stdmsg);
$("#osy-lpOutput").empty();
- $("#osy-lpOutput").html('' + response.glpk_message, response.glpk_stdmsg+ '
');
+ renderLogText('#osy-lpOutput', response.glpk_message, response.glpk_stdmsg);
Message.clearMessages();
// let errormsg = '';
// if (response.glpk_message != "" || response.glpk_stdmsg != "") {
@@ -577,11 +660,11 @@ export default class DataFile {
if (response.status_code == "success") {
$('#osy-validation').modal('toggle');
$("#valCasrunname").text(caserunanme)
- $("#valOutput").html('' + response.msg+ '
')
+ Html.renderPreformatted('#valOutput', response.msg);
}
if (response.status_code == "warning") {
$('#osy-validation').modal('toggle');
- $("#valOutput").html('' + response.msg+ '
');
+ Html.renderPreformatted('#valOutput', response.msg);
}
if (response.status_code == "error") {
//Message.warningOsy(response.msg);
@@ -734,6 +817,7 @@ export default class DataFile {
$(".batchOutput").hide();
$("#osy-batchRun").hide();
+ $("#osy-runCaseDiv").hide();
$('.checkbox').prop('checked', false);
}
//remove case from view json files
@@ -764,7 +848,6 @@ export default class DataFile {
e.stopImmediatePropagation();
});
-
//$(".Cases").off('click');
$('#osy-Cases').on('click', '.checkbox', function(e){
// var val = $(this).val();
@@ -823,7 +906,7 @@ export default class DataFile {
$(".batchOutput").show();
$("#osy-batchOutput").empty();
- $("#osy-batchOutput").html('' + response.log+ '
');
+ Html.renderPreformatted('#osy-batchOutput', response.log);
})
.catch(error => {
@@ -841,14 +924,15 @@ export default class DataFile {
Osemosys.cleanUp(model.casename)
.then(response => {
Message.loaderEnd();
- //Message.smallBoxInfo('Generate message', response.message, 3000);
- // console.log('response ', response.log);
- // Message.bigBoxDefault("BATCH RUN!", response.log)
$(".runOutput").hide();
+ $(".DataFile").hide();
$(".lpOutput").hide();
$(".Results").hide();
+ $(".batchOutput").hide();
+ $("#osy-runCaseDiv").hide();
$("#osy-runOutput").empty();
$("#osy-lpOutput").empty();
+ $('.Cases').tab('show');
console.log('response clean up ', response)
@@ -874,7 +958,3 @@ export default class DataFile {
}
}
-
-
-
-
diff --git a/WebAPP/App/Controller/ModelFile.js b/WebAPP/App/Controller/ModelFile.js
new file mode 100644
index 000000000..66425a0be
--- /dev/null
+++ b/WebAPP/App/Controller/ModelFile.js
@@ -0,0 +1,160 @@
+import { Base } from "../../Classes/Base.Class.js";
+import { Osemosys } from "../../Classes/Osemosys.Class.js";
+
+export default class ModelFile {
+
+ static onLoad() {
+ Promise.all([
+ Base.getSession().catch(() => ({ session: "" })),
+ Osemosys.readModelFile()
+ ])
+ .then(([sessionData, txt]) => {
+ document.getElementById("osy-case").textContent = sessionData.session || "";
+ if (!txt) {
+ document.getElementById("equations").innerHTML =
+ "Unable to load model file.
";
+ return;
+ }
+ const eqs = ModelFile.extractEquations(txt);
+ if (!eqs.length) {
+ document.getElementById("equations").innerHTML =
+ "No equations could be parsed from the model file.
";
+ return;
+ }
+ ModelFile.renderEquations(eqs);
+ });
+ }
+
+ // -----------------------------------------
+ // Extract: objective + constraints
+ // -----------------------------------------
+ static extractEquations(txt) {
+ const src = txt.replace(/\r/g, "");
+ const eqs = [];
+
+ // Objective
+ const objRe = /(minimize|maximize)\s+([A-Za-z_]\w*)\s*:\s*([\s\S]*?)\s*;/i;
+ const mObj = src.match(objRe);
+ if (mObj) {
+ eqs.push({
+ section: "Objective",
+ name: mObj[2],
+ latex: ModelFile.gmplToLatex(mObj[3].trim())
+ });
+ }
+
+ // Constraints
+ const consRe = /s\.t\.\s*([A-Za-z_]\w*)\s*(\{[^}]*\})?\s*:\s*([\s\S]*?)(?=;)/gi;
+ let m;
+ while ((m = consRe.exec(src)) !== null) {
+ eqs.push({
+ section: ModelFile.detectSection(m[1]),
+ name: m[1],
+ latex: ModelFile.gmplToLatex(m[3].trim())
+ });
+ }
+
+ return eqs;
+ }
+
+ // -----------------------------------------
+ // Section assignment
+ // -----------------------------------------
+ static detectSection(name) {
+ const n = name.toUpperCase();
+
+ if (n.startsWith("EB")) return "Energy Balance";
+ if (n.startsWith("E")) return "Emissions";
+ if (n.startsWith("A") || n.startsWith("TAC") || n.startsWith("AAC")) return "Activity";
+ if (n.startsWith("NC") || n.startsWith("TC") || n.startsWith("C")) return "Capacity";
+ if (n.startsWith("S")) return "Storage";
+ if (n.startsWith("UDC")) return "User-defined Constraints";
+ return "Other";
+ }
+
+ // -----------------------------------------
+ // GMPL --> LaTeX
+ // -----------------------------------------
+ static gmplToLatex(expr) {
+ let s = expr;
+
+ s = s.replace(/&&/g, " \\land ");
+ s = s.replace(/<=/g, "\\le ")
+ .replace(/>=/g, "\\ge ")
+ .replace(/\*/g, "\\cdot ");
+
+ // sum{}
+ s = s.replace(/sum\s*\{([^}]*)\}/gi, (_, inside) => {
+ const cleaned = inside
+ .split(',')
+ .map(p => p.trim().replace(/\s+in\s+/i," \\in "))
+ .join(', ');
+ return `\\sum_{${cleaned}}`;
+ });
+
+ // X[a,b]
+ s = s.replace(/([A-Za-z_]\w*)\s*\[([^\]]+)\]/g, "\\mathrm{$1}_{ $2 }");
+
+ // ukloni nove linije
+ s = s.replace(/\n+/g, " ");
+
+ return s;
+ }
+
+ // -----------------------------------------
+ // FINAL RENDER + NUMERACIJA + LINIJE
+ // -----------------------------------------
+ static renderEquations(eqs) {
+ const out = document.getElementById("equations");
+
+ let html = "";
+ let lastSection = "";
+ let counter = 1;
+
+ eqs.forEach((eq, i) => {
+ const isNewSection = eq.section !== lastSection;
+
+ // Deblja linija između sekcija (ali ne prije prve)
+ if (isNewSection && i !== 0) {
+ html += `
`;
+ }
+
+ // Naslov sekcije ako je nova
+ if (isNewSection) {
+ html += `
+
+ ${eq.section}
+
+ `;
+ lastSection = eq.section;
+ }
+
+ // Jednadžba + ručna numeracija + tanka linija nakon svake
+ html += `
+
+
${eq.name}
+
+
+ $$
+
+ \\begin{align}
+ ${eq.latex}
+ \\end{align}
+ \\tag{${counter}}
+
+ $$
+
+
+
+
+ `;
+
+ counter++;
+ });
+
+ out.innerHTML = html;
+ if (window.MathJax?.typesetPromise) {
+ window.MathJax.typesetPromise([out]).catch(() => {});
+ }
+ }
+}
diff --git a/WebAPP/App/Model/Config.Model.js b/WebAPP/App/Model/Config.Model.js
index 627a2c06d..e4dc0f1de 100644
--- a/WebAPP/App/Model/Config.Model.js
+++ b/WebAPP/App/Model/Config.Model.js
@@ -20,9 +20,6 @@ export class Model {
let varById = DataModelResult.getVarById(VARIABLES);
let varNames = DataModelResult.AllVarName(VARIABLES);
- console.log('unitsDef ', unitsDef)
- console.log('PARAMETERS[group] ', PARAMETERS['RYS'])
-
let gridParamData = []
$.each(PARAMORDER, function (id, group) {
$.each(PARAMETERS[group], function (id, obj) {
@@ -142,4 +139,4 @@ export class Model {
this.varNames = varNames;
//this.unitIdByVal = unitIdByVal;
}
-}
\ No newline at end of file
+}
diff --git a/WebAPP/App/Model/DataFile.Model.js b/WebAPP/App/Model/DataFile.Model.js
index ad51ada27..d3d220b1e 100644
--- a/WebAPP/App/Model/DataFile.Model.js
+++ b/WebAPP/App/Model/DataFile.Model.js
@@ -1,6 +1,6 @@
export class Model {
- constructor (casename, genData, resData, pageId) {
+ constructor (casename, genData, resData, modelFile, pageId) {
if(casename){
let cases = resData['osy-cases'];
@@ -54,6 +54,7 @@ export class Model {
this.casename = casename;
this.cs = cs;
+ this.modelFile = modelFile;
this.scBycs = scBycs;
this.title = "Run model";
this.scenarios = scenarios;
diff --git a/WebAPP/App/Model/ModelFile.Model.js b/WebAPP/App/Model/ModelFile.Model.js
new file mode 100644
index 000000000..df000813e
--- /dev/null
+++ b/WebAPP/App/Model/ModelFile.Model.js
@@ -0,0 +1,18 @@
+
+export class Model {
+ constructor (casename, modelFile, pageId) {
+ if(casename){
+
+ this.casename = casename || null;
+ this.title = "Model file";
+ this.modelFile = modelFile || "";
+ this.pageId = pageId;
+
+ }else{
+ this.casename = null;
+ this.title = "Model file";
+ this.scenarios = null;
+ this.pageId = pageId;
+ }
+ }
+}
diff --git a/WebAPP/App/View/DataFile.html b/WebAPP/App/View/DataFile.html
index fe3a92493..fdcfb3f59 100644
--- a/WebAPP/App/View/DataFile.html
+++ b/WebAPP/App/View/DataFile.html
@@ -175,6 +175,7 @@ Case runs
Cases
+
@@ -207,6 +208,16 @@ Case runs
+
+
+
+