feat: password-based doas install, proper dark colors, delete container
- Distrobuilder install now prompts for doas password instead of requiring nopass config. Password is piped to doas via stdin. Manual install hint shown instead of doas.conf rules. - Restore original colored backgrounds (green/yellow/blue/red) for light mode and add proper dark: variants (dark:bg-*-950/30) so both themes look good - Add delete container button with confirmation dialog that requires typing the container name to confirm - Fix snapshot default name to include seconds to avoid duplicates - Fix certificate modal textarea to use theme-aware colors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c42b978df9
commit
f324f04ee9
|
|
@ -366,33 +366,41 @@ def get_setup_status():
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def run_setup(doas_password=None):
|
def run_setup(doas_password=None):
|
||||||
"""Run setup commands to install distrobuilder."""
|
"""Run setup commands to install distrobuilder using doas with password."""
|
||||||
_check_admin_permission()
|
_check_admin_permission()
|
||||||
|
|
||||||
logs = []
|
logs = []
|
||||||
logs.append("=== Starting Distrobuilder Setup ===")
|
logs.append("=== Starting Distrobuilder Setup ===")
|
||||||
|
|
||||||
|
if not doas_password:
|
||||||
|
return {"success": False, "error": "Password is required", "logs": ["Error: Password is required"]}
|
||||||
|
|
||||||
commands = [
|
commands = [
|
||||||
(["doas", "-n", "apk", "add", "distrobuilder"], "Installing distrobuilder"),
|
(["doas", "apk", "add", "distrobuilder"], "Installing distrobuilder"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for cmd, description in commands:
|
for cmd, description in commands:
|
||||||
logs.append(f"\n{description}...")
|
logs.append(f"\n{description}...")
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
proc = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
|
)
|
||||||
|
stdout, stderr = proc.communicate(
|
||||||
|
input=doas_password + "\n",
|
||||||
timeout=120,
|
timeout=120,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.stdout:
|
if stdout:
|
||||||
logs.append(result.stdout.strip())
|
logs.append(stdout.strip())
|
||||||
|
|
||||||
if result.returncode != 0:
|
if proc.returncode != 0:
|
||||||
error = result.stderr or "Unknown error"
|
error = stderr or "Unknown error"
|
||||||
if "already" in error.lower() or "exists" in error.lower():
|
if "already" in error.lower() or "exists" in error.lower():
|
||||||
logs.append(f" (already done)")
|
logs.append(" (already done)")
|
||||||
else:
|
else:
|
||||||
logs.append(f"Failed: {error}")
|
logs.append(f"Failed: {error}")
|
||||||
return {"success": False, "error": error, "logs": logs}
|
return {"success": False, "error": error, "logs": logs}
|
||||||
|
|
@ -400,6 +408,7 @@ def run_setup(doas_password=None):
|
||||||
logs.append(f"{description} - Done")
|
logs.append(f"{description} - Done")
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
logs.append(f"Timeout: {description}")
|
logs.append(f"Timeout: {description}")
|
||||||
return {"success": False, "error": f"Timeout during: {description}", "logs": logs}
|
return {"success": False, "error": f"Timeout during: {description}", "logs": logs}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
||||||
import{o as a,c as r,b as e,t as i,M as s}from"./index-ce79e179.js";const n={class:"rounded-lg border p-4 shadow-sm"},c={class:"flex items-center justify-between mb-4"},d={class:"text-base font-medium text-ink-gray-9"},l={class:"h-[200px]"},p={__name:"ChartCard",props:{title:{type:String,required:!0}},setup(o){return(t,_)=>(a(),r("div",n,[e("div",c,[e("h3",d,i(o.title),1),s(t.$slots,"actions")]),e("div",l,[s(t.$slots,"default")])]))}};export{p as _};
|
import{o as a,c as r,b as e,t as i,M as s}from"./index-3d9a708e.js";const n={class:"rounded-lg border p-4 shadow-sm"},c={class:"flex items-center justify-between mb-4"},d={class:"text-base font-medium text-ink-gray-9"},l={class:"h-[200px]"},p={__name:"ChartCard",props:{title:{type:String,required:!0}},setup(o){return(t,_)=>(a(),r("div",n,[e("div",c,[e("h3",d,i(o.title),1),s(t.$slots,"actions")]),e("div",l,[s(t.$slots,"default")])]))}};export{p as _};
|
||||||
//# sourceMappingURL=ChartCard-ccfa432b.js.map
|
//# sourceMappingURL=ChartCard-55f3f2ee.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"ChartCard-ccfa432b.js","sources":["../../../../frontend/src/components/Common/ChartCard.vue"],"sourcesContent":["<template>\n <div class=\"rounded-lg border p-4 shadow-sm\">\n <div class=\"flex items-center justify-between mb-4\">\n <h3 class=\"text-base font-medium text-ink-gray-9\">\n {{ title }}\n </h3>\n <slot name=\"actions\" />\n </div>\n <div class=\"h-[200px]\">\n <slot />\n </div>\n </div>\n</template>\n\n<script setup>\ndefineProps({\n title: {\n type: String,\n required: true,\n },\n})\n</script>\n"],"names":["_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_toDisplayString","__props","_renderSlot","_ctx","_hoisted_4"],"mappings":"sUACEA,EAAA,EAAAC,EAUM,MAVNC,EAUM,CATJC,EAKM,MALNC,EAKM,CAJJD,EAEK,KAFLE,EAEKC,EADAC,EAAK,KAAA,EAAA,CAAA,EAEVC,EAAuBC,EAAA,OAAA,SAAA,IAEzBN,EAEM,MAFNO,EAEM,CADJF,EAAQC,EAAA,OAAA,SAAA"}
|
{"version":3,"file":"ChartCard-55f3f2ee.js","sources":["../../../../frontend/src/components/Common/ChartCard.vue"],"sourcesContent":["<template>\n <div class=\"rounded-lg border p-4 shadow-sm\">\n <div class=\"flex items-center justify-between mb-4\">\n <h3 class=\"text-base font-medium text-ink-gray-9\">\n {{ title }}\n </h3>\n <slot name=\"actions\" />\n </div>\n <div class=\"h-[200px]\">\n <slot />\n </div>\n </div>\n</template>\n\n<script setup>\ndefineProps({\n title: {\n type: String,\n required: true,\n },\n})\n</script>\n"],"names":["_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_toDisplayString","__props","_renderSlot","_ctx","_hoisted_4"],"mappings":"sUACEA,EAAA,EAAAC,EAUM,MAVNC,EAUM,CATJC,EAKM,MALNC,EAKM,CAJJD,EAEK,KAFLE,EAEKC,EADAC,EAAK,KAAA,EAAA,CAAA,EAEVC,EAAuBC,EAAA,OAAA,SAAA,IAEzBN,EAEM,MAFNO,EAEM,CADJF,EAAQC,EAAA,OAAA,SAAA"}
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
import{_ as g}from"./LayoutHeader-1ab0c734.js";import{c as v}from"./payments-4b7e79ca.js";import{n as k,o as c,c as m,d as n,g as r,b as t,t as s,p as w,q as $,e as i,_ as C,F as d,k as p,j as b,G as V,r as x}from"./index-ce79e179.js";const F={class:"flex flex-col h-full"},B={class:"flex-1 overflow-auto p-5"},L={class:"rounded-lg border overflow-hidden"},N={class:"min-w-full divide-y"},U={class:"bg-surface-gray-2"},j={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},q={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},A={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},D={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},E={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},I={class:"px-6 py-3 text-right text-xs font-medium text-ink-gray-5 uppercase"},R={class:"divide-y"},S={class:"px-6 py-4"},z={class:"flex items-center gap-3"},G={class:"w-8 h-8 rounded-full bg-surface-gray-2 flex items-center justify-center"},Q={class:"text-sm font-medium text-ink-gray-5"},T={class:"text-sm font-medium text-ink-gray-9"},H={class:"px-6 py-4 text-sm text-ink-gray-7"},J={class:"px-6 py-4 text-sm text-ink-gray-7"},K={class:"px-6 py-4 text-sm font-medium text-ink-gray-9"},M={class:"px-6 py-4"},O={class:"px-6 py-4 text-sm text-ink-gray-7"},P={class:"px-6 py-4 text-right"},et={__name:"Customers",setup(W){const l=x(""),u=x(v),_=k(()=>{if(!l.value)return u.value;const e=l.value.toLowerCase();return u.value.filter(o=>o.name.toLowerCase().includes(e)||o.email.toLowerCase().includes(e))});function f(e){return new Intl.NumberFormat("ru-RU",{style:"currency",currency:"RUB",minimumFractionDigits:0}).format(e)}function y(){console.log("Exporting customers...")}function h(e){console.log("Viewing customer:",e.name)}return(e,o)=>(c(),m("div",F,[n(g,{title:e.$t("payments.customers")},{right:r(()=>[n(i(C),{modelValue:l.value,"onUpdate:modelValue":o[0]||(o[0]=a=>l.value=a),type:"text",placeholder:e.$t("common.search"),class:"w-64"},{prefix:r(()=>[n(i(d),{name:"search",class:"h-4 w-4 text-ink-gray-4"})]),_:1},8,["modelValue","placeholder"]),n(i(p),{variant:"subtle",onClick:y},{prefix:r(()=>[n(i(d),{name:"download",class:"h-4 w-4"})]),default:r(()=>[b(" "+s(e.$t("common.export")),1)]),_:1})]),_:1},8,["title"]),t("div",B,[t("div",L,[t("table",N,[t("thead",U,[t("tr",null,[t("th",j,s(e.$t("payments.customer.name")),1),t("th",q,s(e.$t("payments.customer.email")),1),t("th",A,s(e.$t("payments.connections")),1),t("th",D,s(e.$t("payments.customer.amount")),1),t("th",E,s(e.$t("payments.customer.status")),1),o[1]||(o[1]=t("th",{class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},"Since",-1)),t("th",I,s(e.$t("common.actions")),1)])]),t("tbody",R,[(c(!0),m(w,null,$(_.value,a=>(c(),m("tr",{key:a.id},[t("td",S,[t("div",z,[t("div",G,[t("span",Q,s(a.name.charAt(0)),1)]),t("span",T,s(a.name),1)])]),t("td",H,s(a.email),1),t("td",J,s(a.connections),1),t("td",K,s(f(a.monthlyAmount))+"/mo ",1),t("td",M,[n(i(V),{variant:a.status==="active"?"success":"info",label:a.status},null,8,["variant","label"])]),t("td",O,s(a.since),1),t("td",P,[n(i(p),{variant:"ghost",size:"sm",onClick:X=>h(a)},{default:r(()=>[n(i(d),{name:"eye",class:"h-4 w-4"})]),_:1},8,["onClick"])])]))),128))])])])])]))}};export{et as default};
|
import{_ as g}from"./LayoutHeader-cd508225.js";import{c as v}from"./payments-4b7e79ca.js";import{n as k,o as c,c as m,d as n,g as r,b as t,t as s,p as w,q as $,e as i,_ as C,F as d,k as p,j as b,G as V,r as x}from"./index-3d9a708e.js";const F={class:"flex flex-col h-full"},B={class:"flex-1 overflow-auto p-5"},L={class:"rounded-lg border overflow-hidden"},N={class:"min-w-full divide-y"},U={class:"bg-surface-gray-2"},j={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},q={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},A={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},D={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},E={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},I={class:"px-6 py-3 text-right text-xs font-medium text-ink-gray-5 uppercase"},R={class:"divide-y"},S={class:"px-6 py-4"},z={class:"flex items-center gap-3"},G={class:"w-8 h-8 rounded-full bg-surface-gray-2 flex items-center justify-center"},Q={class:"text-sm font-medium text-ink-gray-5"},T={class:"text-sm font-medium text-ink-gray-9"},H={class:"px-6 py-4 text-sm text-ink-gray-7"},J={class:"px-6 py-4 text-sm text-ink-gray-7"},K={class:"px-6 py-4 text-sm font-medium text-ink-gray-9"},M={class:"px-6 py-4"},O={class:"px-6 py-4 text-sm text-ink-gray-7"},P={class:"px-6 py-4 text-right"},et={__name:"Customers",setup(W){const l=x(""),u=x(v),_=k(()=>{if(!l.value)return u.value;const e=l.value.toLowerCase();return u.value.filter(o=>o.name.toLowerCase().includes(e)||o.email.toLowerCase().includes(e))});function f(e){return new Intl.NumberFormat("ru-RU",{style:"currency",currency:"RUB",minimumFractionDigits:0}).format(e)}function y(){console.log("Exporting customers...")}function h(e){console.log("Viewing customer:",e.name)}return(e,o)=>(c(),m("div",F,[n(g,{title:e.$t("payments.customers")},{right:r(()=>[n(i(C),{modelValue:l.value,"onUpdate:modelValue":o[0]||(o[0]=a=>l.value=a),type:"text",placeholder:e.$t("common.search"),class:"w-64"},{prefix:r(()=>[n(i(d),{name:"search",class:"h-4 w-4 text-ink-gray-4"})]),_:1},8,["modelValue","placeholder"]),n(i(p),{variant:"subtle",onClick:y},{prefix:r(()=>[n(i(d),{name:"download",class:"h-4 w-4"})]),default:r(()=>[b(" "+s(e.$t("common.export")),1)]),_:1})]),_:1},8,["title"]),t("div",B,[t("div",L,[t("table",N,[t("thead",U,[t("tr",null,[t("th",j,s(e.$t("payments.customer.name")),1),t("th",q,s(e.$t("payments.customer.email")),1),t("th",A,s(e.$t("payments.connections")),1),t("th",D,s(e.$t("payments.customer.amount")),1),t("th",E,s(e.$t("payments.customer.status")),1),o[1]||(o[1]=t("th",{class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},"Since",-1)),t("th",I,s(e.$t("common.actions")),1)])]),t("tbody",R,[(c(!0),m(w,null,$(_.value,a=>(c(),m("tr",{key:a.id},[t("td",S,[t("div",z,[t("div",G,[t("span",Q,s(a.name.charAt(0)),1)]),t("span",T,s(a.name),1)])]),t("td",H,s(a.email),1),t("td",J,s(a.connections),1),t("td",K,s(f(a.monthlyAmount))+"/mo ",1),t("td",M,[n(i(V),{variant:a.status==="active"?"success":"info",label:a.status},null,8,["variant","label"])]),t("td",O,s(a.since),1),t("td",P,[n(i(p),{variant:"ghost",size:"sm",onClick:X=>h(a)},{default:r(()=>[n(i(d),{name:"eye",class:"h-4 w-4"})]),_:1},8,["onClick"])])]))),128))])])])])]))}};export{et as default};
|
||||||
//# sourceMappingURL=Customers-a9617540.js.map
|
//# sourceMappingURL=Customers-4d8c7660.js.map
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
||||||
import{o as t,c as s,b as n,d as r,e as c,F as o,t as a,i as l,M as d}from"./index-ce79e179.js";const m={class:"flex flex-col items-center justify-center py-12 text-center"},u={class:"mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-surface-gray-2"},f={class:"mb-2 text-lg font-medium text-ink-gray-9"},y={key:0,class:"mb-4 max-w-sm text-sm text-ink-gray-5"},p={__name:"EmptyState",props:{icon:{type:String,default:"inbox"},title:{type:String,required:!0},description:{type:String,default:""}},setup(e){return(i,x)=>(t(),s("div",m,[n("div",u,[r(c(o),{name:e.icon,class:"h-8 w-8 text-ink-gray-4"},null,8,["name"])]),n("h3",f,a(e.title),1),e.description?(t(),s("p",y,a(e.description),1)):l("",!0),d(i.$slots,"action")]))}};export{p as _};
|
import{o as t,c as s,b as n,d as r,e as c,F as o,t as a,i as l,M as d}from"./index-3d9a708e.js";const m={class:"flex flex-col items-center justify-center py-12 text-center"},u={class:"mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-surface-gray-2"},f={class:"mb-2 text-lg font-medium text-ink-gray-9"},y={key:0,class:"mb-4 max-w-sm text-sm text-ink-gray-5"},p={__name:"EmptyState",props:{icon:{type:String,default:"inbox"},title:{type:String,required:!0},description:{type:String,default:""}},setup(e){return(i,x)=>(t(),s("div",m,[n("div",u,[r(c(o),{name:e.icon,class:"h-8 w-8 text-ink-gray-4"},null,8,["name"])]),n("h3",f,a(e.title),1),e.description?(t(),s("p",y,a(e.description),1)):l("",!0),d(i.$slots,"action")]))}};export{p as _};
|
||||||
//# sourceMappingURL=EmptyState-0c9a3a58.js.map
|
//# sourceMappingURL=EmptyState-54c60605.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"EmptyState-0c9a3a58.js","sources":["../../../../frontend/src/components/Common/EmptyState.vue"],"sourcesContent":["<template>\n <div class=\"flex flex-col items-center justify-center py-12 text-center\">\n <div\n class=\"mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-surface-gray-2\"\n >\n <FeatherIcon :name=\"icon\" class=\"h-8 w-8 text-ink-gray-4\" />\n </div>\n <h3 class=\"mb-2 text-lg font-medium text-ink-gray-9\">\n {{ title }}\n </h3>\n <p v-if=\"description\" class=\"mb-4 max-w-sm text-sm text-ink-gray-5\">\n {{ description }}\n </p>\n <slot name=\"action\" />\n </div>\n</template>\n\n<script setup>\nimport { FeatherIcon } from 'frappe-ui'\n\ndefineProps({\n icon: {\n type: String,\n default: 'inbox',\n },\n title: {\n type: String,\n required: true,\n },\n description: {\n type: String,\n default: '',\n },\n})\n</script>\n"],"names":["_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_createVNode","_unref","FeatherIcon","__props","_hoisted_3","_toDisplayString","_hoisted_4","_renderSlot","_ctx"],"mappings":"ohBACEA,EAAA,EAAAC,EAaM,MAbNC,EAaM,CAZJC,EAIM,MAJNC,EAIM,CADJC,EAA4DC,EAAAC,CAAA,EAAA,CAA9C,KAAMC,EAAI,KAAE,MAAM,8CAElCL,EAEK,KAFLM,EAEKC,EADAF,EAAK,KAAA,EAAA,CAAA,EAEDA,EAAW,iBAApBP,EAEI,IAFJU,EAEID,EADCF,EAAW,WAAA,EAAA,CAAA,YAEhBI,EAAsBC,EAAA,OAAA,QAAA"}
|
{"version":3,"file":"EmptyState-54c60605.js","sources":["../../../../frontend/src/components/Common/EmptyState.vue"],"sourcesContent":["<template>\n <div class=\"flex flex-col items-center justify-center py-12 text-center\">\n <div\n class=\"mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-surface-gray-2\"\n >\n <FeatherIcon :name=\"icon\" class=\"h-8 w-8 text-ink-gray-4\" />\n </div>\n <h3 class=\"mb-2 text-lg font-medium text-ink-gray-9\">\n {{ title }}\n </h3>\n <p v-if=\"description\" class=\"mb-4 max-w-sm text-sm text-ink-gray-5\">\n {{ description }}\n </p>\n <slot name=\"action\" />\n </div>\n</template>\n\n<script setup>\nimport { FeatherIcon } from 'frappe-ui'\n\ndefineProps({\n icon: {\n type: String,\n default: 'inbox',\n },\n title: {\n type: String,\n required: true,\n },\n description: {\n type: String,\n default: '',\n },\n})\n</script>\n"],"names":["_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_createVNode","_unref","FeatherIcon","__props","_hoisted_3","_toDisplayString","_hoisted_4","_renderSlot","_ctx"],"mappings":"ohBACEA,EAAA,EAAAC,EAaM,MAbNC,EAaM,CAZJC,EAIM,MAJNC,EAIM,CADJC,EAA4DC,EAAAC,CAAA,EAAA,CAA9C,KAAMC,EAAI,KAAE,MAAM,8CAElCL,EAEK,KAFLM,EAEKC,EADAF,EAAK,KAAA,EAAA,CAAA,EAEDA,EAAW,iBAApBP,EAEI,IAFJU,EAEID,EADCF,EAAW,WAAA,EAAA,CAAA,YAEhBI,EAAsBC,EAAA,OAAA,QAAA"}
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
import{o as a,c as r,b as e,M as s,t as i}from"./index-ce79e179.js";const c={class:"flex items-center justify-between border-b bg-surface-white px-5 py-2.5"},l={class:"flex items-center gap-2"},n={class:"text-xl font-semibold text-ink-gray-9"},d={class:"flex items-center gap-2"},f={__name:"LayoutHeader",props:{title:{type:String,default:""}},setup(o){return(t,_)=>(a(),r("header",c,[e("div",l,[s(t.$slots,"left",{},()=>[e("h1",n,i(o.title),1)])]),e("div",d,[s(t.$slots,"right")])]))}};export{f as _};
|
import{o as a,c as r,b as e,M as s,t as i}from"./index-3d9a708e.js";const c={class:"flex items-center justify-between border-b bg-surface-white px-5 py-2.5"},l={class:"flex items-center gap-2"},n={class:"text-xl font-semibold text-ink-gray-9"},d={class:"flex items-center gap-2"},f={__name:"LayoutHeader",props:{title:{type:String,default:""}},setup(o){return(t,_)=>(a(),r("header",c,[e("div",l,[s(t.$slots,"left",{},()=>[e("h1",n,i(o.title),1)])]),e("div",d,[s(t.$slots,"right")])]))}};export{f as _};
|
||||||
//# sourceMappingURL=LayoutHeader-1ab0c734.js.map
|
//# sourceMappingURL=LayoutHeader-cd508225.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"LayoutHeader-1ab0c734.js","sources":["../../../../frontend/src/components/Common/LayoutHeader.vue"],"sourcesContent":["<template>\n <header class=\"flex items-center justify-between border-b bg-surface-white px-5 py-2.5\">\n <div class=\"flex items-center gap-2\">\n <slot name=\"left\">\n <h1 class=\"text-xl font-semibold text-ink-gray-9\">\n {{ title }}\n </h1>\n </slot>\n </div>\n <div class=\"flex items-center gap-2\">\n <slot name=\"right\" />\n </div>\n </header>\n</template>\n\n<script setup>\ndefineProps({\n title: {\n type: String,\n default: '',\n },\n})\n</script>\n"],"names":["_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_renderSlot","_ctx","_hoisted_3","_toDisplayString","__props","_hoisted_4"],"mappings":"+WACEA,EAAA,EAAAC,EAWS,SAXTC,EAWS,CAVPC,EAMM,MANNC,EAMM,CALJC,EAIOC,mBAJP,IAIO,CAHLH,EAEK,KAFLI,EAEKC,EADAC,EAAK,KAAA,EAAA,CAAA,MAIdN,EAEM,MAFNO,EAEM,CADJL,EAAqBC,EAAA,OAAA,OAAA"}
|
{"version":3,"file":"LayoutHeader-cd508225.js","sources":["../../../../frontend/src/components/Common/LayoutHeader.vue"],"sourcesContent":["<template>\n <header class=\"flex items-center justify-between border-b bg-surface-white px-5 py-2.5\">\n <div class=\"flex items-center gap-2\">\n <slot name=\"left\">\n <h1 class=\"text-xl font-semibold text-ink-gray-9\">\n {{ title }}\n </h1>\n </slot>\n </div>\n <div class=\"flex items-center gap-2\">\n <slot name=\"right\" />\n </div>\n </header>\n</template>\n\n<script setup>\ndefineProps({\n title: {\n type: String,\n default: '',\n },\n})\n</script>\n"],"names":["_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_renderSlot","_ctx","_hoisted_3","_toDisplayString","__props","_hoisted_4"],"mappings":"+WACEA,EAAA,EAAAC,EAWS,SAXTC,EAWS,CAVPC,EAMM,MANNC,EAMM,CALJC,EAIOC,mBAJP,IAIO,CAHLH,EAEK,KAFLI,EAEKC,EADAC,EAAK,KAAA,EAAA,CAAA,MAIdN,EAEM,MAFNO,EAEM,CADJL,EAAqBC,EAAA,OAAA,OAAA"}
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
import{u as b,a as _,o as f,c as y,b as e,d as r,t as a,w as x,e as n,f as w,v as k,g as V,h as $,i as B,r as o,A as L,_ as p,j as M,k as N,l as j,m as q,s as A}from"./index-ce79e179.js";const C={class:"min-h-screen flex items-center justify-center bg-surface-gray-2 py-12 px-4 sm:px-6 lg:px-8"},U={class:"max-w-md w-full space-y-8"},D={class:"text-center"},P={class:"mt-2 text-sm text-ink-gray-5"},R={class:"space-y-4"},S={for:"email",class:"block text-sm font-medium text-ink-gray-7"},E={for:"password",class:"block text-sm font-medium text-ink-gray-7"},T={class:"flex items-center justify-between"},z={class:"flex items-center"},F={for:"remember-me",class:"ml-2 block text-sm text-ink-gray-7"},G={class:"text-sm"},H={href:"#",class:"font-medium text-blue-600 hover:text-blue-500"},K={__name:"Login",setup(I){const h=b(),v=_(),d=o(""),u=o(""),c=o(!1),m=o(!1),i=o("");async function g(){m.value=!0,i.value="";try{await q("login",{usr:d.value,pwd:u.value}),await A.init();const s=v.query.redirect||"/dashboard";h.push(s)}catch(s){i.value=s.message||"Login failed"}finally{m.value=!1}}return(s,t)=>(f(),y("div",C,[e("div",U,[e("div",D,[r(L,{class:"mx-auto h-16 w-16"}),t[3]||(t[3]=e("h2",{class:"mt-6 text-3xl font-bold text-ink-gray-9"}," Admin Panel ",-1)),e("p",P,a(s.$t("auth.login")),1)]),e("form",{class:"mt-8 space-y-6",onSubmit:x(g,["prevent"])},[e("div",R,[e("div",null,[e("label",S,a(s.$t("auth.email")),1),r(n(p),{id:"email",modelValue:d.value,"onUpdate:modelValue":t[0]||(t[0]=l=>d.value=l),type:"email",required:"",class:"mt-1",placeholder:s.$t("auth.email")},null,8,["modelValue","placeholder"])]),e("div",null,[e("label",E,a(s.$t("auth.password")),1),r(n(p),{id:"password",modelValue:u.value,"onUpdate:modelValue":t[1]||(t[1]=l=>u.value=l),type:"password",required:"",class:"mt-1",placeholder:s.$t("auth.password")},null,8,["modelValue","placeholder"])])]),e("div",T,[e("div",z,[w(e("input",{id:"remember-me","onUpdate:modelValue":t[2]||(t[2]=l=>c.value=l),type:"checkbox",class:"h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"},null,512),[[k,c.value]]),e("label",F,a(s.$t("auth.rememberMe")),1)]),e("div",G,[e("a",H,a(s.$t("auth.forgotPassword")),1)])]),e("div",null,[r(n(N),{type:"submit",variant:"solid",class:"w-full",loading:m.value},{default:V(()=>[M(a(s.$t("auth.loginButton")),1)]),_:1},8,["loading"])]),i.value?(f(),$(n(j),{key:0,message:i.value},null,8,["message"])):B("",!0)],32)])]))}};export{K as default};
|
import{u as b,a as _,o as f,c as y,b as e,d as r,t as a,w as x,e as n,f as w,v as k,g as V,h as $,i as B,r as o,A as L,_ as p,j as M,k as N,l as j,m as q,s as A}from"./index-3d9a708e.js";const C={class:"min-h-screen flex items-center justify-center bg-surface-gray-2 py-12 px-4 sm:px-6 lg:px-8"},U={class:"max-w-md w-full space-y-8"},D={class:"text-center"},P={class:"mt-2 text-sm text-ink-gray-5"},R={class:"space-y-4"},S={for:"email",class:"block text-sm font-medium text-ink-gray-7"},E={for:"password",class:"block text-sm font-medium text-ink-gray-7"},T={class:"flex items-center justify-between"},z={class:"flex items-center"},F={for:"remember-me",class:"ml-2 block text-sm text-ink-gray-7"},G={class:"text-sm"},H={href:"#",class:"font-medium text-blue-600 hover:text-blue-500"},K={__name:"Login",setup(I){const h=b(),v=_(),d=o(""),u=o(""),c=o(!1),m=o(!1),i=o("");async function g(){m.value=!0,i.value="";try{await q("login",{usr:d.value,pwd:u.value}),await A.init();const s=v.query.redirect||"/dashboard";h.push(s)}catch(s){i.value=s.message||"Login failed"}finally{m.value=!1}}return(s,t)=>(f(),y("div",C,[e("div",U,[e("div",D,[r(L,{class:"mx-auto h-16 w-16"}),t[3]||(t[3]=e("h2",{class:"mt-6 text-3xl font-bold text-ink-gray-9"}," Admin Panel ",-1)),e("p",P,a(s.$t("auth.login")),1)]),e("form",{class:"mt-8 space-y-6",onSubmit:x(g,["prevent"])},[e("div",R,[e("div",null,[e("label",S,a(s.$t("auth.email")),1),r(n(p),{id:"email",modelValue:d.value,"onUpdate:modelValue":t[0]||(t[0]=l=>d.value=l),type:"email",required:"",class:"mt-1",placeholder:s.$t("auth.email")},null,8,["modelValue","placeholder"])]),e("div",null,[e("label",E,a(s.$t("auth.password")),1),r(n(p),{id:"password",modelValue:u.value,"onUpdate:modelValue":t[1]||(t[1]=l=>u.value=l),type:"password",required:"",class:"mt-1",placeholder:s.$t("auth.password")},null,8,["modelValue","placeholder"])])]),e("div",T,[e("div",z,[w(e("input",{id:"remember-me","onUpdate:modelValue":t[2]||(t[2]=l=>c.value=l),type:"checkbox",class:"h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"},null,512),[[k,c.value]]),e("label",F,a(s.$t("auth.rememberMe")),1)]),e("div",G,[e("a",H,a(s.$t("auth.forgotPassword")),1)])]),e("div",null,[r(n(N),{type:"submit",variant:"solid",class:"w-full",loading:m.value},{default:V(()=>[M(a(s.$t("auth.loginButton")),1)]),_:1},8,["loading"])]),i.value?(f(),$(n(j),{key:0,message:i.value},null,8,["message"])):B("",!0)],32)])]))}};export{K as default};
|
||||||
//# sourceMappingURL=Login-8759ab20.js.map
|
//# sourceMappingURL=Login-9697bc9f.js.map
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
||||||
import{o,c as a,b as e,d as r,g as n,e as i,j as d,k as l}from"./index-ce79e179.js";const x={class:"min-h-screen flex items-center justify-center bg-surface-gray-2 py-12 px-4"},u={class:"text-center"},g={__name:"NotFound",setup(f){return(s,t)=>(o(),a("div",x,[e("div",u,[t[2]||(t[2]=e("h1",{class:"text-9xl font-bold text-ink-gray-2"},"404",-1)),t[3]||(t[3]=e("h2",{class:"mt-4 text-2xl font-semibold text-ink-gray-9"}," Page Not Found ",-1)),t[4]||(t[4]=e("p",{class:"mt-2 text-ink-gray-5"}," The page you're looking for doesn't exist. ",-1)),r(i(l),{variant:"solid",class:"mt-6",onClick:t[0]||(t[0]=m=>s.$router.push("/dashboard"))},{default:n(()=>[...t[1]||(t[1]=[d(" Go to Dashboard ",-1)])]),_:1})])]))}};export{g as default};
|
import{o,c as a,b as e,d as r,g as n,e as i,j as d,k as l}from"./index-3d9a708e.js";const x={class:"min-h-screen flex items-center justify-center bg-surface-gray-2 py-12 px-4"},u={class:"text-center"},g={__name:"NotFound",setup(f){return(s,t)=>(o(),a("div",x,[e("div",u,[t[2]||(t[2]=e("h1",{class:"text-9xl font-bold text-ink-gray-2"},"404",-1)),t[3]||(t[3]=e("h2",{class:"mt-4 text-2xl font-semibold text-ink-gray-9"}," Page Not Found ",-1)),t[4]||(t[4]=e("p",{class:"mt-2 text-ink-gray-5"}," The page you're looking for doesn't exist. ",-1)),r(i(l),{variant:"solid",class:"mt-6",onClick:t[0]||(t[0]=m=>s.$router.push("/dashboard"))},{default:n(()=>[...t[1]||(t[1]=[d(" Go to Dashboard ",-1)])]),_:1})])]))}};export{g as default};
|
||||||
//# sourceMappingURL=NotFound-29d88554.js.map
|
//# sourceMappingURL=NotFound-eaa6081d.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"NotFound-29d88554.js","sources":["../../../../frontend/src/pages/admin/NotFound.vue"],"sourcesContent":["<template>\n <div class=\"min-h-screen flex items-center justify-center bg-surface-gray-2 py-12 px-4\">\n <div class=\"text-center\">\n <h1 class=\"text-9xl font-bold text-ink-gray-2\">404</h1>\n <h2 class=\"mt-4 text-2xl font-semibold text-ink-gray-9\">\n Page Not Found\n </h2>\n <p class=\"mt-2 text-ink-gray-5\">\n The page you're looking for doesn't exist.\n </p>\n <Button\n variant=\"solid\"\n class=\"mt-6\"\n @click=\"$router.push('/dashboard')\"\n >\n Go to Dashboard\n </Button>\n </div>\n </div>\n</template>\n\n<script setup>\nimport { Button } from 'frappe-ui'\n</script>\n"],"names":["_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_createVNode","_unref","Button","$event","$router"],"mappings":"qPACEA,EAAA,EAAAC,EAiBM,MAjBNC,EAiBM,CAhBJC,EAeM,MAfNC,EAeM,CAdJC,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAF,EAAuD,KAAnD,CAAA,MAAM,oCAAoC,EAAC,MAAG,EAAA,GAClDE,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAF,EAEK,KAFD,CAAA,MAAM,6CAA6C,EAAC,mBAExD,EAAA,GACAE,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAF,EAEI,IAFD,CAAA,MAAM,sBAAsB,EAAC,+CAEhC,EAAA,GACAG,EAMSC,EAAAC,CAAA,EAAA,CALP,QAAQ,QACR,MAAM,OACL,QAAKH,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAI,GAAEC,EAAO,QAAC,KAAI,YAAA,eACrB,IAED,CAAA,GAAAL,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAA,GAFC,oBAED,EAAA"}
|
{"version":3,"file":"NotFound-eaa6081d.js","sources":["../../../../frontend/src/pages/admin/NotFound.vue"],"sourcesContent":["<template>\n <div class=\"min-h-screen flex items-center justify-center bg-surface-gray-2 py-12 px-4\">\n <div class=\"text-center\">\n <h1 class=\"text-9xl font-bold text-ink-gray-2\">404</h1>\n <h2 class=\"mt-4 text-2xl font-semibold text-ink-gray-9\">\n Page Not Found\n </h2>\n <p class=\"mt-2 text-ink-gray-5\">\n The page you're looking for doesn't exist.\n </p>\n <Button\n variant=\"solid\"\n class=\"mt-6\"\n @click=\"$router.push('/dashboard')\"\n >\n Go to Dashboard\n </Button>\n </div>\n </div>\n</template>\n\n<script setup>\nimport { Button } from 'frappe-ui'\n</script>\n"],"names":["_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_createVNode","_unref","Button","$event","$router"],"mappings":"qPACEA,EAAA,EAAAC,EAiBM,MAjBNC,EAiBM,CAhBJC,EAeM,MAfNC,EAeM,CAdJC,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAF,EAAuD,KAAnD,CAAA,MAAM,oCAAoC,EAAC,MAAG,EAAA,GAClDE,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAF,EAEK,KAFD,CAAA,MAAM,6CAA6C,EAAC,mBAExD,EAAA,GACAE,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAF,EAEI,IAFD,CAAA,MAAM,sBAAsB,EAAC,+CAEhC,EAAA,GACAG,EAMSC,EAAAC,CAAA,EAAA,CALP,QAAQ,QACR,MAAM,OACL,QAAKH,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAI,GAAEC,EAAO,QAAC,KAAI,YAAA,eACrB,IAED,CAAA,GAAAL,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAA,GAFC,oBAED,EAAA"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
||||||
import{n as s,o as c,c as u,b as r,E as i,d as o,e as d,F as g,t as l,i as m}from"./index-ce79e179.js";const p={class:"rounded-lg border p-4 shadow-sm"},v={class:"flex items-center justify-between"},h={class:"flex items-center gap-3"},w={class:"text-sm text-ink-gray-5"},N={class:"text-2xl font-semibold text-ink-gray-9"},k={class:"text-sm font-medium"},S={key:0,class:"mt-2 text-xs text-ink-gray-5"},F={__name:"StatCard",props:{label:{type:String,required:!0},value:{type:[Number,String],required:!0},iconName:{type:String,default:"activity"},color:{type:String,default:"blue",validator:e=>["blue","green","red","yellow","purple","gray"].includes(e)},trend:{type:Number,default:void 0},subtitle:{type:String,default:""},format:{type:String,default:"number",validator:e=>["number","currency","percent"].includes(e)}},setup(e){const n=e,a={blue:{bg:"bg-blue-100",icon:"text-blue-600"},green:{bg:"bg-green-100",icon:"text-green-600"},red:{bg:"bg-red-100",icon:"text-red-600"},yellow:{bg:"bg-orange-100",icon:"text-orange-600"},purple:{bg:"bg-purple-100",icon:"text-purple-600"},gray:{bg:"bg-surface-gray-2",icon:"text-ink-gray-5"}},b=s(()=>{var t;return((t=a[n.color])==null?void 0:t.bg)||a.blue.bg}),f=s(()=>{var t;return((t=a[n.color])==null?void 0:t.icon)||a.blue.icon}),y=s(()=>n.trend>0?"text-green-600":n.trend<0?"text-red-600":"text-ink-gray-5"),x=s(()=>{const t=typeof n.value=="number"?n.value:parseFloat(n.value);if(isNaN(t))return n.value;switch(n.format){case"currency":return new Intl.NumberFormat("ru-RU",{style:"currency",currency:"RUB",minimumFractionDigits:0}).format(t);case"percent":return`${t}%`;default:return new Intl.NumberFormat("ru-RU").format(t)}});return(t,C)=>(c(),u("div",p,[r("div",v,[r("div",h,[r("div",{class:i(["flex h-10 w-10 items-center justify-center rounded-lg",b.value])},[o(d(g),{name:e.iconName,class:i(["h-5 w-5",f.value])},null,8,["name","class"])],2),r("div",null,[r("p",w,l(e.label),1),r("p",N,l(x.value),1)])]),e.trend!==void 0?(c(),u("div",{key:0,class:i(["flex items-center gap-1",y.value])},[o(d(g),{name:e.trend>0?"trending-up":"trending-down",class:"h-4 w-4"},null,8,["name"]),r("span",k,l(Math.abs(e.trend))+"%",1)],2)):m("",!0)]),e.subtitle?(c(),u("div",S,l(e.subtitle),1)):m("",!0)]))}};export{F as _};
|
import{n as s,o as c,c as u,b as r,E as i,d as o,e as d,F as g,t as l,i as m}from"./index-3d9a708e.js";const p={class:"rounded-lg border p-4 shadow-sm"},v={class:"flex items-center justify-between"},h={class:"flex items-center gap-3"},w={class:"text-sm text-ink-gray-5"},N={class:"text-2xl font-semibold text-ink-gray-9"},k={class:"text-sm font-medium"},S={key:0,class:"mt-2 text-xs text-ink-gray-5"},F={__name:"StatCard",props:{label:{type:String,required:!0},value:{type:[Number,String],required:!0},iconName:{type:String,default:"activity"},color:{type:String,default:"blue",validator:e=>["blue","green","red","yellow","purple","gray"].includes(e)},trend:{type:Number,default:void 0},subtitle:{type:String,default:""},format:{type:String,default:"number",validator:e=>["number","currency","percent"].includes(e)}},setup(e){const n=e,a={blue:{bg:"bg-blue-100",icon:"text-blue-600"},green:{bg:"bg-green-100",icon:"text-green-600"},red:{bg:"bg-red-100",icon:"text-red-600"},yellow:{bg:"bg-orange-100",icon:"text-orange-600"},purple:{bg:"bg-purple-100",icon:"text-purple-600"},gray:{bg:"bg-surface-gray-2",icon:"text-ink-gray-5"}},b=s(()=>{var t;return((t=a[n.color])==null?void 0:t.bg)||a.blue.bg}),f=s(()=>{var t;return((t=a[n.color])==null?void 0:t.icon)||a.blue.icon}),y=s(()=>n.trend>0?"text-green-600":n.trend<0?"text-red-600":"text-ink-gray-5"),x=s(()=>{const t=typeof n.value=="number"?n.value:parseFloat(n.value);if(isNaN(t))return n.value;switch(n.format){case"currency":return new Intl.NumberFormat("ru-RU",{style:"currency",currency:"RUB",minimumFractionDigits:0}).format(t);case"percent":return`${t}%`;default:return new Intl.NumberFormat("ru-RU").format(t)}});return(t,C)=>(c(),u("div",p,[r("div",v,[r("div",h,[r("div",{class:i(["flex h-10 w-10 items-center justify-center rounded-lg",b.value])},[o(d(g),{name:e.iconName,class:i(["h-5 w-5",f.value])},null,8,["name","class"])],2),r("div",null,[r("p",w,l(e.label),1),r("p",N,l(x.value),1)])]),e.trend!==void 0?(c(),u("div",{key:0,class:i(["flex items-center gap-1",y.value])},[o(d(g),{name:e.trend>0?"trending-up":"trending-down",class:"h-4 w-4"},null,8,["name"]),r("span",k,l(Math.abs(e.trend))+"%",1)],2)):m("",!0)]),e.subtitle?(c(),u("div",S,l(e.subtitle),1)):m("",!0)]))}};export{F as _};
|
||||||
//# sourceMappingURL=StatCard-159fe0b3.js.map
|
//# sourceMappingURL=StatCard-edb6f5e9.js.map
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -15,8 +15,8 @@
|
||||||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-ce79e179.js"></script>
|
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-3d9a708e.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-91d9c929.css">
|
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-984848c2.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full">
|
<body class="h-full">
|
||||||
<div id="app" class="h-full"></div>
|
<div id="app" class="h-full"></div>
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@
|
||||||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-ce79e179.js"></script>
|
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-3d9a708e.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-91d9c929.css">
|
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-984848c2.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full">
|
<body class="h-full">
|
||||||
<div id="app" class="h-full"></div>
|
<div id="app" class="h-full"></div>
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Certificate exists -->
|
<!-- Certificate exists -->
|
||||||
<div v-if="certStatus?.has_certificate" class="space-y-4">
|
<div v-if="certStatus?.has_certificate" class="space-y-4">
|
||||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-surface-gray-2">
|
<div class="flex items-center gap-3 p-3 rounded-lg bg-green-50 dark:bg-green-950/30">
|
||||||
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-green-100 dark:bg-green-900">
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-green-100 dark:bg-green-900/50">
|
||||||
<FeatherIcon name="shield" class="h-5 w-5 text-green-600 dark:text-green-400" />
|
<FeatherIcon name="shield" class="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium text-ink-gray-9">Certificate configured</p>
|
<p class="text-sm font-medium text-green-800 dark:text-green-300">Certificate configured</p>
|
||||||
<p class="text-xs text-ink-gray-5 font-mono">{{ certStatus.cert_path }}</p>
|
<p class="text-xs text-green-600 dark:text-green-500 font-mono">{{ certStatus.cert_path }}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="subtle" size="sm" @click="showCertModal = true">
|
<Button variant="subtle" size="sm" @click="showCertModal = true">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
|
|
@ -54,25 +54,25 @@
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-surface-gray-2 rounded-lg p-4">
|
<div class="bg-blue-50 dark:bg-blue-950/30 rounded-lg p-4">
|
||||||
<p class="text-sm text-ink-gray-8 font-medium mb-2">How to trust this certificate on Incus servers:</p>
|
<p class="text-sm text-blue-800 dark:text-blue-300 font-medium mb-2">How to trust this certificate on Incus servers:</p>
|
||||||
<ol class="text-sm text-ink-gray-6 space-y-1 list-decimal list-inside">
|
<ol class="text-sm text-blue-700 dark:text-blue-400 space-y-1 list-decimal list-inside">
|
||||||
<li>Click "View" to copy the certificate content</li>
|
<li>Click "View" to copy the certificate content</li>
|
||||||
<li>Save it as <code class="bg-surface-gray-3 px-1 rounded">admin-panel.crt</code> on your Incus server</li>
|
<li>Save it as <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">admin-panel.crt</code> on your Incus server</li>
|
||||||
<li>Run: <code class="bg-surface-gray-3 px-1 rounded">incus config trust add-certificate admin-panel.crt</code></li>
|
<li>Run: <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">incus config trust add-certificate admin-panel.crt</code></li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No certificate -->
|
<!-- No certificate -->
|
||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-4">
|
||||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-surface-gray-2">
|
<div class="flex items-center gap-3 p-3 rounded-lg bg-yellow-50 dark:bg-yellow-950/30">
|
||||||
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-yellow-100 dark:bg-yellow-900">
|
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-yellow-100 dark:bg-yellow-900/50">
|
||||||
<FeatherIcon name="alert-circle" class="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
|
<FeatherIcon name="alert-circle" class="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium text-ink-gray-9">No certificate</p>
|
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-300">No certificate</p>
|
||||||
<p class="text-xs text-ink-gray-5">Generate a client certificate to connect to Incus servers</p>
|
<p class="text-xs text-yellow-600 dark:text-yellow-500">Generate a client certificate to connect to Incus servers</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="solid" @click="generateCertificate" :loading="generating">
|
<Button variant="solid" @click="generateCertificate" :loading="generating">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
|
|
@ -112,10 +112,12 @@
|
||||||
<Spinner class="h-6 w-6" />
|
<Spinner class="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="flex items-center gap-3 p-3 rounded-lg bg-surface-gray-2">
|
<div v-else class="flex items-center gap-3 p-3 rounded-lg"
|
||||||
|
:class="setupStatus?.distrobuilder_installed ? 'bg-green-50 dark:bg-green-950/30' : 'bg-red-50 dark:bg-red-950/30'"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 rounded-full flex items-center justify-center"
|
class="w-10 h-10 rounded-full flex items-center justify-center"
|
||||||
:class="setupStatus?.distrobuilder_installed ? 'bg-green-100 dark:bg-green-900' : 'bg-red-100 dark:bg-red-900'"
|
:class="setupStatus?.distrobuilder_installed ? 'bg-green-100 dark:bg-green-900/50' : 'bg-red-100 dark:bg-red-900/50'"
|
||||||
>
|
>
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
:name="setupStatus?.distrobuilder_installed ? 'check-circle' : 'alert-circle'"
|
:name="setupStatus?.distrobuilder_installed ? 'check-circle' : 'alert-circle'"
|
||||||
|
|
@ -124,10 +126,14 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium text-ink-gray-9">
|
<p class="text-sm font-medium"
|
||||||
|
:class="setupStatus?.distrobuilder_installed ? 'text-green-800 dark:text-green-300' : 'text-red-800 dark:text-red-300'"
|
||||||
|
>
|
||||||
{{ setupStatus?.distrobuilder_installed ? $t('servers.setup.distrobuilderInstalled') : $t('servers.setup.distrobuilderNotInstalled') }}
|
{{ setupStatus?.distrobuilder_installed ? $t('servers.setup.distrobuilderInstalled') : $t('servers.setup.distrobuilderNotInstalled') }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-ink-gray-5">
|
<p class="text-xs"
|
||||||
|
:class="setupStatus?.distrobuilder_installed ? 'text-green-600 dark:text-green-500' : 'text-red-600 dark:text-red-500'"
|
||||||
|
>
|
||||||
{{ setupStatus?.distrobuilder_version || $t('servers.setup.distrobuilderDesc') }}
|
{{ setupStatus?.distrobuilder_version || $t('servers.setup.distrobuilderDesc') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -159,8 +165,8 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
<div class="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
|
||||||
<FeatherIcon name="cloud" class="h-5 w-5 text-blue-600" />
|
<FeatherIcon name="cloud" class="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-medium text-ink-gray-9">{{ server.name }}</h4>
|
<h4 class="text-sm font-medium text-ink-gray-9">{{ server.name }}</h4>
|
||||||
|
|
@ -240,14 +246,21 @@
|
||||||
<p class="text-sm text-ink-gray-7">
|
<p class="text-sm text-ink-gray-7">
|
||||||
{{ $t('servers.setup.installDescription') }}
|
{{ $t('servers.setup.installDescription') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
v-if="!installResult"
|
||||||
|
v-model="doasPassword"
|
||||||
|
label="Password (doas)"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your doas password"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="bg-surface-gray-2 rounded-lg p-3">
|
<div class="bg-surface-gray-2 rounded-lg p-3">
|
||||||
<p class="text-xs text-ink-gray-7">
|
<p class="text-xs text-ink-gray-5">
|
||||||
{{ $t('servers.setup.doasNote') }}
|
For manual install, run in terminal: <code class="bg-surface-gray-3 px-1 rounded text-ink-gray-7">doas apk add distrobuilder</code>
|
||||||
</p>
|
</p>
|
||||||
<code class="text-xs text-ink-gray-8 block mt-1 bg-surface-gray-3 p-1 rounded whitespace-pre-line">permit nopass frappe as root cmd apk
|
|
||||||
permit nopass frappe as root cmd distrobuilder
|
|
||||||
permit nopass frappe as root cmd rm</code>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="installResult" class="mt-4">
|
<div v-if="installResult" class="mt-4">
|
||||||
<label class="block text-sm font-medium text-ink-gray-7 mb-2">{{ $t('common.output') }}</label>
|
<label class="block text-sm font-medium text-ink-gray-7 mb-2">{{ $t('common.output') }}</label>
|
||||||
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg text-xs font-mono max-h-64 overflow-y-auto">
|
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg text-xs font-mono max-h-64 overflow-y-auto">
|
||||||
|
|
@ -256,7 +269,7 @@ permit nopass frappe as root cmd rm</code>
|
||||||
:key="index"
|
:key="index"
|
||||||
:class="{
|
:class="{
|
||||||
'text-green-400': log.includes('Done'),
|
'text-green-400': log.includes('Done'),
|
||||||
'text-red-400': log.includes('Failed'),
|
'text-red-400': log.includes('Failed') || log.includes('Error'),
|
||||||
'text-yellow-400': log.includes('==='),
|
'text-yellow-400': log.includes('==='),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|
@ -276,6 +289,7 @@ permit nopass frappe as root cmd rm</code>
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@click="runInstall"
|
@click="runInstall"
|
||||||
:loading="installing"
|
:loading="installing"
|
||||||
|
:disabled="!doasPassword"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FeatherIcon name="download" class="h-4 w-4" />
|
<FeatherIcon name="download" class="h-4 w-4" />
|
||||||
|
|
@ -326,7 +340,7 @@ permit nopass frappe as root cmd rm</code>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="addServerError" class="text-sm text-red-500 bg-red-50 p-2 rounded">
|
<div v-if="addServerError" class="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/30 p-2 rounded">
|
||||||
{{ addServerError }}
|
{{ addServerError }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -380,7 +394,7 @@ permit nopass frappe as root cmd rm</code>
|
||||||
<textarea
|
<textarea
|
||||||
:value="certContent.certificate"
|
:value="certContent.certificate"
|
||||||
readonly
|
readonly
|
||||||
class="w-full h-48 rounded-lg border border-gray-300 px-3 py-2 text-xs font-mono bg-gray-50"
|
class="w-full h-48 rounded-lg border px-3 py-2 text-xs font-mono bg-surface-gray-2 text-ink-gray-8 border-outline-gray-2"
|
||||||
></textarea>
|
></textarea>
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
|
@ -395,9 +409,9 @@ permit nopass frappe as root cmd rm</code>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-blue-50 rounded-lg p-4">
|
<div class="bg-blue-50 dark:bg-blue-950/30 rounded-lg p-4">
|
||||||
<p class="text-sm text-blue-800 font-medium mb-2">On your Incus server:</p>
|
<p class="text-sm text-blue-800 dark:text-blue-300 font-medium mb-2">On your Incus server:</p>
|
||||||
<code class="text-sm text-blue-700 block bg-blue-100 p-2 rounded">
|
<code class="text-sm text-blue-700 dark:text-blue-400 block bg-blue-100 dark:bg-blue-900/50 p-2 rounded">
|
||||||
incus config trust add-certificate admin-panel.crt
|
incus config trust add-certificate admin-panel.crt
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -428,6 +442,7 @@ const serverToRemove = ref(null)
|
||||||
|
|
||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
const installResult = ref(null)
|
const installResult = ref(null)
|
||||||
|
const doasPassword = ref('')
|
||||||
|
|
||||||
const addingServer = ref(false)
|
const addingServer = ref(false)
|
||||||
const removingServer = ref(false)
|
const removingServer = ref(false)
|
||||||
|
|
@ -570,7 +585,7 @@ async function runInstall() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await call('admin_panel.api.servers.run_setup', {
|
const result = await call('admin_panel.api.servers.run_setup', {
|
||||||
doas_password: '',
|
doas_password: doasPassword.value,
|
||||||
})
|
})
|
||||||
installResult.value = result
|
installResult.value = result
|
||||||
|
|
||||||
|
|
@ -590,6 +605,7 @@ async function runInstall() {
|
||||||
function closeInstallModal() {
|
function closeInstallModal() {
|
||||||
showInstallModal.value = false
|
showInstallModal.value = false
|
||||||
installResult.value = null
|
installResult.value = null
|
||||||
|
doasPassword.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testServerConnection(serverName) {
|
async function testServerConnection(serverName) {
|
||||||
|
|
@ -658,7 +674,6 @@ async function confirmRemove() {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(result.message)
|
|
||||||
showRemoveModal.value = false
|
showRemoveModal.value = false
|
||||||
serverToRemove.value = null
|
serverToRemove.value = null
|
||||||
serversRes.reload()
|
serversRes.reload()
|
||||||
|
|
@ -679,9 +694,7 @@ async function updateServerRole(serverName, roleField, value) {
|
||||||
|
|
||||||
const result = await call('admin_panel.api.servers.update_server', params)
|
const result = await call('admin_panel.api.servers.update_server', params)
|
||||||
|
|
||||||
if (result.success) {
|
if (!result.success) {
|
||||||
console.log(result.message)
|
|
||||||
} else {
|
|
||||||
console.error(result.error || 'Failed to update server')
|
console.error(result.error || 'Failed to update server')
|
||||||
serversRes.reload()
|
serversRes.reload()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,12 @@
|
||||||
</template>
|
</template>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="subtle" theme="red" @click="showDeleteContainerModal = true">
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.delete') }}
|
||||||
|
</Button>
|
||||||
<Button variant="solid" @click="openSite">
|
<Button variant="solid" @click="openSite">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FeatherIcon name="external-link" class="h-4 w-4" />
|
<FeatherIcon name="external-link" class="h-4 w-4" />
|
||||||
|
|
@ -514,18 +520,53 @@
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Delete Container Modal -->
|
||||||
|
<Dialog v-model="showDeleteContainerModal" :options="{ title: 'Delete Container' }">
|
||||||
|
<template #body-content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center gap-3 p-3 rounded-lg bg-red-50 dark:bg-red-950/30">
|
||||||
|
<FeatherIcon name="alert-triangle" class="h-5 w-5 text-red-600 dark:text-red-400 flex-shrink-0" />
|
||||||
|
<p class="text-sm text-red-800 dark:text-red-300">
|
||||||
|
This action is irreversible. The container <strong>{{ site?.name }}</strong> and all its data will be permanently deleted.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="deleteConfirmName"
|
||||||
|
label="Type the container name to confirm"
|
||||||
|
type="text"
|
||||||
|
:placeholder="site?.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<Button variant="subtle" @click="showDeleteContainerModal = false; deleteConfirmName = ''">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
theme="red"
|
||||||
|
@click="deleteContainer"
|
||||||
|
:loading="actionLoading === 'delete-container'"
|
||||||
|
:disabled="deleteConfirmName !== site?.name"
|
||||||
|
>
|
||||||
|
Delete Container
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { createResource, call } from 'frappe-ui'
|
import { createResource, call } from 'frappe-ui'
|
||||||
import { Button, Badge, Spinner, FormControl, Dialog, FeatherIcon } from 'frappe-ui'
|
import { Button, Badge, Spinner, FormControl, Dialog, FeatherIcon } from 'frappe-ui'
|
||||||
import LayoutHeader from '@/components/Common/LayoutHeader.vue'
|
import LayoutHeader from '@/components/Common/LayoutHeader.vue'
|
||||||
import { siteActivity } from '@/mocks/sites'
|
import { siteActivity } from '@/mocks/sites'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const activeTab = ref('overview')
|
const activeTab = ref('overview')
|
||||||
|
|
||||||
// Get server from query param (for remote containers)
|
// Get server from query param (for remote containers)
|
||||||
|
|
@ -539,6 +580,10 @@ const snapshotCreateError = ref('')
|
||||||
const logsLoaded = ref(false)
|
const logsLoaded = ref(false)
|
||||||
const snapshotsLoaded = ref(false)
|
const snapshotsLoaded = ref(false)
|
||||||
|
|
||||||
|
// Delete container state
|
||||||
|
const showDeleteContainerModal = ref(false)
|
||||||
|
const deleteConfirmName = ref('')
|
||||||
|
|
||||||
// Update/Rebuild state
|
// Update/Rebuild state
|
||||||
const showUpdateModal = ref(false)
|
const showUpdateModal = ref(false)
|
||||||
const updateImage = ref('')
|
const updateImage = ref('')
|
||||||
|
|
@ -668,7 +713,25 @@ function generateDefaultSnapshotName() {
|
||||||
const d = String(now.getDate()).padStart(2, '0')
|
const d = String(now.getDate()).padStart(2, '0')
|
||||||
const h = String(now.getHours()).padStart(2, '0')
|
const h = String(now.getHours()).padStart(2, '0')
|
||||||
const min = String(now.getMinutes()).padStart(2, '0')
|
const min = String(now.getMinutes()).padStart(2, '0')
|
||||||
return `snap-${y}${m}${d}-${h}${min}`
|
const sec = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
return `snap-${y}${m}${d}-${h}${min}${sec}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteContainer() {
|
||||||
|
actionLoading.value = 'delete-container'
|
||||||
|
try {
|
||||||
|
await call('admin_panel.api.sites.delete_container', {
|
||||||
|
container_name: route.params.siteId,
|
||||||
|
target_server: targetServer.value,
|
||||||
|
})
|
||||||
|
showDeleteContainerModal.value = false
|
||||||
|
deleteConfirmName.value = ''
|
||||||
|
router.push({ name: 'Sites' })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete container:', e)
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startSite() {
|
async function startSite() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue