linny-r 1.1.9 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,22 +10,19 @@ The graphical language and WYSIWYG model editor are developed by **Pieter Bots**
10
10
 
11
11
  Originally implemented in Delphi Pascal, Linny-R is now developed in HTML+CSS+JavaScript
12
12
  so as to be platform-independent and 100% transparent open source (under the MIT license).
13
- The software comprises a server that runs on Node.js, and a graphical user interface (GUI) that runs in any modern browser.
13
+ The software comprises a server that runs on **Node.js**,
14
+ and a graphical user interface (GUI) that runs in any modern browser.
14
15
 
15
- You can play with the most recent release of Linny-R on
16
- <a href="https://sysmod.tbm.tudelft.nl/linny-r" target="_blank">this server hosted at TU Delft</a>.
17
- Note that this server imposes restrictions on solver time and the total number of blocks it will solve per run.
18
- If you install Linny-R on your own machine, no such restrictions apply.
16
+ User documentation for Linny-R is still scant, but it is growing. You can contribute yourself (in "wiki fashion")
17
+ via the official user documentation site <a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
18
+ Technical documentation will be developed on GitHub: https://github.com/pwgbots/linny-r/wiki
19
19
 
20
- Documentation for Linny-R is still scant, but it is growing. You can contribute yourself (in "wiki fashion")
21
- via the official documentation site <a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
22
-
23
- ### Installing Node.js
20
+ ## Installing Node.js
24
21
 
25
22
  Linny-R is developed as a JavaScript package, and requires that **Node.js** is installed on your computer.
26
23
  This software can be downloaded from <a href="https://nodejs.org" target="_blank">https://nodejs.org</a>.
27
24
  Make sure that you choose the correct installer for your computer.
28
- Linny-R is developed using the _current_ release. Presently (October 2022) this is 18.10.0.
25
+ Linny-R is developed using the _current_ release. Presently (October 2022) this is 18.11.0.
29
26
 
30
27
  Run the installer and accept the default settings.
31
28
  There is **no** need to install the optional _Tools for Native Modules_.
@@ -36,9 +33,9 @@ Verify the installation by typing:
36
33
 
37
34
  ``node --version``
38
35
 
39
- The response should be the version number of Node.js, for example: v18.10.0.
36
+ The response should be the version number of Node.js, for example: v18.11.0.
40
37
 
41
- ### Installing Linny-R
38
+ ## Installing Linny-R
42
39
  It is advisable to install Linny-R in a directory on your computer, not in a cloud.
43
40
  In this installation guide, the path to this directory is denoted by `WORKING_DIRECTORY`,
44
41
  so in all commands you should replace this with the actual directory path.
@@ -57,6 +54,8 @@ and then type at the command line prompt:
57
54
 
58
55
  ``npm install --prefix . linny-r``
59
56
 
57
+ **NOTE:** The spacing around the dot is important.
58
+
60
59
  After installation has completed, `WORKING_DIRECTORY` should have this directory tree structure:
61
60
 
62
61
  <pre>
@@ -105,7 +104,7 @@ It should also contain the style sheet `linny-r.css` required by the GUI.
105
104
  The sub-directories of `static` contain files that are served to the browser by the script
106
105
  `server.js` when it is running in Node.js.
107
106
 
108
- ### Configuring the MILP solver
107
+ ## Configuring the MILP solver
109
108
 
110
109
  Linny-R presently supports two MILP solvers: Gurobi and LP_solve.
111
110
  Gurobi is _considerably_ more powerful than the open source LP_solve solver that has powered Linny-R since 2009,
@@ -158,18 +157,17 @@ Then return to Terminal and once more type `./lp_solve -h`.
158
157
  The response should then be a listing of all the command line options of LP_solve.
159
158
  If you reach this stage, Linny-R will be able to run LP_solve.
160
159
 
161
-
162
- ### Running Linny-R
160
+ ## Running Linny-R
163
161
 
164
162
  Open the Command Line Interface (CLI) of your computer, change to your `WORKING_DIRECTORY` and type:
165
163
 
166
- ``linny-r``
164
+ ``node node_modules/linny-r/server launch``
167
165
 
168
166
  This response should be something similar to:
169
167
 
170
168
  <pre>
171
- Node.js server for Linny-R version 1.1.9
172
- Node.js version: v18.10.0
169
+ Node.js server for Linny-R version 1.1.11
170
+ Node.js version: v18.11.0
173
171
  ... etc.
174
172
  </pre>
175
173
 
@@ -179,9 +177,9 @@ The Linny-R GUI should show in your browser window,
179
177
  while in the CLI you should see a long series of server log messages like:
180
178
 
181
179
  <pre>
182
- Static file: /index.html
183
- Static file: /scripts/iro.min.js
184
- Static file: /images/open.png
180
+ [2022-10-17 14:55:37] Static file: /index.html
181
+ [2022-10-17 14:55:37] Static file: /scripts/iro.min.js
182
+ [2022-10-17 14:55:37] Static file: /images/open.png
185
183
  ... etc.
186
184
  </pre>
187
185
 
@@ -207,7 +205,45 @@ Meanwhile, in the CLI, you should see a server log message like:
207
205
  Solve block 1 a
208
206
  </pre>
209
207
 
210
- #### User workspace
208
+ To end a modeling session, you can shut down the server by clickicng on the local host icon
209
+ in the upper right corner of the Linny-R GUI in your browser, confirm that you want to leave,
210
+ and then close your browser (tab). If you do not shut down the server from the browser,
211
+ you can also stop the server by repeatedly pressing ``Ctrl+C`` in the CLI box.
212
+
213
+ ## Command line options
214
+
215
+ Optionally, you can add more arguments to the `node` command:
216
+
217
+ <pre>
218
+ dpi=[number] to overrule the default resolution (300 dpi) for Inkscape
219
+ launch to automatically launch Linny-R in your default browser
220
+ port=[number] to overrule the default port number (5050)
221
+ solver=[name] to overrule the default sequence (Gurobi, LP_solve)
222
+ workspace=[path] to overrule the default path for the user directory
223
+ </pre>
224
+
225
+ ## Click-start for Linny-R
226
+
227
+ To facilitate start-up, you can create a shortcut icon for Linny-R on your desktop.
228
+
229
+ On a Windows machine, open the _File Explorer_, select your Linny-R folder,
230
+ right-click on the batch file `linny-r.bat`, and select the _Create shortcut_ option.
231
+ Then right-click on the shortcut file to edit its properties, and click the _Change Icon_ button.
232
+ The dialog that then appears will allow you to go to the sub-folder `node_modules\linny-r\static\images`,
233
+ where you should select the file `linny-r.ico`.
234
+ Finally, rename the shortcut to `Linny-R` and move or copy it to your desktop.
235
+
236
+ On a macOS machine, open _Terminal_ and change to your Linny-R directory, and then type:
237
+
238
+ ``chmod +x linny-r.command``
239
+
240
+ to make the script file executable.
241
+ To set the icon, open the folder that contains the file `linny-r.command`,
242
+ click on its icon (which still is plain) and open the _Info dialog_ by pressing ``Cmd+I``.
243
+ Then open your Linny-R folder in _Finder_, change to the sub-folder `node_modules/linny-r/static/images`,
244
+ and from there drag/drop the file `linny-r.icns` on the icon shown in the top left corner of the _Info dialog_.
245
+
246
+ ## User workspace
211
247
 
212
248
  The user workspace is created when the server is run for the first time.
213
249
  The sub-directories of this directory `user` are used by Linny-R to store files.
@@ -225,19 +261,7 @@ You can overrule this by specifying the path to another directory when you start
225
261
  Note that doing this will create a new, empty workspace (the directories listed above)
226
262
  in the specified path. It will **not** affect or duplicate information from existing workspaces.
227
263
 
228
- #### Command line options
229
-
230
- Optionally, you can add more arguments to the `node` command:
231
-
232
- <pre>
233
- dpi=[number] to overrule the default resolution (300 dpi) for Inkscape
234
- launch to automatically launch Linny-R in your default browser
235
- port=[number] to overrule the default port number (5050)
236
- solver=[name] to overrule the default sequence (Gurobi, LP_solve)
237
- workspace=[path] to overrule the default path for the user directory
238
- </pre>
239
-
240
- ### Installing Inkscape
264
+ ## Installing Inkscape
241
265
 
242
266
  Linny-R creates its diagrams and charts as SVG images.
243
267
  When you download a diagram, it will be saved as a .svg file.
@@ -261,41 +285,7 @@ On a macOS computer, Linny-R will look for Inkscape in /Applications/Inkscape.ap
261
285
  **NOTE:** The current installation wizard for Inkscape (version 1.2) does **not** add the application to the PATH variable,
262
286
  so you need to do this yourself.
263
287
 
264
- ### Click-start for Linny-R
265
-
266
- To facilitate start-up, you can create a shortcut icon on your desktop.
267
-
268
- On a Windows machine, change to your Linny-R folder, right-click on the batch file `linny-r.bat`,
269
- and select the _Create shortcut_ option.
270
- Then right-click on the shortcut file to edit its properties, and click the _Change Icon_ button.
271
- The dialog that then appears will allow you to go to the sub-folder `node_modules\linny-r\static\images`,
272
- where you should select the file `linny-r.ico`.
273
- Finally, rename the shortcut to `Linny-R` and move or copy it to your desktop.
274
-
275
- On a macOS machine, open Terminal and change to your Linny-R directory, and then type:
276
-
277
- ``chmod +x linny-r.command``
278
-
279
- to make the script file executable.
280
- To set the icon, click on the icon of `linny-r.command` (which still is plain) and open the Info dialog by pressing ``Cmd+I``.
281
- Then open your Linny-R folder in the Finder, change to the sub-folder `node_modules/linny-r/static/images`,
282
- and from there drag/drop the file `linny-r.icns` on the icon shown in the top left corner of the Info dialog.
283
-
284
-
285
- ### Normal use after installation
286
-
287
- If you have not configured a "click-start" icon as described above,
288
- you must start a modeling session with Linny-R by opening a CLI box,
289
- then change to the Linny-R directory and type `linny-r`.
290
-
291
- To shut down the server, click on the local host icon in the upper right corner of the Linny-R GUI in your browser.
292
- Alternatively, you can stop the server by repeatedly pressing ``Ctrl+C`` in the CLI box.
293
-
294
- Pressing ``Ctrl+C`` in the Terminal window on a macOS machine may not stop the process.
295
- In that case, you can stop Node.js by stopping the Terminal.
296
-
297
-
298
- ### Using Linny-R console
288
+ ## Using Linny-R console
299
289
 
300
290
  The console-only version of Linny-R allows you to run a Linny-R model without a web browser.
301
291
  This may be useful when you want run models from a script (shell script, Python, ...).
@@ -307,17 +297,17 @@ you will see the command line options that allow you to run models in various wa
307
297
 
308
298
  **NOTE: The console-only version is still in development, and does not provide all functions yet.**
309
299
 
310
- ### Troubleshooting problems
300
+ ## Troubleshooting problems
311
301
 
312
302
  If during any of the steps above you encounter problems, please try to diagnose them and resolve them yourself.
313
- You can find a lot of useful information on the Linny-R documentatio website:
303
+ You can find a lot of useful information on the Linny-R user documentation website:
314
304
  <a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
315
305
 
316
306
  To diagnose a problem, always look in the CLI box where Node.js is running,
317
307
  as informative server-side error messages will appear there.
318
308
 
319
309
  Then also look at the console window of your browser.
320
- Most browsers offer a Web Developer Tools option via their application menu.
310
+ Most browsers offer a _Web Developer Tools_ option via their application menu.
321
311
  This will allow you to view the browser console, which will display JavaScript errors in red font.
322
312
 
323
313
  If you've tried hard, but failed, you can try to contact Pieter Bots at ``p.w.g.bots@tudelft.nl``
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -162,12 +162,29 @@ if(SETTINGS.launch) {
162
162
  const cmd = (PLATFORM.startsWith('win') ? 'start' : 'open');
163
163
  child_process.exec(cmd + ' http://127.0.0.1:' + SETTINGS.port,
164
164
  (error, stdout, stderr) => {
165
- console.log('NOTICE: Failed to launch GUI in browser');
166
- console.log(stdout);
167
- console.log(stderr);
165
+ if(error) {
166
+ console.log('NOTICE: Failed to launch GUI in browser');
167
+ console.log(error);
168
+ console.log(stdout);
169
+ console.log(stderr);
170
+ }
168
171
  });
169
172
  }
170
173
 
174
+ // Server action logging functionality
175
+ // ===================================
176
+ // Only actions are logged to the console as with date and time;
177
+ // error messages are not prefixed, so these are logged directly.
178
+
179
+ function logAction(msg) {
180
+ // Log request processing to console with time-zone-aware date and time
181
+ const
182
+ t = new Date(),
183
+ tzt = new Date(t.getTime() - t.getTimezoneOffset()*60000),
184
+ dts = tzt.toISOString().substring(0, 19).replace('T', ' ');
185
+ console.log(`[${dts}] ${msg}`);
186
+ }
187
+
171
188
  // Version check functionality
172
189
  // ===========================
173
190
  // This section of code implements server responses to the request made
@@ -186,7 +203,21 @@ function autoCheck(res) {
186
203
  }
187
204
 
188
205
  // HTML page to show then the server is shut down by the user
189
- const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
206
+ // NOTE: on a macOS machine, this is slightly more work
207
+ const
208
+ OS_TEXT = (PLATFORM === 'darwin' ? [
209
+ `<p>You can close the <em>Terminal</em> window that shows
210
+ <tt>[Process Terminated]</tt> at the bottom.
211
+ </p>`,
212
+ `open <em>Terminal</em> again, change to your Linny-R directory by typing:
213
+ </p>
214
+ <p><code>cd ${WORKING_DIRECTORY}</code></p>
215
+ <p>`
216
+ ] : [
217
+ '',
218
+ 'switch to your <em>Command Prompt</em> window '
219
+ ]),
220
+ SHUTDOWN_MESSAGE = `<!DOCTYPE html>
190
221
  <html lang="en-US">
191
222
  <head>
192
223
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
@@ -206,15 +237,14 @@ const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
206
237
  </style>
207
238
  </head>
208
239
  <body>
209
- <h3>Linny-R server (127.0.0.1) is shutting down</h3>
210
- <p>To restart Linny-R, switch to your <em>${SETTINGS.cli_name}</em> window
211
- and there at the prompt` +
240
+ <h3>Linny-R server (127.0.0.1) is shutting down</h3>` + OS_TEXT[0] + `
241
+ <p>To restart Linny-R, ` + OS_TEXT[1] + ` and then at the prompt` +
212
242
  (VERSION_INFO.up_to_date ? '' : `
213
243
  first type:</p>
214
244
  <p><code>npm update linny-r</code><p>
215
245
  to upgrade to Linny-R version ${VERSION_INFO.latest}, and then`) +
216
246
  ` type:</p>
217
- <p><code>node node_modules\\linny-r\\server</code></p>
247
+ <p><code>node node_modules${path.sep}linny-r${path.sep}server</code></p>
218
248
  <p>
219
249
  Then switch back to this window, and click this
220
250
  <button type="button"
@@ -225,10 +255,145 @@ const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
225
255
  </body>
226
256
  </html>`;
227
257
 
258
+ // Auto-save & restore model functionality
259
+ // =======================================
260
+ // For auto-save services, the Linny-R JavaScript application communicates with
261
+ // the server via calls to the server like fetch('autosave/', x) where x is a JSON
262
+ // object with at least the entry `action`, which can be one of the following:
263
+ // purge remove all model files older than the set auto-save period
264
+ // store write the property x.xml to the file with name x.name
265
+ // load return the XML contents of the specified model file
266
+ // Each action returns a JSON string that represents the actualized auto-save
267
+ // settings (interval and perdiod) and list of auto-saved model data objects.
268
+ // For each model: {name, file_name, size, time_saved}
269
+
270
+ function asFileName(s) {
271
+ // Returns string `s` in lower case with whitespace converted to a single
272
+ // dash, special characters converted to underscores, and leading and
273
+ // trailing dashes and underscores removed
274
+ return s.normalize('NFKD').trim()
275
+ .replace(/[\s\-]+/g, '-')
276
+ .replace(/[^A-Za-z0-9_\-]/g, '_')
277
+ .replace(/^[\-\_]+|[\-\_]+$/g, '');
278
+ }
279
+
280
+ function autoSave(res, sp) {
281
+ // Processes all auto-save & restore commands
282
+ const action = sp.get('action').trim();
283
+ logAction('Auto-save action: ' + action);
284
+ if(['purge', 'load', 'store'].indexOf(action) < 0) {
285
+ // Invalid action => report error
286
+ return servePlainText(res, `ERROR: Invalid auto-save action: "${action}"`);
287
+ }
288
+ // Always purge the auto-save files before further action; this returns
289
+ // the list with model data objects
290
+ const data = autoSavePurge(res, sp);
291
+ // NOTE: if string instead of array, this string is an error message
292
+ if(typeof data === 'string') return servePlainText(res, data);
293
+ // Perform load or store actions if requested
294
+ if(action === 'load') return autoSaveLoad(res, sp);
295
+ if(action === 'store') return autoSaveStore(res, sp);
296
+ // Otherwise, action was 'purge' => return the auto-saved model list
297
+ serveJSON(res, data);
298
+ }
299
+
300
+ function autoSavePurge(res, sp) {
301
+ // Deletes specified file(s) (if any) as well as all expired files,
302
+ // and returns list with data on remaining files as JSON string
303
+ const
304
+ now = new Date(),
305
+ p = sp.get('period'),
306
+ period = (p ? parseInt(p) : 24) * 3600000,
307
+ df = sp.get('to_delete'),
308
+ all = df === '/*ALL*/';
309
+
310
+ // Get list of data on Linny-R models in `autosave` directory
311
+ data = [];
312
+ try {
313
+ const flist = fs.readdirSync(WORKSPACE.autosave);
314
+ for(let i = 0; i < flist.length; i++) {
315
+ const
316
+ pp = path.parse(flist[i]),
317
+ md = {name: pp.name},
318
+ fp = path.join(WORKSPACE.autosave, flist[i]);
319
+ // NOTE: only consider Linny-R model files (extension .lnr)
320
+ if(pp.ext === '.lnr') {
321
+ let dodel = all || pp.name === df;
322
+ if(!dodel) {
323
+ // Get file properties
324
+ const fstat = fs.statSync(fp);
325
+ md.size = fstat.size;
326
+ md.date = fstat.mtime;
327
+ // Also delete if file has expired
328
+ dodel = now - fstat.mtimeMs > period;
329
+ }
330
+ if(dodel) {
331
+ // Delete model file
332
+ try {
333
+ fs.unlinkSync(fp);
334
+ } catch(err) {
335
+ console.log('WARNING: Failed to delete', fp);
336
+ console.log(err);
337
+ }
338
+ } else {
339
+ // Add model data to the list
340
+ data.push(md);
341
+ }
342
+ }
343
+ }
344
+ } catch(err) {
345
+ console.log(err);
346
+ return 'ERROR: Auto-save failed -- ' + err.message;
347
+ }
348
+ return data;
349
+ }
350
+
351
+ function autoSaveLoad(res, sp) {
352
+ // Return XML content of specified file
353
+ const fn = sp.get('file');
354
+ if(fn) {
355
+ const fp = path.join(WORKSPACE.autosave, fn + '.lnr');
356
+ try {
357
+ data = fs.readFileSync(fp, 'utf8');
358
+ } catch(err) {
359
+ console.log(err);
360
+ data = 'WARNING: Failed to load auto-saved file: ' + err.message;
361
+ }
362
+ } else {
363
+ data = 'ERROR: No auto-saved file name';
364
+ }
365
+ servePlainText(res, data);
366
+ }
367
+
368
+ function autoSaveStore(res, sp) {
369
+ // Stores XML data under specified file name in the auto-save directory
370
+ let data = 'OK';
371
+ const fn = sp.get('file');
372
+ if(!fn) {
373
+ data = 'WARNING: No name for file to auto-save';
374
+ } else {
375
+ const xml = sp.get('xml');
376
+ // Validate XML as a Linny-R model
377
+ try {
378
+ const
379
+ parser = new DOMParser(),
380
+ doc = parser.parseFromString(xml, 'text/xml');
381
+ root = doc.documentElement;
382
+ // Linny-R models have a model element as root
383
+ if(root.nodeName !== 'model') throw 'XML document has no model element';
384
+ fs.writeFileSync(path.join(WORKSPACE.autosave, fn + '.lnr'), xml);
385
+ } catch(err) {
386
+ console.log(err);
387
+ data = 'ERROR: Not a Linny-R model to auto-save';
388
+ }
389
+ }
390
+ servePlainText(res, data);
391
+ }
392
+
228
393
  // Repository functionality
229
394
  // ========================
230
395
  // For repository services, the Linny-R JavaScript application communicates with
231
- // the server via calls to the server like $.post('repo', x) where x is a JSON
396
+ // the server via calls to the server like fetch('repo/', x) where x is a JSON
232
397
  // object with at least the entry `action`, which can be one of the following:
233
398
  // id return the repository URL (for this script: 'local host')
234
399
  // list return list with names of repositories available on the server
@@ -243,7 +408,7 @@ const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
243
408
  function repo(res, sp) {
244
409
  // Processes all repository commands
245
410
  const action = sp.get('action').trim();
246
- console.log('Repository action:', action);
411
+ logAction('Repository action: ' + action);
247
412
  if(action === 'id') return repoID(res);
248
413
  if(action === 'list') return repoList(res);
249
414
  if(action === 'add') return repoAdd(res, sp);
@@ -257,14 +422,7 @@ function repo(res, sp) {
257
422
  if(action === 'store') return repoStore(res, repo, file, sp.get('xml'));
258
423
  if(action === 'delete') return repoDelete(res, repo, file);
259
424
  // Fall-through: report error
260
- servePlainText(res, `ERROR: Invalid action: "${action}"`);
261
- }
262
-
263
- function asFileName(s) {
264
- // Returns string `s` with whitespace converted to a single dash, and special
265
- // characters converted to underscores
266
- s = s.trim().replace(/[\s\-]+/g, '-');
267
- return s.replace(/[^A-Za-z0-9_\-]/g, '_');
425
+ servePlainText(res, `ERROR: Invalid repository action: "${action}"`);
268
426
  }
269
427
 
270
428
  function repositoryByName(name) {
@@ -607,7 +765,7 @@ function repoStore(res, rname, mname, mxml) {
607
765
  parser = new DOMParser(),
608
766
  doc = parser.parseFromString(mxml, 'text/xml');
609
767
  root = doc.documentElement;
610
- // Linny-R model have a model element as root
768
+ // Linny-R models have a model element as root
611
769
  if(root.nodeName !== 'model') throw 'XML document has no model element';
612
770
  valid = true;
613
771
  } catch(err) {
@@ -701,7 +859,7 @@ function anyOSpath(p) {
701
859
 
702
860
  function loadData(res, url) {
703
861
  // Passed parameter is the URL or full path
704
- console.log('Load data from', url);
862
+ logAction('Load data from ' + url);
705
863
  if(!url) servePlainText(res, 'ERROR: No URL or path');
706
864
  if(url.toLowerCase().startsWith('http')) {
707
865
  // URL => validate it, and then try to download its content as text
@@ -761,7 +919,7 @@ function receiver(res, sp) {
761
919
  }
762
920
  // Get the action from the search parameters
763
921
  const action = sp.get('action');
764
- console.log('Receiver action:', action, rpath, rfile);
922
+ logAction(`Receiver action: ${action} ${rpath} ${rfile}`);
765
923
  if(action === 'listen') {
766
924
  rcvrListen(res, rpath);
767
925
  } else if(action === 'abort') {
@@ -914,7 +1072,7 @@ function rcvrCallBack(res, rpath, rfile, script) {
914
1072
  }
915
1073
  }
916
1074
  if(cpath) {
917
- console.log('Deleting', file_type, ' file:', cpath);
1075
+ logAction(`Deleting ${file_type} file: ${cpath}`);
918
1076
  try {
919
1077
  fs.unlinkSync(cpath);
920
1078
  } catch(err) {
@@ -930,7 +1088,7 @@ function rcvrCallBack(res, rpath, rfile, script) {
930
1088
  }
931
1089
  try {
932
1090
  cmd = fs.readFileSync(path.join(WORKSPACE.callback, script), 'utf8');
933
- console.log(`Executing callback command "${cmd}"`);
1091
+ logAction(`Executing callback command "${cmd}"`);
934
1092
  child_process.exec(cmd, (error, stdout, stderr) => {
935
1093
  console.log(stdout);
936
1094
  if(error) {
@@ -1025,6 +1183,8 @@ function processRequest(req, res, cmd, data) {
1025
1183
  SERVER.close();
1026
1184
  } else if(cmd === '/auto-check') {
1027
1185
  autoCheck(res);
1186
+ } else if(cmd === '/autosave/') {
1187
+ autoSave(res, new URLSearchParams(data));
1028
1188
  } else if(cmd === '/repo/') {
1029
1189
  repo(res, new URLSearchParams(data));
1030
1190
  } else if(cmd === '/load-data/') {
@@ -1075,11 +1235,11 @@ function serveStaticFile(res, path) {
1075
1235
  if(path === '/' || path === '') path = '/index.html';
1076
1236
  if(path.startsWith('/diagrams/')) {
1077
1237
  // Serve diagrams from the (main)/user/diagrams/ sub-directory
1078
- console.log('Diagram:', path);
1238
+ logAction('Diagram: ' + path);
1079
1239
  path = '/user' + path;
1080
1240
  } else {
1081
1241
  // Other files from the (main)/static/ subdirectory
1082
- console.log('Static file:', path);
1242
+ logAction('Static file: ' + path);
1083
1243
  path = '/static' + path;
1084
1244
  }
1085
1245
  fs.readFile(MODULE_DIRECTORY + path, (err, data) => {
@@ -1107,7 +1267,7 @@ function convertSVGtoPNG(req, res, sp) {
1107
1267
  (new Date()).toISOString().slice(0, 19).replace(/[\-\:]/g, ''),
1108
1268
  fp = path.join(WORKSPACE.diagrams, fn);
1109
1269
  // NOTE: use binary encoding for SVG file
1110
- console.log('Saving SVG file:', fp);
1270
+ logAction('Saving SVG file: ' + fp);
1111
1271
  try {
1112
1272
  fs.writeFileSync(fp + '.svg', svg);
1113
1273
  } catch(error) {
@@ -1115,7 +1275,7 @@ function convertSVGtoPNG(req, res, sp) {
1115
1275
  }
1116
1276
  // Use Inkscape to convert SVG to the requested format
1117
1277
  if(SETTINGS.inkscape) {
1118
- console.log('Rendering image');
1278
+ logAction('Rendering image');
1119
1279
  let
1120
1280
  cmd = SETTINGS.inkscape,
1121
1281
  svg = fp + '.svg';
package/static/index.html CHANGED
@@ -176,6 +176,9 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
176
176
  // Invalid server response (should not occur, but just in case)
177
177
  UI.warn('Version check failed: "' + data + '"');
178
178
  }
179
+ // Schedule a new check 8 hours from now
180
+ setTimeout(checkForUpdates, 8*3600000);
181
+
179
182
  })
180
183
  .catch((error) => UI.warn(UI.WARNING.NO_CONNECTION, error));
181
184
  }
@@ -206,8 +209,8 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
206
209
  MONITOR = new GUIMonitor();
207
210
  RECEIVER = new GUIReceiver();
208
211
  // Check for software updates only when running on local server
209
- // NOTE: do this *after* GUI elements have been created, as the updater
210
- // uses a dialog
212
+ // NOTE: do this *after* GUI elements have been created, as the
213
+ // updater uses a dialog
211
214
  if(!SOLVER.user_id) checkForUpdates();
212
215
  // Initialize auto-saving function
213
216
  AUTO_SAVE = new ModelAutoSaver();
@@ -470,10 +473,10 @@ and move the cursor over the status bar">
470
473
  <input id="auto-save-hours" type="text" autocomplete="off">
471
474
  hours
472
475
  <img id="auto-save-clear-btn" class="btn enab" src="images/reset.png"
473
- title="Remove all auto-saved models from local storage">
476
+ title="Remove all auto-saved models from user workspace">
474
477
  </div>
475
478
  <div id="confirm-remove-models">
476
- Really remove all auto-saved from local storage?
479
+ Really remove all auto-saved models from user workspace?
477
480
  <strong>
478
481
  <img id="autosave-do-remove" class="inline-ok-btn"
479
482
  src="images/ok.png"> Yes
@@ -1064,7 +1067,7 @@ and move the cursor over the status bar">
1064
1067
  &mu; (mean: the average level &Sigma;/(&delta;+1) )
1065
1068
  </option>
1066
1069
  <option id="link-startup" value="5">
1067
- &#x2B9D; (start-up: 1 if X[t-1] = 0 &and; X[t] > 0, otherwise 0)
1070
+ &#x25B2; (start-up: 1 if X[t-1] = 0 &and; X[t] > 0, otherwise 0)
1068
1071
  </option>
1069
1072
  <option id="link-on" value="6">
1070
1073
  &plus; (positive: 1 if X[t] > 0, otherwise 0)
@@ -1073,7 +1076,7 @@ and move the cursor over the status bar">
1073
1076
  0 (zero: 1 if X[t] = 0, otherwise 0)
1074
1077
  </option>
1075
1078
  <option id="link-shutdown" value="10">
1076
- &#x2B9F; (shut-down: 1 if X[t-1] > 0 &and; X[t] = 0, otherwise 0)
1079
+ &#x25BC; (shut-down: 1 if X[t-1] > 0 &and; X[t] = 0, otherwise 0)
1077
1080
  </option>
1078
1081
  <option id="link-spinning" value="8">
1079
1082
  &#x2934; (spinning reserve: UB - X[t] if X[t] > 0, otherwise 0)
@@ -2422,12 +2425,12 @@ where X can be one or several of these letters: ABCDELPQ">
2422
2425
  <div class="docu-sym" id="docu-link">&#x2192;</div>
2423
2426
  <div class="docu-sym" id="docu-constraint">&#x2911;</div>
2424
2427
  <div class="docu-sym" id="docu-bi-constraint">&#x2194;</div>
2425
- <div class="docu-sym" id="docu-throughput">&#x2B86;</div>
2428
+ <div class="docu-sym" id="docu-throughput">&#x21C9;</div>
2426
2429
  <div class="docu-sym" id="docu-change">&#x0394;</div>
2427
2430
  <div class="docu-sym" id="docu-sum">&#x3A3;</div>
2428
2431
  <div class="docu-sym" id="docu-mean">&#x03BC;</div>
2429
- <div class="docu-sym" id="docu-startup">&#x2B9D;</div>
2430
- <div class="docu-sym" id="docu-shutdown">&#x2B9F;</div>
2432
+ <div class="docu-sym" id="docu-startup">&#x25B2;</div>
2433
+ <div class="docu-sym" id="docu-shutdown">&#x25BC;</div>
2431
2434
  <div class="docu-sym" id="docu-spinning-reserve">&#x2934;</div>
2432
2435
  <div class="docu-sym" id="docu-first-commit">&#x2732;</div>
2433
2436
  <div class="docu-sym" id="docu-infinity">&#x221E;</div>
@@ -37,6 +37,8 @@ body {
37
37
 
38
38
  body.waiting * { cursor: wait; }
39
39
 
40
+ @charset UTF-8;
41
+
40
42
  @font-face {
41
43
  font-family: Lato;
42
44
  src: url(fonts/Lato-Regular.ttf);
@@ -705,7 +707,7 @@ img.del-asm-btn:hover {
705
707
 
706
708
  #auto-save-settings {
707
709
  position: absolute;
708
- bottom: 2px;
710
+ bottom: 3px;
709
711
  left: 2px;
710
712
  width: calc(100% - 4px);
711
713
  }
@@ -3804,6 +3806,11 @@ select.i-param {
3804
3806
  font-family: monospace;
3805
3807
  }
3806
3808
 
3809
+ #confirm-delete-from-repo-msg {
3810
+ height: calc(100% - 55px);
3811
+ }
3812
+
3813
+
3807
3814
  /* the FINDER DIALOG allows lookup of occurrences of entities */
3808
3815
  #finder-dlg {
3809
3816
  display: none;
@@ -4326,7 +4333,6 @@ div.call-stack-expr {
4326
4333
  #confirm-delete-from-repo-msg,
4327
4334
  #check-update-msg {
4328
4335
  width: calc(100% - 8px);
4329
- height: calc(100% - 55px);
4330
4336
  margin: 3px;
4331
4337
  overflow: auto;
4332
4338
  }
@@ -277,8 +277,9 @@ class Controller {
277
277
  // Returns `name` without the object-attribute separator |, backslashes,
278
278
  // and leading and trailing whitespace, and with all internal whitespace
279
279
  // reduced to a single space.
280
- return name.replace(this.OA_SEPARATOR, ' ').replace(/\||\\/g, ' '
281
- ).trim().replace(/\s\s+/g, ' ');
280
+ return name.replace(this.OA_SEPARATOR, ' ')
281
+ .replace(/\||\\/g, ' ').trim()
282
+ .replace(/\s\s+/g, ' ');
282
283
  }
283
284
 
284
285
  validName(name) {
@@ -519,8 +520,10 @@ class RepositoryBrowser {
519
520
  asFileName(s) {
520
521
  // Returns string `s` with whitespace converted to a single dash, and
521
522
  // special characters converted to underscores
522
- return s.normalize('NFKD').trim().replace(/[\s\-]+/g, '-'
523
- ).replace(/[^A-Za-z0-9_\-]/g, '_').trim('-_');
523
+ return s.normalize('NFKD').trim()
524
+ .replace(/[\s\-]+/g, '-')
525
+ .replace(/[^A-Za-z0-9_\-]/g, '_')
526
+ .replace(/^[\-\_]+|[\-\_]+$/g, '');
524
527
  }
525
528
 
526
529
  loadModuleAsModel() {
@@ -3054,9 +3054,10 @@ class GUIController extends Controller {
3054
3054
  // Recall button toggles the documentation dialog
3055
3055
  () => UI.buttons.documentation.dispatchEvent(new Event('click')));
3056
3056
  this.buttons.autosave.addEventListener('click',
3057
- () => AUTO_SAVE.showRestoreDialog());
3057
+ // NOTE: TRUE indicates "show dialog after obtaining the model list"
3058
+ () => AUTO_SAVE.getAutoSavedModels(true));
3058
3059
  this.buttons.autosave.addEventListener('mouseover',
3059
- () => AUTO_SAVE.checkForSavedModels());
3060
+ () => AUTO_SAVE.getAutoSavedModels());
3060
3061
 
3061
3062
  // Make "stay active" buttons respond to Shift-click
3062
3063
  const
@@ -3282,7 +3283,7 @@ class GUIController extends Controller {
3282
3283
  // Undoable operations no longer apply!
3283
3284
  UNDO_STACK.clear();
3284
3285
  // Autosaving should start anew
3285
- AUTO_SAVE.setInterval();
3286
+ AUTO_SAVE.setAutoSaveInterval();
3286
3287
  // Signal success or failure
3287
3288
  return loaded;
3288
3289
  }
@@ -4732,7 +4733,9 @@ class GUIController extends Controller {
4732
4733
  // Update global variable (and force display) only for "real" messages
4733
4734
  this.time_last_message = t;
4734
4735
  dt = this.message_display_time;
4735
- SOUNDS[type].play();
4736
+ SOUNDS[type].play().catch(() => {
4737
+ console.log('NOTICE: Sounds will only play after first user action');
4738
+ });
4736
4739
  const
4737
4740
  now = [d.getHours(), d.getMinutes().toString().padStart(2, '0'),
4738
4741
  d.getSeconds().toString().padStart(2, '0')].join(':'),
@@ -4823,7 +4826,7 @@ class GUIController extends Controller {
4823
4826
  UNDO_STACK.clear();
4824
4827
  VM.reset();
4825
4828
  this.updateButtons();
4826
- AUTO_SAVE.setInterval();
4829
+ AUTO_SAVE.setAutoSaveInterval();
4827
4830
  }
4828
4831
 
4829
4832
  addNode(type) {
@@ -6059,8 +6062,8 @@ class GUIMonitor {
6059
6062
 
6060
6063
 
6061
6064
  // CLASS GUIFileManager provides the GUI for loading and saving models and
6062
- // diagrams and handles
6063
- // the interaction with the MILP solver via POST requests to the server.
6065
+ // diagrams and handles the interaction with the MILP solver via POST requests
6066
+ // to the server.
6064
6067
  // NOTE: because the console-only monitor requires Node.js modules, this
6065
6068
  // GUI class does NOT extend its console-only counterpart
6066
6069
  class GUIFileManager {
@@ -6184,15 +6187,10 @@ class GUIFileManager {
6184
6187
  // Show "Load model" modal
6185
6188
  // @@TO DO: warn user if unsaved changes to current model
6186
6189
  UI.hideStayOnTopDialogs();
6187
- const
6188
- rbtn = document.getElementById('load-autosaved-btn'),
6189
- ml = AUTO_SAVE.savedModelList();
6190
- if(ml.length > 0) {
6191
- rbtn.title = pluralS(ml.length, 'auto-saved model');
6192
- rbtn.style.display = 'block';
6193
- } else {
6194
- rbtn.style.display = 'none';
6195
- }
6190
+ // Update auto-saved model list; if not empty, this will display the
6191
+ // "restore autosaved files" button
6192
+ AUTO_SAVE.getAutoSavedModels();
6193
+ // Show the "Load model" dialog
6196
6194
  UI.modals.load.show();
6197
6195
  }
6198
6196
 
@@ -6282,7 +6280,7 @@ class GUIFileManager {
6282
6280
  console.log('Encoded file size:', el.href.length);
6283
6281
  el.download = 'model.lnr';
6284
6282
  if(el.href.length > 25*1024*1024 &&
6285
- navigator.userAgent.search('Chrome' ) <= 0) {
6283
+ navigator.userAgent.search('Chrome') <= 0) {
6286
6284
  UI.notify('Model file size exceeds 25 MB. ' +
6287
6285
  'If it does not download, store it in a repository');
6288
6286
  }
@@ -6317,47 +6315,55 @@ class GUIFileManager {
6317
6315
  console.log(err);
6318
6316
  });
6319
6317
  }
6320
-
6321
- saveToLocalStorage() {
6322
- // Store model identified by an identifier based on author name and
6323
- // model name
6324
- // (1) autosave is skipped while experiment is running, as it may interfere
6325
- // with storing run results
6326
- // (2) action will overwrite earlier auto-saved version of this model
6327
- // unless its name and/or author have been changed in the meantime
6328
- // (3) browser may be configured to prohibit local storage function,
6329
- // and local storage space is limited (by browser settings)
6318
+
6319
+ loadAutoSavedModel(name) {
6320
+ fetch('autosave/', postData({
6321
+ action: 'load',
6322
+ file: name
6323
+ }))
6324
+ .then((response) => {
6325
+ if(!response.ok) {
6326
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
6327
+ }
6328
+ return response.text();
6329
+ })
6330
+ .then((data) => {
6331
+ if(UI.postResponseOK(data)) UI.loadModelFromXML(data);
6332
+ })
6333
+ .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
6334
+ }
6335
+
6336
+ storeAutoSavedModel() {
6337
+ // Stores the current model in the local auto-save directory
6338
+ const bcl = document.getElementById('autosave-btn').classList;
6330
6339
  if(MODEL.running_experiment) {
6331
6340
  console.log('No autosaving while running an experiment');
6341
+ bcl.remove('stay-activ');
6332
6342
  return;
6333
6343
  }
6334
- try {
6335
- // Store model XML string using its display name as key
6336
- const n = MODEL.displayName;
6337
- console.log('Autosaving', n);
6338
- window.localStorage.setItem(n, MODEL.asXML);
6339
- // Also store the timestamp for this operation
6340
- window.localStorage.setItem(AUTO_SAVE.time_prefix + n, Date.now());
6341
- // Remove the highlighting of the icon on the status bar
6342
- document.getElementById('autosave-btn').classList.remove('stay-activ');
6343
- } catch(err) {
6344
- UI.alert(`Failed to auto-save model: ${err}`);
6345
- }
6344
+ fetch('autosave/', postData({
6345
+ action: 'store',
6346
+ file: REPOSITORY_BROWSER.asFileName(
6347
+ (MODEL.name || 'no-name') + '_by_' +
6348
+ (MODEL.author || 'no-author')),
6349
+ xml: MODEL.asXML
6350
+ }))
6351
+ .then((response) => {
6352
+ if(!response.ok) {
6353
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
6354
+ }
6355
+ return response.text();
6356
+ })
6357
+ .then((data) => {
6358
+ UI.postResponseOK(data);
6359
+ bcl.remove('stay-activ');
6360
+ })
6361
+ .catch((err) => {
6362
+ UI.warn(UI.WARNING.NO_CONNECTION, err);
6363
+ bcl.remove('stay-activ');
6364
+ });
6346
6365
  }
6347
6366
 
6348
- loadFromLocalStorage(key) {
6349
- // Retrieve auto-saved model identified by `key`
6350
- // NOTE: browser may be configured to prohibit local storage function
6351
- try {
6352
- const
6353
- ls = window.localStorage,
6354
- xml = ls.getItem(key);
6355
- if(xml) UI.loadModelFromXML(xml);
6356
- } catch(err) {
6357
- UI.alert(`Failed to restore auto-saved model ${key}: ${err}`);
6358
- }
6359
- }
6360
-
6361
6367
  renderDiagramAsPNG() {
6362
6368
  localStorage.removeItem('png-url');
6363
6369
  UI.paper.fitToSize();
@@ -6811,27 +6817,28 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
6811
6817
  } // END of class ExpressionEditor
6812
6818
 
6813
6819
 
6814
- // CLASS ModelAutoSaver automatically saves the current model at regular time
6815
- // intervals
6816
- // NOTE: it seemed to be a good idea to do this in the browser's local storage,
6817
- // but this breaks for large model files
6818
- // @@TO DO: re-implement via POST requests to server
6820
+ // CLASS ModelAutoSaver automatically saves the current model at regular
6821
+ // time intervals in the user's `autosave` directory
6819
6822
  class ModelAutoSaver {
6820
6823
  constructor() {
6821
6824
  // Keep track of time-out interval of auto-saving feature
6822
6825
  this.timeout_id = 0;
6823
- this.time_prefix = '_A_S_T_$';
6824
6826
  this.interval = 10; // auto-save every 10 minutes
6825
6827
  this.period = 24; // delete models older than 24 hours
6828
+ this.model_list = [];
6826
6829
  // Overwite defaults if settings still in local storage of browser
6827
- this.purgeSavedModels();
6828
- this.setInterval();
6830
+ this.getSettings();
6831
+ // Purge files that have "expired"
6832
+ this.getAutoSavedModels();
6833
+ // Start the interval timer
6834
+ this.setAutoSaveInterval();
6829
6835
  // Add listeners to GUI elements
6830
6836
  this.confirm_dialog = document.getElementById('confirm-remove-models');
6831
6837
  document.getElementById('auto-save-clear-btn').addEventListener('click',
6832
6838
  () => AUTO_SAVE.confirm_dialog.style.display = 'block');
6833
6839
  document.getElementById('autosave-do-remove').addEventListener('click',
6834
- () => AUTO_SAVE.removeSavedModels());
6840
+ // NOTE: file name parameter /*ALL*/ indicates: delete all
6841
+ () => AUTO_SAVE.getAutoSavedModels(true, '/*ALL*/'));
6835
6842
  document.getElementById('autosave-cancel').addEventListener('click',
6836
6843
  () => AUTO_SAVE.confirm_dialog.style.display = 'none');
6837
6844
  document.getElementById('restore-cancel').addEventListener('click',
@@ -6850,14 +6857,14 @@ class ModelAutoSaver {
6850
6857
  m = parseFloat(mh[0]),
6851
6858
  h = parseFloat(mh[1]);
6852
6859
  if(isNaN(m) || isNaN(h)) {
6853
- UI.warn('Invalid local auto-save settings');
6860
+ UI.warn('Ignored invalid local auto-save settings');
6854
6861
  } else {
6855
6862
  this.interval = m;
6856
6863
  this.period = h;
6857
6864
  }
6858
6865
  }
6859
6866
  } catch(err) {
6860
- console.log('No auto-save:', err);
6867
+ console.log('Local storage failed:', err);
6861
6868
  }
6862
6869
  }
6863
6870
 
@@ -6866,8 +6873,10 @@ class ModelAutoSaver {
6866
6873
  try {
6867
6874
  window.localStorage.setItem('Linny-R-autosave',
6868
6875
  this.interval + '|' + this.period);
6876
+ UI.notify('New auto-save settings stored in browser');
6869
6877
  } catch(err) {
6870
6878
  UI.warn('Failed to write auto-save settings to local storage');
6879
+ console.log(err);
6871
6880
  }
6872
6881
  }
6873
6882
 
@@ -6875,128 +6884,80 @@ class ModelAutoSaver {
6875
6884
  document.getElementById('autosave-btn').classList.add('stay-activ');
6876
6885
  // Use setTimeout to let browser always briefly show the active color
6877
6886
  // even when the model file is small and storing hardly takes time
6878
- setTimeout(() => FILE_MANAGER.saveToLocalStorage(), 300);
6887
+ setTimeout(() => FILE_MANAGER.storeAutoSavedModel(), 300);
6879
6888
  }
6880
6889
 
6881
- setInterval() {
6890
+ setAutoSaveInterval() {
6882
6891
  // Activate the auto-save feature (if interval is configured)
6883
6892
  if(this.timeout_id) clearInterval(this.timeout_id);
6884
- this.getSettings();
6893
+ // NOTE: interval = 0 indicates "do not auto-save"
6885
6894
  if(this.interval) {
6886
6895
  // Interval is in minutes, so multiply by 60 thousand to get msec
6887
6896
  this.timeout_id = setInterval(
6888
6897
  () => AUTO_SAVE.saveModel(), this.interval * 60000);
6889
6898
  }
6890
6899
  }
6891
-
6892
- purgeSavedModels() {
6893
- // Remove all autosaved models that have been stored beyond the set period
6894
- try {
6895
- for(let key in window.localStorage) {
6896
- if(key.startsWith(this.time_prefix)) {
6897
- const
6898
- name = key.split(this.time_prefix)[1],
6899
- ts = parseInt(window.localStorage.getItem(key)),
6900
- now = Date.now();
6901
- if((now - ts) / 3600000 > this.period) {
6902
- window.localStorage.removeItem(name);
6903
- console.log('Purged model', name, 'from local storage');
6904
- // Also remove the timestamp item
6905
- window.localStorage.removeItem(key);
6900
+
6901
+ getAutoSavedModels(show_dialog=false, file_to_delete='') {
6902
+ // Get list of auto-saved models from server (after deleting those that
6903
+ // have been stored beyond the set period AND the specified file to
6904
+ // delete (where /*ALL*/ indicates "delete all auto-saved files")
6905
+ const pd = {action: 'purge', period: this.period};
6906
+ if(file_to_delete) pd.to_delete = file_to_delete;
6907
+ fetch('autosave/', postData(pd))
6908
+ .then((response) => {
6909
+ if(!response.ok) {
6910
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
6906
6911
  }
6907
- }
6908
- }
6909
- } catch(err) {
6910
- console.log('No auto-save:', err);
6911
- }
6912
- }
6913
-
6914
- savedModelList() {
6915
- // Returns autosaved models as array of tuples [model name, time, file size]
6916
- // First purge outdated auto-saved models
6917
- this.purgeSavedModels();
6918
- const list = [];
6919
- try {
6920
- for(let key in window.localStorage) {
6921
- if(key.startsWith(this.time_prefix)) {
6922
- const
6923
- name = key.split(this.time_prefix)[1],
6924
- ts = parseInt(window.localStorage.getItem(key)),
6925
- // Retrieve the item to make sure it exists
6926
- xml = window.localStorage.getItem(name);
6927
- if(xml && xml.length > 100) {
6928
- let mdate = new Date();
6929
- mdate.setTime(ts);
6930
- const offset = mdate.getTimezoneOffset();
6931
- mdate = new Date(mdate.getTime() - (offset * 60 * 1000));
6932
- mdate = mdate.toISOString().split(':');
6933
- mdate = mdate[0].replace('T', ' ') + ':' + mdate[1];
6934
- list.push([name, mdate, UI.sizeInBytes(xml.length)]);
6935
- } else {
6936
- console.log('Autosaved model not found or invalid:', xml);
6912
+ return response.text();
6913
+ })
6914
+ .then((data) => {
6915
+ if(UI.postResponseOK(data)) {
6916
+ try {
6917
+ AUTO_SAVE.model_list = JSON.parse(data);
6918
+ } catch(err) {
6919
+ AUTO_SAVE.model_list = [];
6920
+ UI.warn('Data on auto-saved models is not valid');
6921
+ }
6937
6922
  }
6938
- }
6939
- }
6940
- // NOTE: sort models in reverse time order (most recent on top)
6941
- list.sort((a, b) => {
6942
- if(a[1] > b[1]) return -1;
6943
- if(a[1] < b[1]) return 1;
6944
- return 0;
6945
- });
6946
- } catch(err) {
6947
- console.log('No auto-save:', err);
6948
- }
6949
- return list;
6950
- }
6951
-
6952
- checkForSavedModels() {
6953
- const ml = this.savedModelList();
6954
- document.getElementById('autosave-btn').title =
6955
- pluralS(ml.length, 'auto-saved model');
6956
- }
6957
-
6958
- removeSavedModels() {
6959
- const ml = this.savedModelList();
6960
- for(let i = 0; i < ml.length; i++) {
6961
- const n = ml[i][0];
6962
- window.localStorage.removeItem(n);
6963
- window.localStorage.removeItem(this.time_prefix + n);
6964
- }
6965
- this.hideRestoreDialog(true);
6966
- }
6967
-
6968
- deleteSavedModel(n) {
6969
- window.localStorage.removeItem(n);
6970
- window.localStorage.removeItem(this.time_prefix + n);
6971
- const ml = this.savedModelList();
6972
- if(ml.length > 0) {
6973
- this.showRestoreDialog();
6974
- } else {
6975
- this.hideRestoreDialog(true);
6976
- }
6923
+ // Update auto-save-related dialog elements
6924
+ const
6925
+ n = this.model_list.length,
6926
+ ttl = pluralS(n, 'auto-saved model'),
6927
+ rbtn = document.getElementById('load-autosaved-btn');
6928
+ document.getElementById('autosave-btn').title = ttl;
6929
+ rbtn.title = ttl;
6930
+ rbtn.style.display = (n > 0 ? 'block' : 'none');
6931
+ if(show_dialog) AUTO_SAVE.showRestoreDialog();
6932
+ })
6933
+ .catch((err) => {console.log(err); UI.warn(UI.WARNING.NO_CONNECTION, err);});
6977
6934
  }
6978
-
6935
+
6979
6936
  showRestoreDialog() {
6980
6937
  // Shows list of auto-saved models; clicking on one will load it
6981
- const ml = this.savedModelList();
6938
+ // NOTE: hide "Load model" dialog in case it was showing
6982
6939
  document.getElementById('load-modal').style.display = 'none';
6983
6940
  // Contruct the table to select from
6984
6941
  let html = '';
6985
- for(let i = 0; i < ml.length; i++) {
6986
- const bytes = ml[i][2].split(' ');
6942
+ for(let i = 0; i < this.model_list.length; i++) {
6943
+ const
6944
+ m = this.model_list[i],
6945
+ bytes = UI.sizeInBytes(m.size).split(' ');
6987
6946
  html += ['<tr class="dataset" style="color: gray" ',
6988
- 'onclick="FILE_MANAGER.loadFromLocalStorage(\'',
6989
- ml[i][0],'\');"><td class="restore-name">', ml[i][0], '</td><td>',
6990
- ml[i][1], '</td><td style="text-align: right">',
6991
- bytes[0], '</td><td>', bytes[1],
6992
- '</td><td><img class="del-asm-btn" src="images/delete.png" ',
6993
- 'onclick="AUTO_SAVE.deleteSavedModel(\'',
6994
- ml[i][0], '\')"></td></tr>'].join('');
6947
+ 'onclick="FILE_MANAGER.loadAutoSavedModel(\'',
6948
+ m.name,'\');"><td class="restore-name">', m.name, '</td><td>',
6949
+ m.date.substring(1, 16).replace('T', ' '),
6950
+ '</td><td style="text-align: right">',
6951
+ bytes[0], '</td><td>', bytes[1], '</td><td style="width:15px">',
6952
+ '<img class="del-asm-btn" src="images/delete.png" ',
6953
+ 'onclick="event.stopPropagation(); ',
6954
+ 'AUTO_SAVE.getAutoSavedModels(true, \'', m.name,
6955
+ '\')"></td></tr>'].join('');
6995
6956
  }
6996
6957
  document.getElementById('restore-table').innerHTML = html;
6997
6958
  // Adjust dialog height (max-height will limit list to 10 lines)
6998
6959
  document.getElementById('restore-dlg').style.height =
6999
- (45 + 19 * ml.length) + 'px';
6960
+ (48 + 19 * this.model_list.length) + 'px';
7000
6961
  document.getElementById('confirm-remove-models').style.display = 'none';
7001
6962
  // Fill text input fields with present settings
7002
6963
  document.getElementById('auto-save-minutes').value = this.interval;
@@ -7006,7 +6967,7 @@ class ModelAutoSaver {
7006
6967
  ttl = document.getElementById('restore-dlg-title'),
7007
6968
  sa = document.getElementById('restore-scroll-area'),
7008
6969
  btn = document.getElementById('auto-save-clear-btn');
7009
- if(ml.length) {
6970
+ if(this.model_list.length) {
7010
6971
  ttl.innerHTML = 'Restore auto-saved model';
7011
6972
  sa.style.display = 'block';
7012
6973
  btn.style.display = 'block';
@@ -7037,10 +6998,10 @@ class ModelAutoSaver {
7037
6998
  if(!isNaN(h)) {
7038
6999
  // If valid, store in local storage of browser
7039
7000
  if(m !== this.interval || h !== this.period) {
7040
- UI.notify('New auto-save settings stored in browser');
7041
7001
  this.interval = m;
7042
7002
  this.period = h;
7043
7003
  this.setSettings();
7004
+ this.setAutoSaveInterval();
7044
7005
  }
7045
7006
  document.getElementById('restore-modal').style.display = 'none';
7046
7007
  return;
@@ -8188,7 +8149,6 @@ class Repository {
8188
8149
 
8189
8150
  } // END of class Repository
8190
8151
 
8191
-
8192
8152
  //
8193
8153
  // Draggable & resizable dialogs
8194
8154
  //
@@ -112,7 +112,7 @@ function rangeToList(str, max=0) {
112
112
  function dateToString(d) {
113
113
  // Returns date-time `d` in UTC format, accounting for time zone
114
114
  const offset = d.getTimezoneOffset();
115
- d = new Date(d.getTime() - offset*60000);
115
+ d = new Date(d.getTime() - offset * 60000);
116
116
  return d.toISOString().split('T')[0];
117
117
  }
118
118
 
@@ -1451,8 +1451,8 @@ class VirtualMachine {
1451
1451
  this.LM_PEAK_INC = 11; // Symbol: plus inside triangle ("peak-plus")
1452
1452
  // List of link multipliers that require binary ON/OFF variables
1453
1453
  this.LM_NEEDING_ON_OFF = [5, 6, 7, 8, 9, 10];
1454
- this.LM_SYMBOLS = ['', '\u21C9', '\u0394', '\u03A3', '\u03BC', '\u2B9D',
1455
- '+', '0', '\u2934', '\u2732', '\u2B9F', '\u2A39'];
1454
+ this.LM_SYMBOLS = ['', '\u21C9', '\u0394', '\u03A3', '\u03BC', '\u25B2',
1455
+ '+', '0', '\u2934', '\u2732', '\u25BC', '\u2A39'];
1456
1456
 
1457
1457
  // VM max. expression stack size
1458
1458
  this.MAX_STACK = 200;
@@ -1631,6 +1631,8 @@ class VirtualMachine {
1631
1631
  // Initialize error counters (error count will be reset to 0 for each block)
1632
1632
  this.error_count = 0;
1633
1633
  this.block_issues = 0;
1634
+ // NOTE: special tracking of potential solver license errors
1635
+ this.license_expired = 0;
1634
1636
  // Reset solver result arrays
1635
1637
  this.round_times.length = 0;
1636
1638
  this.solver_times.length = 0;
@@ -1708,7 +1710,8 @@ class VirtualMachine {
1708
1710
  // Other special values are very big POSITIVE numbers, so start
1709
1711
  // comparing `n` with the highest value
1710
1712
  if(n >= this.COMPUTING) return [true, '\u25A6']; // Checkered square
1711
- if(n >= this.NOT_COMPUTED) return [true, '\u2BBF']; // Circled X
1713
+ // NOTE: prettier circled bold X 2BBF does not display on macOS !!
1714
+ if(n >= this.NOT_COMPUTED) return [true, '\u2297']; // Circled X
1712
1715
  if(n >= this.UNDEFINED) return [true, '\u2047']; // Double question mark ??
1713
1716
  if(n >= this.PLUS_INFINITY) return [true, '\u221E'];
1714
1717
  return [false, n];
@@ -4526,7 +4529,7 @@ class VirtualMachine {
4526
4529
  checkLicense() {
4527
4530
  // Compares license expiry date (if set) with current time, and notifies
4528
4531
  // when three days or less remain
4529
- if(this.license_expires) {
4532
+ if(this.license_expires && this.license_expires.length) {
4530
4533
  // NOTE: expiry date has YYYY-MM-DD format
4531
4534
  const
4532
4535
  xds = this.license_expires[0].slice(-10).split('-'),
@@ -4582,6 +4585,9 @@ Solver status = ${json.status}`);
4582
4585
  }
4583
4586
  if(json.error) {
4584
4587
  const errmsg = 'Solver error: ' + json.error;
4588
+ if(errmsg.indexOf('license') >= 0 && errmsg.indexOf('expired') >= 0) {
4589
+ this.license_expired += 1;
4590
+ }
4585
4591
  this.logMessage(bnr, errmsg);
4586
4592
  UI.alert(errmsg);
4587
4593
  }
@@ -4633,6 +4639,10 @@ Solver status = ${json.status}`);
4633
4639
  if(this.block_issues) UI.warn('Issues occurred in ' +
4634
4640
  pluralS(this.block_issues, 'block') +
4635
4641
  ' -- check messages in monitor');
4642
+ if(this.license_expired > 0) {
4643
+ // Special message to draw attention to this critical error
4644
+ UI.alert('SOLVER LICENSE EXPIRED: Please check!');
4645
+ }
4636
4646
  // Call back to the console (if callback hook has been set)
4637
4647
  if(this.callback) this.callback(this);
4638
4648
  return;