loki-mode 6.63.0 → 6.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +37 -5
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/web-app/dist/assets/{Badge-Daan3gu4.js → Badge-8l0OZCRe.js} +1 -1
- package/web-app/dist/assets/{Button-BfeQWtXn.js → Button-6k_tnJgc.js} +1 -1
- package/web-app/dist/assets/{Card-JqwSaE0I.js → Card-DwkzVihG.js} +1 -1
- package/web-app/dist/assets/{HomePage-ZrDPLDGe.js → HomePage-C0-_6Avk.js} +1 -1
- package/web-app/dist/assets/{LoginPage-lJUDQIlI.js → LoginPage-BlJm-Tzr.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-kZTYx4v_.js → NotFoundPage-CsRjjzWq.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-DayJk_FX.js → ProjectPage-DQG_ZYM7.js} +51 -46
- package/web-app/dist/assets/{ProjectsPage-4_PqKgaD.js → ProjectsPage-BAQOc1tx.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-DmjFCI0F.js → SettingsPage-DiKaBtvg.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-BOX60wWf.js → TemplatesPage-CyxNji74.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-B9rfXUCC.js → TerminalOutput-BLPNvDc5.js} +1 -1
- package/web-app/dist/assets/{arrow-left-Rh7PJrlD.js → arrow-left-dP_J0CkC.js} +1 -1
- package/web-app/dist/assets/{clock-CDe-IBc9.js → clock-CGZn7bQ1.js} +1 -1
- package/web-app/dist/assets/{external-link-BviPLjiY.js → external-link-ypPFWwc1.js} +1 -1
- package/web-app/dist/assets/index-CQcaFLVo.css +1 -0
- package/web-app/dist/assets/{index--VmvfdEx.js → index-tGQw_JnU.js} +22 -22
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +468 -13
- package/web-app/dist/assets/index-DzYIpBt0.css +0 -1
package/web-app/dist/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
11
|
-
<script type="module" crossorigin src="/assets/index
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-tGQw_JnU.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CQcaFLVo.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="bg-background text-ink font-sans antialiased">
|
|
15
15
|
<div id="root"></div>
|
package/web-app/server.py
CHANGED
|
@@ -649,11 +649,14 @@ class DevServerManager:
|
|
|
649
649
|
"image": svc.get("image"),
|
|
650
650
|
"has_build": "build" in svc,
|
|
651
651
|
})
|
|
652
|
-
# Use
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
652
|
+
# Use smart resolution to pick the user-facing service
|
|
653
|
+
_primary_name, port = self._resolve_primary_service(services_info)
|
|
654
|
+
if _primary_name is None:
|
|
655
|
+
# Fallback: first service with any port
|
|
656
|
+
for svc_entry in services_info:
|
|
657
|
+
if svc_entry["ports"]:
|
|
658
|
+
port = svc_entry["ports"][0]
|
|
659
|
+
break
|
|
657
660
|
except ImportError:
|
|
658
661
|
# yaml not available -- fall back to regex parsing
|
|
659
662
|
try:
|
|
@@ -912,6 +915,36 @@ class DevServerManager:
|
|
|
912
915
|
return port
|
|
913
916
|
return None
|
|
914
917
|
|
|
918
|
+
def _resolve_primary_service(self, services_info: list[dict]) -> tuple[Optional[str], int]:
|
|
919
|
+
"""Determine the primary user-facing service from Docker Compose services."""
|
|
920
|
+
frontend_names = {"frontend", "web", "client", "app", "ui", "next", "vite", "nginx"}
|
|
921
|
+
frontend_ports = {3000, 5173, 8080, 4200, 5000, 4000}
|
|
922
|
+
|
|
923
|
+
# Priority 1: Service name matches frontend patterns
|
|
924
|
+
for svc in services_info:
|
|
925
|
+
if svc["name"].lower() in frontend_names and svc.get("ports"):
|
|
926
|
+
return svc["name"], svc["ports"][0]
|
|
927
|
+
|
|
928
|
+
# Priority 2: Service has a frontend-typical port
|
|
929
|
+
for svc in services_info:
|
|
930
|
+
for p in svc.get("ports", []):
|
|
931
|
+
if p in frontend_ports:
|
|
932
|
+
return svc["name"], p
|
|
933
|
+
|
|
934
|
+
# Priority 3: Custom-built service (has build, no standard image like postgres/redis)
|
|
935
|
+
infra_images = {"postgres", "redis", "mongo", "mysql", "rabbitmq", "memcached", "elasticsearch"}
|
|
936
|
+
for svc in services_info:
|
|
937
|
+
img = (svc.get("image") or "").split(":")[0].split("/")[-1].lower()
|
|
938
|
+
if svc.get("has_build") and img not in infra_images and svc.get("ports"):
|
|
939
|
+
return svc["name"], svc["ports"][0]
|
|
940
|
+
|
|
941
|
+
# Fallback: first service with any port
|
|
942
|
+
for svc in services_info:
|
|
943
|
+
if svc.get("ports"):
|
|
944
|
+
return svc["name"], svc["ports"][0]
|
|
945
|
+
|
|
946
|
+
return None, 3000
|
|
947
|
+
|
|
915
948
|
def _install_pip_deps(self, project_path: Path, build_env: dict) -> None:
|
|
916
949
|
"""Install pip dependencies into a project venv (creates one if needed)."""
|
|
917
950
|
if not (project_path / "requirements.txt").exists():
|
|
@@ -1203,6 +1236,10 @@ class DevServerManager:
|
|
|
1203
1236
|
|
|
1204
1237
|
asyncio.create_task(self._monitor_output(session_id))
|
|
1205
1238
|
|
|
1239
|
+
# For Docker projects, also start the service health monitor
|
|
1240
|
+
if framework == "docker":
|
|
1241
|
+
asyncio.create_task(self._monitor_docker_services(session_id))
|
|
1242
|
+
|
|
1206
1243
|
# Wait for port detection (up to 30s)
|
|
1207
1244
|
for _ in range(60):
|
|
1208
1245
|
await asyncio.sleep(0.5)
|
|
@@ -1387,6 +1424,24 @@ class DevServerManager:
|
|
|
1387
1424
|
f"DEV SERVER ERROR OUTPUT:\n{error_context}"
|
|
1388
1425
|
)
|
|
1389
1426
|
|
|
1427
|
+
# Enrich with Docker context if applicable
|
|
1428
|
+
if info.get("framework") == "docker":
|
|
1429
|
+
try:
|
|
1430
|
+
docker_ctx = await _gather_docker_context(target)
|
|
1431
|
+
if docker_ctx.get("failing_services"):
|
|
1432
|
+
fix_message += "\n\nDOCKER SERVICE STATUS:\n" + json.dumps(docker_ctx["service_status"], indent=2)
|
|
1433
|
+
for svc_name, svc_logs in docker_ctx.get("service_logs", {}).items():
|
|
1434
|
+
if svc_name != "_combined":
|
|
1435
|
+
fix_message += f"\n\nFAILING SERVICE '{svc_name}' LOGS:\n{svc_logs}"
|
|
1436
|
+
diagnoses = _diagnose_errors("\n".join(docker_ctx.get("service_logs", {}).values()))
|
|
1437
|
+
if diagnoses:
|
|
1438
|
+
fix_message += "\n\nAUTO-DIAGNOSIS:\n" + "\n".join(
|
|
1439
|
+
f"- {d['diagnosis']}: {d['suggestion']}" for d in diagnoses)
|
|
1440
|
+
if docker_ctx.get("project_structure"):
|
|
1441
|
+
fix_message += "\n\nPROJECT FILES:\n" + docker_ctx["project_structure"]
|
|
1442
|
+
except Exception:
|
|
1443
|
+
logger.debug("Docker context gathering for auto-fix failed", exc_info=True)
|
|
1444
|
+
|
|
1390
1445
|
# Save original command before stop() removes the info dict
|
|
1391
1446
|
cmd = info.get("original_command")
|
|
1392
1447
|
|
|
@@ -1604,6 +1659,114 @@ class DevServerManager:
|
|
|
1604
1659
|
result["port"] = 1355
|
|
1605
1660
|
return result
|
|
1606
1661
|
|
|
1662
|
+
async def _monitor_docker_services(self, session_id: str) -> None:
|
|
1663
|
+
"""Background loop: poll Docker Compose services, auto-fix failures."""
|
|
1664
|
+
info = self.servers.get(session_id)
|
|
1665
|
+
if not info:
|
|
1666
|
+
return
|
|
1667
|
+
|
|
1668
|
+
project_dir = Path(info.get("project_dir", "."))
|
|
1669
|
+
info["docker_service_health"] = {}
|
|
1670
|
+
|
|
1671
|
+
while info.get("process") and info["process"].poll() is None:
|
|
1672
|
+
await asyncio.sleep(10)
|
|
1673
|
+
|
|
1674
|
+
try:
|
|
1675
|
+
docker_ctx = await _gather_docker_context(project_dir)
|
|
1676
|
+
except Exception:
|
|
1677
|
+
continue
|
|
1678
|
+
|
|
1679
|
+
# Update service health
|
|
1680
|
+
for svc in docker_ctx.get("service_status", []):
|
|
1681
|
+
name = svc["name"]
|
|
1682
|
+
prev = info["docker_service_health"].get(name, {})
|
|
1683
|
+
svc_health: dict = {
|
|
1684
|
+
"name": name,
|
|
1685
|
+
"status": svc["state"],
|
|
1686
|
+
"exit_code": svc.get("exit_code", 0),
|
|
1687
|
+
"restarts": prev.get("restarts", 0),
|
|
1688
|
+
"fix_attempts": prev.get("fix_attempts", 0),
|
|
1689
|
+
"fix_timestamps": prev.get("fix_timestamps", []),
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
# Detect new failure
|
|
1693
|
+
was_running = prev.get("status") in ("running", None)
|
|
1694
|
+
now_failed = svc["state"] in ("exited", "dead")
|
|
1695
|
+
|
|
1696
|
+
if was_running and now_failed:
|
|
1697
|
+
svc_health["restarts"] = prev.get("restarts", 0) + 1
|
|
1698
|
+
logger.warning("Docker service '%s' failed (exit %s)", name, svc.get("exit_code"))
|
|
1699
|
+
|
|
1700
|
+
# Circuit breaker: max 3 fixes per service per 10 min
|
|
1701
|
+
now = time.time()
|
|
1702
|
+
recent_fixes = [t for t in svc_health["fix_timestamps"] if now - t < 600]
|
|
1703
|
+
|
|
1704
|
+
if len(recent_fixes) < 3:
|
|
1705
|
+
svc_logs = docker_ctx.get("service_logs", {}).get(name, "")
|
|
1706
|
+
diagnoses = _diagnose_errors(svc_logs)
|
|
1707
|
+
diag_text = "\n".join(f"- {d['diagnosis']}: {d['suggestion']}" for d in diagnoses)
|
|
1708
|
+
|
|
1709
|
+
fix_prompt = f"The '{name}' Docker service crashed (exit code {svc.get('exit_code', 1)}).\n"
|
|
1710
|
+
if diag_text:
|
|
1711
|
+
fix_prompt += f"\nDiagnosis:\n{diag_text}\n"
|
|
1712
|
+
fix_prompt += f"\nService logs:\n{svc_logs[:2000]}\n"
|
|
1713
|
+
fix_prompt += "\nFix the issue and ensure the service starts correctly."
|
|
1714
|
+
|
|
1715
|
+
svc_health["fix_attempts"] += 1
|
|
1716
|
+
svc_health["fix_timestamps"] = recent_fixes + [now]
|
|
1717
|
+
svc_health["fix_status"] = "fixing"
|
|
1718
|
+
|
|
1719
|
+
# Trigger fix asynchronously
|
|
1720
|
+
asyncio.ensure_future(self._auto_fix_service(session_id, name, fix_prompt))
|
|
1721
|
+
else:
|
|
1722
|
+
svc_health["fix_status"] = "circuit_breaker_open"
|
|
1723
|
+
|
|
1724
|
+
info["docker_service_health"][name] = svc_health
|
|
1725
|
+
|
|
1726
|
+
# Broadcast service health via WebSocket
|
|
1727
|
+
try:
|
|
1728
|
+
await _broadcast({
|
|
1729
|
+
"type": "service_health",
|
|
1730
|
+
"data": {
|
|
1731
|
+
"session_id": session_id,
|
|
1732
|
+
"services": list(info["docker_service_health"].values()),
|
|
1733
|
+
}
|
|
1734
|
+
})
|
|
1735
|
+
except Exception:
|
|
1736
|
+
pass
|
|
1737
|
+
|
|
1738
|
+
async def _auto_fix_service(self, session_id: str, service_name: str, fix_prompt: str) -> None:
|
|
1739
|
+
"""Run targeted fix for a specific Docker service."""
|
|
1740
|
+
info = self.servers.get(session_id)
|
|
1741
|
+
if not info:
|
|
1742
|
+
return
|
|
1743
|
+
project_dir = info.get("project_dir", ".")
|
|
1744
|
+
loki = _find_loki_cli()
|
|
1745
|
+
if not loki:
|
|
1746
|
+
return
|
|
1747
|
+
|
|
1748
|
+
try:
|
|
1749
|
+
proc = await asyncio.to_thread(
|
|
1750
|
+
subprocess.run,
|
|
1751
|
+
[loki, "quick", fix_prompt],
|
|
1752
|
+
capture_output=True, text=True, cwd=project_dir, timeout=300,
|
|
1753
|
+
env={**os.environ, **_load_secrets()}
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
# After fix, restart the specific service
|
|
1757
|
+
await asyncio.to_thread(
|
|
1758
|
+
subprocess.run,
|
|
1759
|
+
["docker", "compose", "restart", service_name],
|
|
1760
|
+
capture_output=True, cwd=project_dir, timeout=30
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
svc_health = info.get("docker_service_health", {}).get(service_name, {})
|
|
1764
|
+
svc_health["fix_status"] = "fixed" if proc.returncode == 0 else "fix_failed"
|
|
1765
|
+
except Exception as exc:
|
|
1766
|
+
logger.error("Auto-fix for service '%s' failed: %s", service_name, exc)
|
|
1767
|
+
svc_health = info.get("docker_service_health", {}).get(service_name, {})
|
|
1768
|
+
svc_health["fix_status"] = "fix_failed"
|
|
1769
|
+
|
|
1607
1770
|
async def stop_all(self) -> None:
|
|
1608
1771
|
"""Stop all dev servers (used on shutdown)."""
|
|
1609
1772
|
for sid in list(self.servers.keys()):
|
|
@@ -1613,6 +1776,135 @@ class DevServerManager:
|
|
|
1613
1776
|
dev_server_manager = DevServerManager()
|
|
1614
1777
|
|
|
1615
1778
|
|
|
1779
|
+
# ---------------------------------------------------------------------------
|
|
1780
|
+
# Docker context gathering and error diagnosis
|
|
1781
|
+
# ---------------------------------------------------------------------------
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
async def _gather_docker_context(project_dir: Path) -> dict:
|
|
1785
|
+
"""Gather Docker Compose service status, logs, and project context."""
|
|
1786
|
+
loop = asyncio.get_running_loop()
|
|
1787
|
+
result: dict = {"service_status": [], "failing_services": [], "service_logs": {},
|
|
1788
|
+
"project_structure": "", "env_keys": []}
|
|
1789
|
+
|
|
1790
|
+
# Get service status via docker compose ps
|
|
1791
|
+
try:
|
|
1792
|
+
ps_proc = await loop.run_in_executor(None, lambda: subprocess.run(
|
|
1793
|
+
["docker", "compose", "ps", "--format", "json"],
|
|
1794
|
+
capture_output=True, text=True, cwd=str(project_dir), timeout=10
|
|
1795
|
+
))
|
|
1796
|
+
if ps_proc.returncode == 0:
|
|
1797
|
+
for line in ps_proc.stdout.strip().split("\n"):
|
|
1798
|
+
if line.strip():
|
|
1799
|
+
try:
|
|
1800
|
+
svc = json.loads(line)
|
|
1801
|
+
status_entry = {
|
|
1802
|
+
"name": svc.get("Service", svc.get("Name", "unknown")),
|
|
1803
|
+
"state": svc.get("State", "unknown"),
|
|
1804
|
+
"status": svc.get("Status", ""),
|
|
1805
|
+
"exit_code": svc.get("ExitCode", 0),
|
|
1806
|
+
}
|
|
1807
|
+
result["service_status"].append(status_entry)
|
|
1808
|
+
if status_entry["state"] in ("exited", "dead", "restarting"):
|
|
1809
|
+
result["failing_services"].append(status_entry["name"])
|
|
1810
|
+
except json.JSONDecodeError:
|
|
1811
|
+
pass
|
|
1812
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
1813
|
+
pass
|
|
1814
|
+
|
|
1815
|
+
# Get logs for failing services
|
|
1816
|
+
for svc_name in result["failing_services"]:
|
|
1817
|
+
try:
|
|
1818
|
+
log_proc = await loop.run_in_executor(None, lambda sn=svc_name: subprocess.run(
|
|
1819
|
+
["docker", "compose", "logs", "--tail", "50", sn],
|
|
1820
|
+
capture_output=True, text=True, cwd=str(project_dir), timeout=15
|
|
1821
|
+
))
|
|
1822
|
+
if log_proc.stdout:
|
|
1823
|
+
result["service_logs"][svc_name] = log_proc.stdout[-3000:] # Cap at 3KB
|
|
1824
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
1825
|
+
pass
|
|
1826
|
+
|
|
1827
|
+
# If no specific failures, get combined logs tail
|
|
1828
|
+
if not result["failing_services"]:
|
|
1829
|
+
try:
|
|
1830
|
+
log_proc = await loop.run_in_executor(None, lambda: subprocess.run(
|
|
1831
|
+
["docker", "compose", "logs", "--tail", "30"],
|
|
1832
|
+
capture_output=True, text=True, cwd=str(project_dir), timeout=15
|
|
1833
|
+
))
|
|
1834
|
+
if log_proc.stdout:
|
|
1835
|
+
result["service_logs"]["_combined"] = log_proc.stdout[-3000:]
|
|
1836
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
1837
|
+
pass
|
|
1838
|
+
|
|
1839
|
+
# Project structure
|
|
1840
|
+
try:
|
|
1841
|
+
ls_proc = await loop.run_in_executor(None, lambda: subprocess.run(
|
|
1842
|
+
["find", ".", "-maxdepth", "2", "-type", "f",
|
|
1843
|
+
"-name", "*.py", "-o", "-name", "*.ts",
|
|
1844
|
+
"-o", "-name", "*.tsx", "-o", "-name", "*.js", "-o", "-name", "package.json",
|
|
1845
|
+
"-o", "-name", "requirements.txt", "-o", "-name", "Dockerfile",
|
|
1846
|
+
"-o", "-name", "docker-compose.yml", "-o", "-name", "*.env"],
|
|
1847
|
+
capture_output=True, text=True, cwd=str(project_dir), timeout=5
|
|
1848
|
+
))
|
|
1849
|
+
result["project_structure"] = ls_proc.stdout[:2000] if ls_proc.stdout else ""
|
|
1850
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
1851
|
+
pass
|
|
1852
|
+
|
|
1853
|
+
# Env variable names (not values)
|
|
1854
|
+
env_file = project_dir / ".env"
|
|
1855
|
+
if env_file.exists():
|
|
1856
|
+
try:
|
|
1857
|
+
for line in env_file.read_text(errors="replace").split("\n"):
|
|
1858
|
+
line = line.strip()
|
|
1859
|
+
if line and not line.startswith("#") and "=" in line:
|
|
1860
|
+
result["env_keys"].append(line.split("=", 1)[0])
|
|
1861
|
+
except OSError:
|
|
1862
|
+
pass
|
|
1863
|
+
|
|
1864
|
+
return result
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
def _diagnose_errors(logs: str) -> list[dict]:
|
|
1868
|
+
"""Pattern-match common errors in Docker/service logs and return diagnoses."""
|
|
1869
|
+
diagnoses: list[dict] = []
|
|
1870
|
+
patterns = [
|
|
1871
|
+
(r"ModuleNotFoundError: No module named ['\"](\w+)['\"]",
|
|
1872
|
+
lambda m: {"pattern": "missing_python_dep", "diagnosis": f"Missing Python dependency: {m.group(1)}",
|
|
1873
|
+
"suggestion": f"Add '{m.group(1)}' to requirements.txt and rebuild"}),
|
|
1874
|
+
(r"Cannot find module ['\"]([^'\"]+)['\"]|ERR_MODULE_NOT_FOUND",
|
|
1875
|
+
lambda m: {"pattern": "missing_node_dep", "diagnosis": "Missing Node.js module",
|
|
1876
|
+
"suggestion": "Run 'npm install' in the service directory"}),
|
|
1877
|
+
(r"ECONNREFUSED.*:(\d+)|connection refused.*:(\d+)",
|
|
1878
|
+
lambda m: {"pattern": "connection_refused",
|
|
1879
|
+
"diagnosis": f"Connection refused on port {m.group(1) or m.group(2)}",
|
|
1880
|
+
"suggestion": "A dependent service may not be ready. Add retry logic or health check wait."}),
|
|
1881
|
+
(r"address already in use|EADDRINUSE",
|
|
1882
|
+
lambda m: {"pattern": "port_conflict", "diagnosis": "Port already in use",
|
|
1883
|
+
"suggestion": "Another process is using the port. Change the port or stop the conflicting process."}),
|
|
1884
|
+
(r"SyntaxError: (.+)",
|
|
1885
|
+
lambda m: {"pattern": "syntax_error", "diagnosis": f"Syntax error: {m.group(1)[:100]}",
|
|
1886
|
+
"suggestion": "Fix the syntax error in the indicated file and line."}),
|
|
1887
|
+
(r"FATAL:.*password authentication failed",
|
|
1888
|
+
lambda m: {"pattern": "db_auth", "diagnosis": "Database authentication failed",
|
|
1889
|
+
"suggestion": "Check DATABASE_URL credentials match the postgres service environment."}),
|
|
1890
|
+
(r"error.*returned a non-zero code: (\d+)",
|
|
1891
|
+
lambda m: {"pattern": "build_failure", "diagnosis": f"Docker build failed (exit code {m.group(1)})",
|
|
1892
|
+
"suggestion": "Check the Dockerfile for errors. Common: missing system dependencies."}),
|
|
1893
|
+
(r"npm ERR!|npm error",
|
|
1894
|
+
lambda m: {"pattern": "npm_error", "diagnosis": "npm encountered an error",
|
|
1895
|
+
"suggestion": "Check package.json for invalid dependencies or run 'npm install' manually."}),
|
|
1896
|
+
]
|
|
1897
|
+
seen: set[str] = set()
|
|
1898
|
+
for pattern, handler in patterns:
|
|
1899
|
+
for match in re.finditer(pattern, logs, re.IGNORECASE):
|
|
1900
|
+
diag = handler(match)
|
|
1901
|
+
key = diag["pattern"]
|
|
1902
|
+
if key not in seen:
|
|
1903
|
+
seen.add(key)
|
|
1904
|
+
diagnoses.append(diag)
|
|
1905
|
+
return diagnoses
|
|
1906
|
+
|
|
1907
|
+
|
|
1616
1908
|
# ---------------------------------------------------------------------------
|
|
1617
1909
|
# Helpers
|
|
1618
1910
|
# ---------------------------------------------------------------------------
|
|
@@ -3135,6 +3427,25 @@ async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
|
|
|
3135
3427
|
"\n\nDEV SERVER ERROR (fix this):\n" + "\n".join(error_lines)
|
|
3136
3428
|
)
|
|
3137
3429
|
|
|
3430
|
+
# Phase 1.5: Inject Docker Compose context
|
|
3431
|
+
ds_info_docker = dev_server_manager.servers.get(session_id)
|
|
3432
|
+
if ds_info_docker and ds_info_docker.get("framework") == "docker":
|
|
3433
|
+
try:
|
|
3434
|
+
docker_ctx = await _gather_docker_context(target)
|
|
3435
|
+
if docker_ctx.get("failing_services"):
|
|
3436
|
+
context_parts.append("\n\nDOCKER SERVICE STATUS:\n" + json.dumps(docker_ctx["service_status"], indent=2))
|
|
3437
|
+
for svc_name, svc_logs in docker_ctx.get("service_logs", {}).items():
|
|
3438
|
+
if svc_name != "_combined":
|
|
3439
|
+
context_parts.append(f"\n\nFAILING SERVICE '{svc_name}' LOGS:\n{svc_logs}")
|
|
3440
|
+
diagnoses = _diagnose_errors("\n".join(docker_ctx.get("service_logs", {}).values()))
|
|
3441
|
+
if diagnoses:
|
|
3442
|
+
context_parts.append("\n\nAUTO-DIAGNOSIS:\n" + "\n".join(
|
|
3443
|
+
f"- {d['diagnosis']}: {d['suggestion']}" for d in diagnoses))
|
|
3444
|
+
if docker_ctx.get("project_structure"):
|
|
3445
|
+
context_parts.append("\n\nPROJECT FILES:\n" + docker_ctx["project_structure"])
|
|
3446
|
+
except Exception:
|
|
3447
|
+
logger.debug("Docker context gathering failed", exc_info=True)
|
|
3448
|
+
|
|
3138
3449
|
# Inject quality gate failures if any
|
|
3139
3450
|
gate_file = target / ".loki" / "quality" / "gate-failures.txt"
|
|
3140
3451
|
if gate_file.exists():
|
|
@@ -3653,22 +3964,29 @@ async def get_preview_info(session_id: str) -> JSONResponse:
|
|
|
3653
3964
|
info["type"] = "docker"
|
|
3654
3965
|
info["dev_command"] = f"docker compose -f {compose_file} up --build"
|
|
3655
3966
|
info["description"] = "Containerized app -- runs via Docker Compose"
|
|
3656
|
-
#
|
|
3967
|
+
# Use smart service resolution to find the primary port
|
|
3657
3968
|
compose_port = 3000
|
|
3969
|
+
compose_services: list[dict] = []
|
|
3970
|
+
primary_service_name: Optional[str] = None
|
|
3658
3971
|
try:
|
|
3659
3972
|
import yaml
|
|
3660
3973
|
with open(project_root / compose_file) as f:
|
|
3661
3974
|
compose_data = yaml.safe_load(f)
|
|
3662
3975
|
if compose_data and "services" in compose_data:
|
|
3663
|
-
for svc in compose_data["services"].
|
|
3664
|
-
|
|
3665
|
-
for p in ports:
|
|
3976
|
+
for svc_name, svc in compose_data["services"].items():
|
|
3977
|
+
svc_ports: list[int] = []
|
|
3978
|
+
for p in svc.get("ports", []):
|
|
3666
3979
|
p_str = str(p)
|
|
3667
3980
|
if ":" in p_str:
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3981
|
+
parts = p_str.split(":")
|
|
3982
|
+
svc_ports.append(int(parts[-2]))
|
|
3983
|
+
compose_services.append({
|
|
3984
|
+
"name": svc_name,
|
|
3985
|
+
"ports": svc_ports,
|
|
3986
|
+
"image": svc.get("image"),
|
|
3987
|
+
"has_build": "build" in svc,
|
|
3988
|
+
})
|
|
3989
|
+
primary_service_name, compose_port = dev_server_manager._resolve_primary_service(compose_services)
|
|
3672
3990
|
except ImportError:
|
|
3673
3991
|
try:
|
|
3674
3992
|
content = (project_root / compose_file).read_text()
|
|
@@ -3680,6 +3998,10 @@ async def get_preview_info(session_id: str) -> JSONResponse:
|
|
|
3680
3998
|
except Exception:
|
|
3681
3999
|
pass
|
|
3682
4000
|
info["port"] = compose_port
|
|
4001
|
+
if primary_service_name:
|
|
4002
|
+
info["primary_service"] = primary_service_name
|
|
4003
|
+
if compose_services:
|
|
4004
|
+
info["services"] = compose_services
|
|
3683
4005
|
elif is_expo:
|
|
3684
4006
|
info["type"] = "expo"
|
|
3685
4007
|
info["port"] = 8081
|
|
@@ -3897,6 +4219,139 @@ async def get_devserver_status(session_id: str) -> JSONResponse:
|
|
|
3897
4219
|
return JSONResponse(content=result)
|
|
3898
4220
|
|
|
3899
4221
|
|
|
4222
|
+
@app.get("/api/sessions/{session_id}/services")
|
|
4223
|
+
async def get_session_services(session_id: str) -> JSONResponse:
|
|
4224
|
+
"""Get Docker Compose service list with primary detection."""
|
|
4225
|
+
if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
|
|
4226
|
+
return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
|
|
4227
|
+
target = _find_session_dir(session_id)
|
|
4228
|
+
if target is None:
|
|
4229
|
+
return JSONResponse(status_code=404, content={"error": "Session not found"})
|
|
4230
|
+
|
|
4231
|
+
# Check if dev server info has cached services
|
|
4232
|
+
ds_info = dev_server_manager.servers.get(session_id)
|
|
4233
|
+
if ds_info and ds_info.get("docker_service_health"):
|
|
4234
|
+
return JSONResponse(content={
|
|
4235
|
+
"services": list(ds_info["docker_service_health"].values()),
|
|
4236
|
+
"framework": ds_info.get("framework"),
|
|
4237
|
+
})
|
|
4238
|
+
|
|
4239
|
+
# Parse compose file directly
|
|
4240
|
+
services_info: list[dict] = []
|
|
4241
|
+
for compose_file in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"):
|
|
4242
|
+
if (target / compose_file).exists():
|
|
4243
|
+
try:
|
|
4244
|
+
import yaml
|
|
4245
|
+
with open(target / compose_file) as f:
|
|
4246
|
+
compose_data = yaml.safe_load(f)
|
|
4247
|
+
if compose_data and "services" in compose_data:
|
|
4248
|
+
for svc_name, svc in compose_data["services"].items():
|
|
4249
|
+
svc_ports: list[int] = []
|
|
4250
|
+
for p in svc.get("ports", []):
|
|
4251
|
+
p_str = str(p)
|
|
4252
|
+
if ":" in p_str:
|
|
4253
|
+
parts = p_str.split(":")
|
|
4254
|
+
svc_ports.append(int(parts[-2]))
|
|
4255
|
+
services_info.append({
|
|
4256
|
+
"name": svc_name,
|
|
4257
|
+
"ports": svc_ports,
|
|
4258
|
+
"image": svc.get("image"),
|
|
4259
|
+
"has_build": "build" in svc,
|
|
4260
|
+
})
|
|
4261
|
+
except (ImportError, Exception):
|
|
4262
|
+
pass
|
|
4263
|
+
break
|
|
4264
|
+
|
|
4265
|
+
primary_name, primary_port = dev_server_manager._resolve_primary_service(services_info)
|
|
4266
|
+
for svc in services_info:
|
|
4267
|
+
svc["is_primary"] = (svc["name"] == primary_name)
|
|
4268
|
+
|
|
4269
|
+
return JSONResponse(content={
|
|
4270
|
+
"services": services_info,
|
|
4271
|
+
"primary_service": primary_name,
|
|
4272
|
+
"primary_port": primary_port,
|
|
4273
|
+
})
|
|
4274
|
+
|
|
4275
|
+
|
|
4276
|
+
@app.get("/api/sessions/{session_id}/devserver/logs")
|
|
4277
|
+
async def get_devserver_logs(session_id: str, service: Optional[str] = None, tail: int = 50) -> JSONResponse:
|
|
4278
|
+
"""Get Docker service logs (optionally filtered to one service)."""
|
|
4279
|
+
if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
|
|
4280
|
+
return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
|
|
4281
|
+
target = _find_session_dir(session_id)
|
|
4282
|
+
if target is None:
|
|
4283
|
+
return JSONResponse(status_code=404, content={"error": "Session not found"})
|
|
4284
|
+
|
|
4285
|
+
ds_info = dev_server_manager.servers.get(session_id)
|
|
4286
|
+
if not ds_info:
|
|
4287
|
+
return JSONResponse(status_code=404, content={"error": "No dev server running"})
|
|
4288
|
+
|
|
4289
|
+
# If not a Docker project, return buffered output lines
|
|
4290
|
+
if ds_info.get("framework") != "docker":
|
|
4291
|
+
return JSONResponse(content={
|
|
4292
|
+
"logs": ds_info.get("output_lines", [])[-tail:],
|
|
4293
|
+
"service": None,
|
|
4294
|
+
})
|
|
4295
|
+
|
|
4296
|
+
# Docker project: use docker compose logs
|
|
4297
|
+
project_dir = ds_info.get("project_dir", str(target))
|
|
4298
|
+
tail = min(tail, 200) # Cap at 200 lines
|
|
4299
|
+
try:
|
|
4300
|
+
cmd = ["docker", "compose", "logs", "--tail", str(tail)]
|
|
4301
|
+
if service:
|
|
4302
|
+
# Validate service name to prevent injection
|
|
4303
|
+
if not re.match(r"^[a-zA-Z0-9._-]+$", service):
|
|
4304
|
+
return JSONResponse(status_code=400, content={"error": "Invalid service name"})
|
|
4305
|
+
cmd.append(service)
|
|
4306
|
+
loop = asyncio.get_running_loop()
|
|
4307
|
+
log_proc = await loop.run_in_executor(None, lambda: subprocess.run(
|
|
4308
|
+
cmd, capture_output=True, text=True, cwd=project_dir, timeout=15
|
|
4309
|
+
))
|
|
4310
|
+
logs_text = log_proc.stdout or ""
|
|
4311
|
+
# Run diagnosis on the logs
|
|
4312
|
+
diagnoses = _diagnose_errors(logs_text)
|
|
4313
|
+
return JSONResponse(content={
|
|
4314
|
+
"logs": logs_text[-5000:],
|
|
4315
|
+
"service": service,
|
|
4316
|
+
"diagnoses": diagnoses,
|
|
4317
|
+
})
|
|
4318
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
|
|
4319
|
+
return JSONResponse(status_code=500, content={"error": f"Failed to get logs: {exc}"})
|
|
4320
|
+
|
|
4321
|
+
|
|
4322
|
+
@app.post("/api/sessions/{session_id}/devserver/restart-service")
|
|
4323
|
+
async def restart_service(session_id: str, req: dict = Body(...)) -> JSONResponse:
|
|
4324
|
+
"""Restart a specific Docker Compose service."""
|
|
4325
|
+
if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
|
|
4326
|
+
return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
|
|
4327
|
+
ds_info = dev_server_manager.servers.get(session_id)
|
|
4328
|
+
if not ds_info:
|
|
4329
|
+
return JSONResponse(status_code=404, content={"error": "No dev server running"})
|
|
4330
|
+
if ds_info.get("framework") != "docker":
|
|
4331
|
+
return JSONResponse(status_code=400, content={"error": "Not a Docker project"})
|
|
4332
|
+
|
|
4333
|
+
service_name = req.get("service")
|
|
4334
|
+
if not service_name or not re.match(r"^[a-zA-Z0-9._-]+$", service_name):
|
|
4335
|
+
return JSONResponse(status_code=400, content={"error": "Invalid or missing service name"})
|
|
4336
|
+
|
|
4337
|
+
project_dir = ds_info.get("project_dir", ".")
|
|
4338
|
+
try:
|
|
4339
|
+
loop = asyncio.get_running_loop()
|
|
4340
|
+
result = await loop.run_in_executor(None, lambda: subprocess.run(
|
|
4341
|
+
["docker", "compose", "restart", service_name],
|
|
4342
|
+
capture_output=True, text=True, cwd=project_dir, timeout=30
|
|
4343
|
+
))
|
|
4344
|
+
if result.returncode == 0:
|
|
4345
|
+
return JSONResponse(content={"status": "restarted", "service": service_name})
|
|
4346
|
+
else:
|
|
4347
|
+
return JSONResponse(status_code=500, content={
|
|
4348
|
+
"error": f"Restart failed: {result.stderr or result.stdout}",
|
|
4349
|
+
"service": service_name,
|
|
4350
|
+
})
|
|
4351
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
|
|
4352
|
+
return JSONResponse(status_code=500, content={"error": f"Restart failed: {exc}"})
|
|
4353
|
+
|
|
4354
|
+
|
|
3900
4355
|
# ---------------------------------------------------------------------------
|
|
3901
4356
|
# HTTP Proxy for dev server preview
|
|
3902
4357
|
# ---------------------------------------------------------------------------
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,-apple-system,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.terminal-scroll::-webkit-scrollbar{width:6px}.terminal-scroll::-webkit-scrollbar-track{background:transparent}.terminal-scroll::-webkit-scrollbar-thumb{background:#553de933;border-radius:3px}.terminal-scroll::-webkit-scrollbar-thumb:hover{background:#553de959}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.card{background:#fff;border:1px solid #ECEAE3;border-radius:5px;box-shadow:0 1px 3px #0000000f}.card:hover,.card-hover:hover{box-shadow:0 5px 10px #00000014}.pattern-nodes{position:absolute;top:0;right:0;bottom:0;left:0;opacity:.04;pointer-events:none;background-image:radial-gradient(circle at 10% 20%,#553DE9 1px,transparent 1px),radial-gradient(circle at 30% 60%,#553DE9 1px,transparent 1px),radial-gradient(circle at 50% 40%,#553DE9 1px,transparent 1px),radial-gradient(circle at 70% 80%,#553DE9 1px,transparent 1px),radial-gradient(circle at 90% 30%,#553DE9 1px,transparent 1px);background-size:200px 200px}*:focus-visible{outline:2px solid #553DE9;outline-offset:2px}::-moz-selection{background:#e8e4fd;color:#201515}::selection{background:#e8e4fd;color:#201515}@media(prefers-reduced-motion:reduce){.phase-active{animation:none}}@keyframes phase-pulse{0%,to{opacity:1}50%{opacity:.5}}.phase-active{animation:phase-pulse 2s ease-in-out infinite}@keyframes cursor-blink{0%,to{opacity:1}50%{opacity:0}}.terminal-cursor:after{content:"";display:inline-block;width:8px;height:16px;background:#553de9;animation:cursor-blink 1s step-end infinite;margin-left:2px;vertical-align:text-bottom}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.\!visible{visibility:visible!important}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-6{bottom:1.5rem}.bottom-full{bottom:100%}.left-0{left:0}.left-3{left:.75rem}.right-0{right:0}.right-2\.5{right:.625rem}.right-6{right:1.5rem}.top-1\/2{top:50%}.top-2\.5{top:.625rem}.top-8{top:2rem}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-5{grid-column:span 5 / span 5}.col-span-6{grid-column:span 6 / span 6}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-28{height:7rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-32{max-height:8rem}.max-h-40{max-height:10rem}.max-h-64{max-height:16rem}.max-h-\[400px\]{max-height:400px}.min-h-0{min-height:0px}.min-h-\[120px\]{min-height:120px}.min-h-\[280px\]{min-height:280px}.min-h-\[60vh\]{min-height:60vh}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-44{width:11rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[160px\]{min-width:160px}.max-w-3xl{max-width:48rem}.max-w-\[1400px\]{max-width:1400px}.max-w-\[1920px\]{max-width:1920px}.max-w-\[200px\]{max-width:200px}.max-w-\[220px\]{max-width:220px}.max-w-\[80\%\]{max-width:80%}.max-w-\[800px\]{max-width:800px}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-\[3px\]{border-radius:3px}.rounded-\[5px\]{border-radius:5px}.rounded-btn{border-radius:4px}.rounded-card{border-radius:5px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-t-card{border-top-left-radius:5px;border-top-right-radius:5px}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-\[\#553DE9\]{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.border-\[\#553DE9\]\/30{border-color:#553de94d}.border-\[\#C45B5B\]\/20{border-color:#c45b5b33}.border-\[\#ECEAE3\],.border-border{--tw-border-opacity: 1;border-color:rgb(236 234 227 / var(--tw-border-opacity, 1))}.border-border-light{--tw-border-opacity: 1;border-color:rgb(197 192 177 / var(--tw-border-opacity, 1))}.border-border\/50{border-color:#eceae380}.border-danger\/10{border-color:#c45b5b1a}.border-danger\/20{border-color:#c45b5b33}.border-danger\/30{border-color:#c45b5b4d}.border-danger\/40{border-color:#c45b5b66}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-muted\/20{border-color:#93908433}.border-primary{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.border-primary\/20{border-color:#553de933}.border-primary\/30{border-color:#553de94d}.border-primary\/40{border-color:#553de966}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-success\/20{border-color:#1fc5a833}.border-transparent{border-color:transparent}.border-warning\/10{border-color:#d4a03c1a}.border-warning\/20{border-color:#d4a03c33}.border-warning\/30{border-color:#d4a03c4d}.border-warning\/40{border-color:#d4a03c66}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.border-t-transparent{border-top-color:transparent}.bg-\[\#1FC5A8\]{--tw-bg-opacity: 1;background-color:rgb(31 197 168 / var(--tw-bg-opacity, 1))}.bg-\[\#1FC5A8\]\/10{background-color:#1fc5a81a}.bg-\[\#24292f\]{--tw-bg-opacity: 1;background-color:rgb(36 41 47 / var(--tw-bg-opacity, 1))}.bg-\[\#36342E\]{--tw-bg-opacity: 1;background-color:rgb(54 52 46 / var(--tw-bg-opacity, 1))}.bg-\[\#553DE9\]{--tw-bg-opacity: 1;background-color:rgb(85 61 233 / var(--tw-bg-opacity, 1))}.bg-\[\#553DE9\]\/10{background-color:#553de91a}.bg-\[\#C45B5B\]{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-\[\#C45B5B\]\/10{background-color:#c45b5b1a}.bg-\[\#D4A03C\]\/10{background-color:#d4a03c1a}.bg-\[\#ECEAE3\]\/60{background-color:#eceae399}.bg-\[\#F8F4F0\]{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.bg-\[\#FAF9F6\]{--tw-bg-opacity: 1;background-color:rgb(250 249 246 / var(--tw-bg-opacity, 1))}.bg-background{--tw-bg-opacity: 1;background-color:rgb(255 254 251 / var(--tw-bg-opacity, 1))}.bg-black\/30{background-color:#0000004d}.bg-black\/40{background-color:#0006}.bg-black\/5{background-color:#0000000d}.bg-border{--tw-bg-opacity: 1;background-color:rgb(236 234 227 / var(--tw-bg-opacity, 1))}.bg-border\/30{background-color:#eceae34d}.bg-card{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-card\/50{background-color:#ffffff80}.bg-current{background-color:currentColor}.bg-danger{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-danger\/10{background-color:#c45b5b1a}.bg-danger\/5{background-color:#c45b5b0d}.bg-gray-400{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.bg-info{--tw-bg-opacity: 1;background-color:rgb(47 113 227 / var(--tw-bg-opacity, 1))}.bg-ink\/20{background-color:#20151533}.bg-ink\/30{background-color:#2015154d}.bg-ink\/\[0\.03\]{background-color:#20151508}.bg-muted{--tw-bg-opacity: 1;background-color:rgb(147 144 132 / var(--tw-bg-opacity, 1))}.bg-muted\/10{background-color:#9390841a}.bg-muted\/30{background-color:#9390844d}.bg-muted\/40{background-color:#93908466}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(85 61 233 / var(--tw-bg-opacity, 1))}.bg-primary\/10{background-color:#553de91a}.bg-primary\/5{background-color:#553de90d}.bg-primary\/60{background-color:#553de999}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-success{--tw-bg-opacity: 1;background-color:rgb(31 197 168 / var(--tw-bg-opacity, 1))}.bg-success\/10{background-color:#1fc5a81a}.bg-transparent{background-color:transparent}.bg-warning{--tw-bg-opacity: 1;background-color:rgb(212 160 60 / var(--tw-bg-opacity, 1))}.bg-warning\/10{background-color:#d4a03c1a}.bg-warning\/5{background-color:#d4a03c0d}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.fill-ink{fill:#201515}.fill-primary{fill:#553de9}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-20{padding-top:5rem;padding-bottom:5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pb-2\.5{padding-bottom:.625rem}.pb-4{padding-bottom:1rem}.pl-3{padding-left:.75rem}.pl-9{padding-left:2.25rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pr-6{padding-right:1.5rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-5{padding-top:1.25rem}.pt-\[20vh\]{padding-top:20vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-text-bottom{vertical-align:text-bottom}.font-heading{font-family:DM Serif Display,Georgia,Times New Roman,serif}.font-mono{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace}.font-sans{font-family:Inter,system-ui,-apple-system,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-6xl{font-size:3.75rem;line-height:1}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-h1{font-size:2.5rem;line-height:1;letter-spacing:-.01em}.text-h3{font-size:1.25rem;line-height:1.4}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1FC5A8\]{--tw-text-opacity: 1;color:rgb(31 197 168 / var(--tw-text-opacity, 1))}.text-\[\#36342E\]{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.text-\[\#553DE9\]{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.text-\[\#6B6960\]{--tw-text-opacity: 1;color:rgb(107 105 96 / var(--tw-text-opacity, 1))}.text-\[\#939084\]{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.text-\[\#B8B5AD\]{--tw-text-opacity: 1;color:rgb(184 181 173 / var(--tw-text-opacity, 1))}.text-\[\#C45B5B\]{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-\[\#D4A03C\]{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-cyan-600{--tw-text-opacity: 1;color:rgb(8 145 178 / var(--tw-text-opacity, 1))}.text-danger{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-danger\/70{color:#c45b5bb3}.text-danger\/80{color:#c45b5bcc}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-info{--tw-text-opacity: 1;color:rgb(47 113 227 / var(--tw-text-opacity, 1))}.text-ink{--tw-text-opacity: 1;color:rgb(32 21 21 / var(--tw-text-opacity, 1))}.text-ink\/70{color:#201515b3}.text-muted{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.text-muted-accessible{--tw-text-opacity: 1;color:rgb(107 105 96 / var(--tw-text-opacity, 1))}.text-muted\/30{color:#9390844d}.text-muted\/40{color:#93908466}.text-muted\/50{color:#93908480}.text-muted\/60{color:#93908499}.text-muted\/70{color:#939084b3}.text-orange-500{--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.text-primary\/20{color:#553de933}.text-primary\/60{color:#553de999}.text-primary\/80{color:#553de9cc}.text-purple-500{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-secondary{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.text-success{--tw-text-opacity: 1;color:rgb(31 197 168 / var(--tw-text-opacity, 1))}.text-warning{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/60{color:#fff9}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-button{--tw-shadow: 0 1px 3px rgba(0,0,0,.08);--tw-shadow-colored: 0 1px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-card{--tw-shadow: 0 1px 3px rgba(0,0,0,.06);--tw-shadow-colored: 0 1px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-card-hover{--tw-shadow: 0 5px 10px rgba(0,0,0,.08);--tw-shadow-colored: 0 5px 10px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[\#553DE9\]\/20{--tw-shadow-color: rgb(85 61 233 / .2);--tw-shadow: var(--tw-shadow-colored)}.shadow-card{--tw-shadow-color: #FFFFFF;--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-\[\#553DE9\]{--tw-ring-opacity: 1;--tw-ring-color: rgb(85 61 233 / var(--tw-ring-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}.placeholder\:text-\[\#939084\]::-moz-placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-\[\#939084\]::placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-muted::-moz-placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-muted::placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-primary\/60::-moz-placeholder{color:#553de999}.placeholder\:text-primary\/60::placeholder{color:#553de999}.last\:border-b-0:last-child{border-bottom-width:0px}.hover\:border-border:hover{--tw-border-opacity: 1;border-color:rgb(236 234 227 / var(--tw-border-opacity, 1))}.hover\:border-primary\/30:hover{border-color:#553de94d}.hover\:bg-\[\#1b1f23\]:hover{--tw-bg-opacity: 1;background-color:rgb(27 31 35 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#4432c4\]:hover{--tw-bg-opacity: 1;background-color:rgb(68 50 196 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#553DE9\]\/5:hover{background-color:#553de90d}.hover\:bg-\[\#553DE9\]\/90:hover{background-color:#553de9e6}.hover\:bg-\[\#C45B5B\]\/20:hover{background-color:#c45b5b33}.hover\:bg-\[\#E8E4FD\]:hover{--tw-bg-opacity: 1;background-color:rgb(232 228 253 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#ECEAE3\]:hover{--tw-bg-opacity: 1;background-color:rgb(236 234 227 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#F8F4F0\]:hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.hover\:bg-card:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:bg-danger\/10:hover{background-color:#c45b5b1a}.hover\:bg-danger\/20:hover{background-color:#c45b5b33}.hover\:bg-hover:hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-hover:hover{--tw-bg-opacity: 1;background-color:rgb(68 50 196 / var(--tw-bg-opacity, 1))}.hover\:bg-primary\/20:hover{background-color:#553de933}.hover\:bg-primary\/30:hover{background-color:#553de94d}.hover\:bg-primary\/5:hover{background-color:#553de90d}.hover\:bg-primary\/90:hover{background-color:#553de9e6}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-red-500\/20:hover{background-color:#ef444433}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-warning\/10:hover{background-color:#d4a03c1a}.hover\:bg-warning\/20:hover{background-color:#d4a03c33}.hover\:text-\[\#36342E\]:hover{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.hover\:text-\[\#553DE9\]:hover{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.hover\:text-danger:hover{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.hover\:text-ink:hover{--tw-text-opacity: 1;color:rgb(32 21 21 / var(--tw-text-opacity, 1))}.hover\:text-primary:hover{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.hover\:text-primary\/80:hover{color:#553de9cc}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-card-hover:hover{--tw-shadow: 0 5px 10px rgba(0,0,0,.08);--tw-shadow-colored: 0 5px 10px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:not-sr-only:focus{position:static;width:auto;height:auto;padding:0;margin:0;overflow:visible;clip:auto;white-space:normal}.focus\:absolute:focus{position:absolute}.focus\:left-2:focus{left:.5rem}.focus\:top-2:focus{top:.5rem}.focus\:z-50:focus{z-index:50}.focus\:rounded-\[3px\]:focus{border-radius:3px}.focus\:border-\[\#553DE9\]:focus{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.focus\:border-primary:focus{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.focus\:border-primary\/30:focus{border-color:#553de94d}.focus\:bg-white:focus{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.focus\:px-4:focus{padding-left:1rem;padding-right:1rem}.focus\:py-2:focus{padding-top:.5rem;padding-bottom:.5rem}.focus\:text-\[\#553DE9\]:focus{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.focus\:shadow-card:focus{--tw-shadow: 0 1px 3px rgba(0,0,0,.06);--tw-shadow-colored: 0 1px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);--tw-shadow-color: #FFFFFF;--tw-shadow: var(--tw-shadow-colored)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-\[\#553DE9\]\/20:focus{--tw-ring-color: rgb(85 61 233 / .2)}.focus\:ring-primary\/20:focus{--tw-ring-color: rgb(85 61 233 / .2)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-primary{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.group\/file:hover .group-hover\/file\:opacity-100{opacity:1}@media(prefers-reduced-motion:reduce){.motion-reduce\:animate-none{animation:none}}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|