fix: restore colored icons, move distrobuilder to Apps, fix doas pty.fork
- Restored colored icon circles (green shield, orange alert) for certificate section in Servers while keeping semantic tokens for backgrounds - Moved distrobuilder setup section from Servers to Apps tab where it logically belongs - Fixed doas TTY error by using pty.fork() instead of pty.openpty() — pty.fork() properly sets up a controlling terminal that doas requires Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4f6c07b14b
commit
a86fecaf2a
|
|
@ -366,27 +366,23 @@ def get_setup_status():
|
|||
|
||||
|
||||
def _run_doas_command(cmd, doas_password, timeout=120):
|
||||
"""Run a command with doas using a pseudo-terminal for password input."""
|
||||
"""Run a command with doas using pty.fork() for a real controlling terminal."""
|
||||
import pty
|
||||
import select
|
||||
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
pid, master_fd = pty.fork()
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
close_fds=True,
|
||||
)
|
||||
os.close(slave_fd)
|
||||
slave_fd = -1
|
||||
if pid == 0:
|
||||
# Child process — exec the command
|
||||
os.execvp(cmd[0], cmd)
|
||||
os._exit(1)
|
||||
|
||||
# Wait for doas password prompt
|
||||
# Parent process
|
||||
output = b""
|
||||
password_sent = False
|
||||
deadline = time.time() + timeout
|
||||
|
||||
try:
|
||||
while time.time() < deadline:
|
||||
ready, _, _ = select.select([master_fd], [], [], 1)
|
||||
if ready:
|
||||
|
|
@ -395,38 +391,46 @@ def _run_doas_command(cmd, doas_password, timeout=120):
|
|||
if not chunk:
|
||||
break
|
||||
output += chunk
|
||||
# doas prompts with "doas (user@host) password:" or similar
|
||||
if b"password:" in output.lower() or b"password" in output.lower():
|
||||
if not password_sent and b"password" in output.lower():
|
||||
time.sleep(0.1)
|
||||
os.write(master_fd, (doas_password + "\n").encode())
|
||||
break
|
||||
except OSError:
|
||||
break
|
||||
|
||||
# Read remaining output
|
||||
while time.time() < deadline:
|
||||
ready, _, _ = select.select([master_fd], [], [], 2)
|
||||
if ready:
|
||||
try:
|
||||
chunk = os.read(master_fd, 4096)
|
||||
if not chunk:
|
||||
break
|
||||
output += chunk
|
||||
password_sent = True
|
||||
except OSError:
|
||||
break
|
||||
else:
|
||||
# No more data
|
||||
if proc.poll() is not None:
|
||||
# Check if child exited
|
||||
result = os.waitpid(pid, os.WNOHANG)
|
||||
if result[0] != 0:
|
||||
# Drain remaining output
|
||||
while True:
|
||||
r, _, _ = select.select([master_fd], [], [], 0.5)
|
||||
if r:
|
||||
try:
|
||||
d = os.read(master_fd, 4096)
|
||||
if d:
|
||||
output += d
|
||||
else:
|
||||
break
|
||||
except OSError:
|
||||
break
|
||||
else:
|
||||
break
|
||||
rc = os.WEXITSTATUS(result[1]) if os.WIFEXITED(result[1]) else 1
|
||||
return rc, output.decode("utf-8", errors="replace")
|
||||
|
||||
proc.wait(timeout=max(1, int(deadline - time.time())))
|
||||
return proc.returncode, output.decode("utf-8", errors="replace")
|
||||
# Final wait
|
||||
_, status = os.waitpid(pid, 0)
|
||||
rc = os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1
|
||||
return rc, output.decode("utf-8", errors="replace")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
try:
|
||||
os.kill(pid, 9)
|
||||
os.waitpid(pid, 0)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
if slave_fd >= 0:
|
||||
os.close(slave_fd)
|
||||
os.close(master_fd)
|
||||
|
||||
|
||||
|
|
|
|||
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{o as a,c as r,b as e,t as i,L as s}from"./index-e9fa3fd2.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-4c1e5cae.js.map
|
||||
import{o as a,c as r,b as e,t as i,M as s}from"./index-1d2d8107.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-809e459c.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"ChartCard-4c1e5cae.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-809e459c.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-f55d9654.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-e9fa3fd2.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-9285933a.js.map
|
||||
import{_ as g}from"./LayoutHeader-da9a4100.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-1d2d8107.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-57fa7b9e.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
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,L as d}from"./index-e9fa3fd2.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-ff6fbcb9.js.map
|
||||
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-1d2d8107.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-1aed7a26.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"EmptyState-ff6fbcb9.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-1aed7a26.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,L as s,t as i}from"./index-e9fa3fd2.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-f55d9654.js.map
|
||||
import{o as a,c as r,b as e,M as s,t as i}from"./index-1d2d8107.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-da9a4100.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"LayoutHeader-f55d9654.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-da9a4100.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-e9fa3fd2.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-8e452bf9.js.map
|
||||
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-1d2d8107.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-70c4bc3a.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-e9fa3fd2.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-22257ff1.js.map
|
||||
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-1d2d8107.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-84c84485.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"NotFound-22257ff1.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-84c84485.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
|
|
@ -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-e9fa3fd2.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-6f3d08a8.js.map
|
||||
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-1d2d8107.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-561c461f.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
|
|
@ -15,7 +15,7 @@
|
|||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-e9fa3fd2.js"></script>
|
||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-1d2d8107.js"></script>
|
||||
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-43b47371.css">
|
||||
</head>
|
||||
<body class="h-full">
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-e9fa3fd2.js"></script>
|
||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-1d2d8107.js"></script>
|
||||
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-43b47371.css">
|
||||
</head>
|
||||
<body class="h-full">
|
||||
|
|
|
|||
|
|
@ -17,7 +17,51 @@
|
|||
</template>
|
||||
</LayoutHeader>
|
||||
|
||||
<div class="flex-1 overflow-auto p-5">
|
||||
<div class="flex-1 overflow-auto p-5 space-y-6">
|
||||
<!-- Distrobuilder Setup -->
|
||||
<div class="rounded-lg border p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ $t('servers.setup.title') }}
|
||||
</h3>
|
||||
<Button
|
||||
variant="solid"
|
||||
@click="showInstallModal = true"
|
||||
:disabled="setupStatus?.distrobuilder_installed"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="download" class="h-4 w-4" />
|
||||
</template>
|
||||
{{ $t('servers.setup.install') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="setupStatusRes.loading" class="flex justify-center py-4">
|
||||
<Spinner class="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<div v-else class="flex items-center gap-3 py-3">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center"
|
||||
:class="setupStatus?.distrobuilder_installed ? 'bg-green-100 text-green-600' : 'bg-red-100 text-red-600'"
|
||||
>
|
||||
<FeatherIcon
|
||||
:name="setupStatus?.distrobuilder_installed ? 'check-circle' : 'alert-circle'"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-ink-gray-9">
|
||||
{{ setupStatus?.distrobuilder_installed ? $t('servers.setup.distrobuilderInstalled') : $t('servers.setup.distrobuilderNotInstalled') }}
|
||||
</p>
|
||||
<p class="text-xs text-ink-gray-5">
|
||||
{{ setupStatus?.distrobuilder_version || $t('servers.setup.distrobuilderDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Templates -->
|
||||
<div v-if="apps.loading" class="flex items-center justify-center h-64">
|
||||
<Spinner class="h-8 w-8" />
|
||||
</div>
|
||||
|
|
@ -152,6 +196,67 @@
|
|||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Install Distrobuilder Modal -->
|
||||
<Dialog v-model="showInstallModal" :options="{ title: $t('servers.setup.title') }">
|
||||
<template #body-content>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-ink-gray-7">
|
||||
{{ $t('servers.setup.installDescription') }}
|
||||
</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">
|
||||
<p class="text-xs text-ink-gray-5">
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div v-if="installResult" class="mt-4">
|
||||
<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
|
||||
v-for="(log, index) in installResult.logs"
|
||||
:key="index"
|
||||
:class="{
|
||||
'text-green-400': log.includes('Done'),
|
||||
'text-red-400': log.includes('Failed') || log.includes('Error'),
|
||||
'text-yellow-400': log.includes('==='),
|
||||
}"
|
||||
>
|
||||
{{ log }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex w-full items-center justify-end gap-2">
|
||||
<Button variant="subtle" @click="closeInstallModal">
|
||||
{{ installResult ? $t('common.close') : $t('common.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!installResult"
|
||||
variant="solid"
|
||||
@click="runInstall"
|
||||
:loading="installing"
|
||||
:disabled="!doasPassword"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="download" class="h-4 w-4" />
|
||||
</template>
|
||||
{{ $t('servers.setup.install') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -166,9 +271,13 @@ const router = useRouter()
|
|||
|
||||
const showCreateModal = ref(false)
|
||||
const showDeleteModal = ref(false)
|
||||
const showInstallModal = ref(false)
|
||||
const creating = ref(false)
|
||||
const deleting = ref(false)
|
||||
const installing = ref(false)
|
||||
const appToDelete = ref(null)
|
||||
const installResult = ref(null)
|
||||
const doasPassword = ref('')
|
||||
|
||||
const defaultYamlTemplate = `image:
|
||||
distribution: "alpinelinux"
|
||||
|
|
@ -224,7 +333,13 @@ const apps = createResource({
|
|||
auto: true,
|
||||
})
|
||||
|
||||
const setupStatusRes = createResource({
|
||||
url: 'admin_panel.api.servers.get_setup_status',
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const appsList = computed(() => apps.data || [])
|
||||
const setupStatus = computed(() => setupStatusRes.data)
|
||||
|
||||
function refreshData() {
|
||||
apps.reload()
|
||||
|
|
@ -308,4 +423,33 @@ async function confirmDelete() {
|
|||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runInstall() {
|
||||
installing.value = true
|
||||
installResult.value = null
|
||||
|
||||
try {
|
||||
const result = await call('admin_panel.api.servers.run_setup', {
|
||||
doas_password: doasPassword.value,
|
||||
})
|
||||
installResult.value = result
|
||||
|
||||
if (result.success) {
|
||||
setupStatusRes.reload()
|
||||
}
|
||||
} catch (error) {
|
||||
installResult.value = {
|
||||
success: false,
|
||||
logs: [`Error: ${error.message}`],
|
||||
}
|
||||
} finally {
|
||||
installing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeInstallModal() {
|
||||
showInstallModal.value = false
|
||||
installResult.value = null
|
||||
doasPassword.value = ''
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -37,40 +37,19 @@
|
|||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<!-- Certificate status row -->
|
||||
<div class="flex items-center justify-between py-3 border-b">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full"
|
||||
:class="certStatus?.has_certificate ? 'bg-green-500' : 'bg-orange-400'"
|
||||
></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-ink-gray-9">
|
||||
{{ certStatus?.has_certificate ? 'Certificate configured' : 'No certificate' }}
|
||||
</p>
|
||||
<p v-if="certStatus?.has_certificate" class="text-xs text-ink-gray-5 font-mono">
|
||||
{{ certStatus.cert_path }}
|
||||
</p>
|
||||
<p v-else class="text-xs text-ink-gray-5">
|
||||
Generate a client certificate to connect to Incus servers
|
||||
</p>
|
||||
<!-- Certificate exists -->
|
||||
<div v-if="certStatus?.has_certificate">
|
||||
<div class="flex items-center gap-3 py-3 border-b">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-green-100 text-green-600">
|
||||
<FeatherIcon name="shield" class="h-5 w-5" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-ink-gray-9">Certificate configured</p>
|
||||
<p class="text-xs text-ink-gray-5 font-mono">{{ certStatus.cert_path }}</p>
|
||||
</div>
|
||||
<Button
|
||||
v-if="!certStatus?.has_certificate"
|
||||
variant="solid"
|
||||
@click="generateCertificate"
|
||||
:loading="generating"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="key" class="h-4 w-4" />
|
||||
</template>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Instructions (only if cert exists) -->
|
||||
<div v-if="certStatus?.has_certificate" class="rounded-lg bg-surface-gray-1 p-4">
|
||||
<div class="rounded-lg bg-surface-gray-1 p-4 mt-4">
|
||||
<p class="text-sm font-medium text-ink-gray-7 mb-2">How to trust this certificate on Incus servers:</p>
|
||||
<ol class="text-sm text-ink-gray-5 space-y-1 list-decimal list-inside">
|
||||
<li>Click "View Certificate" to copy the certificate content</li>
|
||||
|
|
@ -79,47 +58,29 @@
|
|||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Distrobuilder Setup -->
|
||||
<div class="rounded-lg border p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ $t('servers.setup.title') }}
|
||||
</h3>
|
||||
<Button
|
||||
variant="solid"
|
||||
@click="showInstallModal = true"
|
||||
:disabled="setupStatus?.distrobuilder_installed"
|
||||
>
|
||||
<!-- No certificate -->
|
||||
<div v-else class="flex items-center justify-between py-3 border-b">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-orange-100 text-orange-600">
|
||||
<FeatherIcon name="alert-circle" class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-ink-gray-9">No certificate</p>
|
||||
<p class="text-xs text-ink-gray-5">Generate a client certificate to connect to Incus servers</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="solid" @click="generateCertificate" :loading="generating">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="download" class="h-4 w-4" />
|
||||
<FeatherIcon name="key" class="h-4 w-4" />
|
||||
</template>
|
||||
{{ $t('servers.setup.install') }}
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="setupStatusRes.loading" class="flex justify-center py-4">
|
||||
<Spinner class="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<div v-else class="flex items-center gap-3 py-3 border-b">
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full"
|
||||
:class="setupStatus?.distrobuilder_installed ? 'bg-green-500' : 'bg-red-500'"
|
||||
></div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-ink-gray-9">
|
||||
{{ setupStatus?.distrobuilder_installed ? $t('servers.setup.distrobuilderInstalled') : $t('servers.setup.distrobuilderNotInstalled') }}
|
||||
</p>
|
||||
<p class="text-xs text-ink-gray-5">
|
||||
{{ setupStatus?.distrobuilder_version || $t('servers.setup.distrobuilderDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Remote Servers -->
|
||||
<!-- Section 2: Remote Servers -->
|
||||
<div class="rounded-lg border p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-ink-gray-9">
|
||||
|
|
@ -213,67 +174,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Install Modal -->
|
||||
<Dialog v-model="showInstallModal" :options="{ title: $t('servers.setup.title') }">
|
||||
<template #body-content>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-ink-gray-7">
|
||||
{{ $t('servers.setup.installDescription') }}
|
||||
</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">
|
||||
<p class="text-xs text-ink-gray-5">
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div v-if="installResult" class="mt-4">
|
||||
<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
|
||||
v-for="(log, index) in installResult.logs"
|
||||
:key="index"
|
||||
:class="{
|
||||
'text-green-400': log.includes('Done'),
|
||||
'text-red-400': log.includes('Failed') || log.includes('Error'),
|
||||
'text-yellow-400': log.includes('==='),
|
||||
}"
|
||||
>
|
||||
{{ log }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex w-full items-center justify-end gap-2">
|
||||
<Button variant="subtle" @click="closeInstallModal">
|
||||
{{ installResult ? $t('common.close') : $t('common.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!installResult"
|
||||
variant="solid"
|
||||
@click="runInstall"
|
||||
:loading="installing"
|
||||
:disabled="!doasPassword"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="download" class="h-4 w-4" />
|
||||
</template>
|
||||
{{ $t('servers.setup.install') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Add Server Modal -->
|
||||
<Dialog v-model="showAddServerModal" :options="{ title: $t('servers.addServer') }">
|
||||
<template #body-content>
|
||||
|
|
@ -408,16 +308,11 @@ import LayoutHeader from '@/components/Common/LayoutHeader.vue'
|
|||
import EmptyState from '@/components/Common/EmptyState.vue'
|
||||
|
||||
const refreshing = ref(false)
|
||||
const showInstallModal = ref(false)
|
||||
const showAddServerModal = ref(false)
|
||||
const showRemoveModal = ref(false)
|
||||
const showCertModal = ref(false)
|
||||
const serverToRemove = ref(null)
|
||||
|
||||
const installing = ref(false)
|
||||
const installResult = ref(null)
|
||||
const doasPassword = ref('')
|
||||
|
||||
const addingServer = ref(false)
|
||||
const removingServer = ref(false)
|
||||
const generating = ref(false)
|
||||
|
|
@ -438,11 +333,6 @@ const certStatusRes = createResource({
|
|||
auto: true,
|
||||
})
|
||||
|
||||
const setupStatusRes = createResource({
|
||||
url: 'admin_panel.api.servers.get_setup_status',
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const serversRes = createResource({
|
||||
url: 'admin_panel.api.servers.get_servers',
|
||||
auto: true,
|
||||
|
|
@ -453,7 +343,6 @@ const certContentRes = createResource({
|
|||
})
|
||||
|
||||
const certStatus = computed(() => certStatusRes.data)
|
||||
const setupStatus = computed(() => setupStatusRes.data)
|
||||
const serversList = computed(() => serversRes.data || [])
|
||||
const certContent = computed(() => certContentRes.data)
|
||||
|
||||
|
|
@ -468,7 +357,6 @@ function refreshData() {
|
|||
refreshing.value = true
|
||||
Promise.all([
|
||||
certStatusRes.reload(),
|
||||
setupStatusRes.reload(),
|
||||
serversRes.reload(),
|
||||
]).finally(() => {
|
||||
refreshing.value = false
|
||||
|
|
@ -553,35 +441,6 @@ function copyCertificate() {
|
|||
}
|
||||
}
|
||||
|
||||
async function runInstall() {
|
||||
installing.value = true
|
||||
installResult.value = null
|
||||
|
||||
try {
|
||||
const result = await call('admin_panel.api.servers.run_setup', {
|
||||
doas_password: doasPassword.value,
|
||||
})
|
||||
installResult.value = result
|
||||
|
||||
if (result.success) {
|
||||
setupStatusRes.reload()
|
||||
}
|
||||
} catch (error) {
|
||||
installResult.value = {
|
||||
success: false,
|
||||
logs: [`Error: ${error.message}`],
|
||||
}
|
||||
} finally {
|
||||
installing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeInstallModal() {
|
||||
showInstallModal.value = false
|
||||
installResult.value = null
|
||||
doasPassword.value = ''
|
||||
}
|
||||
|
||||
async function testServerConnection(serverName) {
|
||||
try {
|
||||
const result = await call('admin_panel.api.servers.test_connection', {
|
||||
|
|
|
|||
Loading…
Reference in New Issue