fix: semantic dark mode for Servers, doas PTY for install, container-not-found detection
- Servers page now uses frappe-ui semantic tokens (bg-surface-gray-*, text-ink-gray-*) matching Dashboard style instead of explicit bg-green-50/dark:bg-green-950 etc. - doas install uses Python pty module to create a real pseudo-terminal, fixing "doas: a tty is required" error - SiteDetail detects externally deleted containers and shows "Container Not Found" screen with Retry/Back buttons instead of silently failing - Action errors (start/stop/restart/delete) now show dismissible error banner Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f324f04ee9
commit
4f6c07b14b
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
|
|
@ -364,6 +365,71 @@ def get_setup_status():
|
|||
return status
|
||||
|
||||
|
||||
def _run_doas_command(cmd, doas_password, timeout=120):
|
||||
"""Run a command with doas using a pseudo-terminal for password input."""
|
||||
import pty
|
||||
import select
|
||||
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
close_fds=True,
|
||||
)
|
||||
os.close(slave_fd)
|
||||
slave_fd = -1
|
||||
|
||||
# Wait for doas password prompt
|
||||
output = b""
|
||||
deadline = time.time() + timeout
|
||||
|
||||
while time.time() < deadline:
|
||||
ready, _, _ = select.select([master_fd], [], [], 1)
|
||||
if ready:
|
||||
try:
|
||||
chunk = os.read(master_fd, 4096)
|
||||
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():
|
||||
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
|
||||
except OSError:
|
||||
break
|
||||
else:
|
||||
# No more data
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
|
||||
proc.wait(timeout=max(1, int(deadline - time.time())))
|
||||
return proc.returncode, output.decode("utf-8", errors="replace")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
raise
|
||||
finally:
|
||||
if slave_fd >= 0:
|
||||
os.close(slave_fd)
|
||||
os.close(master_fd)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def run_setup(doas_password=None):
|
||||
"""Run setup commands to install distrobuilder using doas with password."""
|
||||
|
|
@ -382,33 +448,31 @@ def run_setup(doas_password=None):
|
|||
for cmd, description in commands:
|
||||
logs.append(f"\n{description}...")
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
stdout, stderr = proc.communicate(
|
||||
input=doas_password + "\n",
|
||||
timeout=120,
|
||||
)
|
||||
returncode, output = _run_doas_command(cmd, doas_password)
|
||||
|
||||
if stdout:
|
||||
logs.append(stdout.strip())
|
||||
# Filter out the password prompt from output
|
||||
clean_lines = []
|
||||
for line in output.split("\n"):
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if "password:" in stripped.lower():
|
||||
continue
|
||||
clean_lines.append(stripped)
|
||||
if clean_lines:
|
||||
logs.append("\n".join(clean_lines))
|
||||
|
||||
if proc.returncode != 0:
|
||||
error = stderr or "Unknown error"
|
||||
if returncode != 0:
|
||||
error = "\n".join(clean_lines) or "Unknown error"
|
||||
if "already" in error.lower() or "exists" in error.lower():
|
||||
logs.append(" (already done)")
|
||||
else:
|
||||
logs.append(f"Failed: {error}")
|
||||
logs.append(f"Failed (exit code {returncode})")
|
||||
return {"success": False, "error": error, "logs": logs}
|
||||
else:
|
||||
logs.append(f"{description} - Done")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
logs.append(f"Timeout: {description}")
|
||||
return {"success": False, "error": f"Timeout during: {description}", "logs": logs}
|
||||
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-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-55f3f2ee.js.map
|
||||
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
|
||||
|
|
@ -1 +1 @@
|
|||
{"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"}
|
||||
{"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"}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
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-4d8c7660.js.map
|
||||
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
|
||||
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,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-54c60605.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,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
|
||||
|
|
@ -1 +1 @@
|
|||
{"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"}
|
||||
{"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"}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
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-cd508225.js.map
|
||||
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
|
||||
|
|
@ -1 +1 @@
|
|||
{"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"}
|
||||
{"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"}
|
||||
|
|
@ -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-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-9697bc9f.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-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
|
||||
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-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-eaa6081d.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-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
|
||||
|
|
@ -1 +1 @@
|
|||
{"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"}
|
||||
{"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"}
|
||||
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-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-edb6f5e9.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-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
|
||||
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');
|
||||
})();
|
||||
</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-984848c2.css">
|
||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-e9fa3fd2.js"></script>
|
||||
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-43b47371.css">
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<div id="app" class="h-full"></div>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||
})();
|
||||
</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-984848c2.css">
|
||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-e9fa3fd2.js"></script>
|
||||
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-43b47371.css">
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<div id="app" class="h-full"></div>
|
||||
|
|
|
|||
|
|
@ -19,83 +19,74 @@
|
|||
|
||||
<div class="flex-1 overflow-auto p-5 space-y-6">
|
||||
<!-- Section 1: Certificate Setup -->
|
||||
<div class="rounded-lg border p-5">
|
||||
<div class="rounded-lg border p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-base font-medium text-ink-gray-9">
|
||||
<h3 class="text-lg font-semibold text-ink-gray-9">
|
||||
Panel Certificate
|
||||
</h3>
|
||||
<p class="text-sm text-ink-gray-5 mt-1">
|
||||
Client certificate used to authenticate with Incus servers
|
||||
</p>
|
||||
</div>
|
||||
<Button v-if="certStatus?.has_certificate" variant="subtle" size="sm" @click="showCertModal = true">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="eye" class="h-4 w-4" />
|
||||
</template>
|
||||
View Certificate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="certStatusRes.loading" class="flex justify-center py-4">
|
||||
<Spinner class="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- Certificate exists -->
|
||||
<div v-if="certStatus?.has_certificate" class="space-y-4">
|
||||
<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/50">
|
||||
<FeatherIcon name="shield" class="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-300">Certificate configured</p>
|
||||
<p class="text-xs text-green-600 dark:text-green-500 font-mono">{{ certStatus.cert_path }}</p>
|
||||
</div>
|
||||
<Button variant="subtle" size="sm" @click="showCertModal = true">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="eye" class="h-4 w-4" />
|
||||
</template>
|
||||
View
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-950/30 rounded-lg p-4">
|
||||
<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-blue-700 dark:text-blue-400 space-y-1 list-decimal list-inside">
|
||||
<li>Click "View" to copy the certificate content</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-blue-100 dark:bg-blue-900/50 px-1 rounded">incus config trust add-certificate admin-panel.crt</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No certificate -->
|
||||
<div v-else class="space-y-4">
|
||||
<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/50">
|
||||
<FeatherIcon name="alert-circle" class="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
|
||||
<!-- 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>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-300">No certificate</p>
|
||||
<p class="text-xs text-yellow-600 dark:text-yellow-500">Generate a client certificate to connect to Incus servers</p>
|
||||
</div>
|
||||
<Button variant="solid" @click="generateCertificate" :loading="generating">
|
||||
<Button
|
||||
v-if="!certStatus?.has_certificate"
|
||||
variant="solid"
|
||||
@click="generateCertificate"
|
||||
:loading="generating"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="key" class="h-4 w-4" />
|
||||
</template>
|
||||
Generate Certificate
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Instructions (only if cert exists) -->
|
||||
<div v-if="certStatus?.has_certificate" class="rounded-lg bg-surface-gray-1 p-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>
|
||||
<li>Save it as <code class="bg-surface-gray-3 px-1 rounded text-ink-gray-7">admin-panel.crt</code> on your Incus server</li>
|
||||
<li>Run: <code class="bg-surface-gray-3 px-1 rounded text-ink-gray-7">incus config trust add-certificate admin-panel.crt</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Distrobuilder Setup -->
|
||||
<div class="rounded-lg border p-5">
|
||||
<div class="rounded-lg border p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-base font-medium text-ink-gray-9">
|
||||
<h3 class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ $t('servers.setup.title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-ink-gray-5 mt-1">
|
||||
{{ $t('servers.setup.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
@click="showInstallModal = true"
|
||||
|
|
@ -112,28 +103,16 @@
|
|||
<Spinner class="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<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 v-else class="flex items-center gap-3 py-3 border-b">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center"
|
||||
:class="setupStatus?.distrobuilder_installed ? 'bg-green-100 dark:bg-green-900/50' : 'bg-red-100 dark:bg-red-900/50'"
|
||||
>
|
||||
<FeatherIcon
|
||||
:name="setupStatus?.distrobuilder_installed ? 'check-circle' : 'alert-circle'"
|
||||
class="h-5 w-5"
|
||||
:class="setupStatus?.distrobuilder_installed ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||
/>
|
||||
</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"
|
||||
:class="setupStatus?.distrobuilder_installed ? 'text-green-800 dark:text-green-300' : 'text-red-800 dark:text-red-300'"
|
||||
>
|
||||
<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"
|
||||
:class="setupStatus?.distrobuilder_installed ? 'text-green-600 dark:text-green-500' : 'text-red-600 dark:text-red-500'"
|
||||
>
|
||||
<p class="text-xs text-ink-gray-5">
|
||||
{{ setupStatus?.distrobuilder_version || $t('servers.setup.distrobuilderDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -141,16 +120,11 @@
|
|||
</div>
|
||||
|
||||
<!-- Section 3: Remote Servers -->
|
||||
<div class="rounded-lg border p-5">
|
||||
<div class="rounded-lg border p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-base font-medium text-ink-gray-9">
|
||||
<h3 class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ $t('servers.remoteServers') }}
|
||||
</h3>
|
||||
<p class="text-sm text-ink-gray-5 mt-1">
|
||||
{{ $t('servers.roles.title') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="serversRes.loading" class="flex justify-center py-8">
|
||||
|
|
@ -165,8 +139,8 @@
|
|||
>
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<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 dark:text-blue-400" />
|
||||
<div class="w-10 h-10 rounded-lg bg-surface-gray-2 flex items-center justify-center">
|
||||
<FeatherIcon name="cloud" class="h-5 w-5 text-ink-gray-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-ink-gray-9">{{ server.name }}</h4>
|
||||
|
|
@ -409,9 +383,9 @@
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-950/30 rounded-lg p-4">
|
||||
<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 dark:text-blue-400 block bg-blue-100 dark:bg-blue-900/50 p-2 rounded">
|
||||
<div class="rounded-lg bg-surface-gray-1 p-4">
|
||||
<p class="text-sm text-ink-gray-7 font-medium mb-2">On your Incus server:</p>
|
||||
<code class="text-sm text-ink-gray-7 block bg-surface-gray-3 p-2 rounded font-mono">
|
||||
incus config trust add-certificate admin-panel.crt
|
||||
</code>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,13 +19,14 @@
|
|||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
v-if="site"
|
||||
v-if="site && !containerError"
|
||||
:variant="getStatusBadge(site.status)"
|
||||
:label="site.status"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<template v-if="!containerError">
|
||||
<Button
|
||||
v-if="site?.status !== 'Running'"
|
||||
variant="subtle"
|
||||
|
|
@ -80,6 +81,7 @@
|
|||
{{ $t('sites.actions.openSite') }}
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
|
||||
<div class="flex-1 overflow-auto">
|
||||
|
|
@ -115,8 +117,44 @@
|
|||
<Button variant="subtle" @click="detail.reload()">Retry</Button>
|
||||
</div>
|
||||
|
||||
<!-- Container not found / API error in response -->
|
||||
<div v-else-if="containerError" class="flex flex-col items-center justify-center h-64 gap-4">
|
||||
<div class="flex items-center justify-center w-16 h-16 rounded-full bg-red-50 dark:bg-red-950/30">
|
||||
<FeatherIcon name="alert-triangle" class="h-8 w-8 text-red-500 dark:text-red-400" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-semibold text-ink-gray-9 mb-1">Container Not Found</h3>
|
||||
<p class="text-sm text-ink-gray-5 max-w-md">
|
||||
The container <strong>{{ route.params.siteId }}</strong> does not exist on the server.
|
||||
It may have been deleted externally.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<Button variant="subtle" @click="detail.reload()">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="refresh-cw" class="h-4 w-4" />
|
||||
</template>
|
||||
Retry
|
||||
</Button>
|
||||
<Button variant="solid" @click="$router.push({ name: 'Sites' })">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="arrow-left" class="h-4 w-4" />
|
||||
</template>
|
||||
Back to Sites
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab content -->
|
||||
<template v-else>
|
||||
<!-- Action error banner -->
|
||||
<div v-if="actionError" class="mb-4 flex items-center gap-3 p-3 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
|
||||
<FeatherIcon name="x-circle" class="h-5 w-5 text-red-500 flex-shrink-0" />
|
||||
<p class="text-sm text-red-700 dark:text-red-300 flex-1">{{ actionError }}</p>
|
||||
<Button variant="ghost" size="sm" @click="actionError = ''">
|
||||
<FeatherIcon name="x" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Overview Tab -->
|
||||
<div v-show="activeTab === 'overview'">
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
|
|
@ -583,6 +621,7 @@ const snapshotsLoaded = ref(false)
|
|||
// Delete container state
|
||||
const showDeleteContainerModal = ref(false)
|
||||
const deleteConfirmName = ref('')
|
||||
const actionError = ref('')
|
||||
|
||||
// Update/Rebuild state
|
||||
const showUpdateModal = ref(false)
|
||||
|
|
@ -635,6 +674,7 @@ const images = createResource({
|
|||
})
|
||||
|
||||
const site = computed(() => detail.data)
|
||||
const containerError = computed(() => site.value?.error || null)
|
||||
|
||||
const imageOptions = computed(() => {
|
||||
const list = images.data || []
|
||||
|
|
@ -719,6 +759,7 @@ function generateDefaultSnapshotName() {
|
|||
|
||||
async function deleteContainer() {
|
||||
actionLoading.value = 'delete-container'
|
||||
actionError.value = ''
|
||||
try {
|
||||
await call('admin_panel.api.sites.delete_container', {
|
||||
container_name: route.params.siteId,
|
||||
|
|
@ -729,6 +770,9 @@ async function deleteContainer() {
|
|||
router.push({ name: 'Sites' })
|
||||
} catch (e) {
|
||||
console.error('Failed to delete container:', e)
|
||||
showDeleteContainerModal.value = false
|
||||
actionError.value = e.messages?.[0] || e.message || 'Failed to delete container'
|
||||
detail.reload()
|
||||
} finally {
|
||||
actionLoading.value = ''
|
||||
}
|
||||
|
|
@ -736,6 +780,7 @@ async function deleteContainer() {
|
|||
|
||||
async function startSite() {
|
||||
actionLoading.value = 'start'
|
||||
actionError.value = ''
|
||||
try {
|
||||
await call('admin_panel.api.sites.start_container', {
|
||||
container_name: route.params.siteId,
|
||||
|
|
@ -744,6 +789,8 @@ async function startSite() {
|
|||
detail.reload()
|
||||
} catch (e) {
|
||||
console.error('Failed to start container:', e)
|
||||
actionError.value = e.messages?.[0] || e.message || 'Failed to start container'
|
||||
detail.reload()
|
||||
} finally {
|
||||
actionLoading.value = ''
|
||||
}
|
||||
|
|
@ -751,6 +798,7 @@ async function startSite() {
|
|||
|
||||
async function stopSite() {
|
||||
actionLoading.value = 'stop'
|
||||
actionError.value = ''
|
||||
try {
|
||||
await call('admin_panel.api.sites.stop_container', {
|
||||
container_name: route.params.siteId,
|
||||
|
|
@ -759,6 +807,8 @@ async function stopSite() {
|
|||
detail.reload()
|
||||
} catch (e) {
|
||||
console.error('Failed to stop container:', e)
|
||||
actionError.value = e.messages?.[0] || e.message || 'Failed to stop container'
|
||||
detail.reload()
|
||||
} finally {
|
||||
actionLoading.value = ''
|
||||
}
|
||||
|
|
@ -766,6 +816,7 @@ async function stopSite() {
|
|||
|
||||
async function restartSite() {
|
||||
actionLoading.value = 'restart'
|
||||
actionError.value = ''
|
||||
try {
|
||||
await call('admin_panel.api.sites.restart_container', {
|
||||
container_name: route.params.siteId,
|
||||
|
|
@ -774,6 +825,8 @@ async function restartSite() {
|
|||
detail.reload()
|
||||
} catch (e) {
|
||||
console.error('Failed to restart container:', e)
|
||||
actionError.value = e.messages?.[0] || e.message || 'Failed to restart container'
|
||||
detail.reload()
|
||||
} finally {
|
||||
actionLoading.value = ''
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue