+(()=>{var q=Object.defineProperty;var L=Object.getOwnPropertySymbols;var A=Object.prototype.hasOwnProperty,D=Object.prototype.propertyIsEnumerable;var R=(s,e,t)=>e in s?q(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t,v=(s,e)=>{for(var t in e||(e={}))A.call(e,t)&&R(s,t,e[t]);if(L)for(var t of L(e))D.call(e,t)&&R(s,t,e[t]);return s};var k=(s,e,t)=>new Promise((i,n)=>{var r=o=>{try{u(t.next(o))}catch(c){n(c)}},l=o=>{try{u(t.throw(o))}catch(c){n(c)}},u=o=>o.done?i(o.value):Promise.resolve(o.value).then(r,l);u((t=t.apply(s,e)).next())});function j(s,e,t,i){return`
+
- `}function b(e){return e.close=function(){this.classList.add("a-hide"),setTimeout(()=>{this.remove()},300)},e.querySelector(".toast-close").addEventListener("click",t=>{e.close()}),e.show=function(){document.querySelector("body").append(e),setTimeout(()=>{e.classList.add("a-show")},10)},e}function l(e,t,o,n){let s=document.createElement("div");return s.innerHTML=h(e,t,o,n),b(s.childNodes[1])}function v(e,t){return l("success",'
',e,t)}var a={create:l,createInfo:y,createSuccess:v,createWarning:w,createError:u,createDanger:u};function L(e,t,o){return`
-
+ `}function M(s){return s.close=function(){this.classList.add("a-hide"),setTimeout(()=>{this.remove()},300)},s.querySelector(".toast-close").addEventListener("click",e=>{s.close()}),s.show=function(){document.querySelector("body").append(s),setTimeout(()=>{s.classList.add("a-show")},10)},s}function _(s,e,t,i){let n=document.createElement("div");return n.innerHTML=j(s,e,t,i),M(n.childNodes[1])}function N(s,e){return _("success",'
',s,e)}function O(s,e){return _("info",'
',s,e)}function z(s,e){return _("warning",'
',s,e)}function x(s,e){return _("danger",'
',s,e)}var p={create:_,createInfo:O,createSuccess:N,createWarning:z,createError:x,createDanger:x};function H(s,e,t){return`
+
- `}function g(e){return e.show=function(){document.querySelector("body").append(e),setTimeout(()=>{this.classList.add("a-show")},10)},e.close=function(){this.classList.add("a-hide"),setTimeout(()=>{this.remove()},300)},e.querySelector(".modal-close").addEventListener("click",t=>{e.close()}),e}function T(e,t){let o=t.title||"",n=t.footer||"",s=document.createElement("div");s.innerHTML=L(e,o,n);let i=s.childNodes[1],r=i.querySelector(".modal-body"),f=i.querySelector(".modal-footer");if(typeof t.actions=="function"){let c=t.actions(i);if(typeof c[0]=="object"){let d=document.createElement("div");d.classList.add("actions");for(let p of c)d.append(p);f.append(d)}}if(typeof t.body=="function"){let c=t.body(i);typeof c=="object"?r.append(c):typeof c=="string"&&(r.innerHTML=c)}return g(i)}var m={create:T};window.demoToastInfo=function(){a.createInfo("Title","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.").show()};window.demoToastSuccess=function(){a.createSuccess("Success","\u0412\u0441\u0451 \u043E\u0442\u043B\u0438\u0447\u043D\u043E, \u0432\u0441\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u044D\u0442\u043E \u0443\u0441\u043F\u0435\u0445 \u0442\u043E\u0441\u0442\u0435\u0440\u0430").show()};window.demoToastWarning=function(){a.createWarning("Warning","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.").show()};window.demoToastDanger=function(){a.createDanger("Danger","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.").show()};window.demoModal=function(){m.create("demo-modals",{title:"Demo modal window",footer:"
Footer text
",actions:e=>{let t=document.createElement("button");t.classList.add("btn"),t.classList.add("btn-primary"),t.innerHTML="Cancel",t.addEventListener("click",n=>{e.close()});let o=document.createElement("button");return o.classList.add("btn"),o.classList.add("btn-success"),o.innerHTML="Apply",o.addEventListener("click",n=>{e.close(),setTimeout(()=>{a.createSuccess("Success","\u0412\u0441\u0451 \u043E\u0442\u043B\u0438\u0447\u043D\u043E, \u0432\u0441\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u044D\u0442\u043E \u0443\u0441\u043F\u0435\u0445").show()},300)}),[t,o]},body:e=>`
+ `}function F(s){return s.show=function(){document.querySelector("body").append(s),setTimeout(()=>{this.classList.add("a-show")},10)},s.close=function(){this.classList.add("a-hide"),setTimeout(()=>{this.remove()},300)},s.querySelector(".modal-close").addEventListener("click",e=>{s.close()}),s}function P(s,e){let t=e.title||"",i=e.footer||"",n=document.createElement("div");n.innerHTML=H(s,t,i);let r=n.childNodes[1],l=r.querySelector(".modal-body"),u=r.querySelector(".modal-footer");if(typeof e.actions=="function"){let o=e.actions(r);if(typeof o[0]=="object"){let c=document.createElement("div");c.classList.add("actions");for(let f of o)c.append(f);u.append(c)}}if(typeof e.body=="function"){let o=e.body(r);typeof o=="object"?l.append(o):typeof o=="string"&&(l.innerHTML=o)}return F(r)}var U={create:P};var b=class{constructor(e){this.core=e}actions_list(e){return this.core.api_get("/api/v1/scripts/actions/list",e)}scopes_list(e){return this.core.api_get("/api/v1/scripts/scopes/list",e)}regular_list(e){return this.core.api_get("/api/v1/scripts/regular/list",e)}scope_get_by_filename(e,t){let i=encodeURIComponent(String(e||""));return this.core.api_get(`/api/v1/scripts/scopes/name/${i}`,t,{})}scope_create(e,t){return this.core.api_post("/api/v1/scripts/scopes/new",e,t)}scope_update(e,t){return this.core.api_post("/api/v1/scripts/scopes/update",e,t)}action_enable(e,t){let i=encodeURIComponent(String(e||""));return this.core.api_get(`/api/v1/scripts/actions/alias/${i}/enable`,t)}action_disable(e,t){let i=encodeURIComponent(String(e||""));return this.core.api_get(`/api/v1/scripts/actions/alias/${i}/disable`,t)}regular_enable(e,t){let i=encodeURIComponent(String(e||""));return this.core.api_get(`/api/v1/scripts/actions/regular/${i}/enable`,t)}regular_disable(e,t){let i=encodeURIComponent(String(e||""));return this.core.api_get(`/api/v1/scripts/actions/regular/${i}/disable`,t)}scope_enable(e,t){let i=encodeURIComponent(String(e||""));return this.core.api_get(`/api/v1/scripts/actions/scope/${i}/enable`,t)}scope_disable(e,t){let i=encodeURIComponent(String(e||""));return this.core.api_get(`/api/v1/scripts/actions/scope/${i}/disable`,t)}scope_remove(e,t){let i=encodeURIComponent(String(e||""));return this.core.api_get(`/api/v1/scripts/scopes/name/${i}/remove`,t)}run(e,t){return this.core.api_post("/api/v1/scripts/actions/run",e,t)}};var y=class{constructor(e){this.core=e}list(e){return this.core.api_get("/api/v1/devices/list",e)}scanning_setup(e){return this.core.api_get("/api/v1/devices/scanning/setup",e)}scanning_all(e){return this.core.api_get("/api/v1/devices/scanning/all",e)}setup_new_device(e,t){return this.core.api_post("/api/v1/devices/setup/new-device",e,t)}info(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/devices/id/${i}/info`,t)}get(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/devices/id/${i}`,t)}status(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/devices/id/${i}/status`,t)}action(e,t){return this.core.api_post("/api/v1/devices/action",e,t)}remove(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/devices/id/${i}/remove`,t)}reboot(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/devices/id/${i}/reboot`,t)}};var w=class{constructor(e){this.core=e}list(e){return this.core.api_get("/api/v1/areas/list",e)}inner_list(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/areas/id/${i}/list`,t)}new_area(e,t){return this.core.api_post("/api/v1/areas/new-area",e,t)}remove(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/areas/id/${i}/remove`,t)}place_in_area(e,t){return this.core.api_post("/api/v1/areas/place-in-area",e,t)}update_display_name(e,t){return this.core.api_post("/api/v1/areas/update-display-name",e,t)}update_alias(e,t){return this.core.api_post("/api/v1/areas/update-alias",e,t)}devices(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/areas/id/${i}/devices`,t)}unassign_from_area(e,t){let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/areas/id/${i}/unassign-from-area`,t)}types_list(e){return this.core.api_get("/api/v1/areas/types/list",e)}reboot_devices(e,t){if(e==null)return this.core.api_get("/api/v1/areas/reboot_devices",t);let i=encodeURIComponent(String(e));return this.core.api_get(`/api/v1/areas/id/${i}/reboot_devices`,t)}};function E(s){if(!s||typeof s!="object")return"";let e=new URLSearchParams;Object.entries(s).forEach(([i,n])=>{n!=null&&e.append(i,String(n))});let t=e.toString();return t?`?${t}`:""}function W(s,e){let t=String(s||"").replace(/\/+$/,""),i=String(e||"").replace(/^\/+/,"");return`${t}/${i}`}function B(s){try{return{ok:!0,data:JSON.parse(s)}}catch(e){return{ok:!1,error:e}}}var S=class{constructor(e){this.base_url=(e==null?void 0:e.base_url)||"",this.token=(e==null?void 0:e.token)||"",this.timeout_ms=Number.isFinite(e==null?void 0:e.timeout_ms)?e.timeout_ms:15e3,this.default_headers=(e==null?void 0:e.default_headers)||{},this.on_unauthorized=typeof(e==null?void 0:e.on_unauthorized)=="function"?e.on_unauthorized:null,this.proxy_path=(e==null?void 0:e.proxy_path)||"",this.scripts=new b(this),this.devices=new y(this),this.areas=new w(this)}set_base_url(e){this.base_url=e||""}set_token(e){this.token=e||""}set_proxy_path(e){this.proxy_path=e||""}_wrap_path(e,t){if(!this.proxy_path)return t?`${e}${E(t)}`:e;let i=v({path:e},t||{});return`${this.proxy_path}${E(i)}`}request(e,t,i,n,r){let l=typeof n=="function"?n:()=>{},u=W(this.base_url,t),o=new AbortController,c=Number.isFinite(r==null?void 0:r.timeout_ms)?r.timeout_ms:this.timeout_ms,f=setTimeout(()=>o.abort(),c),$=v(v({},this.default_headers),(r==null?void 0:r.headers)||{});this.token&&($.Authorization=`Bearer ${this.token}`);let C;i!=null&&($["Content-Type"]="application/json",C=JSON.stringify(i)),fetch(u,{method:e,headers:$,body:C,signal:o.signal}).then(a=>k(this,null,function*(){clearTimeout(f);let h={url:u,method:e,status_code:a.status,headers:a.headers},g=yield a.text(),T=B(g),d=T.ok?T.data:g;if(!a.ok){let m={type:"http_error",message:`HTTP ${a.status}`,status_code:a.status,raw:d};if((a.status===401||a.status===403)&&this.on_unauthorized)try{this.on_unauthorized({error:m,meta:h})}catch(I){}return l(m,null,h)}if(T.ok&&d&&typeof d=="object"){let m=d.status;if(m===!1||m==="error"){let I={type:"api_error",message:d.message||"API error",status_code:a.status,raw:d,field:d.field};return l(I,null,h)}}return l(null,d,h)})).catch(a=>{clearTimeout(f);let g=a&&(a.name==="AbortError"||String(a).includes("AbortError"))?{type:"timeout",message:`Timeout after ${c}ms`}:{type:"network_error",message:(a==null?void 0:a.message)||"Network error",details:a};return l(g,null,{url:u,method:e,status_code:0,headers:null})})}get(e,t,i){return this.request("GET",e,null,t,i)}post(e,t,i,n){return this.request("POST",e,t,i,n)}api_get(e,t,i,n){return this.get(this._wrap_path(e,i),t,n)}api_post(e,t,i,n,r){return this.post(this._wrap_path(e,n),t,i,r)}};window.demoToastInfo=function(){p.createInfo("Title","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.").show()};window.demoToastSuccess=function(){p.createSuccess("Success","\u0412\u0441\u0451 \u043E\u0442\u043B\u0438\u0447\u043D\u043E, \u0432\u0441\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u044D\u0442\u043E \u0443\u0441\u043F\u0435\u0445 \u0442\u043E\u0441\u0442\u0435\u0440\u0430").show()};window.demoToastWarning=function(){p.createWarning("Warning","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.").show()};window.demoToastDanger=function(){p.createDanger("Danger","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.").show()};window.demoModal=function(){U.create("demo-modals",{title:"Demo modal window",footer:"
Footer text
",actions:s=>{let e=document.createElement("button");e.classList.add("btn"),e.classList.add("btn-primary"),e.innerHTML="Cancel",e.addEventListener("click",i=>{s.close()});let t=document.createElement("button");return t.classList.add("btn"),t.classList.add("btn-success"),t.innerHTML="Apply",t.addEventListener("click",i=>{s.close(),setTimeout(()=>{p.createSuccess("Success","\u0412\u0441\u0451 \u043E\u0442\u043B\u0438\u0447\u043D\u043E, \u0432\u0441\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u044D\u0442\u043E \u0443\u0441\u043F\u0435\u0445").show()},300)}),[e,t]},body:s=>`
\u041B\u044E\u0431\u043E\u0439 \u043A\u043E\u043D\u0442\u0435\u043D\u0442: \u0442\u0435\u043A\u0441\u0442, \u0444\u043E\u0440\u043C\u044B, \u0441\u043F\u0438\u0441\u043A\u0438.
@@ -36,5 +36,5 @@
- `}).show()};document.addEventListener("DOMContentLoaded",e=>{console.log("App init")});})();
+ `}).show()};function J(){let s=new S({base_url:"http://shswebclient.local",token:"YOUR_TOKEN",timeout_ms:3e3,on_unauthorized:({error:e})=>console.log("auth problem:",e),proxy_path:"/proxy.php"});s.scripts.actions_list((e,t,i)=>console.log(t)),s.scripts.scopes_list((e,t,i)=>console.log(t)),s.devices.info(4,(e,t,i)=>console.log(t))}document.addEventListener("DOMContentLoaded",s=>{console.log("App init"),J()});})();
//# sourceMappingURL=main.js.map
diff --git a/webclient/dist/js/main.js.map b/webclient/dist/js/main.js.map
index 3929b0f..f358876 100644
--- a/webclient/dist/js/main.js.map
+++ b/webclient/dist/js/main.js.map
@@ -1,7 +1,7 @@
{
"version": 3,
- "sources": ["../../src/js/components/toasts.js", "../../src/js/components/modals.js", "../../src/js/index.js"],
- "sourcesContent": ["function template(type, icon, title, text) {\n\treturn `\n\t\t
\n\t
\n\t
${icon} ${title} \n\t
${text}
\n\t
\n\t
\u2715 \n\t
\n\t`;\n}\n\nfunction init(toast) {\n\ttoast.close = function() {\n\t\tthis.classList.add(\"a-hide\");\n\t\tsetTimeout(() => {\n\t\t\tthis.remove();\n\t\t}, 300);\n\t}\n\n\ttoast.querySelector(\".toast-close\").addEventListener(\"click\", e => {\n\t\ttoast.close();\n\t});\n\n\ttoast.show = function() {\n\t\tdocument.querySelector(\"body\").append(toast);\n\n\t\tsetTimeout(() => {\n\t\t\ttoast.classList.add(\"a-show\");\n\t\t}, 10);\n\t}\n\n\treturn toast;\n}\n\nfunction create(type, icon, title, text) {\n\tconst div = document.createElement(\"div\");\n\tdiv.innerHTML = template(type, icon, title, text);\n\n\treturn init(div.childNodes[1]);\n}\n\nfunction createSuccess(title, text) {\n\treturn create(\n\t\t\"success\", \n\t\t`
`, \n\t\ttitle, \n\t\ttext\n\t);\n}\n\nfunction createInfo(title, text) {\n\treturn create(\n\t\t\"info\", \n\t\t`
`, \n\t\ttitle, \n\t\ttext\n\t);\n}\n\nfunction createWarning(title, text) {\n\treturn create(\n\t\t\"warning\", \n\t\t`
`, \n\t\ttitle, \n\t\ttext\n\t);\n}\n\nfunction createError(title, text) {\n\treturn create(\n\t\t\"danger\", \n\t\t`
`, \n\t\ttitle, \n\t\ttext\n\t);\n}\n\nexport default {\n create,\n createInfo,\n createSuccess,\n createWarning,\n createError,\n \"createDanger\": createError\n};", "function template(id, title, footer) {\n\treturn `\n\t\t
\n\t`;\n}\n\nfunction init(modal) {\n\tmodal.show = function() {\n\t\tdocument.querySelector(\"body\").append(modal);\n\n\t\tsetTimeout(() => {\n\t\t\tthis.classList.add(\"a-show\");\n\t\t}, 10);\n\t}\n\n\tmodal.close = function() {\n\t\tthis.classList.add(\"a-hide\");\n\t\tsetTimeout(() => {\n\t\t\tthis.remove();\n\t\t}, 300);\n\t}\n\n\tmodal.querySelector(\".modal-close\").addEventListener(\"click\", e => {\n\t\tmodal.close();\n\t});\n\n\treturn modal;\n}\n\n\t/**\n\t * Create new modal window;\n\t * @param {string} id Uniq id (selector)\n\t * @param {string} title Display title\n\t * @param {object} props { body: modal => {}, actions => modal => {} }\n\t * @return {object} DOM object\n\t */\nfunction create(id, props) {\n\tconst title = props.title || \"\";\n\tconst footer = props.footer || \"\";\n\n\tconst div = document.createElement(\"div\");\n\tdiv.innerHTML = template(id, title, footer);\n\tconst modal = div.childNodes[1];\n\n\tconst modalBody = modal.querySelector(\".modal-body\");\n\tconst modalFooter = modal.querySelector(\".modal-footer\");\n\n\tif(typeof props.actions == \"function\") {\n\t\tconst actionsResult = props.actions(modal);\n\n\t\tif(typeof actionsResult[0] == \"object\") {\n\t\t\tconst actions = document.createElement(\"div\");\n\t\t\tactions.classList.add(\"actions\");\n\t\t\tfor(let actionElement of actionsResult) {\n\t\t\t\tactions.append(actionElement);\n\t\t\t}\n\n\t\t\tmodalFooter.append(actions);\n\t\t}\n\t}\n\n\tif(typeof props.body == \"function\") {\n\t\tconst bodyResult = props.body(modal);\n\n\t\tif(typeof bodyResult == \"object\") {\n\t\t\tmodalBody.append(bodyResult);\n\t\t} else if(typeof bodyResult == \"string\") {\n\t\t\tmodalBody.innerHTML = bodyResult;\n\t\t}\n\t}\n\n\treturn init(modal);\n}\n\nexport default {\n\tcreate\n}", "import Toasts from \"./components/toasts.js\";\nimport Modals from \"./components/modals.js\";\n\nwindow.demoToastInfo = function () {\n\tToasts.createInfo(\n\t\t\"Title\",\n\t\t\"\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.\"\n\t).show();\n}\n\nwindow.demoToastSuccess = function () {\n\tToasts.createSuccess(\n\t\t\"Success\",\n\t\t\"\u0412\u0441\u0451 \u043E\u0442\u043B\u0438\u0447\u043D\u043E, \u0432\u0441\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u044D\u0442\u043E \u0443\u0441\u043F\u0435\u0445 \u0442\u043E\u0441\u0442\u0435\u0440\u0430\"\n\t).show();\n}\n\nwindow.demoToastWarning = function () {\n\tToasts.createWarning(\n\t\t\"Warning\",\n\t\t\"\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.\"\n\t).show();\n}\n\nwindow.demoToastDanger = function () {\n\tToasts.createDanger(\n\t\t\"Danger\",\n\t\t\"\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.\"\n\t).show();\n}\n\nwindow.demoModal = function() {\n\tModals.create(\"demo-modals\", {\n\t\ttitle: \"Demo modal window\",\n\t\tfooter: \"
Footer text
\",\n\t\tactions: modal => {\n\t\t\tconst buttonCancel = document.createElement(\"button\");\n\t\t\tbuttonCancel.classList.add(\"btn\");\n\t\t\tbuttonCancel.classList.add(\"btn-primary\");\n\t\t\tbuttonCancel.innerHTML = \"Cancel\";\n\n\t\t\tbuttonCancel.addEventListener(\"click\", e => {\n\t\t\t\tmodal.close();\n\t\t\t});\n\n\t\t\tconst buttonApply = document.createElement(\"button\");\n\t\t\tbuttonApply.classList.add(\"btn\");\n\t\t\tbuttonApply.classList.add(\"btn-success\");\n\t\t\tbuttonApply.innerHTML = \"Apply\";\n\n\t\t\tbuttonApply.addEventListener(\"click\", e => {\n\t\t\t\tmodal.close();\n\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tToasts.createSuccess(\n\t\t\t\t\t\t\"Success\",\n\t\t\t\t\t\t\"\u0412\u0441\u0451 \u043E\u0442\u043B\u0438\u0447\u043D\u043E, \u0432\u0441\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u044D\u0442\u043E \u0443\u0441\u043F\u0435\u0445\"\n\t\t\t\t\t).show();\n\t\t\t\t}, 300);\n\t\t\t});\n\n\t\t\treturn [ buttonCancel, buttonApply ];\n\t\t},\n\t\tbody: modal => {\n\t\t\treturn `\n\t\t\t\t
\u041B\u044E\u0431\u043E\u0439 \u043A\u043E\u043D\u0442\u0435\u043D\u0442: \u0442\u0435\u043A\u0441\u0442, \u0444\u043E\u0440\u043C\u044B, \u0441\u043F\u0438\u0441\u043A\u0438.
\n\n
\n \n Device name\n \n \n
\n\n
\n \n Description\n \n \n
\n\t\t\t`;\n\t\t}\n\t}).show();\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", e => {\n\tconsole.log(\"App init\");\n});"],
- "mappings": "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",
- "names": ["template", "type", "icon", "title", "text", "init", "toast", "e", "create", "div", "createSuccess", "createInfo", "createWarning", "createError", "toasts_default", "template", "id", "title", "footer", "init", "modal", "e", "create", "props", "div", "modalBody", "modalFooter", "actionsResult", "actions", "actionElement", "bodyResult", "modals_default", "toasts_default", "modals_default", "modal", "buttonCancel", "e", "buttonApply"]
+ "sources": ["../../src/js/components/toasts.js", "../../src/js/components/modals.js", "../../src/js/sh/modules/ScriptsApi.js", "../../src/js/sh/modules/DevicesApi.js", "../../src/js/sh/modules/AreasApi.js", "../../src/js/sh/SmartHomeApi.js", "../../src/js/index.js"],
+ "sourcesContent": ["function template(type, icon, title, text) {\n\treturn `\n\t\t
\n\t
\n\t
${icon} ${title} \n\t
${text}
\n\t
\n\t
\u2715 \n\t
\n\t`;\n}\n\nfunction init(toast) {\n\ttoast.close = function() {\n\t\tthis.classList.add(\"a-hide\");\n\t\tsetTimeout(() => {\n\t\t\tthis.remove();\n\t\t}, 300);\n\t}\n\n\ttoast.querySelector(\".toast-close\").addEventListener(\"click\", e => {\n\t\ttoast.close();\n\t});\n\n\ttoast.show = function() {\n\t\tdocument.querySelector(\"body\").append(toast);\n\n\t\tsetTimeout(() => {\n\t\t\ttoast.classList.add(\"a-show\");\n\t\t}, 10);\n\t}\n\n\treturn toast;\n}\n\nfunction create(type, icon, title, text) {\n\tconst div = document.createElement(\"div\");\n\tdiv.innerHTML = template(type, icon, title, text);\n\n\treturn init(div.childNodes[1]);\n}\n\nfunction createSuccess(title, text) {\n\treturn create(\n\t\t\"success\", \n\t\t`
`, \n\t\ttitle, \n\t\ttext\n\t);\n}\n\nfunction createInfo(title, text) {\n\treturn create(\n\t\t\"info\", \n\t\t`
`, \n\t\ttitle, \n\t\ttext\n\t);\n}\n\nfunction createWarning(title, text) {\n\treturn create(\n\t\t\"warning\", \n\t\t`
`, \n\t\ttitle, \n\t\ttext\n\t);\n}\n\nfunction createError(title, text) {\n\treturn create(\n\t\t\"danger\", \n\t\t`
`, \n\t\ttitle, \n\t\ttext\n\t);\n}\n\nexport default {\n create,\n createInfo,\n createSuccess,\n createWarning,\n createError,\n \"createDanger\": createError\n};", "function template(id, title, footer) {\n\treturn `\n\t\t
\n\t`;\n}\n\nfunction init(modal) {\n\tmodal.show = function() {\n\t\tdocument.querySelector(\"body\").append(modal);\n\n\t\tsetTimeout(() => {\n\t\t\tthis.classList.add(\"a-show\");\n\t\t}, 10);\n\t}\n\n\tmodal.close = function() {\n\t\tthis.classList.add(\"a-hide\");\n\t\tsetTimeout(() => {\n\t\t\tthis.remove();\n\t\t}, 300);\n\t}\n\n\tmodal.querySelector(\".modal-close\").addEventListener(\"click\", e => {\n\t\tmodal.close();\n\t});\n\n\treturn modal;\n}\n\n\t/**\n\t * Create new modal window;\n\t * @param {string} id Uniq id (selector)\n\t * @param {string} title Display title\n\t * @param {object} props { body: modal => {}, actions => modal => {} }\n\t * @return {object} DOM object\n\t */\nfunction create(id, props) {\n\tconst title = props.title || \"\";\n\tconst footer = props.footer || \"\";\n\n\tconst div = document.createElement(\"div\");\n\tdiv.innerHTML = template(id, title, footer);\n\tconst modal = div.childNodes[1];\n\n\tconst modalBody = modal.querySelector(\".modal-body\");\n\tconst modalFooter = modal.querySelector(\".modal-footer\");\n\n\tif(typeof props.actions == \"function\") {\n\t\tconst actionsResult = props.actions(modal);\n\n\t\tif(typeof actionsResult[0] == \"object\") {\n\t\t\tconst actions = document.createElement(\"div\");\n\t\t\tactions.classList.add(\"actions\");\n\t\t\tfor(let actionElement of actionsResult) {\n\t\t\t\tactions.append(actionElement);\n\t\t\t}\n\n\t\t\tmodalFooter.append(actions);\n\t\t}\n\t}\n\n\tif(typeof props.body == \"function\") {\n\t\tconst bodyResult = props.body(modal);\n\n\t\tif(typeof bodyResult == \"object\") {\n\t\t\tmodalBody.append(bodyResult);\n\t\t} else if(typeof bodyResult == \"string\") {\n\t\t\tmodalBody.innerHTML = bodyResult;\n\t\t}\n\t}\n\n\treturn init(modal);\n}\n\nexport default {\n\tcreate\n}", "/* =========================\n Scripts module\n========================= */\n\nexport class ScriptsApi {\n\tconstructor(core) {\n\t\tthis.core = core;\n\t}\n\n\t// GET /api/v1/scripts/actions/list\n\tactions_list(cb) {\n\t\treturn this.core.api_get(\"/api/v1/scripts/actions/list\", cb);\n\t}\n\n\t// GET /api/v1/scripts/scopes/list\n\tscopes_list(cb) {\n\t\treturn this.core.api_get(\"/api/v1/scripts/scopes/list\", cb);\n\t}\n\n\t// GET /api/v1/scripts/regular/list\n\tregular_list(cb) {\n\t\treturn this.core.api_get(\"/api/v1/scripts/regular/list\", cb);\n\t}\n\n\t// GET /api/v1/scripts/scopes/name/{{filename}}\n\tscope_get_by_filename(filename, cb) {\n\t\tconst safe = encodeURIComponent(String(filename || \"\"));\n\t\treturn this.core.api_get(`/api/v1/scripts/scopes/name/${safe}`, cb, {\n\t\t\t// \u0442\u0443\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 \u043C\u043E\u0436\u0435\u0442 \u0432\u0435\u0440\u043D\u0443\u0442\u044C PHP-\u043A\u043E\u0434 \u0442\u0435\u043A\u0441\u0442\u043E\u043C; request \u0443\u043C\u0435\u0435\u0442 \u044D\u0442\u043E \u043F\u0435\u0440\u0435\u0436\u0438\u0442\u044C\n\t\t});\n\t}\n\n\t// POST /api/v1/scripts/scopes/new\n\tscope_create(payload, cb) {\n\t\t// payload: { alias, filename, path }\n\t\treturn this.core.api_post(\"/api/v1/scripts/scopes/new\", payload, cb);\n\t}\n\n\t// POST /api/v1/scripts/scopes/update\n\tscope_update(payload, cb) {\n\t\t// payload: { name, filename, path }\n\t\treturn this.core.api_post(\"/api/v1/scripts/scopes/update\", payload, cb);\n\t}\n\n\t// GET /api/v1/scripts/actions/alias/{{alias}}/enable\n\taction_enable(alias, cb) {\n\t\tconst safe = encodeURIComponent(String(alias || \"\"));\n\t\treturn this.core.api_get(`/api/v1/scripts/actions/alias/${safe}/enable`, cb);\n\t}\n\n\t// GET /api/v1/scripts/actions/alias/{{alias}}/disable\n\taction_disable(alias, cb) {\n\t\tconst safe = encodeURIComponent(String(alias || \"\"));\n\t\treturn this.core.api_get(`/api/v1/scripts/actions/alias/${safe}/disable`, cb);\n\t}\n\n\t// GET /api/v1/scripts/actions/regular/{{alias}}/enable\n\tregular_enable(alias, cb) {\n\t\tconst safe = encodeURIComponent(String(alias || \"\"));\n\t\treturn this.core.api_get(`/api/v1/scripts/actions/regular/${safe}/enable`, cb);\n\t}\n\n\t// GET /api/v1/scripts/actions/regular/{{alias}}/disable\n\tregular_disable(alias, cb) {\n\t\tconst safe = encodeURIComponent(String(alias || \"\"));\n\t\treturn this.core.api_get(`/api/v1/scripts/actions/regular/${safe}/disable`, cb);\n\t}\n\n\t// GET /api/v1/scripts/actions/scope/{{name}}/enable\n\tscope_enable(name, cb) {\n\t\tconst safe = encodeURIComponent(String(name || \"\"));\n\t\treturn this.core.api_get(`/api/v1/scripts/actions/scope/${safe}/enable`, cb);\n\t}\n\n\t// GET /api/v1/scripts/actions/scope/{{name}}/disable\n\tscope_disable(name, cb) {\n\t\tconst safe = encodeURIComponent(String(name || \"\"));\n\t\treturn this.core.api_get(`/api/v1/scripts/actions/scope/${safe}/disable`, cb);\n\t}\n\n\t// GET /api/v1/scripts/scopes/name/{{name}}/remove\n\tscope_remove(name, cb) {\n\t\tconst safe = encodeURIComponent(String(name || \"\"));\n\t\treturn this.core.api_get(`/api/v1/scripts/scopes/name/${safe}/remove`, cb);\n\t}\n\n\t// POST /api/v1/scripts/actions/run\n\trun(payload, cb) {\n\t\t// payload: { alias, params: {...} }\n\t\treturn this.core.api_post(\"/api/v1/scripts/actions/run\", payload, cb);\n\t}\n}", "/* =========================\n Devices module\n========================= */\n\nexport class DevicesApi {\n\tconstructor(core) {\n\t\tthis.core = core;\n\t}\n\n\t// GET /api/v1/devices/list\n\tlist(cb) {\n\t\treturn this.core.api_get(\"/api/v1/devices/list\", cb);\n\t}\n\n\t// GET /api/v1/devices/scanning/setup\n\tscanning_setup(cb) {\n\t\treturn this.core.api_get(\"/api/v1/devices/scanning/setup\", cb);\n\t}\n\n\t// GET /api/v1/devices/scanning/all\n\tscanning_all(cb) {\n\t\treturn this.core.api_get(\"/api/v1/devices/scanning/all\", cb);\n\t}\n\n\t// POST /api/v1/devices/setup/new-device\n\tsetup_new_device(payload, cb) {\n\t\t// payload: { device_ip, alias, name, description }\n\t\treturn this.core.api_post(\"/api/v1/devices/setup/new-device\", payload, cb);\n\t}\n\n\t// GET /api/v1/devices/id/{{id}}/info\n\tinfo(id, cb) {\n\t\tconst safe = encodeURIComponent(String(id));\n\t\treturn this.core.api_get(`/api/v1/devices/id/${safe}/info`, cb);\n\t}\n\n\t// GET /api/v1/devices/id/{{id}}\n\tget(id, cb) {\n\t\tconst safe = encodeURIComponent(String(id));\n\t\treturn this.core.api_get(`/api/v1/devices/id/${safe}`, cb);\n\t}\n\n\t// GET /api/v1/devices/id/{{id}}/status\n\tstatus(id, cb) {\n\t\tconst safe = encodeURIComponent(String(id));\n\t\treturn this.core.api_get(`/api/v1/devices/id/${safe}/status`, cb);\n\t}\n\n\t// POST /api/v1/devices/action\n\taction(payload, cb) {\n\t\t// payload: { device_id, action, params }\n\t\treturn this.core.api_post(\"/api/v1/devices/action\", payload, cb);\n\t}\n\n\t// GET /api/v1/devices/id/{{id}}/remove\n\tremove(id, cb) {\n\t\tconst safe = encodeURIComponent(String(id));\n\t\treturn this.core.api_get(`/api/v1/devices/id/${safe}/remove`, cb);\n\t}\n\n\t// GET /api/v1/devices/id/{{id}}/reboot\n\treboot(id, cb) {\n\t\tconst safe = encodeURIComponent(String(id));\n\t\treturn this.core.api_get(`/api/v1/devices/id/${safe}/reboot`, cb);\n\t}\n}\n", "export class AreasApi {\n\tconstructor(core) {\n\t\tthis.core = core;\n\t}\n\n\t// GET /api/v1/areas/list\n\tlist(cb) {\n\t\treturn this.core.api_get(\"/api/v1/areas/list\", cb);\n\t}\n\n\t// GET /api/v1/areas/id/{{area_id}}/list\n\tinner_list(area_id, cb) {\n\t\tconst safe = encodeURIComponent(String(area_id));\n\t\treturn this.core.api_get(`/api/v1/areas/id/${safe}/list`, cb);\n\t}\n\n\t// POST /api/v1/areas/new-area\n\tnew_area(payload, cb) {\n\t\t// payload: { type, alias, display_name }\n\t\treturn this.core.api_post(\"/api/v1/areas/new-area\", payload, cb);\n\t}\n\n\t// GET /api/v1/areas/id/{{area_id}}/remove\n\tremove(area_id, cb) {\n\t\tconst safe = encodeURIComponent(String(area_id));\n\t\treturn this.core.api_get(`/api/v1/areas/id/${safe}/remove`, cb);\n\t}\n\n\t// POST /api/v1/areas/place-in-area\n\tplace_in_area(payload, cb) {\n\t\t// payload: { target_area_id, place_in_area_id }\n\t\treturn this.core.api_post(\"/api/v1/areas/place-in-area\", payload, cb);\n\t}\n\n\t// POST /api/v1/areas/update-display-name\n\tupdate_display_name(payload, cb) {\n\t\t// payload: { area_id, display_name }\n\t\treturn this.core.api_post(\"/api/v1/areas/update-display-name\", payload, cb);\n\t}\n\n\t// POST /api/v1/areas/update-alias\n\tupdate_alias(payload, cb) {\n\t\t// payload: { area_id, new_alias }\n\t\treturn this.core.api_post(\"/api/v1/areas/update-alias\", payload, cb);\n\t}\n\n\t// GET /api/v1/areas/id/{{area_id}}/devices\n\tdevices(area_id, cb) {\n\t\tconst safe = encodeURIComponent(String(area_id));\n\t\treturn this.core.api_get(`/api/v1/areas/id/${safe}/devices`, cb);\n\t}\n\n\t// GET /api/v1/areas/id/{{area_id}}/unassign-from-area\n\tunassign_from_area(area_id, cb) {\n\t\tconst safe = encodeURIComponent(String(area_id));\n\t\treturn this.core.api_get(`/api/v1/areas/id/${safe}/unassign-from-area`, cb);\n\t}\n\n\t// GET /api/v1/areas/types/list\n\ttypes_list(cb) {\n\t\treturn this.core.api_get(\"/api/v1/areas/types/list\", cb);\n\t}\n\n\t// GET /api/v1/areas/reboot_devices\n\t// GET /api/v1/areas/id/{{area_id}}/reboot_devices\n\treboot_devices(area_id, cb) {\n\t\tif (area_id === undefined || area_id === null) {\n\t\t\treturn this.core.api_get(\"/api/v1/areas/reboot_devices\", cb);\n\t\t}\n\t\tconst safe = encodeURIComponent(String(area_id));\n\t\treturn this.core.api_get(`/api/v1/areas/id/${safe}/reboot_devices`, cb);\n\t}\n}", "/**\n * smart_home_api.js\n *\n * \u041C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u0430\u044F JS-\u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u0434\u043B\u044F REST-\u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432 \u043A \u0441\u0435\u0440\u0432\u0435\u0440\u0443 (callback-style).\n * - \u0410\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u044F: Bearer token (\u0438\u043B\u0438 \u043A\u0430\u0441\u0442\u043E\u043C\u043D\u044B\u0439 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A, \u0435\u0441\u043B\u0438 \u043F\u043E\u043C\u0435\u043D\u044F\u0435\u0448\u044C)\n * - \u0415\u0434\u0438\u043D\u0430\u044F \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0430 \u043E\u0448\u0438\u0431\u043E\u043A: \u0441\u0435\u0442\u0435\u0432\u044B\u0435, \u0442\u0430\u0439\u043C\u0430\u0443\u0442, \u043D\u0435-JSON, \u0441\u0442\u0430\u0442\u0443\u0441=false/error\n * - \u041C\u043E\u0434\u0443\u043B\u0438: \u0441\u0435\u0439\u0447\u0430\u0441 \u0442\u043E\u043B\u044C\u043A\u043E Scripts, \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u043F\u043E \u0430\u043D\u0430\u043B\u043E\u0433\u0438\u0438\n */\n\nimport { ScriptsApi } from \"./modules/ScriptsApi.js\";\nimport { DevicesApi } from \"./modules/DevicesApi.js\";\nimport { AreasApi } from \"./modules/AreasApi.js\";\n\n/* =========================\n Utils\n========================= */\n\nfunction build_query(params) {\n\tif (!params || typeof params !== \"object\") return \"\";\n\tconst usp = new URLSearchParams();\n\tObject.entries(params).forEach(([k, v]) => {\n\t\tif (v === undefined || v === null) return;\n\t\tusp.append(k, String(v));\n\t});\n\tconst s = usp.toString();\n\treturn s ? `?${s}` : \"\";\n}\n\nfunction join_url(base_url, path) {\n\tconst b = String(base_url || \"\").replace(/\\/+$/, \"\");\n\tconst p = String(path || \"\").replace(/^\\/+/, \"\");\n\treturn `${b}/${p}`;\n}\n\nfunction safe_json_parse(text) {\n\ttry {\n\t\treturn { ok: true, data: JSON.parse(text) };\n\t} catch (e) {\n\t\treturn { ok: false, error: e };\n\t}\n}\n\n/* =========================\n Core client\n========================= */\n\nexport class SmartHomeApi {\n\t/**\n\t * @param {Object} opts\n\t * @param {string} opts.base_url - \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: http://192.168.2.101\n\t * @param {string} [opts.token] - \u0442\u043E\u043A\u0435\u043D \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438\n\t * @param {number} [opts.timeout_ms=15000]\n\t * @param {Object} [opts.default_headers]\n\t * @param {Function} [opts.on_unauthorized] - cb(details)\n\t * @param {string} [opts.proxy_path] \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \"/proxy.php\" (\u0432\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0430\u0432\u0442\u043E-\u043F\u0440\u043E\u043A\u0441\u0438)\n\t */\n\tconstructor(opts) {\n\t\tthis.base_url = opts?.base_url || \"\";\n\t\tthis.token = opts?.token || \"\";\n\t\tthis.timeout_ms = Number.isFinite(opts?.timeout_ms) ? opts.timeout_ms : 15000;\n\t\tthis.default_headers = opts?.default_headers || {};\n\t\tthis.on_unauthorized = typeof opts?.on_unauthorized === \"function\" ? opts.on_unauthorized : null;\n\t\tthis.proxy_path = opts?.proxy_path || \"\"; // \"\" => \u0431\u0435\u0437 \u043F\u0440\u043E\u043A\u0441\u0438\n\n\t\t// modules\n\t\tthis.scripts = new ScriptsApi(this);\n\t\tthis.devices = new DevicesApi(this);\n\t\tthis.areas = new AreasApi(this);\n\t}\n\n\tset_base_url(base_url) {\n\t\tthis.base_url = base_url || \"\";\n\t}\n\n\tset_token(token) {\n\t\tthis.token = token || \"\";\n\t}\n\n\tset_proxy_path(proxy_path) {\n\t\tthis.proxy_path = proxy_path || \"\";\n\t}\n\n\t_wrap_path(path, extra_query) {\n\t\t// \u0415\u0441\u043B\u0438 \u0432\u043A\u043B\u044E\u0447\u0451\u043D \u043F\u0440\u043E\u043A\u0441\u0438 \u2014 \u0445\u043E\u0434\u0438\u043C \u043D\u0430 /proxy.php?path=
&...\n\t\tif (!this.proxy_path) {\n\t\t\tif (!extra_query) return path;\n\t\t\treturn `${path}${build_query(extra_query)}`;\n\t\t}\n\n\t\tconst q = { path, ...(extra_query || {}) };\n\t\treturn `${this.proxy_path}${build_query(q)}`;\n\t}\n\n\t/**\n\t * \u0423\u043D\u0438\u0444\u0438\u0446\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u0437\u0430\u043F\u0440\u043E\u0441.\n\t *\n\t * cb(err, data, meta)\n\t * - err: { type, message, status_code?, raw?, details? }\n\t * - data: \u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0435\u043D\u043D\u044B\u0439 json (\u0438\u043B\u0438 string, \u0435\u0441\u043B\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B json)\n\t * - meta: { url, method, status_code, headers }\n\t */\n\trequest(method, path, body, cb, opts) {\n\t\tconst callback = typeof cb === \"function\" ? cb : () => {};\n\t\tconst url = join_url(this.base_url, path);\n\n\t\tconst controller = new AbortController();\n\t\tconst timeout_ms = Number.isFinite(opts?.timeout_ms) ? opts.timeout_ms : this.timeout_ms;\n\n\t\tconst t = setTimeout(() => controller.abort(), timeout_ms);\n\n\t\tconst headers = {\n\t\t\t...this.default_headers,\n\t\t\t...(opts?.headers || {}),\n\t\t};\n\n\t\t// \u0410\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u044F (\u043F\u043E\u0434\u0441\u0442\u0440\u043E\u0439, \u0435\u0441\u043B\u0438 \u0443 \u0442\u0435\u0431\u044F \u0434\u0440\u0443\u0433\u043E\u0439 \u0444\u043E\u0440\u043C\u0430\u0442)\n\t\tif (this.token) headers[\"Authorization\"] = `Bearer ${this.token}`;\n\n\t\tlet payload = undefined;\n\t\tif (body !== undefined && body !== null) {\n\t\t\theaders[\"Content-Type\"] = \"application/json\";\n\t\t\tpayload = JSON.stringify(body);\n\t\t}\n\n\t\tfetch(url, {\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\tbody: payload,\n\t\t\tsignal: controller.signal,\n\t\t})\n\t\t\t.then(async (res) => {\n\t\t\t\tclearTimeout(t);\n\n\t\t\t\tconst meta = {\n\t\t\t\t\turl,\n\t\t\t\t\tmethod,\n\t\t\t\t\tstatus_code: res.status,\n\t\t\t\t\theaders: res.headers,\n\t\t\t\t};\n\n\t\t\t\tconst text = await res.text();\n\t\t\t\tconst parsed = safe_json_parse(text);\n\t\t\t\tconst data = parsed.ok ? parsed.data : text;\n\n\t\t\t\t// HTTP-level \u043E\u0448\u0438\u0431\u043A\u0438\n\t\t\t\tif (!res.ok) {\n\t\t\t\t\tconst err = {\n\t\t\t\t\t\ttype: \"http_error\",\n\t\t\t\t\t\tmessage: `HTTP ${res.status}`,\n\t\t\t\t\t\tstatus_code: res.status,\n\t\t\t\t\t\traw: data,\n\t\t\t\t\t};\n\n\t\t\t\t\tif (res.status === 401 || res.status === 403) {\n\t\t\t\t\t\tif (this.on_unauthorized) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tthis.on_unauthorized({ error: err, meta });\n\t\t\t\t\t\t\t} catch (_) {}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn callback(err, null, meta);\n\t\t\t\t}\n\n\t\t\t\t// API-level \u043E\u0448\u0438\u0431\u043A\u0438 (\u043F\u043E \u0442\u0432\u043E\u0438\u043C \u043F\u0440\u0438\u043C\u0435\u0440\u0430\u043C \u0431\u044B\u0432\u0430\u0435\u0442 status:false \u0438\u043B\u0438 status:\"error\")\n\t\t\t\tif (parsed.ok && data && typeof data === \"object\") {\n\t\t\t\t\tconst st = data.status;\n\t\t\t\t\tif (st === false || st === \"error\") {\n\t\t\t\t\t\tconst err = {\n\t\t\t\t\t\t\ttype: \"api_error\",\n\t\t\t\t\t\t\tmessage: data.message || \"API error\",\n\t\t\t\t\t\t\tstatus_code: res.status,\n\t\t\t\t\t\t\traw: data,\n\t\t\t\t\t\t\tfield: data.field,\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn callback(err, null, meta);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn callback(null, data, meta);\n\t\t\t})\n\t\t\t.catch((e) => {\n\t\t\t\tclearTimeout(t);\n\n\t\t\t\tconst is_abort = e && (e.name === \"AbortError\" || String(e).includes(\"AbortError\"));\n\t\t\t\tconst err = is_abort\n\t\t\t\t\t? { type: \"timeout\", message: `Timeout after ${timeout_ms}ms` }\n\t\t\t\t\t: { type: \"network_error\", message: e?.message || \"Network error\", details: e };\n\n\t\t\t\treturn callback(err, null, { url, method, status_code: 0, headers: null });\n\t\t\t});\n\t}\n\n\tget(path, cb, opts) {\n\t\treturn this.request(\"GET\", path, null, cb, opts);\n\t}\n\n\tpost(path, body, cb, opts) {\n\t\treturn this.request(\"POST\", path, body, cb, opts);\n\t}\n\n\tapi_get(api_path, cb, extra_query, opts) {\n\t\treturn this.get(this._wrap_path(api_path, extra_query), cb, opts);\n\t}\n\n\tapi_post(api_path, body, cb, extra_query, opts) {\n\t\treturn this.post(this._wrap_path(api_path, extra_query), body, cb, opts);\n\t}\n}\n\n/* =========================\n Example usage\n========================= */\n\n// import { SmartHomeApi } from \"./smart_home_api.js\";\n//\n// const api = new SmartHomeApi({\n// base_url: \"http://192.168.2.101\",\n// token: \"YOUR_TOKEN\",\n// timeout_ms: 20000,\n// on_unauthorized: ({ error }) => console.log(\"auth problem:\", error),\n// });\n//\n// api.scripts.actions_list((err, res) => {\n// if (err) return console.error(\"actions_list error:\", err);\n// console.log(\"actions:\", res);\n// });\n//\n// api.scripts.run({ alias: \"script_alias\", params: { x: 1 } }, (err, res) => {\n// if (err) return console.error(\"run error:\", err);\n// console.log(\"run result:\", res);\n// });\n", "import Toasts from \"./components/toasts.js\";\nimport Modals from \"./components/modals.js\";\n\nwindow.demoToastInfo = function () {\n\tToasts.createInfo(\n\t\t\"Title\",\n\t\t\"\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.\"\n\t).show();\n}\n\nwindow.demoToastSuccess = function () {\n\tToasts.createSuccess(\n\t\t\"Success\",\n\t\t\"\u0412\u0441\u0451 \u043E\u0442\u043B\u0438\u0447\u043D\u043E, \u0432\u0441\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u044D\u0442\u043E \u0443\u0441\u043F\u0435\u0445 \u0442\u043E\u0441\u0442\u0435\u0440\u0430\"\n\t).show();\n}\n\nwindow.demoToastWarning = function () {\n\tToasts.createWarning(\n\t\t\"Warning\",\n\t\t\"\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.\"\n\t).show();\n}\n\nwindow.demoToastDanger = function () {\n\tToasts.createDanger(\n\t\t\"Danger\",\n\t\t\"\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.\"\n\t).show();\n}\n\nwindow.demoModal = function() {\n\tModals.create(\"demo-modals\", {\n\t\ttitle: \"Demo modal window\",\n\t\tfooter: \"Footer text
\",\n\t\tactions: modal => {\n\t\t\tconst buttonCancel = document.createElement(\"button\");\n\t\t\tbuttonCancel.classList.add(\"btn\");\n\t\t\tbuttonCancel.classList.add(\"btn-primary\");\n\t\t\tbuttonCancel.innerHTML = \"Cancel\";\n\n\t\t\tbuttonCancel.addEventListener(\"click\", e => {\n\t\t\t\tmodal.close();\n\t\t\t});\n\n\t\t\tconst buttonApply = document.createElement(\"button\");\n\t\t\tbuttonApply.classList.add(\"btn\");\n\t\t\tbuttonApply.classList.add(\"btn-success\");\n\t\t\tbuttonApply.innerHTML = \"Apply\";\n\n\t\t\tbuttonApply.addEventListener(\"click\", e => {\n\t\t\t\tmodal.close();\n\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tToasts.createSuccess(\n\t\t\t\t\t\t\"Success\",\n\t\t\t\t\t\t\"\u0412\u0441\u0451 \u043E\u0442\u043B\u0438\u0447\u043D\u043E, \u0432\u0441\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u044D\u0442\u043E \u0443\u0441\u043F\u0435\u0445\"\n\t\t\t\t\t).show();\n\t\t\t\t}, 300);\n\t\t\t});\n\n\t\t\treturn [ buttonCancel, buttonApply ];\n\t\t},\n\t\tbody: modal => {\n\t\t\treturn `\n\t\t\t\t\u041B\u044E\u0431\u043E\u0439 \u043A\u043E\u043D\u0442\u0435\u043D\u0442: \u0442\u0435\u043A\u0441\u0442, \u0444\u043E\u0440\u043C\u044B, \u0441\u043F\u0438\u0441\u043A\u0438.
\n\n \n \n Device name\n \n \n
\n\n \n \n Description\n \n \n
\n\t\t\t`;\n\t\t}\n\t}).show();\n}\n\nimport { SmartHomeApi } from \"./sh/SmartHomeApi.js\";\n\nfunction shApiDebuging() {\n\tconst api = new SmartHomeApi({\n\t base_url: \"http://shswebclient.local\",\n\t token: \"YOUR_TOKEN\",\n\t timeout_ms: 3000,\n\t on_unauthorized: ({ error }) => console.log(\"auth problem:\", error),\n\t proxy_path: \"/proxy.php\",\n\t});\n\n\tapi.scripts.actions_list((err, data, meta) => console.log(data));\n\tapi.scripts.scopes_list((err, data, meta) => console.log(data));\n\tapi.devices.info(4, (err, data, meta) => console.log(data));\n}\n\n\ndocument.addEventListener(\"DOMContentLoaded\", e => {\n\tconsole.log(\"App init\");\n\n\tshApiDebuging();\n});"],
+ "mappings": "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",
+ "names": ["template", "type", "icon", "title", "text", "init", "toast", "create", "div", "createSuccess", "createInfo", "createWarning", "createError", "toasts_default", "template", "id", "title", "footer", "init", "modal", "create", "props", "div", "modalBody", "modalFooter", "actionsResult", "actions", "actionElement", "bodyResult", "modals_default", "ScriptsApi", "core", "cb", "filename", "safe", "payload", "alias", "name", "DevicesApi", "core", "cb", "payload", "id", "safe", "AreasApi", "core", "cb", "area_id", "safe", "payload", "build_query", "params", "usp", "k", "v", "s", "join_url", "base_url", "path", "b", "p", "safe_json_parse", "text", "SmartHomeApi", "opts", "ScriptsApi", "DevicesApi", "AreasApi", "token", "proxy_path", "extra_query", "q", "__spreadValues", "method", "body", "cb", "callback", "url", "controller", "timeout_ms", "t", "headers", "payload", "res", "__async", "meta", "parsed", "data", "err", "_", "st", "e", "api_path", "toasts_default", "modals_default", "modal", "buttonCancel", "e", "buttonApply", "shApiDebuging", "api", "SmartHomeApi", "error", "err", "data", "meta"]
}
diff --git a/webclient/proxy.php b/webclient/proxy.php
new file mode 100644
index 0000000..16f8979
--- /dev/null
+++ b/webclient/proxy.php
@@ -0,0 +1,183 @@
+ false,
+ 'message' => $message,
+ ], $extra), JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+function get_request_headers_lower() {
+ $headers = [];
+ foreach (getallheaders() as $k => $v) {
+ $headers[strtolower($k)] = $v;
+ }
+ return $headers;
+}
+
+function cors_headers($origin, $allowed_origins) {
+ if ($origin && in_array($origin, $allowed_origins, true)) {
+ header("Access-Control-Allow-Origin: {$origin}");
+ header("Vary: Origin");
+ } else {
+ // Если хочешь разрешить всем — раскомментируй (но лучше белый список)
+ header("Access-Control-Allow-Origin: *");
+ }
+
+ header("Access-Control-Allow-Credentials: true");
+ header("Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS");
+ header("Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Requested-With");
+ header("Access-Control-Max-Age: 86400");
+}
+
+// =========================
+// CORS preflight
+// =========================
+$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
+cors_headers($origin, $allowed_origins);
+
+if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
+ http_response_code(204);
+ exit;
+}
+
+// =========================
+// Валидация входа
+// =========================
+
+// Ожидаем ?path=/api/v1/...
+$path = $_GET['path'] ?? '';
+if (!$path || $path[0] !== '/') {
+ send_json_error(400, 'Missing or invalid "path" parameter. Example: ?path=/api/v1/scripts/actions/list');
+}
+
+// белый список путей
+$ok = false;
+foreach ($allowed_prefixes as $p) {
+ if (str_starts_with($path, $p)) {
+ $ok = true;
+ break;
+ }
+}
+if (!$ok) {
+ send_json_error(403, 'Path not allowed', ['path' => $path]);
+}
+
+// Собираем URL апстрима + query string (кроме path)
+$query = $_GET;
+unset($query['path']);
+$qs = http_build_query($query);
+$upstream_url = rtrim($upstream_base_url, '/') . $path . ($qs ? ('?' . $qs) : '');
+
+// =========================
+// Проксирование через cURL
+// =========================
+$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
+$headers_in = get_request_headers_lower();
+
+$ch = curl_init($upstream_url);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
+curl_setopt($ch, CURLOPT_HEADER, true); // чтобы разделить headers/body
+curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
+curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+
+// Проксируем body для методов кроме GET/HEAD
+$body = file_get_contents('php://input');
+if (!in_array($method, ['GET', 'HEAD'], true)) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
+}
+
+// Заголовки, которые прокидываем
+$forward_headers = [];
+
+// Authorization
+if (!empty($headers_in['authorization'])) {
+ $forward_headers[] = 'Authorization: ' . $headers_in['authorization'];
+}
+
+// Content-Type
+if (!empty($headers_in['content-type'])) {
+ $forward_headers[] = 'Content-Type: ' . $headers_in['content-type'];
+}
+
+// Accept
+if (!empty($headers_in['accept'])) {
+ $forward_headers[] = 'Accept: ' . $headers_in['accept'];
+}
+
+// Можно добавить свой заголовок, например X-Proxy
+$forward_headers[] = 'X-Proxy: php';
+
+curl_setopt($ch, CURLOPT_HTTPHEADER, $forward_headers);
+
+$response = curl_exec($ch);
+if ($response === false) {
+ $err = curl_error($ch);
+ curl_close($ch);
+ send_json_error(502, 'Upstream request failed', ['details' => $err]);
+}
+
+$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+curl_close($ch);
+
+$raw_headers = substr($response, 0, $header_size);
+$resp_body = substr($response, $header_size);
+
+// =========================
+// Отдаём ответ клиенту
+// =========================
+http_response_code($http_code);
+
+// Проксируем часть заголовков ответа (без CORS/опасных)
+$lines = preg_split("/\r\n|\n|\r/", trim($raw_headers));
+foreach ($lines as $line) {
+ if (stripos($line, 'HTTP/') === 0) continue;
+
+ $pos = strpos($line, ':');
+ if ($pos === false) continue;
+
+ $name = trim(substr($line, 0, $pos));
+ $value = trim(substr($line, $pos + 1));
+
+ $name_l = strtolower($name);
+
+ // не прокидываем hop-by-hop и то, что конфликтует
+ if (in_array($name_l, ['transfer-encoding', 'content-length', 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'upgrade'], true)) {
+ continue;
+ }
+
+ // CORS мы уже выставили сами
+ if (str_starts_with($name_l, 'access-control-')) {
+ continue;
+ }
+
+ header($name . ': ' . $value, false);
+}
+
+echo $resp_body;
diff --git a/webclient/src/js/index.js b/webclient/src/js/index.js
index 57a4d9a..c61d4ea 100644
--- a/webclient/src/js/index.js
+++ b/webclient/src/js/index.js
@@ -83,6 +83,25 @@
}).show();
}
+import { SmartHomeApi } from "./sh/SmartHomeApi.js";
+
+function shApiDebuging() {
+ const api = new SmartHomeApi({
+ base_url: "http://shswebclient.local",
+ token: "YOUR_TOKEN",
+ timeout_ms: 3000,
+ on_unauthorized: ({ error }) => console.log("auth problem:", error),
+ proxy_path: "/proxy.php",
+ });
+
+ api.scripts.actions_list((err, data, meta) => console.log(data));
+ api.scripts.scopes_list((err, data, meta) => console.log(data));
+ api.devices.info(4, (err, data, meta) => console.log(data));
+}
+
+
document.addEventListener("DOMContentLoaded", e => {
console.log("App init");
+
+ shApiDebuging();
});
\ No newline at end of file
diff --git a/webclient/src/js/sh/SmartHomeApi.js b/webclient/src/js/sh/SmartHomeApi.js
new file mode 100644
index 0000000..acca308
--- /dev/null
+++ b/webclient/src/js/sh/SmartHomeApi.js
@@ -0,0 +1,232 @@
+/**
+ * smart_home_api.js
+ *
+ * Минимальная JS-библиотека для REST-запросов к серверу (callback-style).
+ * - Авторизация: Bearer token (или кастомный заголовок, если поменяешь)
+ * - Единая обработка ошибок: сетевые, таймаут, не-JSON, статус=false/error
+ * - Модули: сейчас только Scripts, остальные по аналогии
+ */
+
+import { ScriptsApi } from "./modules/ScriptsApi.js";
+import { DevicesApi } from "./modules/DevicesApi.js";
+import { AreasApi } from "./modules/AreasApi.js";
+
+/* =========================
+ Utils
+========================= */
+
+function build_query(params) {
+ if (!params || typeof params !== "object") return "";
+ const usp = new URLSearchParams();
+ Object.entries(params).forEach(([k, v]) => {
+ if (v === undefined || v === null) return;
+ usp.append(k, String(v));
+ });
+ const s = usp.toString();
+ return s ? `?${s}` : "";
+}
+
+function join_url(base_url, path) {
+ const b = String(base_url || "").replace(/\/+$/, "");
+ const p = String(path || "").replace(/^\/+/, "");
+ return `${b}/${p}`;
+}
+
+function safe_json_parse(text) {
+ try {
+ return { ok: true, data: JSON.parse(text) };
+ } catch (e) {
+ return { ok: false, error: e };
+ }
+}
+
+/* =========================
+ Core client
+========================= */
+
+export class SmartHomeApi {
+ /**
+ * @param {Object} opts
+ * @param {string} opts.base_url - например: http://192.168.2.101
+ * @param {string} [opts.token] - токен авторизации
+ * @param {number} [opts.timeout_ms=15000]
+ * @param {Object} [opts.default_headers]
+ * @param {Function} [opts.on_unauthorized] - cb(details)
+ * @param {string} [opts.proxy_path] например "/proxy.php" (включает авто-прокси)
+ */
+ constructor(opts) {
+ this.base_url = opts?.base_url || "";
+ this.token = opts?.token || "";
+ this.timeout_ms = Number.isFinite(opts?.timeout_ms) ? opts.timeout_ms : 15000;
+ this.default_headers = opts?.default_headers || {};
+ this.on_unauthorized = typeof opts?.on_unauthorized === "function" ? opts.on_unauthorized : null;
+ this.proxy_path = opts?.proxy_path || ""; // "" => без прокси
+
+ // modules
+ this.scripts = new ScriptsApi(this);
+ this.devices = new DevicesApi(this);
+ this.areas = new AreasApi(this);
+ }
+
+ set_base_url(base_url) {
+ this.base_url = base_url || "";
+ }
+
+ set_token(token) {
+ this.token = token || "";
+ }
+
+ set_proxy_path(proxy_path) {
+ this.proxy_path = proxy_path || "";
+ }
+
+ _wrap_path(path, extra_query) {
+ // Если включён прокси — ходим на /proxy.php?path=&...
+ if (!this.proxy_path) {
+ if (!extra_query) return path;
+ return `${path}${build_query(extra_query)}`;
+ }
+
+ const q = { path, ...(extra_query || {}) };
+ return `${this.proxy_path}${build_query(q)}`;
+ }
+
+ /**
+ * Унифицированный запрос.
+ *
+ * cb(err, data, meta)
+ * - err: { type, message, status_code?, raw?, details? }
+ * - data: распарсенный json (или string, если сервер не вернул json)
+ * - meta: { url, method, status_code, headers }
+ */
+ request(method, path, body, cb, opts) {
+ const callback = typeof cb === "function" ? cb : () => {};
+ const url = join_url(this.base_url, path);
+
+ const controller = new AbortController();
+ const timeout_ms = Number.isFinite(opts?.timeout_ms) ? opts.timeout_ms : this.timeout_ms;
+
+ const t = setTimeout(() => controller.abort(), timeout_ms);
+
+ const headers = {
+ ...this.default_headers,
+ ...(opts?.headers || {}),
+ };
+
+ // Авторизация (подстрой, если у тебя другой формат)
+ if (this.token) headers["Authorization"] = `Bearer ${this.token}`;
+
+ let payload = undefined;
+ if (body !== undefined && body !== null) {
+ headers["Content-Type"] = "application/json";
+ payload = JSON.stringify(body);
+ }
+
+ fetch(url, {
+ method,
+ headers,
+ body: payload,
+ signal: controller.signal,
+ })
+ .then(async (res) => {
+ clearTimeout(t);
+
+ const meta = {
+ url,
+ method,
+ status_code: res.status,
+ headers: res.headers,
+ };
+
+ const text = await res.text();
+ const parsed = safe_json_parse(text);
+ const data = parsed.ok ? parsed.data : text;
+
+ // HTTP-level ошибки
+ if (!res.ok) {
+ const err = {
+ type: "http_error",
+ message: `HTTP ${res.status}`,
+ status_code: res.status,
+ raw: data,
+ };
+
+ if (res.status === 401 || res.status === 403) {
+ if (this.on_unauthorized) {
+ try {
+ this.on_unauthorized({ error: err, meta });
+ } catch (_) {}
+ }
+ }
+
+ return callback(err, null, meta);
+ }
+
+ // API-level ошибки (по твоим примерам бывает status:false или status:"error")
+ if (parsed.ok && data && typeof data === "object") {
+ const st = data.status;
+ if (st === false || st === "error") {
+ const err = {
+ type: "api_error",
+ message: data.message || "API error",
+ status_code: res.status,
+ raw: data,
+ field: data.field,
+ };
+ return callback(err, null, meta);
+ }
+ }
+
+ return callback(null, data, meta);
+ })
+ .catch((e) => {
+ clearTimeout(t);
+
+ const is_abort = e && (e.name === "AbortError" || String(e).includes("AbortError"));
+ const err = is_abort
+ ? { type: "timeout", message: `Timeout after ${timeout_ms}ms` }
+ : { type: "network_error", message: e?.message || "Network error", details: e };
+
+ return callback(err, null, { url, method, status_code: 0, headers: null });
+ });
+ }
+
+ get(path, cb, opts) {
+ return this.request("GET", path, null, cb, opts);
+ }
+
+ post(path, body, cb, opts) {
+ return this.request("POST", path, body, cb, opts);
+ }
+
+ api_get(api_path, cb, extra_query, opts) {
+ return this.get(this._wrap_path(api_path, extra_query), cb, opts);
+ }
+
+ api_post(api_path, body, cb, extra_query, opts) {
+ return this.post(this._wrap_path(api_path, extra_query), body, cb, opts);
+ }
+}
+
+/* =========================
+ Example usage
+========================= */
+
+// import { SmartHomeApi } from "./smart_home_api.js";
+//
+// const api = new SmartHomeApi({
+// base_url: "http://192.168.2.101",
+// token: "YOUR_TOKEN",
+// timeout_ms: 20000,
+// on_unauthorized: ({ error }) => console.log("auth problem:", error),
+// });
+//
+// api.scripts.actions_list((err, res) => {
+// if (err) return console.error("actions_list error:", err);
+// console.log("actions:", res);
+// });
+//
+// api.scripts.run({ alias: "script_alias", params: { x: 1 } }, (err, res) => {
+// if (err) return console.error("run error:", err);
+// console.log("run result:", res);
+// });
diff --git a/webclient/src/js/sh/modules/AreasApi.js b/webclient/src/js/sh/modules/AreasApi.js
new file mode 100644
index 0000000..c4c36a5
--- /dev/null
+++ b/webclient/src/js/sh/modules/AreasApi.js
@@ -0,0 +1,73 @@
+export class AreasApi {
+ constructor(core) {
+ this.core = core;
+ }
+
+ // GET /api/v1/areas/list
+ list(cb) {
+ return this.core.api_get("/api/v1/areas/list", cb);
+ }
+
+ // GET /api/v1/areas/id/{{area_id}}/list
+ inner_list(area_id, cb) {
+ const safe = encodeURIComponent(String(area_id));
+ return this.core.api_get(`/api/v1/areas/id/${safe}/list`, cb);
+ }
+
+ // POST /api/v1/areas/new-area
+ new_area(payload, cb) {
+ // payload: { type, alias, display_name }
+ return this.core.api_post("/api/v1/areas/new-area", payload, cb);
+ }
+
+ // GET /api/v1/areas/id/{{area_id}}/remove
+ remove(area_id, cb) {
+ const safe = encodeURIComponent(String(area_id));
+ return this.core.api_get(`/api/v1/areas/id/${safe}/remove`, cb);
+ }
+
+ // POST /api/v1/areas/place-in-area
+ place_in_area(payload, cb) {
+ // payload: { target_area_id, place_in_area_id }
+ return this.core.api_post("/api/v1/areas/place-in-area", payload, cb);
+ }
+
+ // POST /api/v1/areas/update-display-name
+ update_display_name(payload, cb) {
+ // payload: { area_id, display_name }
+ return this.core.api_post("/api/v1/areas/update-display-name", payload, cb);
+ }
+
+ // POST /api/v1/areas/update-alias
+ update_alias(payload, cb) {
+ // payload: { area_id, new_alias }
+ return this.core.api_post("/api/v1/areas/update-alias", payload, cb);
+ }
+
+ // GET /api/v1/areas/id/{{area_id}}/devices
+ devices(area_id, cb) {
+ const safe = encodeURIComponent(String(area_id));
+ return this.core.api_get(`/api/v1/areas/id/${safe}/devices`, cb);
+ }
+
+ // GET /api/v1/areas/id/{{area_id}}/unassign-from-area
+ unassign_from_area(area_id, cb) {
+ const safe = encodeURIComponent(String(area_id));
+ return this.core.api_get(`/api/v1/areas/id/${safe}/unassign-from-area`, cb);
+ }
+
+ // GET /api/v1/areas/types/list
+ types_list(cb) {
+ return this.core.api_get("/api/v1/areas/types/list", cb);
+ }
+
+ // GET /api/v1/areas/reboot_devices
+ // GET /api/v1/areas/id/{{area_id}}/reboot_devices
+ reboot_devices(area_id, cb) {
+ if (area_id === undefined || area_id === null) {
+ return this.core.api_get("/api/v1/areas/reboot_devices", cb);
+ }
+ const safe = encodeURIComponent(String(area_id));
+ return this.core.api_get(`/api/v1/areas/id/${safe}/reboot_devices`, cb);
+ }
+}
\ No newline at end of file
diff --git a/webclient/src/js/sh/modules/DevicesApi.js b/webclient/src/js/sh/modules/DevicesApi.js
new file mode 100644
index 0000000..3e2745c
--- /dev/null
+++ b/webclient/src/js/sh/modules/DevicesApi.js
@@ -0,0 +1,66 @@
+/* =========================
+ Devices module
+========================= */
+
+export class DevicesApi {
+ constructor(core) {
+ this.core = core;
+ }
+
+ // GET /api/v1/devices/list
+ list(cb) {
+ return this.core.api_get("/api/v1/devices/list", cb);
+ }
+
+ // GET /api/v1/devices/scanning/setup
+ scanning_setup(cb) {
+ return this.core.api_get("/api/v1/devices/scanning/setup", cb);
+ }
+
+ // GET /api/v1/devices/scanning/all
+ scanning_all(cb) {
+ return this.core.api_get("/api/v1/devices/scanning/all", cb);
+ }
+
+ // POST /api/v1/devices/setup/new-device
+ setup_new_device(payload, cb) {
+ // payload: { device_ip, alias, name, description }
+ return this.core.api_post("/api/v1/devices/setup/new-device", payload, cb);
+ }
+
+ // GET /api/v1/devices/id/{{id}}/info
+ info(id, cb) {
+ const safe = encodeURIComponent(String(id));
+ return this.core.api_get(`/api/v1/devices/id/${safe}/info`, cb);
+ }
+
+ // GET /api/v1/devices/id/{{id}}
+ get(id, cb) {
+ const safe = encodeURIComponent(String(id));
+ return this.core.api_get(`/api/v1/devices/id/${safe}`, cb);
+ }
+
+ // GET /api/v1/devices/id/{{id}}/status
+ status(id, cb) {
+ const safe = encodeURIComponent(String(id));
+ return this.core.api_get(`/api/v1/devices/id/${safe}/status`, cb);
+ }
+
+ // POST /api/v1/devices/action
+ action(payload, cb) {
+ // payload: { device_id, action, params }
+ return this.core.api_post("/api/v1/devices/action", payload, cb);
+ }
+
+ // GET /api/v1/devices/id/{{id}}/remove
+ remove(id, cb) {
+ const safe = encodeURIComponent(String(id));
+ return this.core.api_get(`/api/v1/devices/id/${safe}/remove`, cb);
+ }
+
+ // GET /api/v1/devices/id/{{id}}/reboot
+ reboot(id, cb) {
+ const safe = encodeURIComponent(String(id));
+ return this.core.api_get(`/api/v1/devices/id/${safe}/reboot`, cb);
+ }
+}
diff --git a/webclient/src/js/sh/modules/ScriptsApi.js b/webclient/src/js/sh/modules/ScriptsApi.js
new file mode 100644
index 0000000..95eefa7
--- /dev/null
+++ b/webclient/src/js/sh/modules/ScriptsApi.js
@@ -0,0 +1,92 @@
+/* =========================
+ Scripts module
+========================= */
+
+export class ScriptsApi {
+ constructor(core) {
+ this.core = core;
+ }
+
+ // GET /api/v1/scripts/actions/list
+ actions_list(cb) {
+ return this.core.api_get("/api/v1/scripts/actions/list", cb);
+ }
+
+ // GET /api/v1/scripts/scopes/list
+ scopes_list(cb) {
+ return this.core.api_get("/api/v1/scripts/scopes/list", cb);
+ }
+
+ // GET /api/v1/scripts/regular/list
+ regular_list(cb) {
+ return this.core.api_get("/api/v1/scripts/regular/list", cb);
+ }
+
+ // GET /api/v1/scripts/scopes/name/{{filename}}
+ scope_get_by_filename(filename, cb) {
+ const safe = encodeURIComponent(String(filename || ""));
+ return this.core.api_get(`/api/v1/scripts/scopes/name/${safe}`, cb, {
+ // тут сервер может вернуть PHP-код текстом; request умеет это пережить
+ });
+ }
+
+ // POST /api/v1/scripts/scopes/new
+ scope_create(payload, cb) {
+ // payload: { alias, filename, path }
+ return this.core.api_post("/api/v1/scripts/scopes/new", payload, cb);
+ }
+
+ // POST /api/v1/scripts/scopes/update
+ scope_update(payload, cb) {
+ // payload: { name, filename, path }
+ return this.core.api_post("/api/v1/scripts/scopes/update", payload, cb);
+ }
+
+ // GET /api/v1/scripts/actions/alias/{{alias}}/enable
+ action_enable(alias, cb) {
+ const safe = encodeURIComponent(String(alias || ""));
+ return this.core.api_get(`/api/v1/scripts/actions/alias/${safe}/enable`, cb);
+ }
+
+ // GET /api/v1/scripts/actions/alias/{{alias}}/disable
+ action_disable(alias, cb) {
+ const safe = encodeURIComponent(String(alias || ""));
+ return this.core.api_get(`/api/v1/scripts/actions/alias/${safe}/disable`, cb);
+ }
+
+ // GET /api/v1/scripts/actions/regular/{{alias}}/enable
+ regular_enable(alias, cb) {
+ const safe = encodeURIComponent(String(alias || ""));
+ return this.core.api_get(`/api/v1/scripts/actions/regular/${safe}/enable`, cb);
+ }
+
+ // GET /api/v1/scripts/actions/regular/{{alias}}/disable
+ regular_disable(alias, cb) {
+ const safe = encodeURIComponent(String(alias || ""));
+ return this.core.api_get(`/api/v1/scripts/actions/regular/${safe}/disable`, cb);
+ }
+
+ // GET /api/v1/scripts/actions/scope/{{name}}/enable
+ scope_enable(name, cb) {
+ const safe = encodeURIComponent(String(name || ""));
+ return this.core.api_get(`/api/v1/scripts/actions/scope/${safe}/enable`, cb);
+ }
+
+ // GET /api/v1/scripts/actions/scope/{{name}}/disable
+ scope_disable(name, cb) {
+ const safe = encodeURIComponent(String(name || ""));
+ return this.core.api_get(`/api/v1/scripts/actions/scope/${safe}/disable`, cb);
+ }
+
+ // GET /api/v1/scripts/scopes/name/{{name}}/remove
+ scope_remove(name, cb) {
+ const safe = encodeURIComponent(String(name || ""));
+ return this.core.api_get(`/api/v1/scripts/scopes/name/${safe}/remove`, cb);
+ }
+
+ // POST /api/v1/scripts/actions/run
+ run(payload, cb) {
+ // payload: { alias, params: {...} }
+ return this.core.api_post("/api/v1/scripts/actions/run", payload, cb);
+ }
+}
\ No newline at end of file