improve: graceful rebuild process with service readiness checks
Backend (sites.py): - Graceful stop with fallback to force stop on timeout - Verify container actually stopped/started before proceeding - Wait for database readiness before running bench migrate - Better log formatting: ✓/✗/⚠/→ prefixes, step numbers [1/5]-[5/5] - Show manual command hint when migrate output is empty - Separate migrate success tracking from overall rebuild success Frontend (SiteDetail.vue): - Step-by-step progress indicators during rebuild (5 steps) - Elapsed time counter for both rebuild and migrate - Terminal-style log viewer (dark background, colored output) - Empty lines preserved for readability Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
430a8c521f
commit
da71d91294
|
|
@ -639,33 +639,60 @@ def run_bench_migrate(container_name, target_server=None):
|
||||||
frappe.throw("Server name is required")
|
frappe.throw("Server name is required")
|
||||||
|
|
||||||
logs = []
|
logs = []
|
||||||
logs.append(f"=== Running bench migrate ===")
|
logs.append("=== bench migrate ===")
|
||||||
logs.append(f"Container: {container_name}")
|
logs.append(f"Container: {container_name}")
|
||||||
|
logs.append("")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check container is running
|
# Check container is running
|
||||||
detail = _incus_api(target_server, "GET", f"/1.0/instances/{container_name}")
|
detail = _incus_api(target_server, "GET", f"/1.0/instances/{container_name}")
|
||||||
status = detail.get("metadata", {}).get("status")
|
status = detail.get("metadata", {}).get("status")
|
||||||
if status != "Running":
|
if status != "Running":
|
||||||
|
logs.append(f"✗ Container status is '{status}', must be Running")
|
||||||
return {"success": False, "error": "Container must be running", "logs": logs}
|
return {"success": False, "error": "Container must be running", "logs": logs}
|
||||||
|
|
||||||
logs.append("Executing bench migrate...")
|
# Check services are ready
|
||||||
|
logs.append("Checking database connection...")
|
||||||
|
if _wait_services_ready(target_server, container_name, timeout=30):
|
||||||
|
logs.append("✓ Database is ready")
|
||||||
|
else:
|
||||||
|
logs.append("⚠ Could not verify database, attempting migrate anyway")
|
||||||
|
|
||||||
|
logs.append("")
|
||||||
|
logs.append("Running: uvx --from frappe-bench bench migrate")
|
||||||
|
logs.append("")
|
||||||
|
|
||||||
result = _exec_in_container(
|
result = _exec_in_container(
|
||||||
target_server, container_name,
|
target_server, container_name,
|
||||||
"su frappe -c 'cd /home/frappe/frappe-bench && uvx --from frappe-bench bench migrate' 2>&1",
|
"su frappe -c 'cd /home/frappe/frappe-bench && uvx --from frappe-bench bench migrate' 2>&1",
|
||||||
timeout=600,
|
timeout=600,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result["output"]:
|
output = result.get("output", "")
|
||||||
for line in result["output"].split("\n"):
|
if output:
|
||||||
if line.strip():
|
for line in output.split("\n"):
|
||||||
logs.append(line.strip())
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if "error" in line.lower() or "traceback" in line.lower() or "exception" in line.lower():
|
||||||
|
logs.append(f" ✗ {line}")
|
||||||
|
elif "migrating" in line.lower() or "patching" in line.lower():
|
||||||
|
logs.append(f" → {line}")
|
||||||
|
elif "success" in line.lower() or "done" in line.lower() or "completed" in line.lower():
|
||||||
|
logs.append(f" ✓ {line}")
|
||||||
|
else:
|
||||||
|
logs.append(f" {line}")
|
||||||
|
|
||||||
|
logs.append("")
|
||||||
if result["return_code"] == 0:
|
if result["return_code"] == 0:
|
||||||
logs.append("✓ bench migrate completed")
|
logs.append("✓ bench migrate completed successfully")
|
||||||
return {"success": True, "logs": logs, "message": "bench migrate completed successfully"}
|
return {"success": True, "logs": logs, "message": "bench migrate completed successfully"}
|
||||||
else:
|
else:
|
||||||
logs.append(f"✗ bench migrate failed (exit code {result['return_code']})")
|
logs.append(f"✗ bench migrate failed (exit code {result['return_code']})")
|
||||||
|
if not output:
|
||||||
|
logs.append("")
|
||||||
|
logs.append("No output captured. Try running manually:")
|
||||||
|
logs.append(f" incus exec {container_name} -- su frappe -c 'cd /home/frappe/frappe-bench && uvx --from frappe-bench bench migrate'")
|
||||||
return {"success": False, "logs": logs, "error": "bench migrate failed"}
|
return {"success": False, "logs": logs, "error": "bench migrate failed"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -673,6 +700,39 @@ def run_bench_migrate(container_name, target_server=None):
|
||||||
return {"success": False, "error": str(e), "logs": logs}
|
return {"success": False, "error": str(e), "logs": logs}
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_container_status(target_server, container_name, expected_status, timeout=60, poll_interval=2):
|
||||||
|
"""Poll container status until it matches expected_status or timeout."""
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < timeout:
|
||||||
|
try:
|
||||||
|
detail = _incus_api(target_server, "GET", f"/1.0/instances/{container_name}")
|
||||||
|
status = detail.get("metadata", {}).get("status", "")
|
||||||
|
if status == expected_status:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_services_ready(target_server, container_name, timeout=90, poll_interval=5):
|
||||||
|
"""Wait until MariaDB/MySQL is accepting connections inside container."""
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < timeout:
|
||||||
|
try:
|
||||||
|
result = _exec_in_container(
|
||||||
|
target_server, container_name,
|
||||||
|
"su frappe -c 'cd /home/frappe/frappe-bench && python -c \"import frappe; frappe.init(site=frappe.utils.get_sites()[0]); frappe.connect(); print(frappe.db.sql(\\\"SELECT 1\\\")[0][0])\"' 2>&1",
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
if result["return_code"] == 0 and "1" in result.get("output", ""):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def rebuild_container(container_name, image, target_server=None, run_migrate=False):
|
def rebuild_container(container_name, image, target_server=None, run_migrate=False):
|
||||||
"""Rebuild a container with a new image."""
|
"""Rebuild a container with a new image."""
|
||||||
|
|
@ -685,30 +745,55 @@ def rebuild_container(container_name, image, target_server=None, run_migrate=Fal
|
||||||
run_migrate = run_migrate.lower() in ("true", "1")
|
run_migrate = run_migrate.lower() in ("true", "1")
|
||||||
|
|
||||||
logs = []
|
logs = []
|
||||||
logs.append(f"=== Rebuild Container ===")
|
migrate_success = True
|
||||||
|
logs.append(f"=== Update Container ===")
|
||||||
logs.append(f"Container: {container_name}")
|
logs.append(f"Container: {container_name}")
|
||||||
logs.append(f"New image: {image}")
|
logs.append(f"New image: {image}")
|
||||||
logs.append(f"Server: {target_server}")
|
logs.append(f"Server: {target_server}")
|
||||||
|
logs.append(f"Run migrate: {'yes' if run_migrate else 'no'}")
|
||||||
|
logs.append("")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Step 1: Check current status
|
||||||
detail = _incus_api(target_server, "GET", f"/1.0/instances/{container_name}")
|
detail = _incus_api(target_server, "GET", f"/1.0/instances/{container_name}")
|
||||||
meta = detail.get("metadata", {})
|
meta = detail.get("metadata", {})
|
||||||
was_running = meta.get("status") == "Running"
|
was_running = meta.get("status") == "Running"
|
||||||
logs.append(f"Current status: {meta.get('status')}")
|
logs.append(f"[1/5] Current status: {meta.get('status')}")
|
||||||
|
|
||||||
|
# Step 2: Stop container gracefully, then verify
|
||||||
if was_running:
|
if was_running:
|
||||||
logs.append("Stopping container...")
|
logs.append("[2/5] Stopping container (graceful)...")
|
||||||
resp = _incus_api(target_server, "PUT", f"/1.0/instances/{container_name}/state", {
|
try:
|
||||||
"action": "stop",
|
resp = _incus_api(target_server, "PUT", f"/1.0/instances/{container_name}/state", {
|
||||||
"timeout": 30,
|
"action": "stop",
|
||||||
"force": True,
|
"timeout": 60,
|
||||||
})
|
"force": False,
|
||||||
op = resp.get("operation")
|
})
|
||||||
if op:
|
op = resp.get("operation")
|
||||||
_wait_for_operation(target_server, op)
|
if op:
|
||||||
logs.append("✓ Container stopped")
|
_wait_for_operation(target_server, op, timeout=90)
|
||||||
|
except Exception:
|
||||||
|
# Graceful stop failed, try force
|
||||||
|
logs.append(" Graceful stop timed out, forcing...")
|
||||||
|
resp = _incus_api(target_server, "PUT", f"/1.0/instances/{container_name}/state", {
|
||||||
|
"action": "stop",
|
||||||
|
"timeout": 30,
|
||||||
|
"force": True,
|
||||||
|
})
|
||||||
|
op = resp.get("operation")
|
||||||
|
if op:
|
||||||
|
_wait_for_operation(target_server, op, timeout=60)
|
||||||
|
|
||||||
logs.append(f"Rebuilding with image '{image}'...")
|
# Verify container actually stopped
|
||||||
|
if _wait_container_status(target_server, container_name, "Stopped", timeout=30):
|
||||||
|
logs.append(" ✓ Container stopped")
|
||||||
|
else:
|
||||||
|
logs.append(" ⚠ Container may not have fully stopped, proceeding anyway")
|
||||||
|
else:
|
||||||
|
logs.append("[2/5] Container already stopped, skipping")
|
||||||
|
|
||||||
|
# Step 3: Rebuild with new image
|
||||||
|
logs.append(f"[3/5] Rebuilding with image '{image}'...")
|
||||||
resp = _incus_api(target_server, "POST", f"/1.0/instances/{container_name}/rebuild", {
|
resp = _incus_api(target_server, "POST", f"/1.0/instances/{container_name}/rebuild", {
|
||||||
"source": {
|
"source": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
|
|
@ -718,46 +803,103 @@ def rebuild_container(container_name, image, target_server=None, run_migrate=Fal
|
||||||
op = resp.get("operation")
|
op = resp.get("operation")
|
||||||
if op:
|
if op:
|
||||||
_wait_for_operation(target_server, op, timeout=300)
|
_wait_for_operation(target_server, op, timeout=300)
|
||||||
logs.append("✓ Container rebuilt")
|
logs.append(" ✓ Container rebuilt successfully")
|
||||||
|
|
||||||
|
# Step 4: Start container and wait for services
|
||||||
if was_running:
|
if was_running:
|
||||||
logs.append("Starting container...")
|
logs.append("[4/5] Starting container...")
|
||||||
resp = _incus_api(target_server, "PUT", f"/1.0/instances/{container_name}/state", {
|
resp = _incus_api(target_server, "PUT", f"/1.0/instances/{container_name}/state", {
|
||||||
"action": "start",
|
"action": "start",
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
})
|
})
|
||||||
op = resp.get("operation")
|
op = resp.get("operation")
|
||||||
if op:
|
if op:
|
||||||
_wait_for_operation(target_server, op)
|
_wait_for_operation(target_server, op, timeout=60)
|
||||||
logs.append("✓ Container started")
|
|
||||||
|
|
||||||
# Run bench migrate if requested
|
# Verify container is running
|
||||||
|
if _wait_container_status(target_server, container_name, "Running", timeout=30):
|
||||||
|
logs.append(" ✓ Container is running")
|
||||||
|
else:
|
||||||
|
logs.append(" ✗ Container failed to start")
|
||||||
|
logs.append("")
|
||||||
|
logs.append("=== Update finished with errors ===")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"container_name": container_name,
|
||||||
|
"image": image,
|
||||||
|
"logs": logs,
|
||||||
|
"message": "Container failed to start after rebuild",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for services (DB, Redis, etc.)
|
||||||
|
if run_migrate:
|
||||||
|
logs.append(" Waiting for services (database, redis)...")
|
||||||
|
if _wait_services_ready(target_server, container_name, timeout=90):
|
||||||
|
logs.append(" ✓ Services are ready")
|
||||||
|
else:
|
||||||
|
logs.append(" ⚠ Services may not be fully ready, attempting migrate anyway")
|
||||||
|
else:
|
||||||
|
logs.append("[4/5] Container was stopped, skipping start")
|
||||||
|
|
||||||
|
# Step 5: Run bench migrate
|
||||||
if run_migrate and was_running:
|
if run_migrate and was_running:
|
||||||
|
logs.append("[5/5] Running bench migrate...")
|
||||||
logs.append("")
|
logs.append("")
|
||||||
logs.append("=== Running bench migrate ===")
|
|
||||||
time.sleep(5) # Wait for services to start
|
|
||||||
migrate_result = _exec_in_container(
|
migrate_result = _exec_in_container(
|
||||||
target_server, container_name,
|
target_server, container_name,
|
||||||
"su frappe -c 'cd /home/frappe/frappe-bench && uvx --from frappe-bench bench migrate' 2>&1",
|
"su frappe -c 'cd /home/frappe/frappe-bench && uvx --from frappe-bench bench migrate' 2>&1",
|
||||||
timeout=600,
|
timeout=600,
|
||||||
)
|
)
|
||||||
if migrate_result["output"]:
|
|
||||||
for line in migrate_result["output"].split("\n"):
|
|
||||||
if line.strip():
|
|
||||||
logs.append(line.strip())
|
|
||||||
if migrate_result["return_code"] == 0:
|
|
||||||
logs.append("✓ bench migrate completed")
|
|
||||||
else:
|
|
||||||
logs.append(f"✗ bench migrate failed (exit code {migrate_result['return_code']})")
|
|
||||||
|
|
||||||
logs.append("\n=== Rebuild completed successfully! ===")
|
output = migrate_result.get("output", "")
|
||||||
return {
|
if output:
|
||||||
"success": True,
|
# Parse and format output nicely
|
||||||
"container_name": container_name,
|
for line in output.split("\n"):
|
||||||
"image": image,
|
line = line.strip()
|
||||||
"logs": logs,
|
if not line:
|
||||||
"message": f"Container '{container_name}' rebuilt with image '{image}'",
|
continue
|
||||||
}
|
# Highlight important lines
|
||||||
|
if "error" in line.lower() or "traceback" in line.lower() or "exception" in line.lower():
|
||||||
|
logs.append(f" ✗ {line}")
|
||||||
|
elif "migrating" in line.lower() or "patching" in line.lower():
|
||||||
|
logs.append(f" → {line}")
|
||||||
|
elif "success" in line.lower() or "done" in line.lower() or "completed" in line.lower():
|
||||||
|
logs.append(f" ✓ {line}")
|
||||||
|
else:
|
||||||
|
logs.append(f" {line}")
|
||||||
|
|
||||||
|
logs.append("")
|
||||||
|
if migrate_result["return_code"] == 0:
|
||||||
|
logs.append(" ✓ bench migrate completed successfully")
|
||||||
|
else:
|
||||||
|
migrate_success = False
|
||||||
|
logs.append(f" ✗ bench migrate failed (exit code {migrate_result['return_code']})")
|
||||||
|
if not output:
|
||||||
|
logs.append(" No output captured. The command may have crashed.")
|
||||||
|
logs.append(" Try running manually inside the container:")
|
||||||
|
logs.append(f" incus exec {container_name} -- su frappe -c 'cd /home/frappe/frappe-bench && uvx --from frappe-bench bench migrate'")
|
||||||
|
else:
|
||||||
|
logs.append("[5/5] Skipping migrate" + (" (container was stopped)" if not was_running else ""))
|
||||||
|
|
||||||
|
logs.append("")
|
||||||
|
if migrate_success:
|
||||||
|
logs.append("=== Update completed successfully! ===")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"container_name": container_name,
|
||||||
|
"image": image,
|
||||||
|
"logs": logs,
|
||||||
|
"message": f"Container '{container_name}' updated with image '{image}'",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logs.append("=== Update completed, but migrate failed ===")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"container_name": container_name,
|
||||||
|
"image": image,
|
||||||
|
"logs": logs,
|
||||||
|
"message": f"Container rebuilt, but bench migrate failed. Check logs for details.",
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logs.append(f"✗ Error: {str(e)}")
|
logs.append(f"✗ Error: {str(e)}")
|
||||||
|
|
@ -765,7 +907,7 @@ def rebuild_container(container_name, image, target_server=None, run_migrate=Fal
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
"logs": logs,
|
"logs": logs,
|
||||||
"message": f"Rebuild failed: {str(e)}",
|
"message": f"Update failed: {str(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,N as s}from"./index-840692da.js";const n={class:"rounded-lg border p-4 shadow-sm"},c={class:"flex items-center justify-between mb-4"},d={class:"text-base font-medium text-ink-gray-9"},l={class:"h-[200px]"},p={__name:"ChartCard",props:{title:{type:String,required:!0}},setup(o){return(t,_)=>(a(),r("div",n,[e("div",c,[e("h3",d,i(o.title),1),s(t.$slots,"actions")]),e("div",l,[s(t.$slots,"default")])]))}};export{p as _};
|
import{o as a,c as r,b as e,t as i,N as s}from"./index-6984cb4d.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-bf2862cd.js.map
|
//# sourceMappingURL=ChartCard-32bd6a37.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"ChartCard-bf2862cd.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-32bd6a37.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-aeb3d2f2.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-840692da.js";const F={class:"flex flex-col h-full"},B={class:"flex-1 overflow-auto p-5"},L={class:"rounded-lg border overflow-hidden"},N={class:"min-w-full divide-y"},U={class:"bg-surface-gray-2"},j={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},q={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},A={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},D={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},E={class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},I={class:"px-6 py-3 text-right text-xs font-medium text-ink-gray-5 uppercase"},R={class:"divide-y"},S={class:"px-6 py-4"},z={class:"flex items-center gap-3"},G={class:"w-8 h-8 rounded-full bg-surface-gray-2 flex items-center justify-center"},Q={class:"text-sm font-medium text-ink-gray-5"},T={class:"text-sm font-medium text-ink-gray-9"},H={class:"px-6 py-4 text-sm text-ink-gray-7"},J={class:"px-6 py-4 text-sm text-ink-gray-7"},K={class:"px-6 py-4 text-sm font-medium text-ink-gray-9"},M={class:"px-6 py-4"},O={class:"px-6 py-4 text-sm text-ink-gray-7"},P={class:"px-6 py-4 text-right"},et={__name:"Customers",setup(W){const l=x(""),u=x(v),_=k(()=>{if(!l.value)return u.value;const e=l.value.toLowerCase();return u.value.filter(o=>o.name.toLowerCase().includes(e)||o.email.toLowerCase().includes(e))});function f(e){return new Intl.NumberFormat("ru-RU",{style:"currency",currency:"RUB",minimumFractionDigits:0}).format(e)}function y(){console.log("Exporting customers...")}function h(e){console.log("Viewing customer:",e.name)}return(e,o)=>(c(),m("div",F,[n(g,{title:e.$t("payments.customers")},{right:r(()=>[n(i(C),{modelValue:l.value,"onUpdate:modelValue":o[0]||(o[0]=a=>l.value=a),type:"text",placeholder:e.$t("common.search"),class:"w-64"},{prefix:r(()=>[n(i(d),{name:"search",class:"h-4 w-4 text-ink-gray-4"})]),_:1},8,["modelValue","placeholder"]),n(i(p),{variant:"subtle",onClick:y},{prefix:r(()=>[n(i(d),{name:"download",class:"h-4 w-4"})]),default:r(()=>[b(" "+s(e.$t("common.export")),1)]),_:1})]),_:1},8,["title"]),t("div",B,[t("div",L,[t("table",N,[t("thead",U,[t("tr",null,[t("th",j,s(e.$t("payments.customer.name")),1),t("th",q,s(e.$t("payments.customer.email")),1),t("th",A,s(e.$t("payments.connections")),1),t("th",D,s(e.$t("payments.customer.amount")),1),t("th",E,s(e.$t("payments.customer.status")),1),o[1]||(o[1]=t("th",{class:"px-6 py-3 text-left text-xs font-medium text-ink-gray-5 uppercase"},"Since",-1)),t("th",I,s(e.$t("common.actions")),1)])]),t("tbody",R,[(c(!0),m(w,null,$(_.value,a=>(c(),m("tr",{key:a.id},[t("td",S,[t("div",z,[t("div",G,[t("span",Q,s(a.name.charAt(0)),1)]),t("span",T,s(a.name),1)])]),t("td",H,s(a.email),1),t("td",J,s(a.connections),1),t("td",K,s(f(a.monthlyAmount))+"/mo ",1),t("td",M,[n(i(V),{variant:a.status==="active"?"success":"info",label:a.status},null,8,["variant","label"])]),t("td",O,s(a.since),1),t("td",P,[n(i(p),{variant:"ghost",size:"sm",onClick:X=>h(a)},{default:r(()=>[n(i(d),{name:"eye",class:"h-4 w-4"})]),_:1},8,["onClick"])])]))),128))])])])])]))}};export{et as default};
|
import{_ as g}from"./LayoutHeader-756499bb.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-6984cb4d.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-4e585bd9.js.map
|
//# sourceMappingURL=Customers-a9befce5.js.map
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
||||||
import{o as t,c as s,b as n,d as r,e as c,F as o,t as a,i as l,N as d}from"./index-840692da.js";const m={class:"flex flex-col items-center justify-center py-12 text-center"},u={class:"mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-surface-gray-2"},f={class:"mb-2 text-lg font-medium text-ink-gray-9"},y={key:0,class:"mb-4 max-w-sm text-sm text-ink-gray-5"},p={__name:"EmptyState",props:{icon:{type:String,default:"inbox"},title:{type:String,required:!0},description:{type:String,default:""}},setup(e){return(i,x)=>(t(),s("div",m,[n("div",u,[r(c(o),{name:e.icon,class:"h-8 w-8 text-ink-gray-4"},null,8,["name"])]),n("h3",f,a(e.title),1),e.description?(t(),s("p",y,a(e.description),1)):l("",!0),d(i.$slots,"action")]))}};export{p as _};
|
import{o as t,c as s,b as n,d as r,e as c,F as o,t as a,i as l,N as d}from"./index-6984cb4d.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-c9a94953.js.map
|
//# sourceMappingURL=EmptyState-a41cdda0.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"EmptyState-c9a94953.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-a41cdda0.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,N as s,t as i}from"./index-840692da.js";const c={class:"flex items-center justify-between border-b bg-surface-white px-5 py-2.5"},l={class:"flex items-center gap-2"},n={class:"text-xl font-semibold text-ink-gray-9"},d={class:"flex items-center gap-2"},f={__name:"LayoutHeader",props:{title:{type:String,default:""}},setup(o){return(t,_)=>(a(),r("header",c,[e("div",l,[s(t.$slots,"left",{},()=>[e("h1",n,i(o.title),1)])]),e("div",d,[s(t.$slots,"right")])]))}};export{f as _};
|
import{o as a,c as r,b as e,N as s,t as i}from"./index-6984cb4d.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-aeb3d2f2.js.map
|
//# sourceMappingURL=LayoutHeader-756499bb.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"LayoutHeader-aeb3d2f2.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-756499bb.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-840692da.js";const C={class:"min-h-screen flex items-center justify-center bg-surface-gray-2 py-12 px-4 sm:px-6 lg:px-8"},U={class:"max-w-md w-full space-y-8"},D={class:"text-center"},P={class:"mt-2 text-sm text-ink-gray-5"},R={class:"space-y-4"},S={for:"email",class:"block text-sm font-medium text-ink-gray-7"},E={for:"password",class:"block text-sm font-medium text-ink-gray-7"},T={class:"flex items-center justify-between"},z={class:"flex items-center"},F={for:"remember-me",class:"ml-2 block text-sm text-ink-gray-7"},G={class:"text-sm"},H={href:"#",class:"font-medium text-blue-600 hover:text-blue-500"},K={__name:"Login",setup(I){const h=b(),v=_(),d=o(""),u=o(""),c=o(!1),m=o(!1),i=o("");async function g(){m.value=!0,i.value="";try{await q("login",{usr:d.value,pwd:u.value}),await A.init();const s=v.query.redirect||"/dashboard";h.push(s)}catch(s){i.value=s.message||"Login failed"}finally{m.value=!1}}return(s,t)=>(f(),y("div",C,[e("div",U,[e("div",D,[r(L,{class:"mx-auto h-16 w-16"}),t[3]||(t[3]=e("h2",{class:"mt-6 text-3xl font-bold text-ink-gray-9"}," Admin Panel ",-1)),e("p",P,a(s.$t("auth.login")),1)]),e("form",{class:"mt-8 space-y-6",onSubmit:x(g,["prevent"])},[e("div",R,[e("div",null,[e("label",S,a(s.$t("auth.email")),1),r(n(p),{id:"email",modelValue:d.value,"onUpdate:modelValue":t[0]||(t[0]=l=>d.value=l),type:"email",required:"",class:"mt-1",placeholder:s.$t("auth.email")},null,8,["modelValue","placeholder"])]),e("div",null,[e("label",E,a(s.$t("auth.password")),1),r(n(p),{id:"password",modelValue:u.value,"onUpdate:modelValue":t[1]||(t[1]=l=>u.value=l),type:"password",required:"",class:"mt-1",placeholder:s.$t("auth.password")},null,8,["modelValue","placeholder"])])]),e("div",T,[e("div",z,[w(e("input",{id:"remember-me","onUpdate:modelValue":t[2]||(t[2]=l=>c.value=l),type:"checkbox",class:"h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"},null,512),[[k,c.value]]),e("label",F,a(s.$t("auth.rememberMe")),1)]),e("div",G,[e("a",H,a(s.$t("auth.forgotPassword")),1)])]),e("div",null,[r(n(N),{type:"submit",variant:"solid",class:"w-full",loading:m.value},{default:V(()=>[M(a(s.$t("auth.loginButton")),1)]),_:1},8,["loading"])]),i.value?(f(),$(n(j),{key:0,message:i.value},null,8,["message"])):B("",!0)],32)])]))}};export{K as default};
|
import{u as b,a as _,o as f,c as y,b as e,d as r,t as a,w as x,e as n,f as w,v as k,g as V,h as $,i as B,r as o,A as L,_ as p,j as M,k as N,l as j,m as q,s as A}from"./index-6984cb4d.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-5943e925.js.map
|
//# sourceMappingURL=Login-9c5fd274.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-840692da.js";const x={class:"min-h-screen flex items-center justify-center bg-surface-gray-2 py-12 px-4"},u={class:"text-center"},g={__name:"NotFound",setup(f){return(s,t)=>(o(),a("div",x,[e("div",u,[t[2]||(t[2]=e("h1",{class:"text-9xl font-bold text-ink-gray-2"},"404",-1)),t[3]||(t[3]=e("h2",{class:"mt-4 text-2xl font-semibold text-ink-gray-9"}," Page Not Found ",-1)),t[4]||(t[4]=e("p",{class:"mt-2 text-ink-gray-5"}," The page you're looking for doesn't exist. ",-1)),r(i(l),{variant:"solid",class:"mt-6",onClick:t[0]||(t[0]=m=>s.$router.push("/dashboard"))},{default:n(()=>[...t[1]||(t[1]=[d(" Go to Dashboard ",-1)])]),_:1})])]))}};export{g as default};
|
import{o,c as a,b as e,d as r,g as n,e as i,j as d,k as l}from"./index-6984cb4d.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-c369cbc5.js.map
|
//# sourceMappingURL=NotFound-b92a28d0.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"NotFound-c369cbc5.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-b92a28d0.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
|
|
@ -1 +0,0 @@
|
||||||
.migrate-progress[data-v-9073c711]{animation:migrate-bar-9073c711 3s ease-in-out infinite}@keyframes migrate-bar-9073c711{0%{width:5%}50%{width:70%}to{width:95%}}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.migrate-progress[data-v-1d4c10fc]{animation:migrate-bar-1d4c10fc 3s ease-in-out infinite}@keyframes migrate-bar-1d4c10fc{0%{width:5%}50%{width:70%}to{width:95%}}
|
||||||
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-840692da.js";const p={class:"rounded-lg border p-4 shadow-sm"},v={class:"flex items-center justify-between"},h={class:"flex items-center gap-3"},w={class:"text-sm text-ink-gray-5"},N={class:"text-2xl font-semibold text-ink-gray-9"},k={class:"text-sm font-medium"},S={key:0,class:"mt-2 text-xs text-ink-gray-5"},F={__name:"StatCard",props:{label:{type:String,required:!0},value:{type:[Number,String],required:!0},iconName:{type:String,default:"activity"},color:{type:String,default:"blue",validator:e=>["blue","green","red","yellow","purple","gray"].includes(e)},trend:{type:Number,default:void 0},subtitle:{type:String,default:""},format:{type:String,default:"number",validator:e=>["number","currency","percent"].includes(e)}},setup(e){const n=e,a={blue:{bg:"bg-blue-100",icon:"text-blue-600"},green:{bg:"bg-green-100",icon:"text-green-600"},red:{bg:"bg-red-100",icon:"text-red-600"},yellow:{bg:"bg-orange-100",icon:"text-orange-600"},purple:{bg:"bg-purple-100",icon:"text-purple-600"},gray:{bg:"bg-surface-gray-2",icon:"text-ink-gray-5"}},b=s(()=>{var t;return((t=a[n.color])==null?void 0:t.bg)||a.blue.bg}),f=s(()=>{var t;return((t=a[n.color])==null?void 0:t.icon)||a.blue.icon}),y=s(()=>n.trend>0?"text-green-600":n.trend<0?"text-red-600":"text-ink-gray-5"),x=s(()=>{const t=typeof n.value=="number"?n.value:parseFloat(n.value);if(isNaN(t))return n.value;switch(n.format){case"currency":return new Intl.NumberFormat("ru-RU",{style:"currency",currency:"RUB",minimumFractionDigits:0}).format(t);case"percent":return`${t}%`;default:return new Intl.NumberFormat("ru-RU").format(t)}});return(t,C)=>(c(),u("div",p,[r("div",v,[r("div",h,[r("div",{class:i(["flex h-10 w-10 items-center justify-center rounded-lg",b.value])},[o(d(g),{name:e.iconName,class:i(["h-5 w-5",f.value])},null,8,["name","class"])],2),r("div",null,[r("p",w,l(e.label),1),r("p",N,l(x.value),1)])]),e.trend!==void 0?(c(),u("div",{key:0,class:i(["flex items-center gap-1",y.value])},[o(d(g),{name:e.trend>0?"trending-up":"trending-down",class:"h-4 w-4"},null,8,["name"]),r("span",k,l(Math.abs(e.trend))+"%",1)],2)):m("",!0)]),e.subtitle?(c(),u("div",S,l(e.subtitle),1)):m("",!0)]))}};export{F as _};
|
import{n as s,o as c,c as u,b as r,E as i,d as o,e as d,F as g,t as l,i as m}from"./index-6984cb4d.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-39bd0409.js.map
|
//# sourceMappingURL=StatCard-140179fb.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
|
|
@ -15,8 +15,8 @@
|
||||||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-840692da.js"></script>
|
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-6984cb4d.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-4119d0df.css">
|
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-b6e1d020.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full">
|
<body class="h-full">
|
||||||
<div id="app" class="h-full"></div>
|
<div id="app" class="h-full"></div>
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@
|
||||||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-840692da.js"></script>
|
<script type="module" crossorigin src="/assets/admin_panel/frontend/assets/index-6984cb4d.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-4119d0df.css">
|
<link rel="stylesheet" href="/assets/admin_panel/frontend/assets/index-b6e1d020.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full">
|
<body class="h-full">
|
||||||
<div id="app" class="h-full"></div>
|
<div id="app" class="h-full"></div>
|
||||||
|
|
|
||||||
|
|
@ -515,31 +515,28 @@
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- Rebuild Progress/Result Modal -->
|
<!-- Progress/Result Modal -->
|
||||||
<Dialog v-model="showRebuildProgress" :options="{ title: rebuilding ? 'Updating...' : (migrateRunning ? 'Running migrate...' : 'Result'), size: 'lg' }">
|
<Dialog v-model="showRebuildProgress" :options="{ title: progressTitle, size: 'lg' }">
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Progress indicator during rebuild -->
|
<!-- Progress indicator during rebuild or migrate -->
|
||||||
<div v-if="rebuilding" class="flex flex-col items-center gap-4 py-6">
|
<div v-if="rebuilding || migrateRunning" class="flex flex-col gap-4 py-4">
|
||||||
<Spinner class="h-10 w-10 text-blue-500" />
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm font-medium text-ink-gray-8">Rebuilding container...</p>
|
|
||||||
<p class="text-xs text-ink-gray-5 mt-1">This may take a few minutes</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-full bg-surface-gray-2 rounded-full h-2 overflow-hidden">
|
|
||||||
<div class="h-full bg-blue-500 rounded-full animate-pulse" style="width: 60%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progress indicator during migrate -->
|
|
||||||
<div v-else-if="migrateRunning" class="flex flex-col gap-4 py-4">
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Spinner class="h-6 w-6 text-blue-500 flex-shrink-0" />
|
<Spinner class="h-6 w-6 text-blue-500 flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-ink-gray-8">Running bench migrate...</p>
|
<p class="text-sm font-medium text-ink-gray-8">{{ progressMessage }}</p>
|
||||||
<p class="text-xs text-ink-gray-5 mt-0.5">
|
<p class="text-xs text-ink-gray-5 mt-0.5">{{ progressDetail }}</p>
|
||||||
Executing: <code class="bg-surface-gray-2 px-1 rounded">uvx --from frappe-bench bench migrate</code>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
|
<!-- Steps for rebuild -->
|
||||||
|
<div v-if="rebuilding" class="space-y-2 text-sm">
|
||||||
|
<div v-for="step in rebuildSteps" :key="step.num" class="flex items-center gap-2">
|
||||||
|
<FeatherIcon v-if="step.num < currentStep" name="check-circle" class="h-4 w-4 text-green-500 flex-shrink-0" />
|
||||||
|
<Spinner v-else-if="step.num === currentStep" class="h-4 w-4 text-blue-500 flex-shrink-0" />
|
||||||
|
<FeatherIcon v-else name="circle" class="h-4 w-4 text-ink-gray-3 flex-shrink-0" />
|
||||||
|
<span :class="step.num <= currentStep ? 'text-ink-gray-8' : 'text-ink-gray-4'">
|
||||||
|
{{ step.label }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-surface-gray-2 rounded-full h-2 overflow-hidden">
|
<div class="w-full bg-surface-gray-2 rounded-full h-2 overflow-hidden">
|
||||||
|
|
@ -566,22 +563,25 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rebuild logs -->
|
<!-- Logs -->
|
||||||
<div v-if="rebuildResult.logs?.length" class="mt-4">
|
<div v-if="rebuildResult.logs?.length" class="mt-4">
|
||||||
<p class="text-xs font-medium text-ink-gray-5 uppercase mb-2">Rebuild Logs</p>
|
<p class="text-xs font-medium text-ink-gray-5 uppercase mb-2">Logs</p>
|
||||||
<div class="bg-surface-gray-1 rounded-lg p-3 max-h-64 overflow-y-auto font-mono text-xs">
|
<div ref="logContainer" class="bg-gray-900 rounded-lg p-3 max-h-80 overflow-y-auto font-mono text-xs leading-5">
|
||||||
<div
|
<div
|
||||||
v-for="(log, idx) in rebuildResult.logs"
|
v-for="(log, idx) in rebuildResult.logs"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="py-0.5"
|
|
||||||
:class="{
|
:class="{
|
||||||
'text-green-600': log.includes('✓'),
|
'text-green-400': log.includes('✓'),
|
||||||
'text-red-600': log.includes('✗') || log.includes('Error'),
|
'text-red-400': log.includes('✗'),
|
||||||
'text-ink-gray-6': !log.includes('✓') && !log.includes('✗'),
|
'text-yellow-400': log.includes('⚠'),
|
||||||
'font-semibold': log.includes('==='),
|
'text-blue-400': log.includes('→'),
|
||||||
|
'text-gray-300': !log.includes('✓') && !log.includes('✗') && !log.includes('⚠') && !log.includes('→'),
|
||||||
|
'text-white font-semibold': log.includes('==='),
|
||||||
|
'mt-2': log === '',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ log }}
|
<template v-if="log === ''"> </template>
|
||||||
|
<template v-else>{{ log }}</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -672,12 +672,44 @@ const showRebuildProgress = ref(false)
|
||||||
const rebuildResult = ref({ success: false, message: '', logs: [] })
|
const rebuildResult = ref({ success: false, message: '', logs: [] })
|
||||||
const runMigrateAfterRebuild = ref(true)
|
const runMigrateAfterRebuild = ref(true)
|
||||||
|
|
||||||
// Migrate progress state
|
// Migrate/rebuild progress state
|
||||||
const migrateRunning = ref(false)
|
const migrateRunning = ref(false)
|
||||||
const migrateStartTime = ref(null)
|
const migrateStartTime = ref(null)
|
||||||
const migrateElapsed = ref('Elapsed: 0s')
|
const migrateElapsed = ref('Elapsed: 0s')
|
||||||
|
const currentStep = ref(0)
|
||||||
let migrateTimer = null
|
let migrateTimer = null
|
||||||
|
|
||||||
|
const rebuildSteps = [
|
||||||
|
{ num: 1, label: 'Check current status' },
|
||||||
|
{ num: 2, label: 'Stop container gracefully' },
|
||||||
|
{ num: 3, label: 'Rebuild with new image' },
|
||||||
|
{ num: 4, label: 'Start container & wait for services' },
|
||||||
|
{ num: 5, label: 'Run bench migrate' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const progressTitle = computed(() => {
|
||||||
|
if (rebuilding.value) return 'Updating container...'
|
||||||
|
if (migrateRunning.value) return 'Running migrate...'
|
||||||
|
return rebuildResult.value.success ? 'Completed' : 'Result'
|
||||||
|
})
|
||||||
|
|
||||||
|
const progressMessage = computed(() => {
|
||||||
|
if (migrateRunning.value && !rebuilding.value) return 'Running bench migrate...'
|
||||||
|
if (!rebuilding.value) return ''
|
||||||
|
const step = rebuildSteps.find(s => s.num === currentStep.value)
|
||||||
|
return step ? step.label : 'Processing...'
|
||||||
|
})
|
||||||
|
|
||||||
|
const progressDetail = computed(() => {
|
||||||
|
if (migrateRunning.value && !rebuilding.value) {
|
||||||
|
return 'uvx --from frappe-bench bench migrate'
|
||||||
|
}
|
||||||
|
if (rebuilding.value) {
|
||||||
|
return `Step ${currentStep.value} of ${rebuildSteps.length}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
function startMigrateTimer() {
|
function startMigrateTimer() {
|
||||||
migrateStartTime.value = Date.now()
|
migrateStartTime.value = Date.now()
|
||||||
migrateElapsed.value = 'Elapsed: 0s'
|
migrateElapsed.value = 'Elapsed: 0s'
|
||||||
|
|
@ -1034,8 +1066,17 @@ async function rebuildContainer() {
|
||||||
|
|
||||||
showUpdateModal.value = false
|
showUpdateModal.value = false
|
||||||
rebuilding.value = true
|
rebuilding.value = true
|
||||||
|
currentStep.value = 1
|
||||||
rebuildResult.value = { success: false, message: '', logs: [] }
|
rebuildResult.value = { success: false, message: '', logs: [] }
|
||||||
showRebuildProgress.value = true
|
showRebuildProgress.value = true
|
||||||
|
startMigrateTimer()
|
||||||
|
|
||||||
|
// Simulate step progression (the backend does all steps in one call)
|
||||||
|
const stepTimer = setInterval(() => {
|
||||||
|
if (currentStep.value < 5) {
|
||||||
|
currentStep.value++
|
||||||
|
}
|
||||||
|
}, 8000) // advance steps roughly every 8s
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await call('admin_panel.api.sites.rebuild_container', {
|
const resp = await call('admin_panel.api.sites.rebuild_container', {
|
||||||
|
|
@ -1045,7 +1086,9 @@ async function rebuildContainer() {
|
||||||
run_migrate: runMigrateAfterRebuild.value,
|
run_migrate: runMigrateAfterRebuild.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
clearInterval(stepTimer)
|
||||||
rebuilding.value = false
|
rebuilding.value = false
|
||||||
|
stopMigrateTimer()
|
||||||
rebuildResult.value = {
|
rebuildResult.value = {
|
||||||
success: resp.success,
|
success: resp.success,
|
||||||
message: resp.message,
|
message: resp.message,
|
||||||
|
|
@ -1053,11 +1096,13 @@ async function rebuildContainer() {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to rebuild container:', e)
|
console.error('Failed to rebuild container:', e)
|
||||||
|
clearInterval(stepTimer)
|
||||||
rebuilding.value = false
|
rebuilding.value = false
|
||||||
|
stopMigrateTimer()
|
||||||
rebuildResult.value = {
|
rebuildResult.value = {
|
||||||
success: false,
|
success: false,
|
||||||
message: e.message || e.exc || 'Rebuild failed',
|
message: e.message || e.exc || 'Update failed',
|
||||||
logs: [`Error: ${e.message || e.exc}`],
|
logs: [`✗ Error: ${e.message || e.exc}`],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue