[{"id":"e99119303f2e614d","type":"group","z":"8d5542876c246ef6","name":"Persistence/context","style":{"stroke":"#ff0000","label":true},"nodes":["b3e72b5b88a040b2","44773b671f82c051","d1a39f9bb57004e6","b7c0cfa5f8e04d0b","468d462f933c47e5","ad8db70e82eb4f45","mod_persist_fail","fn_show_modal","fn_set_fail_flag","fn_acknowledge_modal","d379ad022c393034","105b2f270ed429e5","bceefcfe39af28d1","btn_persist_health","fn_persist_health","debug_persist_health","074b6241262c30ad","btn_context_master","fn_prepare_context_modal","tmpl_context_modal","sw_context_choice","fn_clear_global_ram","fn_clear_global_all","fn_clear_flow_ram","fn_clear_flow_all","fn_clear_all_all","e1784102c9339bf6","c68f992a45f44e70","d4490bd64fcf45ae","e029c599f28949fc","ac1d5c676b5c4273","88238466a4e49759"],"x":14,"y":799,"w":872,"h":822},{"id":"b3e72b5b88a040b2","type":"inject","z":"8d5542876c246ef6","d":true,"g":"e99119303f2e614d","name":"make file \"settings-user.js\"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"// settings-user.js for persistent file contextmodule.exports = { contextStorage: { default: { module: \"memory\" }, file: { module: \"localfilesystem\" } }};","payloadType":"str","x":170,"y":840,"wires":[["44773b671f82c051"]]},{"id":"44773b671f82c051","type":"file","z":"8d5542876c246ef6","d":true,"g":"e99119303f2e614d","name":"to /data/home/nodered/.node-red/settings-user.js","filename":"/data/home/nodered/.node-red/settings-user.js","appendNewline":false,"createDir":false,"overwriteFile":"true","encoding":"utf8","x":490,"y":840,"wires":[["d1a39f9bb57004e6"]]},{"id":"d1a39f9bb57004e6","type":"debug","z":"8d5542876c246ef6","d":true,"g":"e99119303f2e614d","name":"Write status","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":770,"y":840,"wires":[]},{"id":"b7c0cfa5f8e04d0b","type":"inject","z":"8d5542876c246ef6","d":true,"g":"e99119303f2e614d","name":"List /data/home/nodered/.node-red/","props":[{"p":"payload","v":"/data/home/nodered/.node-red/","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":200,"y":900,"wires":[["468d462f933c47e5"]]},{"id":"468d462f933c47e5","type":"exec","z":"8d5542876c246ef6","d":true,"g":"e99119303f2e614d","command":"ls -lh","addpay":true,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"ls folder","x":420,"y":900,"wires":[["ad8db70e82eb4f45"],[],[]]},{"id":"ad8db70e82eb4f45","type":"debug","z":"8d5542876c246ef6","d":true,"g":"e99119303f2e614d","name":"Folder/File List","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":650,"y":900,"wires":[]},{"id":"mod_persist_fail","type":"ui_template","z":"8d5542876c246ef6","g":"e99119303f2e614d","group":"a985e96a8135e229","name":"Persistence Fail Modal","order":8,"width":"0","height":"0","format":"<script>\n(function(scope){\n function showModal(msg) {\n if (!window.PERSIST_FAIL_MODAL) {\n var modal = document.createElement('div');\n modal.id = 'persist-fail-modal';\n modal.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(24,10,10,0.92);z-index:99999;display:flex;align-items:center;justify-content:center;';\n modal.innerHTML = `\n <div style=\"background:#b71c1c;color:#fff;padding:32px 28px 28px 28px;border-radius:14px;box-shadow:0 4px 30px #000a;width:320px;max-width:90vw;text-align:center;font-size:1.12em;\">\n <div style='font-size:2.2em;margin-bottom:8px;'>⚠️</div>\n <b>Persistence Error</b><br>\n Node-RED file context <b>NOT working</b>.<br>\n Settings/logs <u>will not survive reboot</u>!<br>\n <span style='font-size:0.95em;opacity:0.7;'>Check SD card, storage, or settings-user.js</span>\n <br><br>\n <button id='acknowledge-persist-fail' style=\"margin-top:6px;background:#fff;color:#b71c1c;padding:8px 16px;font-weight:bold;border:none;border-radius:7px;cursor:pointer;font-size:1em;\">Acknowledge</button>\n </div>`;\n document.body.appendChild(modal);\n window.PERSIST_FAIL_MODAL = modal;\n document.getElementById('acknowledge-persist-fail').onclick = function(){\n scope.send({payload:'ack'});\n modal.remove();\n window.PERSIST_FAIL_MODAL = null;\n };\n }\n }\n scope.$watch('msg', function(msg){\n if(msg && msg.payload===true){\n showModal(msg);\n }\n });\n})(scope);\n</script>","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":440,"y":1020,"wires":[["fn_acknowledge_modal"]]},{"id":"fn_show_modal","type":"function","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Show Modal if Fail","func":"// pass = true means fail; set by health check\n// Only trigger modal if flag is set and not acknowledged\nif(msg.pass===false || msg.pass===undefined){\n var fail = flow.get('PERSIST_FAIL', 'file');\n if(fail){\n msg.payload = true; // triggers modal\n return msg;\n }\n}\nreturn null;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":210,"y":1020,"wires":[["mod_persist_fail","105b2f270ed429e5"]]},{"id":"fn_set_fail_flag","type":"function","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Set Fail Flag on Health Check","func":"// Call this from health check node: pass = false -> fail\nflow.set('PERSIST_FAIL', !msg.pass, 'file');\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":180,"y":960,"wires":[["fn_show_modal"]]},{"id":"fn_acknowledge_modal","type":"function","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Acknowledge Button","func":"// Clear fail flag when user acknowledges\nif(msg && msg.payload==='ack'){\n flow.set('PERSIST_FAIL', false, 'file');\n}\nreturn null;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":1020,"wires":[[]]},{"id":"d379ad022c393034","type":"inject","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"fail","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":210,"y":1080,"wires":[["mod_persist_fail","105b2f270ed429e5"]]},{"id":"105b2f270ed429e5","type":"ui_template","z":"8d5542876c246ef6","g":"e99119303f2e614d","group":"2ce15f4144b5de0a","name":"Persistence Fail Modal","order":0,"width":"0","height":"0","format":"<script>\n(function(scope){\n function showModal(msg) {\n if (!window.PERSIST_FAIL_MODAL) {\n var modal = document.createElement('div');\n modal.id = 'persist-fail-modal';\n modal.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(24,10,10,0.92);z-index:99999;display:flex;align-items:center;justify-content:center;';\n modal.innerHTML = `\n <div style=\"background:#b71c1c;color:#fff;padding:32px 28px 28px 28px;border-radius:14px;box-shadow:0 4px 30px #000a;width:320px;max-width:90vw;text-align:center;font-size:1.12em;\">\n <div style='font-size:2.2em;margin-bottom:8px;'>⚠️</div>\n <b>Persistence Error</b><br>\n Node-RED file context <b>NOT working</b>.<br>\n Settings/logs <u>will not survive reboot</u>!<br>\n <span style='font-size:0.95em;opacity:0.7;'>Check SD card, storage, or settings-user.js</span>\n <br><br>\n <button id='acknowledge-persist-fail' style=\"margin-top:6px;background:#fff;color:#b71c1c;padding:8px 16px;font-weight:bold;border:none;border-radius:7px;cursor:pointer;font-size:1em;\">Acknowledge</button>\n </div>`;\n document.body.appendChild(modal);\n window.PERSIST_FAIL_MODAL = modal;\n document.getElementById('acknowledge-persist-fail').onclick = function(){\n scope.send({payload:'ack'});\n modal.remove();\n window.PERSIST_FAIL_MODAL = null;\n };\n }\n }\n scope.$watch('msg', function(msg){\n if(msg && msg.payload===true){\n showModal(msg);\n }\n });\n})(scope);\n</script>","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":440,"y":1060,"wires":[["fn_acknowledge_modal"]]},{"id":"bceefcfe39af28d1","type":"inject","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"pass","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"false","payloadType":"bool","x":210,"y":1120,"wires":[["mod_persist_fail","105b2f270ed429e5"]]},{"id":"btn_persist_health","type":"ui_button","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Check Persistence Health","group":"uigroup_maint","order":3,"width":"3","height":"1","passthru":false,"label":"Check Persistence Health","tooltip":"Checks if file context is working","color":"#333333","bgcolor":"#D9D9D9","className":"","icon":"verified_user","payload":"","payloadType":"str","topic":"","topicType":"str","x":170,"y":1160,"wires":[["fn_persist_health"]]},{"id":"fn_persist_health","type":"function","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Save test value to FILE context","func":"// Write a test value to file context and check after 1 sec\nconst k = '_persist_test';\nconst v = 'OK-' + Math.floor(Math.random()*1e6);\nflow.set(k, v);\nflow.set(k, v, 'file');\nnode.status({fill:'yellow',shape:'dot',text:'Checking...'});\nsetTimeout(function(){\n var got = flow.get(k,'file');\n var pass = (got === v);\n node.status(pass ? {fill:'green',shape:'dot',text:'File context OK'} : {fill:'red',shape:'ring',text:'FAIL'});\n msg.payload = pass ? `Persistence OK: ${got}` : `FAIL: Got \"${got}\"`;\n msg.pass = pass;\n node.send(msg);\n},1000);\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":1160,"wires":[["debug_persist_health","074b6241262c30ad"]]},{"id":"debug_persist_health","type":"debug","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Persistence Health Result","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":710,"y":1160,"wires":[]},{"id":"074b6241262c30ad","type":"ui_toast","z":"8d5542876c246ef6","g":"e99119303f2e614d","position":"top right","displayTime":"5","highlight":"","sendall":true,"outputs":0,"ok":"OK","cancel":"","raw":false,"className":"","topic":"payload","name":"","x":710,"y":1200,"wires":[]},{"id":"btn_context_master","type":"ui_button","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Clear Context Data","group":"uigroup_maint","order":4,"width":"3","height":"1","passthru":false,"label":"Clear Context Data","tooltip":"","color":"#333333","bgcolor":"#D9D9D9","className":"","icon":"delete_sweep","payload":"","payloadType":"str","topic":"context_menu","topicType":"str","x":150,"y":1260,"wires":[["fn_prepare_context_modal"]]},{"id":"fn_prepare_context_modal","type":"function","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Prepare Context Modal","func":"msg.topic = 'Clear Context Data';\nmsg.payload = 'Choose what to clear:';\n// Buttons shown in the modal\nmsg.buttons = [\n { id:'flow_ram', label:'Flow (RAM only)' },\n { id:'global_ram', label:'Global (RAM only)' },\n { id:'flow_all', label:'Flow (RAM + Persistent)' },\n { id:'global_all', label:'Global (RAM + Persistent)' },\n { id:'all_all', label:'ALL (RAM + Persistent)' }\n];\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":1260,"wires":[["tmpl_context_modal"]]},{"id":"tmpl_context_modal","type":"ui_template","z":"8d5542876c246ef6","g":"e99119303f2e614d","group":"a985e96a8135e229","name":"clear Context Modal","order":4,"width":0,"height":0,"format":"<div style=\"display:none\"></div>\n<script>\n(function(scope){\n // Reset dialog tracking on refresh\n scope.lastDialogActive = false;\n\n function openModal(msg){\n if (!msg || !Array.isArray(msg.buttons) || scope.lastDialogActive) return;\n\n // Ensure enough buttons for layout (5 expected)\n if (msg.buttons.length < 5) return;\n\n // Update labels for persistent (file) options\n msg.buttons[2].label = \"FLOW (RAM + FILE)\";\n msg.buttons[3].label = \"GLOBAL (RAM + FILE)\";\n msg.buttons[4].label = \"ALL (RAM + FILE)\";\n\n var icons = {\n flow_ram: \"memory\",\n global_ram: \"layers\",\n flow_all: \"sticky_note_2\",\n global_all: \"folder\",\n all_all: \"delete_forever\"\n };\n\n function makeButton(b){\n var icon = icons[b.id] || \"help\";\n return `\n <md-button class=\"md-raised md-primary\"\n style=\"margin:7px auto; width:90%; border-radius:14px; height:40px; min-height:40px; position:relative; overflow:hidden; text-align:center;\"\n ng-click=\"choose('${b.id}')\">\n <span style=\"display:block; width:100%; height:100%; text-align:center; line-height:40px; position:relative; pointer-events:none;\">\n <md-icon class=\"material-icons\"\n style=\"position:absolute; left:14px; top:50%; transform:translateY(-50%); line-height:1;\">\n ${icon}\n </md-icon>\n <span style=\"display:inline-block; vertical-align:middle; line-height:normal; font-size:13px; font-weight:500;\">\n ${b.label}\n </span>\n </span>\n </md-button>`;\n }\n\n var ramHeader = '<div style=\"width:100%; text-align:center; font-size:14px; font-weight:600; color:#8cd7ff; margin-top:10px; margin-bottom:4px;\">RAM MEMORY</div>';\n var persistentHeader = '<div style=\"width:100%; text-align:center; font-size:14px; font-weight:600; color:#ffda8c; margin-top:10px; margin-bottom:4px;\">FILE MEMORY</div>';\n var ramButtons = makeButton(msg.buttons[0]) + makeButton(msg.buttons[1]);\n var persistentButtons = makeButton(msg.buttons[2]) + makeButton(msg.buttons[3]) + makeButton(msg.buttons[4]);\n var divider = '<div style=\"width:90%; margin:9px auto; border-top:2px solid #666;\"></div>';\n var cancelButton =\n `<md-button class=\"md-raised md-warn\"\n style=\"margin:12px auto 6px auto; width:90%; height:40px; min-height:40px; border-radius:14px; position:relative; overflow:hidden; text-align:center;\"\n ng-click=\"cancel()\">\n <span style=\"display:block; width:100%; height:100%; text-align:center; line-height:40px; position:relative; pointer-events:none;\">\n <md-icon class=\"material-icons\" style=\"position:absolute; left:14px; top:50%; transform:translateY(-50%); line-height:1;\">cancel</md-icon>\n <span style=\"display:inline-block; vertical-align:middle; line-height:normal; font-size:13px; font-weight:500;\">CANCEL</span>\n </span>\n </md-button>`;\n\n // Define modal dialog\n var dialog = {\n template: `\n <md-dialog aria-label=\"Clear Context\"\n style=\"min-width:330px; max-width:370px; padding:0; border-radius:12px; overflow:hidden; max-height:90vh;\">\n <md-dialog-content\n style=\"padding:16px 16px 6px 16px; font-size:17px; font-weight:600; display:flex; justify-content:center; text-align:center; color:#fff;\">\n ${(msg.payload || \"Choose what to clear:\")}\n </md-dialog-content>\n <md-dialog-actions layout=\"column\" layout-align=\"center center\"\n style=\"padding-bottom:10px; max-height:none; overflow:hidden;\">\n ${ramHeader}\n ${ramButtons}\n ${divider}\n ${persistentHeader}\n ${persistentButtons}\n ${cancelButton}\n </md-dialog-actions>\n </md-dialog>`,\n controller: function($scope, $mdDialog){\n $scope.choose = function(id){\n scope.lastDialogActive = false;\n $mdDialog.hide(id);\n };\n $scope.cancel = function(){\n scope.lastDialogActive = false;\n $mdDialog.cancel(\"cancel\");\n };\n },\n clickOutsideToClose: true,\n multiple: false\n };\n\n // Keyboard handling (Enter/Esc)\n var handler = function(e){\n if (e.key === \"Enter\"){\n document.removeEventListener('keydown', handler);\n scope.lastDialogActive = false;\n $mdDialog.hide(msg.buttons[0].id);\n }\n else if(e.key === \"Escape\"){\n document.removeEventListener('keydown', handler);\n scope.lastDialogActive = false;\n $mdDialog.cancel(\"cancel\");\n }\n };\n document.addEventListener('keydown', handler);\n\n // Open modal\n scope.lastDialogActive = true;\n var inj = angular.element(document.body).injector();\n var $mdDialog = inj.get('$mdDialog');\n $mdDialog.show(dialog).then(\n function(result){ scope.lastDialogActive = false; scope.send({topic:result}); },\n function(){ scope.lastDialogActive = false; scope.send({topic:\"cancel\"}); }\n );\n }\n\n // Listen for incoming messages\n scope.$watch('msg', openModal);\n\n})(scope);\n</script>","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","className":"","x":600,"y":1260,"wires":[["sw_context_choice"]]},{"id":"sw_context_choice","type":"switch","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Which operation?","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"flow_ram","vt":"str"},{"t":"eq","v":"global_ram","vt":"str"},{"t":"eq","v":"flow_all","vt":"str"},{"t":"eq","v":"global_all","vt":"str"},{"t":"eq","v":"all_all","vt":"str"}],"checkall":"true","repair":false,"outputs":5,"x":190,"y":1360,"wires":[["fn_clear_flow_ram"],["fn_clear_global_ram"],["fn_clear_flow_all"],["fn_clear_global_all"],["fn_clear_all_all","e1784102c9339bf6"]]},{"id":"fn_clear_global_ram","type":"function","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Global (RAM only)","func":"for (let key in global.keys()) {\n global.set(key, undefined);\n}\nreturn null;","outputs":0,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":1360,"wires":[]},{"id":"fn_clear_global_all","type":"exec","z":"8d5542876c246ef6","g":"e99119303f2e614d","command":"rm -f /data/home/nodered/.node-red/context/global_*.json","addpay":false,"append":"","useSpawn":"false","timer":"","winHide":false,"name":"Global (RAM + persistent)","x":650,"y":1380,"wires":[[],[],[]]},{"id":"fn_clear_flow_ram","type":"function","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"Flow (RAM only)","func":"for (let key in flow.keys()) {\n flow.set(key, undefined);\n}\nreturn null;","outputs":0,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":1320,"wires":[]},{"id":"fn_clear_flow_all","type":"exec","z":"8d5542876c246ef6","g":"e99119303f2e614d","command":"rm -f /data/home/nodered/.node-red/context/flow_*.json","addpay":false,"append":"","useSpawn":"false","timer":"","winHide":false,"name":"Flow (RAM + persistent)","x":650,"y":1320,"wires":[[],[],[]]},{"id":"fn_clear_all_all","type":"exec","z":"8d5542876c246ef6","g":"e99119303f2e614d","command":"rm -f /data/home/nodered/.node-red/context/*.json","addpay":false,"append":"","useSpawn":"false","timer":"","winHide":false,"name":"ALL (RAM + persistent)","x":650,"y":1440,"wires":[[],[],[]]},{"id":"e1784102c9339bf6","type":"debug","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"debug 14","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":400,"y":1400,"wires":[]},{"id":"c68f992a45f44e70","type":"exec","z":"8d5542876c246ef6","g":"e99119303f2e614d","command":"ls -lh /data/home/nodered/.node-red/context/","addpay":false,"append":"","useSpawn":false,"timer":"","oldrc":false,"name":"Quick test: ls context folder","x":220,"y":1520,"wires":[["d4490bd64fcf45ae"],["e029c599f28949fc"],["ac1d5c676b5c4273"]]},{"id":"d4490bd64fcf45ae","type":"debug","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"CTX ls stdout","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":620,"y":1500,"wires":[]},{"id":"e029c599f28949fc","type":"debug","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"CTX ls stderr","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":620,"y":1540,"wires":[]},{"id":"ac1d5c676b5c4273","type":"debug","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"CTX ls rc","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":600,"y":1580,"wires":[]},{"id":"88238466a4e49759","type":"inject","z":"8d5542876c246ef6","g":"e99119303f2e614d","name":"RUN: ls context folder","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":180,"y":1460,"wires":[["c68f992a45f44e70"]]},{"id":"a985e96a8135e229","type":"ui_group","name":"connectivity","tab":"uitab_tools","order":2,"disp":true,"width":"5","collapse":true,"className":""},{"id":"2ce15f4144b5de0a","type":"ui_group","name":"Power Flow","tab":"dashboard_tab","order":1,"disp":true,"width":"10","collapse":true,"className":""},{"id":"uigroup_maint","type":"ui_group","name":"Maintenance","tab":"uitab_tools","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"uitab_tools","type":"ui_tab","name":"Tools","icon":"settings_backup_restore","disabled":false,"hidden":false},{"id":"dashboard_tab","type":"ui_tab","name":"Main","icon":"developer_dashboard","order":1,"disabled":false,"hidden":false},{"id":"e877c81c01565429","type":"global-config","env":[],"modules":{"node-red-dashboard":"3.6.6"}}]