diff --git a/README.md b/README.md
index 59890c6..db54122 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,45 @@
-# X-Financial
-
-项目结构已按前后端拆开:
-
-- `web/`:前端工程(当前 Vue + Vite 项目)
-- `server/`:后端工程目录
-- `docs/`:方案和阶段文档
-- `UI/`:界面参考稿
-- `document/`:业务文档
-
-根目录统一环境变量:
-
-- `.env`
-- `.env.example`
-
-这里集中维护:
-
-- 前端启动端口
-- 后端启动端口
-- PostgreSQL 连接参数
-- `DATABASE_URL`
-- `REDIS_URL`
-
-从根目录统一启动:
-
-```bash
-./start.sh
-```
-
-可选模式:
-
-```bash
-./start.sh web
-./start.sh server
-./start.sh all
-```
-
-根目录 `start.sh` 是统一编排入口;前端和后端的子启动脚本分别是 `web/web_start.sh` 与 `server/server_start.sh`。
-
-手动进入前端目录:
-
-```bash
-cd web
-npm run dev
-```
+# X-Financial
+
+项目结构已按前后端拆开:
+
+- `web/`:前端工程(当前 Vue + Vite 项目)
+- `server/`:后端工程目录
+- `docs/`:方案和阶段文档
+- `UI/`:界面参考稿
+- `document/`:业务文档
+
+根目录统一环境变量:
+
+- `.env`
+- `.env.example`
+
+这里集中维护:
+
+- 前端启动端口
+- 后端启动端口
+- PostgreSQL 连接参数
+- `DATABASE_URL`
+- `REDIS_URL`
+
+从根目录统一启动:
+
+```bash
+./start.sh
+```
+
+可选模式:
+
+```bash
+./start.sh web
+./start.sh server
+./start.sh all
+```
+
+根目录 `start.sh` 是统一编排入口;前端和后端的子启动脚本分别是 `web/web_start.sh` 与 `server/server_start.sh`。
+
+手动进入前端目录:
+
+```bash
+cd web
+npm run dev
+```
diff --git a/document/development/plan/ai_agent_dual_layer_arch.md b/document/development/plan/ai_agent_dual_layer_arch.md
new file mode 100644
index 0000000..c262a97
--- /dev/null
+++ b/document/development/plan/ai_agent_dual_layer_arch.md
@@ -0,0 +1,169 @@
+# X-Financial 智能化财务系统:双层 Agent 架构设计与开发落地全景指南
+
+> **核心设计理念:确定性与概率性的完美解耦**
+>
+> 在企业级财务系统中,“合规性”与“准确性”是不可妥协的底线。大语言模型(LLM)天生具有概率性(会产生幻觉),因此不能直接赋予其修改核心财务数据或放行审批的最高权限。
+>
+> 本架构设计的核心,在于构建一个**“双层防线”**:
+> 1. **外层 Agent (自研流程大脑)**:提供 100% 的确定性。它是系统的执行者,严格按照预设流程和固化的规则行事,不具备“自我意识”,只负责“路由”、“拦截”和“记录”。
+> 2. **内层 Agent (Hermes 智囊核心)**:提供强大的概率性推理能力。它是系统的思考者,负责处理所有复杂、模糊、非结构化的任务(如阅读长文档、识别潜在风险),但它的输出**不能直接作用于业务**,而是转化为**规则配置**或**建议意见**,交由外层 Agent 或人类管理员执行。
+>
+> 这两层架构不是相互独立的两个系统,而是形成一个**“闭环”**:内层提炼规则,外层执行规则;外层收集数据,内层分析数据。这种深度协同,既保障了系统的安全性,又赋予了系统极高的智能化水平。
+
+---
+
+## 一、 系统架构图景与职责边界深度剖析
+
+### 1. 外层 Agent (Outer Agent):流程与路由的绝对掌控者
+
+**本质:一个高度可配置的业务工作流引擎与意图分发器。**
+
+* **开发技术栈建议**:FastAPI (后端) + Vue3 (前端) + PostgreSQL (持久化) + Redis (可选,用于状态缓存)。
+* **交互形态**:它直接面对用户。它可以是一个类似对话框的界面,但背后的逻辑是基于**状态机 (State Machine)** 驱动的。
+* **核心模块与职责 (What to do & How to do)**:
+
+ * **模块 1: 意图漏斗 (Intent Router)**
+ * **职责**:精准捕捉用户请求的第一诉求,并将其导向正确的处理管线。
+ * **方法**:
+ * *规则匹配优先*:使用简单的关键词或正则(例如:匹配到“报销”、“打车”字眼,直接激活报销向导)。
+ * *轻量级分类模型兜底*:对于模糊表述(如:“我上周去上海开会的钱怎么还没发?”),调用一个小参数的分类模型(或内层的快捷接口),将其分类为“状态查询”意图,并提取关键实体(如时间:上周,地点:上海)。
+ * **模块 2: 结构化状态机引擎 (State & Flow Controller)**
+ * **职责**:管理每一个业务对象(如一张报销单)的生命周期。从“草稿” -> “提交” -> “一级审批” -> “财务复核” -> “已打款”。
+ * **方法**:拒绝让大模型控制流程走向。流程流转必须基于代码逻辑中的条件判断(例如:如果金额 < 500,且员工级别为 M1,则跳过一级审批,直接进入财务复核)。外层 Agent 负责维护并推进这个状态。
+ * **模块 3: 确定性规则执行器 (Rule Execution Engine)**
+ * **职责**:财务合规的第一道硬性防线。不讲道理,只看数据。
+ * **方法**:当用户提交报销数据时,该模块会查询本地的 `business_rules` 数据库表。如果用户提交的住宿费是 850,而数据库规则明确上限是 800,则立刻抛出“阻断型错误” (Blocking Error)。**此过程绝对禁止调用大模型进行实时推断。**
+ * **模块 4: 标准化 API 网关 (API Gateway & Handshake Layer)**
+ * **职责**:封装所有对外层系统(如 ERP、HR 系统)和对内层 Hermes 的通信接口。控制并发,记录调用日志。
+
+### 2. 内层 Agent (Hermes):非结构化信息的提炼者与深度思考者
+
+**本质:一个被严格隔离的智能计算引擎,专门处理人类擅长但传统代码难以处理的“软逻辑”。**
+
+* **开发技术栈建议**:Hermes 框架 + 向量数据库 (如 Milvus/PGVector) + 强力 LLM (如 GPT-4 或开源大模型)。
+* **交互形态**:对用户不可见,只作为外层 Agent 的“后端服务”存在。
+* **核心模块与职责 (What to do & How to do)**:
+
+ * **模块 1: 政策蒸馏器 (Policy Distiller) —— 解决“知行合一”的关键**
+ * **职责**:打破知识库(死文件)与业务流(活代码)之间的壁垒。
+ * **方法 (核心思路)**:
+ 1. *触发*:管理员上传一份《差旅新规.pdf》。
+ 2. *解析*:Hermes 逐段阅读文档。
+ 3. *提取*:使用精心设计的 **Few-Shot Prompt 链**,强制模型识别特定的“控制变量”。
+ *(Prompt 示例: "你是一个专业的财务合规审计员。请阅读以下段落,如果包含任何关于费用上限、职级限制、审批层级的规定,请严格按照以下 JSON Schema 输出:{category, location, level_req, max_amount, is_hard_limit}。如果未找到,输出空。")*
+ 4. *回写*:Hermes 将提炼出的 JSON 结构转化为标准的 SQL Update 指令(或通过专用 API 接口),更新外层 Agent 依赖的 `business_rules` 表。
+ * **模块 2: 深度知识检索 (Deep RAG & Interpretation)**
+ * **职责**:为用户提供复杂制度的个性化解读。
+ * **方法**:当外层 Agent 无法解答用户的合规疑问时(意图识别为“政策咨询”),外层将请求转发给 Hermes。Hermes 在向量库中检索相关段落,并结合用户当前的上下文(如:员工职级、出差地),生成一份连贯、人性化的解答。
+ * **模块 3: 异步风险探针 (Asynchronous Risk Auditor)**
+ * **职责**:像“老会计”一样,在海量已发生或正在发生的业务数据中寻找蛛丝马迹。
+ * **方法**:
+ 1. *定时任务*:每天凌晨启动。
+ 2. *数据聚合*:从外层数据库提取当天的报销流水(去除敏感个资)。
+ 3. *模式识别*:通过特定的 Prompt(例如寻找“拆单报销”、“异常高频的出租车票”)。
+ 4. *生成报告*:生成结构化的风险预警报告,存入专用表,供管理员次日早晨审核,而不是直接去冻结员工账号。
+
+---
+
+## 二、 核心通信协议 (The Handshake):两层的握手与数据交互
+
+双层架构的成败,取决于这两层能否顺畅地交换信息,且保证安全。我们需要定义清晰的接口协议。
+
+### 1. 同步查询接口 (外 -> 内:求知与解惑)
+
+当外层遇到处理不了的“软逻辑”时触发。
+
+* **Endpoint (示例)**: `POST /hermes/api/v1/consult`
+* **外层 Request 结构**:
+ ```json
+ {
+ "context": {
+ "user_id": "emp_1001",
+ "current_task": "travel_reimbursement",
+ "form_data": {"city": "北京", "amount": 900}
+ },
+ "query": "因为展会原因酒店全满,只能订900的,能报销吗?"
+ }
+ ```
+* **内层 Hermes Response 结构**:
+ ```json
+ {
+ "status": "success",
+ "interpretation": "根据《差旅管理办法》第15条,展会期间允许上浮 20%。您的标准是800,上浮后为960,可以报销。",
+ "action_recommendation": "require_special_approval", // 建议外层采取的动作
+ "citations": ["policy_doc_v2_page_4"]
+ }
+ ```
+
+### 2. 异步任务接口 (外 -> 内:派发耗时任务)
+
+例如请求生成长篇分析报告或进行全量风险巡检。
+
+* **流程**:
+ 1. 外层调用 `POST /hermes/api/v1/jobs/generate_report`。
+ 2. 内层 Hermes 立即返回 `202 Accepted` 和一个 `job_id`。
+ 3. 内层 Hermes 在后台慢慢算。
+ 4. 计算完成后,内层通过 Webhook 回调外层的通知接口,外层再通过系统消息通知用户“您的报告已就绪”。
+
+### 3. 规则推送机制 (内 -> 外:自动化立法)
+
+这是最核心的逆向通信。内层提炼出的规则如何生效?
+
+* **流程**:
+ 1. Hermes 提炼出新规则。
+ 2. Hermes 调用外层的特权 API (如 `POST /admin/api/rules/sync`),推送规则 payload。
+ 3. 外层 Agent 收到后,执行数据库 `UPSERT` 操作更新 `business_rules` 表。
+ 4. *(可选但强烈建议)*:进入“待激活”状态,需要人类管理员在系统中点击“确认应用新规则”后,新规才正式生效。
+
+---
+
+## 三、 分阶段开发落地全景计划 (Implementation Roadmap)
+
+开发应当遵循“先基建后上层、先确定后智能”的原则。
+
+### Phase 1: 骨架搭建与基石铺设 (Foundation & Outer Shell)
+*目标:构建一个哪怕没有 AI 也能运转的硬核流程系统,确立两层隔离。*
+
+1. **架构拆分验证**:在服务器层面,确保 Outer Agent (FastAPI) 和 Inner Hermes 分别在独立的进程(或容器)中运行,仅通过 HTTP/gRPC 通信。
+2. **动态规则引擎实现 (核心基建)**:
+ * 在 PostgreSQL 中设计 `business_rules` 表结构。必须支持高度扩展性(例如采用 `JSONB` 字段存储具体约束参数)。
+ * 在外层 Agent 开发一个“规则校验服务 (Rule Validation Service)”,该服务能够在任何报销动作发生前,拦截并比对 `business_rules`。
+3. **标准化流程闭环**:开发一个完整的、基于硬规则驱动的差旅报销单据流转全流程(填单 -> 校验 -> 提交 -> 审批)。验证在“硬规则”下系统运转良好。
+
+### Phase 2: 知识注入与基础问答 (Hermes RAG Integration)
+*目标:赋予系统“解答疑问”的能力。*
+
+1. **内层基建**:配置 Hermes 环境,接入向量数据库。
+2. **文档清洗管道 (ETL pipeline)**:将现有的财务政策 PDF/Word 文档清洗、分块 (Chunking) 并向量化入库。
+3. **问答桥接**:
+ * 在外层前端 (Vue3) 提供一个“智能咨询”悬浮窗或独立页面。
+ * 外层 Agent 接收问题,附带上用户的上下文(角色、权限),一并转发给内层 Hermes。
+ * 验证 Hermes 能够根据向量库的内容,给出带出处的准确回答。
+
+### Phase 3: 核心攻坚 —— 自动立法与双层联通 (Policy Distillation & Sync)
+*目标:实现从“死文档”到“活规则”的自动化转化。*
+
+1. **蒸馏 Prompt 工程**:在 Hermes 中反复打磨“政策提炼”的 Prompt。针对你们公司常见的政策描述方式进行微调。
+2. **结构化提取测试**:手动上传不同版本的政策文档,测试 Hermes 能否稳定、准确地输出 JSON 格式的规则参数。
+3. **闭环联调**:
+ * 开发 Hermes 向外层推送规则的 API。
+ * 完成全链路测试:管理员界面上传新文档 -> Hermes 后台解析 -> 外层规则库自动更新 -> 前端即时生效新的金额限制。
+
+### Phase 4: 高阶进化 —— 异步审计与主动防御 (Proactive Risk Auditing)
+*目标:将系统从“被动响应”升级为“主动防护”。*
+
+1. **数据安全隧道**:建立从外层业务库向内层 Hermes 传递“脱敏业务快照”的通道。
+2. **风险模式定义**:梳理出 3-5 种典型的财务风险模式(如:异常聚集的餐饮发票、连续的单日高额交通费)。
+3. **Hermes 巡检任务**:编写定时任务逻辑,利用大模型的推理能力去比对这些模式和当天的业务快照数据。
+4. **风险看板**:在外层系统的管理后台开发“风险报告台”,展示 Hermes 生成的预警结果。
+
+---
+
+## 四、 关键风险与防范策略总结
+
+1. **大模型幻觉污染规则库**:
+ * **防范**:Hermes 提炼的所有硬性规则(尤其是金额、审批级数),在写入外层正式库之前,必须增加一个**“人工审核 (Human-in-the-loop)”** 环节。系统提示“检测到政策更新,提炼出 5 条新规则,请管理员确认应用”。
+2. **状态机混乱**:
+ * **防范**:外层 Agent 的流程控制代码必须使用强类型和严格的事务控制 (Transaction)。绝不允许任何组件(包括 AI)在不经过状态机合法校验的情况下直接修改数据库中的 `status` 字段。
+3. **性能瓶颈**:
+ * **防范**:所有外层必须做的事情(拦截、查询)必须在毫秒级完成。所有涉及调用 Hermes 的操作(问答、提炼、分析)全部采用异步设计或提供明确的 Loading 反馈。
diff --git a/server/src/app/api/deps.py b/server/src/app/api/deps.py
index afdef03..e73c07a 100644
--- a/server/src/app/api/deps.py
+++ b/server/src/app/api/deps.py
@@ -1,5 +1,8 @@
from collections.abc import Generator
+from dataclasses import dataclass
+from typing import Annotated
+from fastapi import Depends, Header, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_session_factory
@@ -11,3 +14,49 @@ def get_db() -> Generator[Session, None, None]:
yield db
finally:
db.close()
+
+
+@dataclass(slots=True)
+class CurrentUserContext:
+ username: str
+ name: str
+ role_codes: list[str]
+ is_admin: bool
+
+
+def get_current_user(
+ x_auth_username: Annotated[str | None, Header()] = None,
+ x_auth_name: Annotated[str | None, Header()] = None,
+ x_auth_role_codes: Annotated[str | None, Header()] = None,
+ x_auth_is_admin: Annotated[str | None, Header()] = None,
+) -> CurrentUserContext:
+ role_codes = [item.strip() for item in (x_auth_role_codes or "").split(",") if item.strip()]
+ is_admin = str(x_auth_is_admin or "").strip().lower() in {"1", "true", "yes", "on"}
+
+ username = (x_auth_username or "").strip()
+ name = (x_auth_name or username).strip()
+
+ if not username and not name:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="请先登录后再访问知识库。",
+ )
+
+ return CurrentUserContext(
+ username=username or name,
+ name=name or username,
+ role_codes=role_codes,
+ is_admin=is_admin,
+ )
+
+
+def require_admin_user(
+ current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
+) -> CurrentUserContext:
+ if current_user.is_admin or "manager" in current_user.role_codes:
+ return current_user
+
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="只有管理员可以上传、删除或修改知识库文件。",
+ )
diff --git a/server/src/app/api/v1/endpoints/knowledge.py b/server/src/app/api/v1/endpoints/knowledge.py
new file mode 100644
index 0000000..35def5f
--- /dev/null
+++ b/server/src/app/api/v1/endpoints/knowledge.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+
+from typing import Annotated
+
+from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
+from fastapi.responses import FileResponse
+
+from app.api.deps import CurrentUserContext, get_current_user, require_admin_user
+from app.schemas.knowledge import (
+ KnowledgeActionResponse,
+ KnowledgeDocumentDetailRead,
+ KnowledgeLibraryRead,
+)
+from app.services.knowledge import KnowledgeService
+
+router = APIRouter(prefix="/knowledge")
+
+
+@router.get("/library", response_model=KnowledgeLibraryRead)
+def get_knowledge_library(
+ _: Annotated[CurrentUserContext, Depends(get_current_user)],
+) -> KnowledgeLibraryRead:
+ return KnowledgeService().list_library()
+
+
+@router.get("/documents/{document_id}", response_model=KnowledgeDocumentDetailRead)
+def get_knowledge_document(
+ document_id: str,
+ _: Annotated[CurrentUserContext, Depends(get_current_user)],
+) -> KnowledgeDocumentDetailRead:
+ try:
+ return KnowledgeService().get_document_detail(document_id)
+ except FileNotFoundError as exc:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。") from exc
+
+
+@router.post("/documents", response_model=KnowledgeDocumentDetailRead, status_code=status.HTTP_201_CREATED)
+async def upload_knowledge_document(
+ request: Request,
+ folder: Annotated[str, Query(min_length=1)],
+ filename: Annotated[str, Query(min_length=1)],
+ current_user: Annotated[CurrentUserContext, Depends(require_admin_user)],
+) -> KnowledgeDocumentDetailRead:
+ content = await request.body()
+ try:
+ return KnowledgeService().upload_document(folder, filename, content, current_user)
+ except ValueError as exc:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
+
+
+@router.delete("/documents/{document_id}", response_model=KnowledgeActionResponse)
+def delete_knowledge_document(
+ document_id: str,
+ _: Annotated[CurrentUserContext, Depends(require_admin_user)],
+) -> KnowledgeActionResponse:
+ try:
+ KnowledgeService().delete_document(document_id)
+ except FileNotFoundError as exc:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。") from exc
+
+ return KnowledgeActionResponse(detail="知识库文件已删除。")
+
+
+@router.get("/documents/{document_id}/content")
+def get_knowledge_document_content(
+ document_id: str,
+ disposition: Annotated[str, Query(pattern="^(inline|attachment)$")] = "inline",
+ _: Annotated[CurrentUserContext, Depends(get_current_user)] = None,
+) -> FileResponse:
+ try:
+ file_path, media_type, filename = KnowledgeService().get_document_content(document_id)
+ except FileNotFoundError as exc:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。") from exc
+
+ _ = disposition
+ return FileResponse(file_path, media_type=media_type, filename=filename)
diff --git a/server/src/app/api/v1/router.py b/server/src/app/api/v1/router.py
index dec86d9..51ae53c 100644
--- a/server/src/app/api/v1/router.py
+++ b/server/src/app/api/v1/router.py
@@ -4,6 +4,7 @@ from app.api.v1.endpoints.auth import router as auth_router
from app.api.v1.endpoints.bootstrap import router as bootstrap_router
from app.api.v1.endpoints.employees import router as employees_router
from app.api.v1.endpoints.health import router as health_router
+from app.api.v1.endpoints.knowledge import router as knowledge_router
from app.api.v1.endpoints.reimbursements import router as reimbursements_router
from app.api.v1.endpoints.settings import router as settings_router
@@ -11,6 +12,7 @@ router = APIRouter()
router.include_router(health_router, tags=["health"])
router.include_router(bootstrap_router, tags=["bootstrap"])
router.include_router(auth_router, tags=["auth"])
+router.include_router(knowledge_router, tags=["knowledge"])
router.include_router(employees_router, prefix="/employees", tags=["employees"])
router.include_router(reimbursements_router, prefix="/reimbursements", tags=["reimbursements"])
router.include_router(settings_router, tags=["settings"])
diff --git a/server/src/app/core/admin_secret.py b/server/src/app/core/admin_secret.py
index 35f0aa1..5fc24a5 100644
--- a/server/src/app/core/admin_secret.py
+++ b/server/src/app/core/admin_secret.py
@@ -1,63 +1,63 @@
-from __future__ import annotations
-
-import hashlib
-import json
-import secrets
-from pathlib import Path
-
-from app.core.config import SERVER_DIR
-
-ADMIN_SECRET_FILE = SERVER_DIR / ".secrets" / "admin.json"
-
-
-def read_admin_secret() -> dict[str, object] | None:
- if not ADMIN_SECRET_FILE.exists():
- return None
-
- try:
- payload = json.loads(ADMIN_SECRET_FILE.read_text(encoding="utf-8"))
- except (OSError, json.JSONDecodeError):
- return None
-
- if (
- payload
- and payload.get("algorithm") == "scrypt"
- and isinstance(payload.get("username"), str)
- and isinstance(payload.get("salt"), str)
- and isinstance(payload.get("derived_key"), str)
- ):
- return payload
-
- return None
-
-
-def verify_admin_secret(password: str, record: dict[str, object]) -> bool:
- try:
- salt = bytes.fromhex(str(record["salt"]))
- stored_key = bytes.fromhex(str(record["derived_key"]))
- key_length = int(record.get("key_length", 64))
- n_value = int(record.get("N", 16384))
- r_value = int(record.get("r", 8))
- p_value = int(record.get("p", 1))
- except (KeyError, TypeError, ValueError):
- return False
-
- derived_key = hashlib.scrypt(
- password.encode("utf-8"),
- salt=salt,
- n=n_value,
- r=r_value,
- p=p_value,
- dklen=key_length,
- )
- return secrets.compare_digest(derived_key, stored_key)
-
-
-def legacy_admin_secret_to_password_hash(record: dict[str, object]) -> str:
- salt = str(record["salt"])
- derived_key = str(record["derived_key"])
- key_length = int(record.get("key_length", 64))
- n_value = int(record.get("N", 16384))
- r_value = int(record.get("r", 8))
- p_value = int(record.get("p", 1))
- return f"scrypt${n_value}${r_value}${p_value}${key_length}${salt}${derived_key}"
+from __future__ import annotations
+
+import hashlib
+import json
+import secrets
+from pathlib import Path
+
+from app.core.config import SERVER_DIR
+
+ADMIN_SECRET_FILE = SERVER_DIR / ".secrets" / "admin.json"
+
+
+def read_admin_secret() -> dict[str, object] | None:
+ if not ADMIN_SECRET_FILE.exists():
+ return None
+
+ try:
+ payload = json.loads(ADMIN_SECRET_FILE.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ return None
+
+ if (
+ payload
+ and payload.get("algorithm") == "scrypt"
+ and isinstance(payload.get("username"), str)
+ and isinstance(payload.get("salt"), str)
+ and isinstance(payload.get("derived_key"), str)
+ ):
+ return payload
+
+ return None
+
+
+def verify_admin_secret(password: str, record: dict[str, object]) -> bool:
+ try:
+ salt = bytes.fromhex(str(record["salt"]))
+ stored_key = bytes.fromhex(str(record["derived_key"]))
+ key_length = int(record.get("key_length", 64))
+ n_value = int(record.get("N", 16384))
+ r_value = int(record.get("r", 8))
+ p_value = int(record.get("p", 1))
+ except (KeyError, TypeError, ValueError):
+ return False
+
+ derived_key = hashlib.scrypt(
+ password.encode("utf-8"),
+ salt=salt,
+ n=n_value,
+ r=r_value,
+ p=p_value,
+ dklen=key_length,
+ )
+ return secrets.compare_digest(derived_key, stored_key)
+
+
+def legacy_admin_secret_to_password_hash(record: dict[str, object]) -> str:
+ salt = str(record["salt"])
+ derived_key = str(record["derived_key"])
+ key_length = int(record.get("key_length", 64))
+ n_value = int(record.get("N", 16384))
+ r_value = int(record.get("r", 8))
+ p_value = int(record.get("p", 1))
+ return f"scrypt${n_value}${r_value}${p_value}${key_length}${salt}${derived_key}"
diff --git a/server/src/app/core/config.py b/server/src/app/core/config.py
index 4aaf824..5c98e25 100644
--- a/server/src/app/core/config.py
+++ b/server/src/app/core/config.py
@@ -1,76 +1,84 @@
-from __future__ import annotations
-
-from functools import lru_cache
-from os import environ
-from pathlib import Path
-
-from pydantic import Field
-from pydantic_settings import BaseSettings, SettingsConfigDict
-
-SERVER_DIR = Path(__file__).resolve().parents[3]
-ROOT_DIR = SERVER_DIR.parent
-
-
-class Settings(BaseSettings):
- model_config = SettingsConfigDict(
- env_file=(ROOT_DIR / ".env", SERVER_DIR / ".env"),
- env_file_encoding="utf-8",
- extra="ignore",
- )
-
- app_name: str = Field(default="X-Financial Server", alias="APP_NAME")
- app_env: str = Field(default="local", alias="APP_ENV")
- app_debug: bool = Field(default=True, alias="APP_DEBUG")
- setup_completed: bool = Field(default=False, alias="SETUP_COMPLETED")
-
- company_name: str = Field(default="", alias="COMPANY_NAME")
- company_code: str = Field(default="", alias="COMPANY_CODE")
- admin_email: str = Field(default="", alias="ADMIN_EMAIL")
-
- web_host: str = Field(default="0.0.0.0", alias="WEB_HOST")
- web_port: int = Field(default=5173, alias="WEB_PORT")
- app_host: str = Field(default="0.0.0.0", alias="SERVER_HOST")
- app_port: int = Field(default=8000, alias="SERVER_PORT")
- api_v1_prefix: str = Field(default="/api/v1", alias="API_V1_PREFIX")
-
- postgres_host: str = Field(default="127.0.0.1", alias="POSTGRES_HOST")
- postgres_port: int = Field(default=5432, alias="POSTGRES_PORT")
- postgres_db: str = Field(default="x_financial", alias="POSTGRES_DB")
- postgres_user: str = Field(default="postgres", alias="POSTGRES_USER")
- postgres_password: str = Field(default="postgres", alias="POSTGRES_PASSWORD")
-
- database_url: str | None = Field(default=None, alias="DATABASE_URL")
- sqlalchemy_echo: bool = Field(default=False, alias="SQLALCHEMY_ECHO")
-
- redis_url: str | None = Field(default=None, alias="REDIS_URL")
- cors_origins: list[str] = Field(default_factory=list, alias="CORS_ORIGINS")
- vite_api_base_url: str = Field(
- default="http://127.0.0.1:8000/api/v1", alias="VITE_API_BASE_URL"
- )
-
+from __future__ import annotations
+
+from functools import lru_cache
+from os import environ
+from pathlib import Path
+
+from pydantic import Field
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+SERVER_DIR = Path(__file__).resolve().parents[3]
+ROOT_DIR = SERVER_DIR.parent
+
+
+class Settings(BaseSettings):
+ model_config = SettingsConfigDict(
+ env_file=(ROOT_DIR / ".env", SERVER_DIR / ".env"),
+ env_file_encoding="utf-8",
+ extra="ignore",
+ )
+
+ app_name: str = Field(default="X-Financial Server", alias="APP_NAME")
+ app_env: str = Field(default="local", alias="APP_ENV")
+ app_debug: bool = Field(default=True, alias="APP_DEBUG")
+ setup_completed: bool = Field(default=False, alias="SETUP_COMPLETED")
+
+ company_name: str = Field(default="", alias="COMPANY_NAME")
+ company_code: str = Field(default="", alias="COMPANY_CODE")
+ admin_email: str = Field(default="", alias="ADMIN_EMAIL")
+
+ web_host: str = Field(default="0.0.0.0", alias="WEB_HOST")
+ web_port: int = Field(default=5173, alias="WEB_PORT")
+ app_host: str = Field(default="0.0.0.0", alias="SERVER_HOST")
+ app_port: int = Field(default=8000, alias="SERVER_PORT")
+ api_v1_prefix: str = Field(default="/api/v1", alias="API_V1_PREFIX")
+
+ postgres_host: str = Field(default="127.0.0.1", alias="POSTGRES_HOST")
+ postgres_port: int = Field(default=5432, alias="POSTGRES_PORT")
+ postgres_db: str = Field(default="x_financial", alias="POSTGRES_DB")
+ postgres_user: str = Field(default="postgres", alias="POSTGRES_USER")
+ postgres_password: str = Field(default="postgres", alias="POSTGRES_PASSWORD")
+
+ database_url: str | None = Field(default=None, alias="DATABASE_URL")
+ sqlalchemy_echo: bool = Field(default=False, alias="SQLALCHEMY_ECHO")
+
+ redis_url: str | None = Field(default=None, alias="REDIS_URL")
+ cors_origins: list[str] = Field(default_factory=list, alias="CORS_ORIGINS")
+ vite_api_base_url: str = Field(
+ default="http://127.0.0.1:8000/api/v1", alias="VITE_API_BASE_URL"
+ )
+
log_level: str = Field(default="INFO", alias="LOG_LEVEL")
log_dir: str = Field(default="logs", alias="LOG_DIR")
log_file_enabled: bool = Field(default=True, alias="LOG_FILE_ENABLED")
+ storage_root_dir: str = Field(default="storage", alias="STORAGE_ROOT_DIR")
@property
def resolved_database_url(self) -> str:
if self.database_url:
return self.database_url
-
+
return (
f"postgresql+psycopg://{self.postgres_user}:{self.postgres_password}"
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
)
-
-@lru_cache
-def get_settings() -> Settings:
- return Settings()
-
-
-def refresh_settings(updated_values: dict[str, str]) -> Settings:
- for key, value in updated_values.items():
- environ[key] = value
-
- get_settings.cache_clear()
- return get_settings()
+ @property
+ def resolved_storage_root_dir(self) -> Path:
+ path = Path(self.storage_root_dir)
+ if not path.is_absolute():
+ path = SERVER_DIR / path
+ return path.resolve()
+
+
+@lru_cache
+def get_settings() -> Settings:
+ return Settings()
+
+
+def refresh_settings(updated_values: dict[str, str]) -> Settings:
+ for key, value in updated_values.items():
+ environ[key] = value
+
+ get_settings.cache_clear()
+ return get_settings()
diff --git a/server/src/app/core/security.py b/server/src/app/core/security.py
index a7cefcc..cdc8787 100644
--- a/server/src/app/core/security.py
+++ b/server/src/app/core/security.py
@@ -1,71 +1,71 @@
-from __future__ import annotations
-
-import hashlib
-import secrets
-from base64 import urlsafe_b64decode, urlsafe_b64encode
-
-PBKDF2_ALGORITHM = "sha256"
-PBKDF2_ITERATIONS = 120_000
-SALT_BYTES = 16
-
-
-def hash_password(password: str) -> str:
- salt = secrets.token_bytes(SALT_BYTES)
- digest = hashlib.pbkdf2_hmac(
- PBKDF2_ALGORITHM,
- password.encode("utf-8"),
- salt,
- PBKDF2_ITERATIONS,
- )
- encoded_salt = urlsafe_b64encode(salt).decode("utf-8")
- encoded_digest = urlsafe_b64encode(digest).decode("utf-8")
- return f"pbkdf2_{PBKDF2_ALGORITHM}${PBKDF2_ITERATIONS}${encoded_salt}${encoded_digest}"
-
-
-def verify_password(password: str, password_hash: str) -> bool:
- if password_hash.startswith("scrypt$"):
- return verify_scrypt_password(password, password_hash)
-
- try:
- scheme, iterations, encoded_salt, encoded_digest = password_hash.split("$", 3)
- except ValueError:
- return False
-
- if scheme != f"pbkdf2_{PBKDF2_ALGORITHM}":
- return False
-
- salt = urlsafe_b64decode(encoded_salt.encode("utf-8"))
- expected_digest = urlsafe_b64decode(encoded_digest.encode("utf-8"))
- computed_digest = hashlib.pbkdf2_hmac(
- PBKDF2_ALGORITHM,
- password.encode("utf-8"),
- salt,
- int(iterations),
- )
- return secrets.compare_digest(computed_digest, expected_digest)
-
-
-def verify_scrypt_password(password: str, password_hash: str) -> bool:
- try:
- scheme, n_value, r_value, p_value, key_length, salt_hex, derived_key_hex = password_hash.split("$", 6)
- except ValueError:
- return False
-
- if scheme != "scrypt":
- return False
-
- try:
- salt = bytes.fromhex(salt_hex)
- expected_key = bytes.fromhex(derived_key_hex)
- derived_key = hashlib.scrypt(
- password.encode("utf-8"),
- salt=salt,
- n=int(n_value),
- r=int(r_value),
- p=int(p_value),
- dklen=int(key_length),
- )
- except ValueError:
- return False
-
- return secrets.compare_digest(derived_key, expected_key)
+from __future__ import annotations
+
+import hashlib
+import secrets
+from base64 import urlsafe_b64decode, urlsafe_b64encode
+
+PBKDF2_ALGORITHM = "sha256"
+PBKDF2_ITERATIONS = 120_000
+SALT_BYTES = 16
+
+
+def hash_password(password: str) -> str:
+ salt = secrets.token_bytes(SALT_BYTES)
+ digest = hashlib.pbkdf2_hmac(
+ PBKDF2_ALGORITHM,
+ password.encode("utf-8"),
+ salt,
+ PBKDF2_ITERATIONS,
+ )
+ encoded_salt = urlsafe_b64encode(salt).decode("utf-8")
+ encoded_digest = urlsafe_b64encode(digest).decode("utf-8")
+ return f"pbkdf2_{PBKDF2_ALGORITHM}${PBKDF2_ITERATIONS}${encoded_salt}${encoded_digest}"
+
+
+def verify_password(password: str, password_hash: str) -> bool:
+ if password_hash.startswith("scrypt$"):
+ return verify_scrypt_password(password, password_hash)
+
+ try:
+ scheme, iterations, encoded_salt, encoded_digest = password_hash.split("$", 3)
+ except ValueError:
+ return False
+
+ if scheme != f"pbkdf2_{PBKDF2_ALGORITHM}":
+ return False
+
+ salt = urlsafe_b64decode(encoded_salt.encode("utf-8"))
+ expected_digest = urlsafe_b64decode(encoded_digest.encode("utf-8"))
+ computed_digest = hashlib.pbkdf2_hmac(
+ PBKDF2_ALGORITHM,
+ password.encode("utf-8"),
+ salt,
+ int(iterations),
+ )
+ return secrets.compare_digest(computed_digest, expected_digest)
+
+
+def verify_scrypt_password(password: str, password_hash: str) -> bool:
+ try:
+ scheme, n_value, r_value, p_value, key_length, salt_hex, derived_key_hex = password_hash.split("$", 6)
+ except ValueError:
+ return False
+
+ if scheme != "scrypt":
+ return False
+
+ try:
+ salt = bytes.fromhex(salt_hex)
+ expected_key = bytes.fromhex(derived_key_hex)
+ derived_key = hashlib.scrypt(
+ password.encode("utf-8"),
+ salt=salt,
+ n=int(n_value),
+ r=int(r_value),
+ p=int(p_value),
+ dklen=int(key_length),
+ )
+ except ValueError:
+ return False
+
+ return secrets.compare_digest(derived_key, expected_key)
diff --git a/server/src/app/db/base.py b/server/src/app/db/base.py
index 9082282..04a2783 100644
--- a/server/src/app/db/base.py
+++ b/server/src/app/db/base.py
@@ -1,23 +1,23 @@
-from app.db.base_class import Base
-from app.models.approval import ApprovalRecord
-from app.models.employee_change_log import EmployeeChangeLog
-from app.models.employee import Employee
-from app.models.organization import OrganizationUnit
-from app.models.reimbursement import ReimbursementRequest
-from app.models.role import Role
-from app.models.system_model_setting import SystemModelSetting
-from app.models.system_setting import SystemSetting
-from app.models.system_setting_secret import SystemSettingSecret
-
-__all__ = [
- "Base",
- "ApprovalRecord",
- "Employee",
- "EmployeeChangeLog",
- "OrganizationUnit",
- "ReimbursementRequest",
- "Role",
- "SystemModelSetting",
- "SystemSetting",
- "SystemSettingSecret",
-]
+from app.db.base_class import Base
+from app.models.approval import ApprovalRecord
+from app.models.employee_change_log import EmployeeChangeLog
+from app.models.employee import Employee
+from app.models.organization import OrganizationUnit
+from app.models.reimbursement import ReimbursementRequest
+from app.models.role import Role
+from app.models.system_model_setting import SystemModelSetting
+from app.models.system_setting import SystemSetting
+from app.models.system_setting_secret import SystemSettingSecret
+
+__all__ = [
+ "Base",
+ "ApprovalRecord",
+ "Employee",
+ "EmployeeChangeLog",
+ "OrganizationUnit",
+ "ReimbursementRequest",
+ "Role",
+ "SystemModelSetting",
+ "SystemSetting",
+ "SystemSettingSecret",
+]
diff --git a/server/src/app/main.py b/server/src/app/main.py
index 2d594c9..44398ef 100644
--- a/server/src/app/main.py
+++ b/server/src/app/main.py
@@ -8,6 +8,7 @@ from app.core.config import get_settings
from app.core.logging import get_logger, setup_logging
from app.middleware.logging import AccessLogMiddleware
from app.services.employee import prepare_employee_directory
+from app.services.knowledge import prepare_knowledge_library
def create_app() -> FastAPI:
@@ -50,6 +51,7 @@ def create_app() -> FastAPI:
@app.on_event("startup")
def _on_startup() -> None:
prepare_employee_directory()
+ prepare_knowledge_library()
logger.info(
"Server ready - host=%s port=%s prefix=%s",
settings.app_host,
diff --git a/server/src/app/models/__init__.py b/server/src/app/models/__init__.py
index b4a9671..6aad399 100644
--- a/server/src/app/models/__init__.py
+++ b/server/src/app/models/__init__.py
@@ -1,21 +1,21 @@
-from app.models.approval import ApprovalRecord
-from app.models.employee_change_log import EmployeeChangeLog
-from app.models.employee import Employee
-from app.models.organization import OrganizationUnit
-from app.models.reimbursement import ReimbursementRequest
-from app.models.role import Role
-from app.models.system_model_setting import SystemModelSetting
-from app.models.system_setting import SystemSetting
-from app.models.system_setting_secret import SystemSettingSecret
-
-__all__ = [
- "ApprovalRecord",
- "Employee",
- "EmployeeChangeLog",
- "OrganizationUnit",
- "ReimbursementRequest",
- "Role",
- "SystemModelSetting",
- "SystemSetting",
- "SystemSettingSecret",
-]
+from app.models.approval import ApprovalRecord
+from app.models.employee_change_log import EmployeeChangeLog
+from app.models.employee import Employee
+from app.models.organization import OrganizationUnit
+from app.models.reimbursement import ReimbursementRequest
+from app.models.role import Role
+from app.models.system_model_setting import SystemModelSetting
+from app.models.system_setting import SystemSetting
+from app.models.system_setting_secret import SystemSettingSecret
+
+__all__ = [
+ "ApprovalRecord",
+ "Employee",
+ "EmployeeChangeLog",
+ "OrganizationUnit",
+ "ReimbursementRequest",
+ "Role",
+ "SystemModelSetting",
+ "SystemSetting",
+ "SystemSettingSecret",
+]
diff --git a/server/src/app/models/system_model_setting.py b/server/src/app/models/system_model_setting.py
index 0a38dbd..7d89f8f 100644
--- a/server/src/app/models/system_model_setting.py
+++ b/server/src/app/models/system_model_setting.py
@@ -1,28 +1,28 @@
-from __future__ import annotations
-
-from datetime import datetime
-
-from sqlalchemy import Boolean, DateTime, Integer, String, Text, func
-from sqlalchemy.orm import Mapped, mapped_column
-
-from app.db.base_class import Base
-
-
-class SystemModelSetting(Base):
- __tablename__ = "system_model_settings"
-
- slot: Mapped[str] = mapped_column(String(32), primary_key=True)
- provider: Mapped[str] = mapped_column(String(64), default="")
- model_name: Mapped[str] = mapped_column(String(255), default="")
- endpoint: Mapped[str] = mapped_column(String(512), default="")
- capability: Mapped[str] = mapped_column(String(32), default="chat")
- priority: Mapped[int] = mapped_column(Integer, default=0)
- enabled: Mapped[bool] = mapped_column(Boolean, default=True)
- api_key_encrypted: Mapped[str] = mapped_column(Text, default="")
-
- created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
- updated_at: Mapped[datetime] = mapped_column(
- DateTime(timezone=True),
- server_default=func.now(),
- onupdate=func.now(),
- )
+from __future__ import annotations
+
+from datetime import datetime
+
+from sqlalchemy import Boolean, DateTime, Integer, String, Text, func
+from sqlalchemy.orm import Mapped, mapped_column
+
+from app.db.base_class import Base
+
+
+class SystemModelSetting(Base):
+ __tablename__ = "system_model_settings"
+
+ slot: Mapped[str] = mapped_column(String(32), primary_key=True)
+ provider: Mapped[str] = mapped_column(String(64), default="")
+ model_name: Mapped[str] = mapped_column(String(255), default="")
+ endpoint: Mapped[str] = mapped_column(String(512), default="")
+ capability: Mapped[str] = mapped_column(String(32), default="chat")
+ priority: Mapped[int] = mapped_column(Integer, default=0)
+ enabled: Mapped[bool] = mapped_column(Boolean, default=True)
+ api_key_encrypted: Mapped[str] = mapped_column(Text, default="")
+
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+ updated_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True),
+ server_default=func.now(),
+ onupdate=func.now(),
+ )
diff --git a/server/src/app/repositories/settings.py b/server/src/app/repositories/settings.py
index 49908d7..52ac084 100644
--- a/server/src/app/repositories/settings.py
+++ b/server/src/app/repositories/settings.py
@@ -1,43 +1,43 @@
-from __future__ import annotations
-
-from sqlalchemy import select
-from sqlalchemy.orm import Session
-
-from app.models.system_model_setting import SystemModelSetting
-from app.models.system_setting import SystemSetting
-from app.models.system_setting_secret import SystemSettingSecret
-
-SETTINGS_ROW_ID = "default"
-
-
-class SettingsRepository:
- def __init__(self, db: Session) -> None:
- self.db = db
-
- def get_settings(self) -> SystemSetting | None:
- stmt = select(SystemSetting).where(SystemSetting.id == SETTINGS_ROW_ID)
- return self.db.execute(stmt).scalars().first()
-
- def get_secrets(self) -> SystemSettingSecret | None:
- stmt = select(SystemSettingSecret).where(SystemSettingSecret.id == SETTINGS_ROW_ID)
- return self.db.execute(stmt).scalars().first()
-
- def get_model_settings(self) -> list[SystemModelSetting]:
- stmt = select(SystemModelSetting)
- return list(self.db.execute(stmt).scalars().all())
-
- def get_model_setting(self, slot: str) -> SystemModelSetting | None:
- stmt = select(SystemModelSetting).where(SystemModelSetting.slot == slot)
- return self.db.execute(stmt).scalars().first()
-
- def save_settings(self, settings: SystemSetting) -> SystemSetting:
- self.db.add(settings)
- self.db.commit()
- self.db.refresh(settings)
- return settings
-
- def save_secrets(self, secrets: SystemSettingSecret) -> SystemSettingSecret:
- self.db.add(secrets)
- self.db.commit()
- self.db.refresh(secrets)
- return secrets
+from __future__ import annotations
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from app.models.system_model_setting import SystemModelSetting
+from app.models.system_setting import SystemSetting
+from app.models.system_setting_secret import SystemSettingSecret
+
+SETTINGS_ROW_ID = "default"
+
+
+class SettingsRepository:
+ def __init__(self, db: Session) -> None:
+ self.db = db
+
+ def get_settings(self) -> SystemSetting | None:
+ stmt = select(SystemSetting).where(SystemSetting.id == SETTINGS_ROW_ID)
+ return self.db.execute(stmt).scalars().first()
+
+ def get_secrets(self) -> SystemSettingSecret | None:
+ stmt = select(SystemSettingSecret).where(SystemSettingSecret.id == SETTINGS_ROW_ID)
+ return self.db.execute(stmt).scalars().first()
+
+ def get_model_settings(self) -> list[SystemModelSetting]:
+ stmt = select(SystemModelSetting)
+ return list(self.db.execute(stmt).scalars().all())
+
+ def get_model_setting(self, slot: str) -> SystemModelSetting | None:
+ stmt = select(SystemModelSetting).where(SystemModelSetting.slot == slot)
+ return self.db.execute(stmt).scalars().first()
+
+ def save_settings(self, settings: SystemSetting) -> SystemSetting:
+ self.db.add(settings)
+ self.db.commit()
+ self.db.refresh(settings)
+ return settings
+
+ def save_secrets(self, secrets: SystemSettingSecret) -> SystemSettingSecret:
+ self.db.add(secrets)
+ self.db.commit()
+ self.db.refresh(secrets)
+ return secrets
diff --git a/server/src/app/schemas/__init__.py b/server/src/app/schemas/__init__.py
index 84e477f..12a7ac8 100644
--- a/server/src/app/schemas/__init__.py
+++ b/server/src/app/schemas/__init__.py
@@ -1 +1 @@
-__all__ = ["employee", "reimbursement"]
+__all__ = ["employee", "knowledge", "reimbursement"]
diff --git a/server/src/app/schemas/knowledge.py b/server/src/app/schemas/knowledge.py
new file mode 100644
index 0000000..e8b181d
--- /dev/null
+++ b/server/src/app/schemas/knowledge.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+from pydantic import BaseModel, Field
+
+
+class KnowledgeFolderRead(BaseModel):
+ name: str
+ count: int
+ icon: str = "mdi mdi-folder"
+
+
+class KnowledgePreviewStatRead(BaseModel):
+ label: str
+ value: str
+
+
+class KnowledgePreviewBlockRead(BaseModel):
+ heading: str
+ lines: list[str] = Field(default_factory=list)
+
+
+class KnowledgePreviewPageRead(BaseModel):
+ title: str
+ subtitle: str
+ stats: list[KnowledgePreviewStatRead] = Field(default_factory=list)
+ blocks: list[KnowledgePreviewBlockRead] = Field(default_factory=list)
+
+
+class KnowledgeDocumentRead(BaseModel):
+ id: str
+ name: str
+ folder: str
+ tag: str
+ time: str
+ version: str
+ state: str
+ stateTone: str
+ owner: str
+ icon: str
+ fileType: str
+ fileTypeLabel: str
+ summary: str
+ mimeType: str
+ extension: str
+ sizeBytes: int
+ canPreview: bool = False
+
+
+class KnowledgeDocumentDetailRead(KnowledgeDocumentRead):
+ previewKind: str
+ previewPages: list[KnowledgePreviewPageRead] = Field(default_factory=list)
+
+
+class KnowledgeLibraryRead(BaseModel):
+ folders: list[KnowledgeFolderRead] = Field(default_factory=list)
+ documents: list[KnowledgeDocumentRead] = Field(default_factory=list)
+
+
+class KnowledgeActionResponse(BaseModel):
+ ok: bool = True
+ detail: str
diff --git a/server/src/app/services/knowledge.py b/server/src/app/services/knowledge.py
new file mode 100644
index 0000000..454002e
--- /dev/null
+++ b/server/src/app/services/knowledge.py
@@ -0,0 +1,634 @@
+from __future__ import annotations
+
+import hashlib
+import json
+import mimetypes
+import re
+from datetime import UTC, datetime
+from pathlib import Path
+from typing import Any
+from uuid import uuid4
+from xml.etree import ElementTree
+from zipfile import BadZipFile, ZipFile
+
+from app.api.deps import CurrentUserContext
+from app.core.config import get_settings
+from app.core.logging import get_logger
+from app.schemas.knowledge import (
+ KnowledgeDocumentDetailRead,
+ KnowledgeDocumentRead,
+ KnowledgeFolderRead,
+ KnowledgeLibraryRead,
+ KnowledgePreviewBlockRead,
+ KnowledgePreviewPageRead,
+ KnowledgePreviewStatRead,
+)
+
+logger = get_logger("app.services.knowledge")
+
+FIXED_KNOWLEDGE_FOLDERS = [
+ "财务知识库",
+ "制度政策",
+ "报销制度",
+ "差旅规范",
+ "发票管理",
+ "税务合规",
+ "预算管理",
+ "财务共享",
+ "培训资料",
+ "常见问答",
+]
+
+ICON_BY_TYPE = {
+ "pdf": "mdi mdi-file-document-outline-pdf pdf",
+ "word": "mdi mdi-file-document-outline-word word",
+ "excel": "mdi mdi-file-document-outline-excel excel",
+ "ppt": "mdi mdi-file-powerpoint-box ppt",
+ "image": "mdi mdi-file-image-outline image",
+ "text": "mdi mdi-file-document-outline text",
+ "archive": "mdi mdi-folder-zip-outline archive",
+ "binary": "mdi mdi-file-outline",
+}
+
+TEXT_EXTENSIONS = {"txt", "md", "csv", "json", "xml", "yml", "yaml", "log"}
+WORD_EXTENSIONS = {"doc", "docx"}
+EXCEL_EXTENSIONS = {"xls", "xlsx", "csv"}
+PPT_EXTENSIONS = {"ppt", "pptx"}
+IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"}
+ARCHIVE_EXTENSIONS = {"zip", "rar", "7z"}
+STRUCTURED_PREVIEW_EXTENSIONS = {"docx", "xlsx", "pptx"} | TEXT_EXTENSIONS
+INLINE_PREVIEW_EXTENSIONS = {"pdf"} | IMAGE_EXTENSIONS
+
+
+def prepare_knowledge_library() -> None:
+ KnowledgeService().ensure_library_ready()
+
+
+class KnowledgeService:
+ def __init__(self, storage_root: Path | None = None) -> None:
+ settings = get_settings()
+ self.storage_root = Path(storage_root or settings.resolved_storage_root_dir)
+ self.library_root = self.storage_root / "knowledge"
+ self.index_path = self.library_root / ".index.json"
+
+ def ensure_library_ready(self) -> None:
+ self.library_root.mkdir(parents=True, exist_ok=True)
+ for folder_name in FIXED_KNOWLEDGE_FOLDERS:
+ (self.library_root / folder_name).mkdir(parents=True, exist_ok=True)
+
+ if not self.index_path.exists():
+ self._save_index({"version": 1, "documents": []})
+
+ index = self._load_index()
+ if self._reconcile_index(index):
+ self._save_index(index)
+
+ def list_library(self) -> KnowledgeLibraryRead:
+ documents = self._load_documents()
+ folders = [
+ KnowledgeFolderRead(
+ name=folder_name,
+ count=sum(1 for item in documents if item.folder == folder_name),
+ icon="mdi mdi-folder-open" if folder_name == "差旅规范" else "mdi mdi-folder",
+ )
+ for folder_name in FIXED_KNOWLEDGE_FOLDERS
+ ]
+ return KnowledgeLibraryRead(folders=folders, documents=documents)
+
+ def get_document_detail(self, document_id: str) -> KnowledgeDocumentDetailRead:
+ self.ensure_library_ready()
+ index = self._load_index()
+ entry = self._require_entry(index, document_id)
+ preview_kind, preview_pages = self._build_preview(entry)
+ document = self._serialize_document(entry)
+ return KnowledgeDocumentDetailRead(
+ **document.model_dump(),
+ previewKind=preview_kind,
+ previewPages=preview_pages,
+ )
+
+ def upload_document(
+ self,
+ folder: str,
+ filename: str,
+ content: bytes,
+ current_user: CurrentUserContext,
+ ) -> KnowledgeDocumentDetailRead:
+ self.ensure_library_ready()
+ normalized_folder = self._normalize_folder(folder)
+ normalized_name = self._normalize_filename(filename)
+
+ if not content:
+ raise ValueError("上传文件不能为空。")
+
+ index = self._load_index()
+ existing_entry = next(
+ (
+ item
+ for item in index["documents"]
+ if item["folder"] == normalized_folder
+ and item["original_name"].lower() == normalized_name.lower()
+ ),
+ None,
+ )
+
+ document_id = existing_entry["id"] if existing_entry else uuid4().hex
+ stored_name = f"{document_id}__{normalized_name}"
+ target_path = self.library_root / normalized_folder / stored_name
+
+ if existing_entry is not None and existing_entry["stored_name"] != stored_name:
+ old_path = self.library_root / existing_entry["folder"] / existing_entry["stored_name"]
+ if old_path.exists():
+ old_path.unlink()
+
+ target_path.write_bytes(content)
+
+ now = datetime.now(UTC).isoformat()
+ mime_type = mimetypes.guess_type(normalized_name)[0] or "application/octet-stream"
+ checksum = hashlib.sha256(content).hexdigest()
+ extension = self._extract_extension(normalized_name)
+
+ if existing_entry is None:
+ entry = {
+ "id": document_id,
+ "folder": normalized_folder,
+ "original_name": normalized_name,
+ "stored_name": stored_name,
+ "mime_type": mime_type,
+ "extension": extension,
+ "size_bytes": len(content),
+ "sha256": checksum,
+ "created_at": now,
+ "updated_at": now,
+ "uploaded_by": current_user.name,
+ "version_number": 1,
+ }
+ index["documents"].append(entry)
+ logger.info(
+ "Knowledge document uploaded id=%s folder=%s filename=%s by=%s",
+ document_id,
+ normalized_folder,
+ normalized_name,
+ current_user.name,
+ )
+ else:
+ existing_entry.update(
+ {
+ "stored_name": stored_name,
+ "mime_type": mime_type,
+ "extension": extension,
+ "size_bytes": len(content),
+ "sha256": checksum,
+ "updated_at": now,
+ "uploaded_by": current_user.name,
+ "version_number": int(existing_entry.get("version_number", 1)) + 1,
+ }
+ )
+ entry = existing_entry
+ logger.info(
+ "Knowledge document updated id=%s folder=%s filename=%s by=%s",
+ document_id,
+ normalized_folder,
+ normalized_name,
+ current_user.name,
+ )
+
+ self._save_index(index)
+ return self.get_document_detail(document_id)
+
+ def delete_document(self, document_id: str) -> None:
+ self.ensure_library_ready()
+ index = self._load_index()
+ entry = self._require_entry(index, document_id)
+ file_path = self._resolve_document_path(entry)
+ if file_path.exists():
+ file_path.unlink()
+
+ index["documents"] = [item for item in index["documents"] if item["id"] != document_id]
+ self._save_index(index)
+ logger.info("Knowledge document deleted id=%s filename=%s", document_id, entry["original_name"])
+
+ def get_document_content(self, document_id: str) -> tuple[Path, str, str]:
+ self.ensure_library_ready()
+ index = self._load_index()
+ entry = self._require_entry(index, document_id)
+ file_path = self._resolve_document_path(entry)
+
+ if not file_path.exists():
+ raise FileNotFoundError(entry["original_name"])
+
+ return file_path, entry["mime_type"], entry["original_name"]
+
+ def _load_documents(self) -> list[KnowledgeDocumentRead]:
+ self.ensure_library_ready()
+ index = self._load_index()
+ self._reconcile_index(index)
+ self._save_index(index)
+
+ documents = [self._serialize_document(entry) for entry in index["documents"]]
+ return sorted(documents, key=lambda item: item.time, reverse=True)
+
+ def _serialize_document(self, entry: dict[str, Any]) -> KnowledgeDocumentRead:
+ extension = entry.get("extension") or self._extract_extension(entry["original_name"])
+ file_type = self._resolve_file_type(extension)
+ size_bytes = int(entry.get("size_bytes") or 0)
+ updated_at = self._format_time(entry.get("updated_at") or entry.get("created_at"))
+
+ return KnowledgeDocumentRead(
+ id=entry["id"],
+ name=entry["original_name"],
+ folder=entry["folder"],
+ tag=f"{entry['folder']} / {extension.upper() or 'FILE'}",
+ time=updated_at,
+ version=f"v{int(entry.get('version_number', 1))}.0",
+ state="已发布",
+ stateTone="success",
+ owner=entry.get("uploaded_by") or "系统导入",
+ icon=ICON_BY_TYPE.get(file_type, ICON_BY_TYPE["binary"]),
+ fileType=file_type,
+ fileTypeLabel=self._resolve_file_type_label(file_type),
+ summary=f"{entry['folder']} · {extension.upper() or 'FILE'} · {self._format_size(size_bytes)}",
+ mimeType=entry.get("mime_type") or "application/octet-stream",
+ extension=extension,
+ sizeBytes=size_bytes,
+ canPreview=self._can_preview(extension),
+ )
+
+ def _build_preview(
+ self, entry: dict[str, Any]
+ ) -> tuple[str, list[KnowledgePreviewPageRead]]:
+ extension = self._extract_extension(entry["original_name"])
+ file_path = self._resolve_document_path(entry)
+
+ if extension == "pdf":
+ return "pdf", []
+
+ if extension in IMAGE_EXTENSIONS:
+ return "image", []
+
+ if extension in TEXT_EXTENSIONS:
+ text = self._read_text_preview(file_path)
+ return "text", [self._build_text_preview_page(entry, text)]
+
+ if extension == "docx":
+ text = self._extract_docx_text(file_path)
+ return "text", [self._build_text_preview_page(entry, text)]
+
+ if extension == "xlsx":
+ return "table", [self._build_xlsx_preview_page(entry, file_path)]
+
+ if extension == "pptx":
+ return "slides", self._build_pptx_preview_pages(entry, file_path)
+
+ return (
+ "unsupported",
+ [
+ KnowledgePreviewPageRead(
+ title=entry["original_name"],
+ subtitle="当前格式暂不支持在线解析预览。",
+ stats=[
+ KnowledgePreviewStatRead(label="文件格式", value=extension.upper() or "FILE"),
+ KnowledgePreviewStatRead(label="文件大小", value=self._format_size(entry["size_bytes"])),
+ KnowledgePreviewStatRead(label="建议操作", value="下载后查看"),
+ ],
+ blocks=[
+ KnowledgePreviewBlockRead(
+ heading="预览说明",
+ lines=[
+ "当前系统已支持该文件的上传、下载和权限控制。",
+ "如需在线预览,可后续接入专门的文档转换服务。",
+ ],
+ )
+ ],
+ )
+ ],
+ )
+
+ def _build_text_preview_page(
+ self, entry: dict[str, Any], text: str
+ ) -> KnowledgePreviewPageRead:
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
+ if not lines:
+ lines = ["文件内容为空,或当前文档未提取到可展示文本。"]
+
+ groups = [lines[index : index + 8] for index in range(0, min(len(lines), 24), 8)]
+ blocks = [
+ KnowledgePreviewBlockRead(heading=f"内容片段 {index + 1}", lines=group)
+ for index, group in enumerate(groups)
+ ]
+
+ return KnowledgePreviewPageRead(
+ title=entry["original_name"],
+ subtitle="文本提取预览",
+ stats=[
+ KnowledgePreviewStatRead(label="文件格式", value=entry["extension"].upper() or "TEXT"),
+ KnowledgePreviewStatRead(label="可见行数", value=str(len(lines))),
+ KnowledgePreviewStatRead(label="文件大小", value=self._format_size(entry["size_bytes"])),
+ ],
+ blocks=blocks,
+ )
+
+ def _build_xlsx_preview_page(
+ self, entry: dict[str, Any], file_path: Path
+ ) -> KnowledgePreviewPageRead:
+ rows, sheet_count = self._extract_xlsx_rows(file_path)
+ if not rows:
+ rows = [["未提取到表格内容。"]]
+
+ blocks = [
+ KnowledgePreviewBlockRead(
+ heading=f"第 {index + 1} 行",
+ lines=[" | ".join(cell for cell in row if cell) or "(空行)"],
+ )
+ for index, row in enumerate(rows[:12])
+ ]
+
+ return KnowledgePreviewPageRead(
+ title=entry["original_name"],
+ subtitle="表格内容预览",
+ stats=[
+ KnowledgePreviewStatRead(label="工作表数量", value=str(sheet_count)),
+ KnowledgePreviewStatRead(label="预览行数", value=str(min(len(rows), 12))),
+ KnowledgePreviewStatRead(label="文件大小", value=self._format_size(entry["size_bytes"])),
+ ],
+ blocks=blocks,
+ )
+
+ def _build_pptx_preview_pages(
+ self, entry: dict[str, Any], file_path: Path
+ ) -> list[KnowledgePreviewPageRead]:
+ slides = self._extract_pptx_slides(file_path)
+ if not slides:
+ slides = [["未提取到幻灯片文本。"]]
+
+ pages: list[KnowledgePreviewPageRead] = []
+ for index, slide_lines in enumerate(slides[:8]):
+ pages.append(
+ KnowledgePreviewPageRead(
+ title=entry["original_name"],
+ subtitle=f"幻灯片 {index + 1}",
+ stats=[
+ KnowledgePreviewStatRead(label="页码", value=str(index + 1)),
+ KnowledgePreviewStatRead(label="文本条数", value=str(len(slide_lines))),
+ KnowledgePreviewStatRead(label="文件格式", value="PPTX"),
+ ],
+ blocks=[
+ KnowledgePreviewBlockRead(
+ heading="幻灯片内容",
+ lines=slide_lines or ["该页未提取到文本内容。"],
+ )
+ ],
+ )
+ )
+
+ return pages
+
+ def _load_index(self) -> dict[str, Any]:
+ try:
+ payload = json.loads(self.index_path.read_text(encoding="utf-8"))
+ except (FileNotFoundError, json.JSONDecodeError):
+ payload = {"version": 1, "documents": []}
+ payload.setdefault("documents", [])
+ return payload
+
+ def _save_index(self, index: dict[str, Any]) -> None:
+ self.index_path.write_text(
+ json.dumps(index, ensure_ascii=False, indent=2),
+ encoding="utf-8",
+ )
+
+ def _reconcile_index(self, index: dict[str, Any]) -> bool:
+ changed = False
+ documents = index.setdefault("documents", [])
+ known_by_stored = {
+ (item["folder"], item["stored_name"]): item
+ for item in documents
+ if item.get("folder") and item.get("stored_name")
+ }
+
+ existing_items: list[dict[str, Any]] = []
+ for item in documents:
+ file_path = self._resolve_document_path(item)
+ if file_path.exists():
+ item["size_bytes"] = file_path.stat().st_size
+ item["extension"] = self._extract_extension(item["original_name"])
+ item["mime_type"] = item.get("mime_type") or (
+ mimetypes.guess_type(item["original_name"])[0] or "application/octet-stream"
+ )
+ existing_items.append(item)
+ else:
+ changed = True
+
+ for folder_name in FIXED_KNOWLEDGE_FOLDERS:
+ folder_path = self.library_root / folder_name
+ for file_path in folder_path.iterdir():
+ if not file_path.is_file() or file_path.name.startswith("."):
+ continue
+
+ key = (folder_name, file_path.name)
+ if key in known_by_stored:
+ continue
+
+ document_id, original_name = self._parse_stored_name(file_path.name)
+ stat = file_path.stat()
+ existing_items.append(
+ {
+ "id": document_id,
+ "folder": folder_name,
+ "original_name": original_name,
+ "stored_name": file_path.name,
+ "mime_type": mimetypes.guess_type(original_name)[0]
+ or "application/octet-stream",
+ "extension": self._extract_extension(original_name),
+ "size_bytes": stat.st_size,
+ "sha256": "",
+ "created_at": datetime.fromtimestamp(stat.st_ctime, tz=UTC).isoformat(),
+ "updated_at": datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat(),
+ "uploaded_by": "系统导入",
+ "version_number": 1,
+ }
+ )
+ changed = True
+
+ if changed or len(existing_items) != len(documents):
+ index["documents"] = existing_items
+ return True
+ return False
+
+ def _require_entry(self, index: dict[str, Any], document_id: str) -> dict[str, Any]:
+ for entry in index["documents"]:
+ if entry["id"] == document_id:
+ return entry
+ raise FileNotFoundError(document_id)
+
+ def _resolve_document_path(self, entry: dict[str, Any]) -> Path:
+ return self.library_root / entry["folder"] / entry["stored_name"]
+
+ @staticmethod
+ def _normalize_filename(filename: str) -> str:
+ normalized = Path(str(filename or "").strip()).name.strip()
+ normalized = normalized.replace("/", "_").replace("\\", "_")
+ if not normalized:
+ raise ValueError("文件名不能为空。")
+ return normalized
+
+ @staticmethod
+ def _normalize_folder(folder: str) -> str:
+ normalized = str(folder or "").strip()
+ if normalized not in FIXED_KNOWLEDGE_FOLDERS:
+ raise ValueError("只能上传到预设知识库文件夹。")
+ return normalized
+
+ @staticmethod
+ def _extract_extension(filename: str) -> str:
+ suffix = Path(filename).suffix.lower().lstrip(".")
+ return suffix
+
+ @staticmethod
+ def _parse_stored_name(stored_name: str) -> tuple[str, str]:
+ if "__" not in stored_name:
+ return uuid4().hex, stored_name
+ document_id, original_name = stored_name.split("__", 1)
+ return document_id or uuid4().hex, original_name or stored_name
+
+ @staticmethod
+ def _format_time(value: str | None) -> str:
+ if not value:
+ return ""
+ try:
+ parsed = datetime.fromisoformat(value)
+ except ValueError:
+ return value
+ return parsed.astimezone(UTC).strftime("%Y-%m-%d %H:%M")
+
+ @staticmethod
+ def _format_size(size_bytes: int) -> str:
+ if size_bytes < 1024:
+ return f"{size_bytes} B"
+ if size_bytes < 1024 * 1024:
+ return f"{size_bytes / 1024:.1f} KB"
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
+
+ @staticmethod
+ def _resolve_file_type(extension: str) -> str:
+ if extension == "pdf":
+ return "pdf"
+ if extension in WORD_EXTENSIONS:
+ return "word"
+ if extension in EXCEL_EXTENSIONS:
+ return "excel"
+ if extension in PPT_EXTENSIONS:
+ return "ppt"
+ if extension in IMAGE_EXTENSIONS:
+ return "image"
+ if extension in TEXT_EXTENSIONS:
+ return "text"
+ if extension in ARCHIVE_EXTENSIONS:
+ return "archive"
+ return "binary"
+
+ @staticmethod
+ def _resolve_file_type_label(file_type: str) -> str:
+ mapping = {
+ "pdf": "PDF 预览",
+ "word": "Word 预览",
+ "excel": "Excel 预览",
+ "ppt": "PPT 预览",
+ "image": "图片预览",
+ "text": "文本预览",
+ "archive": "压缩包",
+ "binary": "文件预览",
+ }
+ return mapping.get(file_type, "文件预览")
+
+ @staticmethod
+ def _can_preview(extension: str) -> bool:
+ return extension in INLINE_PREVIEW_EXTENSIONS or extension in STRUCTURED_PREVIEW_EXTENSIONS
+
+ @staticmethod
+ def _read_text_preview(file_path: Path) -> str:
+ encodings = ("utf-8", "utf-8-sig", "gbk")
+ for encoding in encodings:
+ try:
+ return file_path.read_text(encoding=encoding)
+ except UnicodeDecodeError:
+ continue
+ return "当前文本文件编码暂不支持在线解析。"
+
+ @staticmethod
+ def _extract_docx_text(file_path: Path) -> str:
+ try:
+ with ZipFile(file_path) as archive:
+ xml_content = archive.read("word/document.xml")
+ except (BadZipFile, KeyError):
+ return "当前 Word 文件解析失败。"
+
+ root = ElementTree.fromstring(xml_content)
+ texts = [node.text.strip() for node in root.iter() if node.tag.endswith("}t") and node.text]
+ return "\n".join(texts)
+
+ @staticmethod
+ def _extract_xlsx_rows(file_path: Path) -> tuple[list[list[str]], int]:
+ try:
+ with ZipFile(file_path) as archive:
+ shared_strings: list[str] = []
+ if "xl/sharedStrings.xml" in archive.namelist():
+ shared_root = ElementTree.fromstring(archive.read("xl/sharedStrings.xml"))
+ shared_strings = [
+ "".join(node.itertext()).strip()
+ for node in shared_root.iter()
+ if node.tag.endswith("}si")
+ ]
+
+ sheet_names = sorted(
+ name
+ for name in archive.namelist()
+ if re.fullmatch(r"xl/worksheets/sheet\d+\.xml", name)
+ )
+ if not sheet_names:
+ return [], 0
+
+ first_sheet = ElementTree.fromstring(archive.read(sheet_names[0]))
+ rows: list[list[str]] = []
+ for row in first_sheet.iter():
+ if not row.tag.endswith("}row"):
+ continue
+ row_values: list[str] = []
+ for cell in row:
+ if not cell.tag.endswith("}c"):
+ continue
+ cell_type = cell.attrib.get("t")
+ value_node = next((item for item in cell if item.tag.endswith("}v")), None)
+ if value_node is None or value_node.text is None:
+ row_values.append("")
+ continue
+ raw_value = value_node.text.strip()
+ if cell_type == "s" and raw_value.isdigit():
+ index = int(raw_value)
+ row_values.append(shared_strings[index] if index < len(shared_strings) else raw_value)
+ else:
+ row_values.append(raw_value)
+ if row_values:
+ rows.append(row_values)
+
+ return rows, len(sheet_names)
+ except (BadZipFile, ElementTree.ParseError, KeyError, ValueError):
+ return [], 0
+
+ @staticmethod
+ def _extract_pptx_slides(file_path: Path) -> list[list[str]]:
+ try:
+ with ZipFile(file_path) as archive:
+ slide_names = sorted(
+ name
+ for name in archive.namelist()
+ if re.fullmatch(r"ppt/slides/slide\d+\.xml", name)
+ )
+ slides: list[list[str]] = []
+ for slide_name in slide_names:
+ root = ElementTree.fromstring(archive.read(slide_name))
+ texts = [node.text.strip() for node in root.iter() if node.tag.endswith("}t") and node.text]
+ slides.append(texts)
+ return slides
+ except (BadZipFile, ElementTree.ParseError, KeyError):
+ return []
diff --git a/server/src/app/services/settings.py b/server/src/app/services/settings.py
index 4028016..d000c9e 100644
--- a/server/src/app/services/settings.py
+++ b/server/src/app/services/settings.py
@@ -1,500 +1,500 @@
-from __future__ import annotations
-
-from dataclasses import dataclass
-from datetime import datetime
-
-from sqlalchemy.orm import Session
-
-from app.core.admin_secret import legacy_admin_secret_to_password_hash, read_admin_secret, verify_admin_secret
-from app.core.config import get_settings
-from app.core.secret_box import decrypt_secret, encrypt_secret
-from app.core.security import hash_password, verify_password
-from app.db.base import Base
-from app.models.system_model_setting import SystemModelSetting
-from app.models.system_setting import SystemSetting
-from app.models.system_setting_secret import SystemSettingSecret
-from app.repositories.settings import SETTINGS_ROW_ID, SettingsRepository
-from app.schemas.settings import SettingsRead, SettingsWrite
-
-
-@dataclass(frozen=True, slots=True)
-class ModelSlotConfig:
- provider_attr: str
- model_attr: str
- endpoint_attr: str
- legacy_secret_attr: str
- default_provider: str
- default_model: str
- default_endpoint: str
- capability: str
- priority: int
-
-
-MODEL_SLOT_CONFIGS = {
- "main": ModelSlotConfig(
- provider_attr="main_provider",
- model_attr="main_model",
- endpoint_attr="main_endpoint",
- legacy_secret_attr="main_api_key_encrypted",
- default_provider="Codex",
- default_model="codex-mini-latest",
- default_endpoint="https://api.openai.com/v1",
- capability="chat",
- priority=10,
- ),
- "backup": ModelSlotConfig(
- provider_attr="backup_provider",
- model_attr="backup_model",
- endpoint_attr="backup_endpoint",
- legacy_secret_attr="backup_api_key_encrypted",
- default_provider="GLM",
- default_model="glm-5.1",
- default_endpoint="https://open.bigmodel.cn/api/paas/v4/",
- capability="chat",
- priority=20,
- ),
- "vlm": ModelSlotConfig(
- provider_attr="vlm_provider",
- model_attr="vlm_model",
- endpoint_attr="vlm_endpoint",
- legacy_secret_attr="vlm_api_key_encrypted",
- default_provider="Gemini",
- default_model="gemini-2.5-flash",
- default_endpoint="https://generativelanguage.googleapis.com/v1beta/openai/",
- capability="chat",
- priority=30,
- ),
- "embedding": ModelSlotConfig(
- provider_attr="embedding_provider",
- model_attr="embedding_model",
- endpoint_attr="embedding_endpoint",
- legacy_secret_attr="embedding_api_key_encrypted",
- default_provider="GLM",
- default_model="Embedding-3",
- default_endpoint="https://open.bigmodel.cn/api/paas/v4/",
- capability="embedding",
- priority=40,
- ),
-}
-
-
-@dataclass(slots=True)
-class AdminCredentialRecord:
- account: str
- email: str
- password_hash: str
-
-
-class SettingsService:
- def __init__(self, db: Session) -> None:
- self.db = db
- self.repository = SettingsRepository(db)
- self.runtime_settings = get_settings()
-
- def ensure_settings_ready(self) -> tuple[SystemSetting, SystemSettingSecret]:
- Base.metadata.create_all(bind=self.db.get_bind())
-
- settings_row = self.repository.get_settings()
- secrets_row = self.repository.get_secrets()
- should_commit = False
- legacy_admin = read_admin_secret()
-
- if settings_row is None:
- settings_row = self._build_default_settings()
- self.db.add(settings_row)
- should_commit = True
-
- if secrets_row is None:
- secrets_row = SystemSettingSecret(id=SETTINGS_ROW_ID)
- self.db.add(secrets_row)
- should_commit = True
-
- if legacy_admin is not None and not secrets_row.admin_password_hash:
- secrets_row.admin_password_hash = legacy_admin_secret_to_password_hash(legacy_admin)
- admin_username = str(legacy_admin.get("username", "")).strip()
- if admin_username and str(settings_row.admin_account or "").strip() in {"", "superadmin"}:
- settings_row.admin_account = admin_username
- should_commit = True
-
- if should_commit:
- self.db.commit()
- self.db.refresh(settings_row)
- self.db.refresh(secrets_row)
-
- return settings_row, secrets_row
-
- def ensure_model_settings_ready(
- self,
- settings_row: SystemSetting,
- secrets_row: SystemSettingSecret,
- ) -> dict[str, SystemModelSetting]:
- model_rows = {row.slot: row for row in self.repository.get_model_settings()}
- should_commit = False
-
- for slot, config in MODEL_SLOT_CONFIGS.items():
- if slot in model_rows:
- continue
-
- model_row = SystemModelSetting(
- slot=slot,
- provider=str(getattr(settings_row, config.provider_attr, "") or config.default_provider),
- model_name=str(getattr(settings_row, config.model_attr, "") or config.default_model),
- endpoint=str(getattr(settings_row, config.endpoint_attr, "") or config.default_endpoint),
- capability=config.capability,
- priority=config.priority,
- enabled=True,
- api_key_encrypted=str(getattr(secrets_row, config.legacy_secret_attr, "") or ""),
- )
- self.db.add(model_row)
- model_rows[slot] = model_row
- should_commit = True
-
- if should_commit:
- self.db.commit()
- for model_row in model_rows.values():
- self.db.refresh(model_row)
-
- return model_rows
-
- def get_settings_snapshot(self) -> SettingsRead:
- settings_row, secrets_row = self.ensure_settings_ready()
- model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
- return self._serialize(settings_row, secrets_row, model_rows)
-
- def save_settings_snapshot(self, payload: SettingsWrite) -> SettingsRead:
- settings_row, secrets_row = self.ensure_settings_ready()
- model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
-
- if payload.adminForm.newPassword:
- if len(payload.adminForm.newPassword) < 5:
- raise ValueError("管理员密码至少需要 5 位。")
- if payload.adminForm.newPassword != payload.adminForm.confirmPassword:
- raise ValueError("两次输入的管理员密码不一致。")
- secrets_row.admin_password_hash = hash_password(payload.adminForm.newPassword)
-
- settings_row.company_name = payload.companyForm.companyName
- settings_row.display_name = payload.companyForm.displayName
- settings_row.company_code = payload.companyForm.companyCode
- settings_row.record_number = payload.companyForm.recordNumber
- settings_row.copyright_text = payload.companyForm.copyright
-
- settings_row.admin_account = payload.adminForm.adminAccount
- settings_row.admin_email = payload.adminForm.adminEmail
- settings_row.session_timeout = payload.adminForm.sessionTimeout
- settings_row.notice_email = payload.adminForm.noticeEmail
- settings_row.mfa_enabled = payload.adminForm.mfaEnabled
- settings_row.strong_password = payload.adminForm.strongPassword
- settings_row.login_alert_enabled = payload.adminForm.loginAlertEnabled
-
- self._apply_model_setting(
- model_rows["main"],
- payload.llmForm.mainProvider,
- payload.llmForm.mainModel,
- payload.llmForm.mainEndpoint,
- payload.llmForm.mainApiKey,
- )
- self._apply_model_setting(
- model_rows["backup"],
- payload.llmForm.backupProvider,
- payload.llmForm.backupModel,
- payload.llmForm.backupEndpoint,
- payload.llmForm.backupApiKey,
- )
- self._apply_model_setting(
- model_rows["vlm"],
- payload.llmForm.vlmProvider,
- payload.llmForm.vlmModel,
- payload.llmForm.vlmEndpoint,
- payload.llmForm.vlmApiKey,
- )
- self._apply_model_setting(
- model_rows["embedding"],
- payload.llmForm.embeddingProvider,
- payload.llmForm.embeddingModel,
- payload.llmForm.embeddingEndpoint,
- payload.llmForm.embeddingApiKey,
- )
-
- settings_row.main_provider = model_rows["main"].provider
- settings_row.main_model = model_rows["main"].model_name
- settings_row.main_endpoint = model_rows["main"].endpoint
- settings_row.backup_provider = model_rows["backup"].provider
- settings_row.backup_model = model_rows["backup"].model_name
- settings_row.backup_endpoint = model_rows["backup"].endpoint
- settings_row.vlm_provider = model_rows["vlm"].provider
- settings_row.vlm_model = model_rows["vlm"].model_name
- settings_row.vlm_endpoint = model_rows["vlm"].endpoint
- settings_row.embedding_provider = model_rows["embedding"].provider
- settings_row.embedding_model = model_rows["embedding"].model_name
- settings_row.embedding_endpoint = model_rows["embedding"].endpoint
-
- settings_row.log_level = payload.logForm.level
- settings_row.retention_days = payload.logForm.retentionDays
- settings_row.archive_cycle = payload.logForm.archiveCycle
- settings_row.log_path = payload.logForm.logPath
- settings_row.alert_email = payload.logForm.alertEmail
- settings_row.operation_audit = payload.logForm.operationAudit
- settings_row.login_audit = payload.logForm.loginAudit
- settings_row.mask_sensitive = payload.logForm.maskSensitive
-
- settings_row.smtp_host = payload.mailForm.smtpHost
- settings_row.smtp_port = payload.mailForm.port
- settings_row.smtp_encryption = payload.mailForm.encryption
- settings_row.sender_name = payload.mailForm.senderName
- settings_row.sender_address = payload.mailForm.senderAddress
- settings_row.smtp_username = payload.mailForm.username
- settings_row.alert_enabled = payload.mailForm.alertEnabled
- settings_row.digest_enabled = payload.mailForm.digestEnabled
- settings_row.digest_time = payload.mailForm.digestTime
- settings_row.default_receiver = payload.mailForm.defaultReceiver
-
- self._replace_secret_if_present(secrets_row, "smtp_password_encrypted", payload.mailForm.password)
-
- self.db.add(settings_row)
- self.db.add(secrets_row)
- for model_row in model_rows.values():
- self.db.add(model_row)
- self.db.commit()
- self.db.refresh(settings_row)
- self.db.refresh(secrets_row)
- for model_row in model_rows.values():
- self.db.refresh(model_row)
-
- return self._serialize(settings_row, secrets_row, model_rows)
-
- def load_saved_model_api_key(self, slot: str | None) -> str:
- if not slot or slot not in MODEL_SLOT_CONFIGS:
- return ""
-
- settings_row, secrets_row = self.ensure_settings_ready()
- model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
- encrypted_value = model_rows[slot].api_key_encrypted
- if not encrypted_value:
- return ""
-
- return decrypt_secret(encrypted_value)
-
- def get_admin_credentials(self) -> AdminCredentialRecord | None:
- settings_row, secrets_row = self.ensure_settings_ready()
-
- if secrets_row.admin_password_hash:
- return AdminCredentialRecord(
- account=settings_row.admin_account,
- email=settings_row.admin_email,
- password_hash=secrets_row.admin_password_hash,
- )
-
- legacy_record = read_admin_secret()
- if legacy_record is None:
- return None
-
- username = str(legacy_record.get("username", "")).strip()
- email = str(settings_row.admin_email or self.runtime_settings.admin_email or "").strip()
- password_hash = ""
-
- # Legacy admin.json uses scrypt fields rather than the app password format.
- # The auth flow handles this file separately when no DB-backed admin password exists.
- if username or email:
- return AdminCredentialRecord(account=username, email=email, password_hash=password_hash)
-
- return None
-
- def verify_admin_login(self, identifier: str, password: str) -> AdminCredentialRecord | None:
- settings_row, secrets_row = self.ensure_settings_ready()
- normalized_identifier = identifier.casefold()
-
- if secrets_row.admin_password_hash:
- allowed_identifiers = {
- value.casefold()
- for value in [settings_row.admin_account, settings_row.admin_email]
- if value
- }
-
- if normalized_identifier not in allowed_identifiers:
- return None
-
- if not verify_password(password, secrets_row.admin_password_hash):
- return None
-
- return AdminCredentialRecord(
- account=settings_row.admin_account,
- email=settings_row.admin_email,
- password_hash=secrets_row.admin_password_hash,
- )
-
- legacy_record = read_admin_secret()
- if legacy_record is None:
- return None
-
- admin_username = str(legacy_record.get("username", "")).strip()
- admin_email = str(settings_row.admin_email or self.runtime_settings.admin_email or "").strip()
- allowed_identifiers = {
- value.casefold()
- for value in [admin_username, admin_email]
- if value
- }
-
- if normalized_identifier not in allowed_identifiers:
- return None
-
- if not verify_admin_secret(password, legacy_record):
- return None
-
- return AdminCredentialRecord(account=admin_username, email=admin_email, password_hash="")
-
- def _build_default_settings(self) -> SystemSetting:
- current_year = datetime.now().year
- company_name = str(self.runtime_settings.company_name or "X-Financial").strip() or "X-Financial"
- company_code = str(self.runtime_settings.company_code or "XF-001").strip() or "XF-001"
- admin_email = str(self.runtime_settings.admin_email or "").strip()
- legacy_admin = read_admin_secret() or {}
- admin_account = str(legacy_admin.get("username", "")).strip() or "superadmin"
-
- return SystemSetting(
- id=SETTINGS_ROW_ID,
- company_name=company_name,
- display_name=company_name,
- company_code=company_code,
- record_number="",
- copyright_text=f"Copyright © 2024-{current_year} {company_name}. All Rights Reserved.",
- admin_account=admin_account,
- admin_email=admin_email,
- session_timeout=30,
- notice_email=admin_email,
- mfa_enabled=True,
- strong_password=True,
- login_alert_enabled=True,
- main_provider="Codex",
- main_model="codex-mini-latest",
- main_endpoint="https://api.openai.com/v1",
- backup_provider="GLM",
- backup_model="glm-5.1",
- backup_endpoint="https://open.bigmodel.cn/api/paas/v4/",
- vlm_provider="Gemini",
- vlm_model="gemini-2.5-flash",
- vlm_endpoint="https://generativelanguage.googleapis.com/v1beta/openai/",
- embedding_provider="GLM",
- embedding_model="Embedding-3",
- embedding_endpoint="https://open.bigmodel.cn/api/paas/v4/",
- log_level="INFO",
- retention_days=180,
- archive_cycle="weekly",
- log_path="server/logs/app.log",
- alert_email=admin_email,
- operation_audit=True,
- login_audit=True,
- mask_sensitive=True,
- smtp_host="smtp.exmail.qq.com",
- smtp_port=465,
- smtp_encryption="SSL/TLS",
- sender_name=company_name,
- sender_address=admin_email,
- smtp_username=admin_email,
- alert_enabled=True,
- digest_enabled=False,
- digest_time="09:00",
- default_receiver=admin_email,
- )
-
- @staticmethod
- def _replace_secret_if_present(secret_row: SystemSettingSecret, field_name: str, value: str) -> None:
- normalized = value.strip()
- if not normalized:
- return
-
- setattr(secret_row, field_name, encrypt_secret(normalized))
-
- @staticmethod
- def _apply_model_setting(
- model_row: SystemModelSetting,
- provider: str,
- model_name: str,
- endpoint: str,
- api_key: str,
- ) -> None:
- model_row.provider = provider
- model_row.model_name = model_name
- model_row.endpoint = endpoint
-
- normalized_api_key = api_key.strip()
- if normalized_api_key:
- model_row.api_key_encrypted = encrypt_secret(normalized_api_key)
-
- @staticmethod
- def _serialize(
- settings_row: SystemSetting,
- secrets_row: SystemSettingSecret,
- model_rows: dict[str, SystemModelSetting],
- ) -> SettingsRead:
- main_model = model_rows["main"]
- backup_model = model_rows["backup"]
- vlm_model = model_rows["vlm"]
- embedding_model = model_rows["embedding"]
-
- return SettingsRead(
- companyForm={
- "companyName": settings_row.company_name,
- "displayName": settings_row.display_name,
- "companyCode": settings_row.company_code,
- "recordNumber": settings_row.record_number,
- "copyright": settings_row.copyright_text,
- },
- adminForm={
- "adminAccount": settings_row.admin_account,
- "adminEmail": settings_row.admin_email,
- "newPassword": "",
- "confirmPassword": "",
- "sessionTimeout": settings_row.session_timeout,
- "noticeEmail": settings_row.notice_email,
- "mfaEnabled": settings_row.mfa_enabled,
- "strongPassword": settings_row.strong_password,
- "loginAlertEnabled": settings_row.login_alert_enabled,
- "adminPasswordConfigured": bool(secrets_row.admin_password_hash),
- },
- llmForm={
- "mainProvider": main_model.provider,
- "mainModel": main_model.model_name,
- "mainEndpoint": main_model.endpoint,
- "mainApiKey": "",
- "mainApiKeyConfigured": bool(main_model.api_key_encrypted),
- "backupProvider": backup_model.provider,
- "backupModel": backup_model.model_name,
- "backupEndpoint": backup_model.endpoint,
- "backupApiKey": "",
- "backupApiKeyConfigured": bool(backup_model.api_key_encrypted),
- "vlmProvider": vlm_model.provider,
- "vlmModel": vlm_model.model_name,
- "vlmEndpoint": vlm_model.endpoint,
- "vlmApiKey": "",
- "vlmApiKeyConfigured": bool(vlm_model.api_key_encrypted),
- "embeddingProvider": embedding_model.provider,
- "embeddingModel": embedding_model.model_name,
- "embeddingEndpoint": embedding_model.endpoint,
- "embeddingApiKey": "",
- "embeddingApiKeyConfigured": bool(embedding_model.api_key_encrypted),
- },
- logForm={
- "level": settings_row.log_level,
- "retentionDays": settings_row.retention_days,
- "archiveCycle": settings_row.archive_cycle,
- "logPath": settings_row.log_path,
- "alertEmail": settings_row.alert_email,
- "operationAudit": settings_row.operation_audit,
- "loginAudit": settings_row.login_audit,
- "maskSensitive": settings_row.mask_sensitive,
- },
- mailForm={
- "smtpHost": settings_row.smtp_host,
- "port": settings_row.smtp_port,
- "encryption": settings_row.smtp_encryption,
- "senderName": settings_row.sender_name,
- "senderAddress": settings_row.sender_address,
- "username": settings_row.smtp_username,
- "password": "",
- "passwordConfigured": bool(secrets_row.smtp_password_encrypted),
- "alertEnabled": settings_row.alert_enabled,
- "digestEnabled": settings_row.digest_enabled,
- "digestTime": settings_row.digest_time,
- "defaultReceiver": settings_row.default_receiver,
- },
- )
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import datetime
+
+from sqlalchemy.orm import Session
+
+from app.core.admin_secret import legacy_admin_secret_to_password_hash, read_admin_secret, verify_admin_secret
+from app.core.config import get_settings
+from app.core.secret_box import decrypt_secret, encrypt_secret
+from app.core.security import hash_password, verify_password
+from app.db.base import Base
+from app.models.system_model_setting import SystemModelSetting
+from app.models.system_setting import SystemSetting
+from app.models.system_setting_secret import SystemSettingSecret
+from app.repositories.settings import SETTINGS_ROW_ID, SettingsRepository
+from app.schemas.settings import SettingsRead, SettingsWrite
+
+
+@dataclass(frozen=True, slots=True)
+class ModelSlotConfig:
+ provider_attr: str
+ model_attr: str
+ endpoint_attr: str
+ legacy_secret_attr: str
+ default_provider: str
+ default_model: str
+ default_endpoint: str
+ capability: str
+ priority: int
+
+
+MODEL_SLOT_CONFIGS = {
+ "main": ModelSlotConfig(
+ provider_attr="main_provider",
+ model_attr="main_model",
+ endpoint_attr="main_endpoint",
+ legacy_secret_attr="main_api_key_encrypted",
+ default_provider="Codex",
+ default_model="codex-mini-latest",
+ default_endpoint="https://api.openai.com/v1",
+ capability="chat",
+ priority=10,
+ ),
+ "backup": ModelSlotConfig(
+ provider_attr="backup_provider",
+ model_attr="backup_model",
+ endpoint_attr="backup_endpoint",
+ legacy_secret_attr="backup_api_key_encrypted",
+ default_provider="GLM",
+ default_model="glm-5.1",
+ default_endpoint="https://open.bigmodel.cn/api/paas/v4/",
+ capability="chat",
+ priority=20,
+ ),
+ "vlm": ModelSlotConfig(
+ provider_attr="vlm_provider",
+ model_attr="vlm_model",
+ endpoint_attr="vlm_endpoint",
+ legacy_secret_attr="vlm_api_key_encrypted",
+ default_provider="Gemini",
+ default_model="gemini-2.5-flash",
+ default_endpoint="https://generativelanguage.googleapis.com/v1beta/openai/",
+ capability="chat",
+ priority=30,
+ ),
+ "embedding": ModelSlotConfig(
+ provider_attr="embedding_provider",
+ model_attr="embedding_model",
+ endpoint_attr="embedding_endpoint",
+ legacy_secret_attr="embedding_api_key_encrypted",
+ default_provider="GLM",
+ default_model="Embedding-3",
+ default_endpoint="https://open.bigmodel.cn/api/paas/v4/",
+ capability="embedding",
+ priority=40,
+ ),
+}
+
+
+@dataclass(slots=True)
+class AdminCredentialRecord:
+ account: str
+ email: str
+ password_hash: str
+
+
+class SettingsService:
+ def __init__(self, db: Session) -> None:
+ self.db = db
+ self.repository = SettingsRepository(db)
+ self.runtime_settings = get_settings()
+
+ def ensure_settings_ready(self) -> tuple[SystemSetting, SystemSettingSecret]:
+ Base.metadata.create_all(bind=self.db.get_bind())
+
+ settings_row = self.repository.get_settings()
+ secrets_row = self.repository.get_secrets()
+ should_commit = False
+ legacy_admin = read_admin_secret()
+
+ if settings_row is None:
+ settings_row = self._build_default_settings()
+ self.db.add(settings_row)
+ should_commit = True
+
+ if secrets_row is None:
+ secrets_row = SystemSettingSecret(id=SETTINGS_ROW_ID)
+ self.db.add(secrets_row)
+ should_commit = True
+
+ if legacy_admin is not None and not secrets_row.admin_password_hash:
+ secrets_row.admin_password_hash = legacy_admin_secret_to_password_hash(legacy_admin)
+ admin_username = str(legacy_admin.get("username", "")).strip()
+ if admin_username and str(settings_row.admin_account or "").strip() in {"", "superadmin"}:
+ settings_row.admin_account = admin_username
+ should_commit = True
+
+ if should_commit:
+ self.db.commit()
+ self.db.refresh(settings_row)
+ self.db.refresh(secrets_row)
+
+ return settings_row, secrets_row
+
+ def ensure_model_settings_ready(
+ self,
+ settings_row: SystemSetting,
+ secrets_row: SystemSettingSecret,
+ ) -> dict[str, SystemModelSetting]:
+ model_rows = {row.slot: row for row in self.repository.get_model_settings()}
+ should_commit = False
+
+ for slot, config in MODEL_SLOT_CONFIGS.items():
+ if slot in model_rows:
+ continue
+
+ model_row = SystemModelSetting(
+ slot=slot,
+ provider=str(getattr(settings_row, config.provider_attr, "") or config.default_provider),
+ model_name=str(getattr(settings_row, config.model_attr, "") or config.default_model),
+ endpoint=str(getattr(settings_row, config.endpoint_attr, "") or config.default_endpoint),
+ capability=config.capability,
+ priority=config.priority,
+ enabled=True,
+ api_key_encrypted=str(getattr(secrets_row, config.legacy_secret_attr, "") or ""),
+ )
+ self.db.add(model_row)
+ model_rows[slot] = model_row
+ should_commit = True
+
+ if should_commit:
+ self.db.commit()
+ for model_row in model_rows.values():
+ self.db.refresh(model_row)
+
+ return model_rows
+
+ def get_settings_snapshot(self) -> SettingsRead:
+ settings_row, secrets_row = self.ensure_settings_ready()
+ model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
+ return self._serialize(settings_row, secrets_row, model_rows)
+
+ def save_settings_snapshot(self, payload: SettingsWrite) -> SettingsRead:
+ settings_row, secrets_row = self.ensure_settings_ready()
+ model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
+
+ if payload.adminForm.newPassword:
+ if len(payload.adminForm.newPassword) < 5:
+ raise ValueError("管理员密码至少需要 5 位。")
+ if payload.adminForm.newPassword != payload.adminForm.confirmPassword:
+ raise ValueError("两次输入的管理员密码不一致。")
+ secrets_row.admin_password_hash = hash_password(payload.adminForm.newPassword)
+
+ settings_row.company_name = payload.companyForm.companyName
+ settings_row.display_name = payload.companyForm.displayName
+ settings_row.company_code = payload.companyForm.companyCode
+ settings_row.record_number = payload.companyForm.recordNumber
+ settings_row.copyright_text = payload.companyForm.copyright
+
+ settings_row.admin_account = payload.adminForm.adminAccount
+ settings_row.admin_email = payload.adminForm.adminEmail
+ settings_row.session_timeout = payload.adminForm.sessionTimeout
+ settings_row.notice_email = payload.adminForm.noticeEmail
+ settings_row.mfa_enabled = payload.adminForm.mfaEnabled
+ settings_row.strong_password = payload.adminForm.strongPassword
+ settings_row.login_alert_enabled = payload.adminForm.loginAlertEnabled
+
+ self._apply_model_setting(
+ model_rows["main"],
+ payload.llmForm.mainProvider,
+ payload.llmForm.mainModel,
+ payload.llmForm.mainEndpoint,
+ payload.llmForm.mainApiKey,
+ )
+ self._apply_model_setting(
+ model_rows["backup"],
+ payload.llmForm.backupProvider,
+ payload.llmForm.backupModel,
+ payload.llmForm.backupEndpoint,
+ payload.llmForm.backupApiKey,
+ )
+ self._apply_model_setting(
+ model_rows["vlm"],
+ payload.llmForm.vlmProvider,
+ payload.llmForm.vlmModel,
+ payload.llmForm.vlmEndpoint,
+ payload.llmForm.vlmApiKey,
+ )
+ self._apply_model_setting(
+ model_rows["embedding"],
+ payload.llmForm.embeddingProvider,
+ payload.llmForm.embeddingModel,
+ payload.llmForm.embeddingEndpoint,
+ payload.llmForm.embeddingApiKey,
+ )
+
+ settings_row.main_provider = model_rows["main"].provider
+ settings_row.main_model = model_rows["main"].model_name
+ settings_row.main_endpoint = model_rows["main"].endpoint
+ settings_row.backup_provider = model_rows["backup"].provider
+ settings_row.backup_model = model_rows["backup"].model_name
+ settings_row.backup_endpoint = model_rows["backup"].endpoint
+ settings_row.vlm_provider = model_rows["vlm"].provider
+ settings_row.vlm_model = model_rows["vlm"].model_name
+ settings_row.vlm_endpoint = model_rows["vlm"].endpoint
+ settings_row.embedding_provider = model_rows["embedding"].provider
+ settings_row.embedding_model = model_rows["embedding"].model_name
+ settings_row.embedding_endpoint = model_rows["embedding"].endpoint
+
+ settings_row.log_level = payload.logForm.level
+ settings_row.retention_days = payload.logForm.retentionDays
+ settings_row.archive_cycle = payload.logForm.archiveCycle
+ settings_row.log_path = payload.logForm.logPath
+ settings_row.alert_email = payload.logForm.alertEmail
+ settings_row.operation_audit = payload.logForm.operationAudit
+ settings_row.login_audit = payload.logForm.loginAudit
+ settings_row.mask_sensitive = payload.logForm.maskSensitive
+
+ settings_row.smtp_host = payload.mailForm.smtpHost
+ settings_row.smtp_port = payload.mailForm.port
+ settings_row.smtp_encryption = payload.mailForm.encryption
+ settings_row.sender_name = payload.mailForm.senderName
+ settings_row.sender_address = payload.mailForm.senderAddress
+ settings_row.smtp_username = payload.mailForm.username
+ settings_row.alert_enabled = payload.mailForm.alertEnabled
+ settings_row.digest_enabled = payload.mailForm.digestEnabled
+ settings_row.digest_time = payload.mailForm.digestTime
+ settings_row.default_receiver = payload.mailForm.defaultReceiver
+
+ self._replace_secret_if_present(secrets_row, "smtp_password_encrypted", payload.mailForm.password)
+
+ self.db.add(settings_row)
+ self.db.add(secrets_row)
+ for model_row in model_rows.values():
+ self.db.add(model_row)
+ self.db.commit()
+ self.db.refresh(settings_row)
+ self.db.refresh(secrets_row)
+ for model_row in model_rows.values():
+ self.db.refresh(model_row)
+
+ return self._serialize(settings_row, secrets_row, model_rows)
+
+ def load_saved_model_api_key(self, slot: str | None) -> str:
+ if not slot or slot not in MODEL_SLOT_CONFIGS:
+ return ""
+
+ settings_row, secrets_row = self.ensure_settings_ready()
+ model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
+ encrypted_value = model_rows[slot].api_key_encrypted
+ if not encrypted_value:
+ return ""
+
+ return decrypt_secret(encrypted_value)
+
+ def get_admin_credentials(self) -> AdminCredentialRecord | None:
+ settings_row, secrets_row = self.ensure_settings_ready()
+
+ if secrets_row.admin_password_hash:
+ return AdminCredentialRecord(
+ account=settings_row.admin_account,
+ email=settings_row.admin_email,
+ password_hash=secrets_row.admin_password_hash,
+ )
+
+ legacy_record = read_admin_secret()
+ if legacy_record is None:
+ return None
+
+ username = str(legacy_record.get("username", "")).strip()
+ email = str(settings_row.admin_email or self.runtime_settings.admin_email or "").strip()
+ password_hash = ""
+
+ # Legacy admin.json uses scrypt fields rather than the app password format.
+ # The auth flow handles this file separately when no DB-backed admin password exists.
+ if username or email:
+ return AdminCredentialRecord(account=username, email=email, password_hash=password_hash)
+
+ return None
+
+ def verify_admin_login(self, identifier: str, password: str) -> AdminCredentialRecord | None:
+ settings_row, secrets_row = self.ensure_settings_ready()
+ normalized_identifier = identifier.casefold()
+
+ if secrets_row.admin_password_hash:
+ allowed_identifiers = {
+ value.casefold()
+ for value in [settings_row.admin_account, settings_row.admin_email]
+ if value
+ }
+
+ if normalized_identifier not in allowed_identifiers:
+ return None
+
+ if not verify_password(password, secrets_row.admin_password_hash):
+ return None
+
+ return AdminCredentialRecord(
+ account=settings_row.admin_account,
+ email=settings_row.admin_email,
+ password_hash=secrets_row.admin_password_hash,
+ )
+
+ legacy_record = read_admin_secret()
+ if legacy_record is None:
+ return None
+
+ admin_username = str(legacy_record.get("username", "")).strip()
+ admin_email = str(settings_row.admin_email or self.runtime_settings.admin_email or "").strip()
+ allowed_identifiers = {
+ value.casefold()
+ for value in [admin_username, admin_email]
+ if value
+ }
+
+ if normalized_identifier not in allowed_identifiers:
+ return None
+
+ if not verify_admin_secret(password, legacy_record):
+ return None
+
+ return AdminCredentialRecord(account=admin_username, email=admin_email, password_hash="")
+
+ def _build_default_settings(self) -> SystemSetting:
+ current_year = datetime.now().year
+ company_name = str(self.runtime_settings.company_name or "X-Financial").strip() or "X-Financial"
+ company_code = str(self.runtime_settings.company_code or "XF-001").strip() or "XF-001"
+ admin_email = str(self.runtime_settings.admin_email or "").strip()
+ legacy_admin = read_admin_secret() or {}
+ admin_account = str(legacy_admin.get("username", "")).strip() or "superadmin"
+
+ return SystemSetting(
+ id=SETTINGS_ROW_ID,
+ company_name=company_name,
+ display_name=company_name,
+ company_code=company_code,
+ record_number="",
+ copyright_text=f"Copyright © 2024-{current_year} {company_name}. All Rights Reserved.",
+ admin_account=admin_account,
+ admin_email=admin_email,
+ session_timeout=30,
+ notice_email=admin_email,
+ mfa_enabled=True,
+ strong_password=True,
+ login_alert_enabled=True,
+ main_provider="Codex",
+ main_model="codex-mini-latest",
+ main_endpoint="https://api.openai.com/v1",
+ backup_provider="GLM",
+ backup_model="glm-5.1",
+ backup_endpoint="https://open.bigmodel.cn/api/paas/v4/",
+ vlm_provider="Gemini",
+ vlm_model="gemini-2.5-flash",
+ vlm_endpoint="https://generativelanguage.googleapis.com/v1beta/openai/",
+ embedding_provider="GLM",
+ embedding_model="Embedding-3",
+ embedding_endpoint="https://open.bigmodel.cn/api/paas/v4/",
+ log_level="INFO",
+ retention_days=180,
+ archive_cycle="weekly",
+ log_path="server/logs/app.log",
+ alert_email=admin_email,
+ operation_audit=True,
+ login_audit=True,
+ mask_sensitive=True,
+ smtp_host="smtp.exmail.qq.com",
+ smtp_port=465,
+ smtp_encryption="SSL/TLS",
+ sender_name=company_name,
+ sender_address=admin_email,
+ smtp_username=admin_email,
+ alert_enabled=True,
+ digest_enabled=False,
+ digest_time="09:00",
+ default_receiver=admin_email,
+ )
+
+ @staticmethod
+ def _replace_secret_if_present(secret_row: SystemSettingSecret, field_name: str, value: str) -> None:
+ normalized = value.strip()
+ if not normalized:
+ return
+
+ setattr(secret_row, field_name, encrypt_secret(normalized))
+
+ @staticmethod
+ def _apply_model_setting(
+ model_row: SystemModelSetting,
+ provider: str,
+ model_name: str,
+ endpoint: str,
+ api_key: str,
+ ) -> None:
+ model_row.provider = provider
+ model_row.model_name = model_name
+ model_row.endpoint = endpoint
+
+ normalized_api_key = api_key.strip()
+ if normalized_api_key:
+ model_row.api_key_encrypted = encrypt_secret(normalized_api_key)
+
+ @staticmethod
+ def _serialize(
+ settings_row: SystemSetting,
+ secrets_row: SystemSettingSecret,
+ model_rows: dict[str, SystemModelSetting],
+ ) -> SettingsRead:
+ main_model = model_rows["main"]
+ backup_model = model_rows["backup"]
+ vlm_model = model_rows["vlm"]
+ embedding_model = model_rows["embedding"]
+
+ return SettingsRead(
+ companyForm={
+ "companyName": settings_row.company_name,
+ "displayName": settings_row.display_name,
+ "companyCode": settings_row.company_code,
+ "recordNumber": settings_row.record_number,
+ "copyright": settings_row.copyright_text,
+ },
+ adminForm={
+ "adminAccount": settings_row.admin_account,
+ "adminEmail": settings_row.admin_email,
+ "newPassword": "",
+ "confirmPassword": "",
+ "sessionTimeout": settings_row.session_timeout,
+ "noticeEmail": settings_row.notice_email,
+ "mfaEnabled": settings_row.mfa_enabled,
+ "strongPassword": settings_row.strong_password,
+ "loginAlertEnabled": settings_row.login_alert_enabled,
+ "adminPasswordConfigured": bool(secrets_row.admin_password_hash),
+ },
+ llmForm={
+ "mainProvider": main_model.provider,
+ "mainModel": main_model.model_name,
+ "mainEndpoint": main_model.endpoint,
+ "mainApiKey": "",
+ "mainApiKeyConfigured": bool(main_model.api_key_encrypted),
+ "backupProvider": backup_model.provider,
+ "backupModel": backup_model.model_name,
+ "backupEndpoint": backup_model.endpoint,
+ "backupApiKey": "",
+ "backupApiKeyConfigured": bool(backup_model.api_key_encrypted),
+ "vlmProvider": vlm_model.provider,
+ "vlmModel": vlm_model.model_name,
+ "vlmEndpoint": vlm_model.endpoint,
+ "vlmApiKey": "",
+ "vlmApiKeyConfigured": bool(vlm_model.api_key_encrypted),
+ "embeddingProvider": embedding_model.provider,
+ "embeddingModel": embedding_model.model_name,
+ "embeddingEndpoint": embedding_model.endpoint,
+ "embeddingApiKey": "",
+ "embeddingApiKeyConfigured": bool(embedding_model.api_key_encrypted),
+ },
+ logForm={
+ "level": settings_row.log_level,
+ "retentionDays": settings_row.retention_days,
+ "archiveCycle": settings_row.archive_cycle,
+ "logPath": settings_row.log_path,
+ "alertEmail": settings_row.alert_email,
+ "operationAudit": settings_row.operation_audit,
+ "loginAudit": settings_row.login_audit,
+ "maskSensitive": settings_row.mask_sensitive,
+ },
+ mailForm={
+ "smtpHost": settings_row.smtp_host,
+ "port": settings_row.smtp_port,
+ "encryption": settings_row.smtp_encryption,
+ "senderName": settings_row.sender_name,
+ "senderAddress": settings_row.sender_address,
+ "username": settings_row.smtp_username,
+ "password": "",
+ "passwordConfigured": bool(secrets_row.smtp_password_encrypted),
+ "alertEnabled": settings_row.alert_enabled,
+ "digestEnabled": settings_row.digest_enabled,
+ "digestTime": settings_row.digest_time,
+ "defaultReceiver": settings_row.default_receiver,
+ },
+ )
diff --git a/server/storage/knowledge/.index.json b/server/storage/knowledge/.index.json
new file mode 100644
index 0000000..56ca49d
--- /dev/null
+++ b/server/storage/knowledge/.index.json
@@ -0,0 +1,4 @@
+{
+ "version": 1,
+ "documents": []
+}
\ No newline at end of file
diff --git a/server/tests/test_auth_service.py b/server/tests/test_auth_service.py
index 0fff7d1..d615d3c 100644
--- a/server/tests/test_auth_service.py
+++ b/server/tests/test_auth_service.py
@@ -1,70 +1,70 @@
-from __future__ import annotations
-
-from sqlalchemy import create_engine
-from sqlalchemy.orm import Session, sessionmaker
-from sqlalchemy.pool import StaticPool
-
-from app.db.base import Base
-from app.schemas.auth import LoginRequest
-from app.schemas.settings import SettingsWrite
-from app.services.auth import AuthService
-from app.services.employee import EmployeeService
-from app.services.settings import SettingsService
-
-
-def build_session() -> Session:
- engine = create_engine(
- "sqlite+pysqlite:///:memory:",
- connect_args={"check_same_thread": False},
- poolclass=StaticPool,
- )
- Base.metadata.create_all(bind=engine)
- session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
- return session_factory()
-
-
-def test_employee_can_login_with_seed_default_password() -> None:
- with build_session() as db:
- employee = EmployeeService(db).list_employees()[0]
- result = AuthService(db).login(
- LoginRequest(username=employee.email, password="123456")
- )
-
- assert result.ok is True
- assert result.user.username == employee.email
- assert result.user.name == employee.name
- assert result.user.roleCodes
- assert result.user.isAdmin is False
-
-
-def test_admin_can_login_with_database_password() -> None:
- with build_session() as db:
- settings_service = SettingsService(db)
- payload = settings_service.get_settings_snapshot().model_dump()
- payload["adminForm"]["adminAccount"] = "superadmin"
- payload["adminForm"]["newPassword"] = "admin123"
- payload["adminForm"]["confirmPassword"] = "admin123"
- settings_service.save_settings_snapshot(SettingsWrite(**payload))
-
- result = AuthService(db).login(
- LoginRequest(username="superadmin", password="admin123")
- )
-
- assert result.ok is True
- assert result.user.username == "superadmin"
- assert result.user.isAdmin is True
- assert result.user.roleCodes == ["manager"]
-
-
-def test_disabled_employee_cannot_login() -> None:
- with build_session() as db:
- service = EmployeeService(db)
- employee = service.list_employees()[0]
- service.disable_employee(employee.id)
-
- try:
- AuthService(db).login(LoginRequest(username=employee.email, password="123456"))
- except ValueError as exc:
- assert "账号或密码错误" in str(exc)
- else:
- raise AssertionError("disabled employee login should be rejected")
+from __future__ import annotations
+
+from sqlalchemy import create_engine
+from sqlalchemy.orm import Session, sessionmaker
+from sqlalchemy.pool import StaticPool
+
+from app.db.base import Base
+from app.schemas.auth import LoginRequest
+from app.schemas.settings import SettingsWrite
+from app.services.auth import AuthService
+from app.services.employee import EmployeeService
+from app.services.settings import SettingsService
+
+
+def build_session() -> Session:
+ engine = create_engine(
+ "sqlite+pysqlite:///:memory:",
+ connect_args={"check_same_thread": False},
+ poolclass=StaticPool,
+ )
+ Base.metadata.create_all(bind=engine)
+ session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
+ return session_factory()
+
+
+def test_employee_can_login_with_seed_default_password() -> None:
+ with build_session() as db:
+ employee = EmployeeService(db).list_employees()[0]
+ result = AuthService(db).login(
+ LoginRequest(username=employee.email, password="123456")
+ )
+
+ assert result.ok is True
+ assert result.user.username == employee.email
+ assert result.user.name == employee.name
+ assert result.user.roleCodes
+ assert result.user.isAdmin is False
+
+
+def test_admin_can_login_with_database_password() -> None:
+ with build_session() as db:
+ settings_service = SettingsService(db)
+ payload = settings_service.get_settings_snapshot().model_dump()
+ payload["adminForm"]["adminAccount"] = "superadmin"
+ payload["adminForm"]["newPassword"] = "admin123"
+ payload["adminForm"]["confirmPassword"] = "admin123"
+ settings_service.save_settings_snapshot(SettingsWrite(**payload))
+
+ result = AuthService(db).login(
+ LoginRequest(username="superadmin", password="admin123")
+ )
+
+ assert result.ok is True
+ assert result.user.username == "superadmin"
+ assert result.user.isAdmin is True
+ assert result.user.roleCodes == ["manager"]
+
+
+def test_disabled_employee_cannot_login() -> None:
+ with build_session() as db:
+ service = EmployeeService(db)
+ employee = service.list_employees()[0]
+ service.disable_employee(employee.id)
+
+ try:
+ AuthService(db).login(LoginRequest(username=employee.email, password="123456"))
+ except ValueError as exc:
+ assert "账号或密码错误" in str(exc)
+ else:
+ raise AssertionError("disabled employee login should be rejected")
diff --git a/server/tests/test_settings_persistence.py b/server/tests/test_settings_persistence.py
index 7c8669a..8b26aee 100644
--- a/server/tests/test_settings_persistence.py
+++ b/server/tests/test_settings_persistence.py
@@ -1,132 +1,132 @@
-from __future__ import annotations
-
-from pathlib import Path
-import hashlib
-import json
-import secrets
-import tempfile
-
-from sqlalchemy import create_engine
-from sqlalchemy.orm import Session, sessionmaker
-
-from app.core import admin_secret
-from app.core import secret_box
-from app.db.base import Base
-from app.models.system_model_setting import SystemModelSetting
-from app.models.system_setting import SystemSetting
-from app.models.system_setting_secret import SystemSettingSecret
-from app.schemas.settings import SettingsWrite
-from app.services.settings import SettingsService
-
-
-def build_session(db_file: Path) -> Session:
- engine = create_engine(
- f"sqlite+pysqlite:///{db_file.as_posix()}",
- connect_args={"check_same_thread": False},
- )
- SystemSetting.__table__.create(bind=engine)
- SystemSettingSecret.__table__.create(bind=engine)
- SystemModelSetting.__table__.create(bind=engine)
- session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
- return session_factory()
-
-
-def build_temp_secret_dir() -> Path:
- return Path(tempfile.mkdtemp(prefix="xf-settings-test-"))
-
-
-def test_settings_service_persists_non_secret_and_secret_fields(monkeypatch) -> None:
- temp_dir = build_temp_secret_dir()
- monkeypatch.setattr(secret_box, "SECRET_KEY_FILE", temp_dir / "settings.key")
- monkeypatch.setattr(Base.metadata, "create_all", lambda *args, **kwargs: None)
-
- with build_session(temp_dir / "settings.db") as db:
- service = SettingsService(db)
- initial_snapshot = service.get_settings_snapshot()
- payload = initial_snapshot.model_dump()
-
- payload["companyForm"]["companyName"] = "YGSOFT"
- payload["companyForm"]["displayName"] = "云广软件"
- payload["adminForm"]["adminAccount"] = "admin-root"
- payload["adminForm"]["adminEmail"] = "admin@example.com"
- payload["adminForm"]["newPassword"] = "54321"
- payload["adminForm"]["confirmPassword"] = "54321"
- payload["llmForm"]["mainModel"] = "glm-4.5"
- payload["llmForm"]["mainApiKey"] = "main-secret"
- payload["mailForm"]["password"] = "smtp-secret"
-
- saved_snapshot = service.save_settings_snapshot(SettingsWrite(**payload))
-
- assert saved_snapshot.companyForm.companyName == "YGSOFT"
- assert saved_snapshot.companyForm.displayName == "云广软件"
- assert saved_snapshot.llmForm.mainModel == "glm-4.5"
- assert saved_snapshot.llmForm.mainApiKey == ""
- assert saved_snapshot.llmForm.mainApiKeyConfigured is True
- assert saved_snapshot.mailForm.password == ""
- assert saved_snapshot.mailForm.passwordConfigured is True
- assert saved_snapshot.adminForm.newPassword == ""
- assert saved_snapshot.adminForm.adminPasswordConfigured is True
-
- model_row = db.get(SystemModelSetting, "main")
- assert model_row is not None
- assert model_row.model_name == "glm-4.5"
- assert model_row.api_key_encrypted
-
- assert service.load_saved_model_api_key("main") == "main-secret"
- assert service.verify_admin_login("admin-root", "54321") is not None
- assert service.verify_admin_login("admin@example.com", "54321") is not None
-
-
-def test_blank_secret_input_does_not_clear_saved_secret(monkeypatch) -> None:
- temp_dir = build_temp_secret_dir()
- monkeypatch.setattr(secret_box, "SECRET_KEY_FILE", temp_dir / "settings.key")
- monkeypatch.setattr(Base.metadata, "create_all", lambda *args, **kwargs: None)
-
- with build_session(temp_dir / "settings.db") as db:
- service = SettingsService(db)
- first_payload = service.get_settings_snapshot().model_dump()
- first_payload["llmForm"]["mainApiKey"] = "persisted-key"
- service.save_settings_snapshot(SettingsWrite(**first_payload))
-
- second_payload = service.get_settings_snapshot().model_dump()
- second_payload["llmForm"]["mainApiKey"] = ""
- service.save_settings_snapshot(SettingsWrite(**second_payload))
-
- assert service.load_saved_model_api_key("main") == "persisted-key"
-
-
-def test_legacy_setup_admin_password_is_migrated_to_database(monkeypatch) -> None:
- temp_dir = build_temp_secret_dir()
- admin_file = temp_dir / "admin.json"
- monkeypatch.setattr(admin_secret, "ADMIN_SECRET_FILE", admin_file)
- monkeypatch.setattr(secret_box, "SECRET_KEY_FILE", temp_dir / "settings.key")
- monkeypatch.setattr(Base.metadata, "create_all", lambda *args, **kwargs: None)
-
- password = "setup-secret"
- salt = secrets.token_bytes(16)
- derived_key = hashlib.scrypt(password.encode("utf-8"), salt=salt, n=16384, r=8, p=1, dklen=64)
- admin_file.write_text(
- json.dumps(
- {
- "algorithm": "scrypt",
- "username": "setup-admin",
- "salt": salt.hex(),
- "derived_key": derived_key.hex(),
- "key_length": 64,
- "N": 16384,
- "r": 8,
- "p": 1,
- }
- ),
- encoding="utf-8",
- )
-
- with build_session(temp_dir / "settings.db") as db:
- service = SettingsService(db)
- snapshot = service.get_settings_snapshot()
- secrets_row = db.get(SystemSettingSecret, "default")
-
- assert snapshot.adminForm.adminPasswordConfigured is True
- assert secrets_row is not None
- assert secrets_row.admin_password_hash.startswith("scrypt$")
- assert service.verify_admin_login("setup-admin", password) is not None
+from __future__ import annotations
+
+from pathlib import Path
+import hashlib
+import json
+import secrets
+import tempfile
+
+from sqlalchemy import create_engine
+from sqlalchemy.orm import Session, sessionmaker
+
+from app.core import admin_secret
+from app.core import secret_box
+from app.db.base import Base
+from app.models.system_model_setting import SystemModelSetting
+from app.models.system_setting import SystemSetting
+from app.models.system_setting_secret import SystemSettingSecret
+from app.schemas.settings import SettingsWrite
+from app.services.settings import SettingsService
+
+
+def build_session(db_file: Path) -> Session:
+ engine = create_engine(
+ f"sqlite+pysqlite:///{db_file.as_posix()}",
+ connect_args={"check_same_thread": False},
+ )
+ SystemSetting.__table__.create(bind=engine)
+ SystemSettingSecret.__table__.create(bind=engine)
+ SystemModelSetting.__table__.create(bind=engine)
+ session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
+ return session_factory()
+
+
+def build_temp_secret_dir() -> Path:
+ return Path(tempfile.mkdtemp(prefix="xf-settings-test-"))
+
+
+def test_settings_service_persists_non_secret_and_secret_fields(monkeypatch) -> None:
+ temp_dir = build_temp_secret_dir()
+ monkeypatch.setattr(secret_box, "SECRET_KEY_FILE", temp_dir / "settings.key")
+ monkeypatch.setattr(Base.metadata, "create_all", lambda *args, **kwargs: None)
+
+ with build_session(temp_dir / "settings.db") as db:
+ service = SettingsService(db)
+ initial_snapshot = service.get_settings_snapshot()
+ payload = initial_snapshot.model_dump()
+
+ payload["companyForm"]["companyName"] = "YGSOFT"
+ payload["companyForm"]["displayName"] = "云广软件"
+ payload["adminForm"]["adminAccount"] = "admin-root"
+ payload["adminForm"]["adminEmail"] = "admin@example.com"
+ payload["adminForm"]["newPassword"] = "54321"
+ payload["adminForm"]["confirmPassword"] = "54321"
+ payload["llmForm"]["mainModel"] = "glm-4.5"
+ payload["llmForm"]["mainApiKey"] = "main-secret"
+ payload["mailForm"]["password"] = "smtp-secret"
+
+ saved_snapshot = service.save_settings_snapshot(SettingsWrite(**payload))
+
+ assert saved_snapshot.companyForm.companyName == "YGSOFT"
+ assert saved_snapshot.companyForm.displayName == "云广软件"
+ assert saved_snapshot.llmForm.mainModel == "glm-4.5"
+ assert saved_snapshot.llmForm.mainApiKey == ""
+ assert saved_snapshot.llmForm.mainApiKeyConfigured is True
+ assert saved_snapshot.mailForm.password == ""
+ assert saved_snapshot.mailForm.passwordConfigured is True
+ assert saved_snapshot.adminForm.newPassword == ""
+ assert saved_snapshot.adminForm.adminPasswordConfigured is True
+
+ model_row = db.get(SystemModelSetting, "main")
+ assert model_row is not None
+ assert model_row.model_name == "glm-4.5"
+ assert model_row.api_key_encrypted
+
+ assert service.load_saved_model_api_key("main") == "main-secret"
+ assert service.verify_admin_login("admin-root", "54321") is not None
+ assert service.verify_admin_login("admin@example.com", "54321") is not None
+
+
+def test_blank_secret_input_does_not_clear_saved_secret(monkeypatch) -> None:
+ temp_dir = build_temp_secret_dir()
+ monkeypatch.setattr(secret_box, "SECRET_KEY_FILE", temp_dir / "settings.key")
+ monkeypatch.setattr(Base.metadata, "create_all", lambda *args, **kwargs: None)
+
+ with build_session(temp_dir / "settings.db") as db:
+ service = SettingsService(db)
+ first_payload = service.get_settings_snapshot().model_dump()
+ first_payload["llmForm"]["mainApiKey"] = "persisted-key"
+ service.save_settings_snapshot(SettingsWrite(**first_payload))
+
+ second_payload = service.get_settings_snapshot().model_dump()
+ second_payload["llmForm"]["mainApiKey"] = ""
+ service.save_settings_snapshot(SettingsWrite(**second_payload))
+
+ assert service.load_saved_model_api_key("main") == "persisted-key"
+
+
+def test_legacy_setup_admin_password_is_migrated_to_database(monkeypatch) -> None:
+ temp_dir = build_temp_secret_dir()
+ admin_file = temp_dir / "admin.json"
+ monkeypatch.setattr(admin_secret, "ADMIN_SECRET_FILE", admin_file)
+ monkeypatch.setattr(secret_box, "SECRET_KEY_FILE", temp_dir / "settings.key")
+ monkeypatch.setattr(Base.metadata, "create_all", lambda *args, **kwargs: None)
+
+ password = "setup-secret"
+ salt = secrets.token_bytes(16)
+ derived_key = hashlib.scrypt(password.encode("utf-8"), salt=salt, n=16384, r=8, p=1, dklen=64)
+ admin_file.write_text(
+ json.dumps(
+ {
+ "algorithm": "scrypt",
+ "username": "setup-admin",
+ "salt": salt.hex(),
+ "derived_key": derived_key.hex(),
+ "key_length": 64,
+ "N": 16384,
+ "r": 8,
+ "p": 1,
+ }
+ ),
+ encoding="utf-8",
+ )
+
+ with build_session(temp_dir / "settings.db") as db:
+ service = SettingsService(db)
+ snapshot = service.get_settings_snapshot()
+ secrets_row = db.get(SystemSettingSecret, "default")
+
+ assert snapshot.adminForm.adminPasswordConfigured is True
+ assert secrets_row is not None
+ assert secrets_row.admin_password_hash.startswith("scrypt$")
+ assert service.verify_admin_login("setup-admin", password) is not None
diff --git a/web/demo/main_demo.html b/web/demo/main_demo.html
index 75d5e37..3a52a29 100644
--- a/web/demo/main_demo.html
+++ b/web/demo/main_demo.html
@@ -1,834 +1,834 @@
-
-
-
-
-
- 企业报销智能运营台
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Smart Expense Operations
-
企业报销智能运营台
-
面向财务共享中心的审批、风控、SLA与自动化运营看板
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 12.5%
-
- 较昨日 -18 单
-
-
-
-
-
-
-
-
- 8.3%
-
- 较昨日 +¥27,400
-
-
-
-
-
-
-
-
- 14.8%
-
- 较昨日 -1.2h
-
-
-
-
-
-
-
-
- 6.2%
-
- 较昨日 +4.6%
-
-
-
-
-
-
-
-
- 16.7%
-
- 较昨日 +2 单
-
-
-
-
-
-
-
-
- 3.1%
-
- 较昨日 +2.9%
-
-
-
-
-
-
-
-
-
-
报销申请与审批趋势
-
-
-
-
-
-
-
-
-
-
-
费用结构
-
-
-
-
-
* 百分比为占待处理金额比例
-
-
-
-
-
-
风险异常分布
-
-
-
-
-
* 近30天数据
-
-
-
-
-
-
-
-
-
部门报销排行(待处理金额)
-
-
-
-
-
-
2
-
研发中心
-
-
¥146,000
-
-
-
-
-
-
-
-
-
-
-
审批瓶颈(平均处理时长)
-
-
-
-
-

-
-
-
-
-
-
-

-
-
-
-
-
-
-

-
-
-
-
-
-
-
-
-
-
-
-
预算执行率(本月)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ 企业报销智能运营台
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Smart Expense Operations
+
企业报销智能运营台
+
面向财务共享中心的审批、风控、SLA与自动化运营看板
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12.5%
+
+ 较昨日 -18 单
+
+
+
+
+
+
+
+
+ 8.3%
+
+ 较昨日 +¥27,400
+
+
+
+
+
+
+
+
+ 14.8%
+
+ 较昨日 -1.2h
+
+
+
+
+
+
+
+
+ 6.2%
+
+ 较昨日 +4.6%
+
+
+
+
+
+
+
+
+ 16.7%
+
+ 较昨日 +2 单
+
+
+
+
+
+
+
+
+ 3.1%
+
+ 较昨日 +2.9%
+
+
+
+
+
+
+
+
+
+
报销申请与审批趋势
+
+
+
+
+
+
+
+
+
+
+
费用结构
+
+
+
+
+
* 百分比为占待处理金额比例
+
+
+
+
+
+
风险异常分布
+
+
+
+
+
* 近30天数据
+
+
+
+
+
+
+
+
+
部门报销排行(待处理金额)
+
+
+
+
+
+
2
+
研发中心
+
+
¥146,000
+
+
+
+
+
+
+
+
+
+
+
审批瓶颈(平均处理时长)
+
+
+
+
+

+
+
+
+
+
+
+

+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
预算执行率(本月)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/package-lock.json b/web/package-lock.json
index cbfd80f..d940706 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,2047 +1,2047 @@
-{
- "name": "x-financial-reimbursement-admin",
- "version": "0.1.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "x-financial-reimbursement-admin",
- "version": "0.1.0",
- "dependencies": {
- "@primevue/themes": "^4.5.4",
- "@vitejs/plugin-vue": "^5.2.4",
- "@vueuse/motion": "^3.0.3",
- "chart.js": "^4.5.1",
- "pg": "^8.13.1",
- "primeicons": "^7.0.0",
- "primevue": "^4.5.5",
- "vite": "^5.4.19",
- "vue": "^3.5.13",
- "vue-chartjs": "^5.3.3",
- "vue-router": "^4.5.1"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
- "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
- "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
- "cpu": [
- "ia32"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
- "cpu": [
- "loong64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
- "cpu": [
- "mips64el"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
- "cpu": [
- "s390x"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
- "cpu": [
- "ia32"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@kurkle/color": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
- "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
- "license": "MIT"
- },
- "node_modules/@nuxt/kit": {
- "version": "3.21.2",
- "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.2.tgz",
- "integrity": "sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "c12": "^3.3.3",
- "consola": "^3.4.2",
- "defu": "^6.1.4",
- "destr": "^2.0.5",
- "errx": "^0.1.0",
- "exsolve": "^1.0.8",
- "ignore": "^7.0.5",
- "jiti": "^2.6.1",
- "klona": "^2.0.6",
- "knitwork": "^1.3.0",
- "mlly": "^1.8.1",
- "ohash": "^2.0.11",
- "pathe": "^2.0.3",
- "pkg-types": "^2.3.0",
- "rc9": "^3.0.0",
- "scule": "^1.3.0",
- "semver": "^7.7.4",
- "tinyglobby": "^0.2.15",
- "ufo": "^1.6.3",
- "unctx": "^2.5.0",
- "untyped": "^2.0.0"
- },
- "engines": {
- "node": ">=18.12.0"
- }
- },
- "node_modules/@primeuix/styled": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz",
- "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==",
- "license": "MIT",
- "dependencies": {
- "@primeuix/utils": "^0.6.1"
- },
- "engines": {
- "node": ">=12.11.0"
- }
- },
- "node_modules/@primeuix/styles": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.3.tgz",
- "integrity": "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw==",
- "license": "MIT",
- "dependencies": {
- "@primeuix/styled": "^0.7.4"
- }
- },
- "node_modules/@primeuix/themes": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-2.0.3.tgz",
- "integrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==",
- "license": "MIT",
- "dependencies": {
- "@primeuix/styled": "^0.7.4"
- }
- },
- "node_modules/@primeuix/utils": {
- "version": "0.6.4",
- "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz",
- "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==",
- "license": "MIT",
- "engines": {
- "node": ">=12.11.0"
- }
- },
- "node_modules/@primevue/core": {
- "version": "4.5.5",
- "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.5.5.tgz",
- "integrity": "sha512-JpkXhq1ddc70JdsC3CC4dM+UbeeWuCW/8DpS9dNBfrOk824TLSlRlMEGFyVKqRMn5WPQvYLiy3xXfLQeNdSqhQ==",
- "license": "MIT",
- "dependencies": {
- "@primeuix/styled": "^0.7.4",
- "@primeuix/utils": "^0.6.2"
- },
- "engines": {
- "node": ">=12.11.0"
- },
- "peerDependencies": {
- "vue": "^3.5.0"
- }
- },
- "node_modules/@primevue/icons": {
- "version": "4.5.5",
- "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.5.5.tgz",
- "integrity": "sha512-eteOhTdAOXEYE9qW1AOrBBgDxQ2szHJxSkEK1XVdV2TKxGM5FQf03Ovms0VDyZTc16XBIgvwYjXJQS0BPbhPaA==",
- "license": "MIT",
- "dependencies": {
- "@primeuix/utils": "^0.6.2",
- "@primevue/core": "4.5.5"
- },
- "engines": {
- "node": ">=12.11.0"
- }
- },
- "node_modules/@primevue/themes": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.5.4.tgz",
- "integrity": "sha512-rUFZxMHLanTZdvZq4zgZPk+KRBZ3s7fE3bBK32OrZBkHQhEJmkJ7Ftd4w4QFlXyz1B7c+k5invZiOOCjwHXg9Q==",
- "deprecated": "Deprecated. This package is no longer maintained. Please migrate to @primeuix/themes: https://www.npmjs.com/package/@primeuix/themes",
- "license": "MIT",
- "dependencies": {
- "@primeuix/styled": "^0.7.4",
- "@primeuix/themes": "^2.0.2"
- },
- "engines": {
- "node": ">=12.11.0"
- }
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
- "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
- "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
- "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
- "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
- "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
- "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
- "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
- "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
- "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
- "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
- "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
- "cpu": [
- "loong64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
- "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
- "cpu": [
- "loong64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
- "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
- "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
- "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
- "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
- "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
- "cpu": [
- "s390x"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
- "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
- "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
- "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
- "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
- "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
- "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
- "cpu": [
- "ia32"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
- "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
- "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "license": "MIT"
- },
- "node_modules/@types/web-bluetooth": {
- "version": "0.0.21",
- "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
- "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
- "license": "MIT"
- },
- "node_modules/@vitejs/plugin-vue": {
- "version": "5.2.4",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
- "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
- "license": "MIT",
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "peerDependencies": {
- "vite": "^5.0.0 || ^6.0.0",
- "vue": "^3.2.25"
- }
- },
- "node_modules/@vue/compiler-core": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz",
- "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.2",
- "@vue/shared": "3.5.33",
- "entities": "^7.0.1",
- "estree-walker": "^2.0.2",
- "source-map-js": "^1.2.1"
- }
- },
- "node_modules/@vue/compiler-core/node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "license": "MIT"
- },
- "node_modules/@vue/compiler-dom": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz",
- "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-core": "3.5.33",
- "@vue/shared": "3.5.33"
- }
- },
- "node_modules/@vue/compiler-sfc": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz",
- "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.2",
- "@vue/compiler-core": "3.5.33",
- "@vue/compiler-dom": "3.5.33",
- "@vue/compiler-ssr": "3.5.33",
- "@vue/shared": "3.5.33",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.30.21",
- "postcss": "^8.5.10",
- "source-map-js": "^1.2.1"
- }
- },
- "node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "license": "MIT"
- },
- "node_modules/@vue/compiler-ssr": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz",
- "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-dom": "3.5.33",
- "@vue/shared": "3.5.33"
- }
- },
- "node_modules/@vue/devtools-api": {
- "version": "6.6.4",
- "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
- "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
- "license": "MIT"
- },
- "node_modules/@vue/reactivity": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz",
- "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==",
- "license": "MIT",
- "dependencies": {
- "@vue/shared": "3.5.33"
- }
- },
- "node_modules/@vue/runtime-core": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz",
- "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==",
- "license": "MIT",
- "dependencies": {
- "@vue/reactivity": "3.5.33",
- "@vue/shared": "3.5.33"
- }
- },
- "node_modules/@vue/runtime-dom": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz",
- "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==",
- "license": "MIT",
- "dependencies": {
- "@vue/reactivity": "3.5.33",
- "@vue/runtime-core": "3.5.33",
- "@vue/shared": "3.5.33",
- "csstype": "^3.2.3"
- }
- },
- "node_modules/@vue/server-renderer": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz",
- "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-ssr": "3.5.33",
- "@vue/shared": "3.5.33"
- },
- "peerDependencies": {
- "vue": "3.5.33"
- }
- },
- "node_modules/@vue/shared": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz",
- "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==",
- "license": "MIT"
- },
- "node_modules/@vueuse/core": {
- "version": "13.9.0",
- "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz",
- "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
- "license": "MIT",
- "dependencies": {
- "@types/web-bluetooth": "^0.0.21",
- "@vueuse/metadata": "13.9.0",
- "@vueuse/shared": "13.9.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- },
- "peerDependencies": {
- "vue": "^3.5.0"
- }
- },
- "node_modules/@vueuse/metadata": {
- "version": "13.9.0",
- "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz",
- "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/@vueuse/motion": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@vueuse/motion/-/motion-3.0.3.tgz",
- "integrity": "sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==",
- "license": "MIT",
- "dependencies": {
- "@vueuse/core": "^13.0.0",
- "@vueuse/shared": "^13.0.0",
- "defu": "^6.1.4",
- "framesync": "^6.1.2",
- "popmotion": "^11.0.5",
- "style-value-types": "^5.1.2"
- },
- "optionalDependencies": {
- "@nuxt/kit": "^3.13.0"
- },
- "peerDependencies": {
- "vue": ">=3.0.0"
- }
- },
- "node_modules/@vueuse/shared": {
- "version": "13.9.0",
- "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz",
- "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- },
- "peerDependencies": {
- "vue": "^3.5.0"
- }
- },
- "node_modules/acorn": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
- "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
- "license": "MIT",
- "optional": true,
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/c12": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
- "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "chokidar": "^5.0.0",
- "confbox": "^0.2.4",
- "defu": "^6.1.6",
- "dotenv": "^17.3.1",
- "exsolve": "^1.0.8",
- "giget": "^3.2.0",
- "jiti": "^2.6.1",
- "ohash": "^2.0.11",
- "pathe": "^2.0.3",
- "perfect-debounce": "^2.1.0",
- "pkg-types": "^2.3.0",
- "rc9": "^3.0.1"
- },
- "peerDependencies": {
- "magicast": "*"
- },
- "peerDependenciesMeta": {
- "magicast": {
- "optional": true
- }
- }
- },
- "node_modules/chart.js": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
- "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
- "license": "MIT",
- "dependencies": {
- "@kurkle/color": "^0.3.0"
- },
- "engines": {
- "pnpm": ">=8"
- }
- },
- "node_modules/chokidar": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
- "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "readdirp": "^5.0.0"
- },
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/citty": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
- "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "consola": "^3.2.3"
- }
- },
- "node_modules/confbox": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
- "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/consola": {
- "version": "3.4.2",
- "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
- "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": "^14.18.0 || >=16.10.0"
- }
- },
- "node_modules/csstype": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "license": "MIT"
- },
- "node_modules/defu": {
- "version": "6.1.7",
- "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
- "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
- "license": "MIT"
- },
- "node_modules/destr": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
- "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/dotenv": {
- "version": "17.4.2",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
- "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
- "license": "BSD-2-Clause",
- "optional": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
- "node_modules/entities": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
- "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/errx": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz",
- "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
- }
- },
- "node_modules/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
- },
- "node_modules/exsolve": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
- "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/framesync": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
- "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
- "license": "MIT",
- "dependencies": {
- "tslib": "2.4.0"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/giget": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
- "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
- "license": "MIT",
- "optional": true,
- "bin": {
- "giget": "dist/cli.mjs"
- }
- },
- "node_modules/hey-listen": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
- "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
- "license": "MIT"
- },
- "node_modules/ignore": {
- "version": "7.0.5",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
- "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/jiti": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
- "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "license": "MIT",
- "optional": true,
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
- "node_modules/klona": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
- "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/knitwork": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.3.0.tgz",
- "integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/mlly": {
- "version": "1.8.2",
- "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
- "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "acorn": "^8.16.0",
- "pathe": "^2.0.3",
- "pkg-types": "^1.3.1",
- "ufo": "^1.6.3"
- }
- },
- "node_modules/mlly/node_modules/confbox": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
- "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/mlly/node_modules/pkg-types": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
- "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "confbox": "^0.1.8",
- "mlly": "^1.7.4",
- "pathe": "^2.0.1"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/ohash": {
- "version": "2.0.11",
- "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
- "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/perfect-debounce": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
- "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/pg": {
- "version": "8.20.0",
- "resolved": "https://registry.npmmirror.com/pg/-/pg-8.20.0.tgz",
- "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
- "license": "MIT",
- "dependencies": {
- "pg-connection-string": "^2.12.0",
- "pg-pool": "^3.13.0",
- "pg-protocol": "^1.13.0",
- "pg-types": "2.2.0",
- "pgpass": "1.0.5"
- },
- "engines": {
- "node": ">= 16.0.0"
- },
- "optionalDependencies": {
- "pg-cloudflare": "^1.3.0"
- },
- "peerDependencies": {
- "pg-native": ">=3.0.1"
- },
- "peerDependenciesMeta": {
- "pg-native": {
- "optional": true
- }
- }
- },
- "node_modules/pg-cloudflare": {
- "version": "1.3.0",
- "resolved": "https://registry.npmmirror.com/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
- "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/pg-connection-string": {
- "version": "2.12.0",
- "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
- "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
- "license": "MIT"
- },
- "node_modules/pg-int8": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz",
- "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
- "license": "ISC",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/pg-pool": {
- "version": "3.13.0",
- "resolved": "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.13.0.tgz",
- "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
- "license": "MIT",
- "peerDependencies": {
- "pg": ">=8.0"
- }
- },
- "node_modules/pg-protocol": {
- "version": "1.13.0",
- "resolved": "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.13.0.tgz",
- "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
- "license": "MIT"
- },
- "node_modules/pg-types": {
- "version": "2.2.0",
- "resolved": "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz",
- "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
- "license": "MIT",
- "dependencies": {
- "pg-int8": "1.0.1",
- "postgres-array": "~2.0.0",
- "postgres-bytea": "~1.0.0",
- "postgres-date": "~1.0.4",
- "postgres-interval": "^1.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/pgpass": {
- "version": "1.0.5",
- "resolved": "https://registry.npmmirror.com/pgpass/-/pgpass-1.0.5.tgz",
- "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
- "license": "MIT",
- "dependencies": {
- "split2": "^4.1.0"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pkg-types": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
- "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "confbox": "^0.2.4",
- "exsolve": "^1.0.8",
- "pathe": "^2.0.3"
- }
- },
- "node_modules/popmotion": {
- "version": "11.0.5",
- "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz",
- "integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==",
- "license": "MIT",
- "dependencies": {
- "framesync": "6.1.2",
- "hey-listen": "^1.0.8",
- "style-value-types": "5.1.2",
- "tslib": "2.4.0"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.12",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
- "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postgres-array": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz",
- "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postgres-bytea": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
- "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-date": {
- "version": "1.0.7",
- "resolved": "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz",
- "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-interval": {
- "version": "1.2.0",
- "resolved": "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz",
- "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
- "license": "MIT",
- "dependencies": {
- "xtend": "^4.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/primeicons": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
- "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
- "license": "MIT"
- },
- "node_modules/primevue": {
- "version": "4.5.5",
- "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.5.tgz",
- "integrity": "sha512-Kv5REIewCdP806QaoU+4nBXfmpzOGFKkZ9qH4KsL6MjiAQVc4PUzypt8erl4r3Vzh3nr3aWZIxkxYRRsLGiX2A==",
- "license": "MIT",
- "dependencies": {
- "@primeuix/styled": "^0.7.4",
- "@primeuix/styles": "^2.0.3",
- "@primeuix/utils": "^0.6.2",
- "@primevue/core": "4.5.5",
- "@primevue/icons": "4.5.5"
- },
- "engines": {
- "node": ">=12.11.0"
- }
- },
- "node_modules/rc9": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz",
- "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "defu": "^6.1.6",
- "destr": "^2.0.5"
- }
- },
- "node_modules/readdirp": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
- "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/rollup": {
- "version": "4.60.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
- "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.8"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.60.2",
- "@rollup/rollup-android-arm64": "4.60.2",
- "@rollup/rollup-darwin-arm64": "4.60.2",
- "@rollup/rollup-darwin-x64": "4.60.2",
- "@rollup/rollup-freebsd-arm64": "4.60.2",
- "@rollup/rollup-freebsd-x64": "4.60.2",
- "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
- "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
- "@rollup/rollup-linux-arm64-gnu": "4.60.2",
- "@rollup/rollup-linux-arm64-musl": "4.60.2",
- "@rollup/rollup-linux-loong64-gnu": "4.60.2",
- "@rollup/rollup-linux-loong64-musl": "4.60.2",
- "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
- "@rollup/rollup-linux-ppc64-musl": "4.60.2",
- "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
- "@rollup/rollup-linux-riscv64-musl": "4.60.2",
- "@rollup/rollup-linux-s390x-gnu": "4.60.2",
- "@rollup/rollup-linux-x64-gnu": "4.60.2",
- "@rollup/rollup-linux-x64-musl": "4.60.2",
- "@rollup/rollup-openbsd-x64": "4.60.2",
- "@rollup/rollup-openharmony-arm64": "4.60.2",
- "@rollup/rollup-win32-arm64-msvc": "4.60.2",
- "@rollup/rollup-win32-ia32-msvc": "4.60.2",
- "@rollup/rollup-win32-x64-gnu": "4.60.2",
- "@rollup/rollup-win32-x64-msvc": "4.60.2",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/scule": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
- "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "license": "ISC",
- "optional": true,
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/split2": {
- "version": "4.2.0",
- "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
- "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
- "license": "ISC",
- "engines": {
- "node": ">= 10.x"
- }
- },
- "node_modules/style-value-types": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz",
- "integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==",
- "license": "MIT",
- "dependencies": {
- "hey-listen": "^1.0.8",
- "tslib": "2.4.0"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tslib": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
- "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
- "license": "0BSD"
- },
- "node_modules/ufo": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
- "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/unctx": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.5.0.tgz",
- "integrity": "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "acorn": "^8.15.0",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.21",
- "unplugin": "^2.3.11"
- }
- },
- "node_modules/unplugin": {
- "version": "2.3.11",
- "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
- "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@jridgewell/remapping": "^2.3.5",
- "acorn": "^8.15.0",
- "picomatch": "^4.0.3",
- "webpack-virtual-modules": "^0.6.2"
- },
- "engines": {
- "node": ">=18.12.0"
- }
- },
- "node_modules/untyped": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz",
- "integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "citty": "^0.1.6",
- "defu": "^6.1.4",
- "jiti": "^2.4.2",
- "knitwork": "^1.2.0",
- "scule": "^1.3.0"
- },
- "bin": {
- "untyped": "dist/cli.mjs"
- }
- },
- "node_modules/vite": {
- "version": "5.4.21",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
- "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- }
- }
- },
- "node_modules/vue": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz",
- "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-dom": "3.5.33",
- "@vue/compiler-sfc": "3.5.33",
- "@vue/runtime-dom": "3.5.33",
- "@vue/server-renderer": "3.5.33",
- "@vue/shared": "3.5.33"
- },
- "peerDependencies": {
- "typescript": "*"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/vue-chartjs": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz",
- "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==",
- "license": "MIT",
- "peerDependencies": {
- "chart.js": "^4.1.1",
- "vue": "^3.0.0-0 || ^2.7.0"
- }
- },
- "node_modules/vue-router": {
- "version": "4.5.1",
- "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
- "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
- "license": "MIT",
- "dependencies": {
- "@vue/devtools-api": "^6.6.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/posva"
- },
- "peerDependencies": {
- "vue": "^3.2.0"
- }
- },
- "node_modules/webpack-virtual-modules": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
- "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4"
- }
- }
- }
-}
+{
+ "name": "x-financial-reimbursement-admin",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "x-financial-reimbursement-admin",
+ "version": "0.1.0",
+ "dependencies": {
+ "@primevue/themes": "^4.5.4",
+ "@vitejs/plugin-vue": "^5.2.4",
+ "@vueuse/motion": "^3.0.3",
+ "chart.js": "^4.5.1",
+ "pg": "^8.13.1",
+ "primeicons": "^7.0.0",
+ "primevue": "^4.5.5",
+ "vite": "^5.4.19",
+ "vue": "^3.5.13",
+ "vue-chartjs": "^5.3.3",
+ "vue-router": "^4.5.1"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
+ "node_modules/@nuxt/kit": {
+ "version": "3.21.2",
+ "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.2.tgz",
+ "integrity": "sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "c12": "^3.3.3",
+ "consola": "^3.4.2",
+ "defu": "^6.1.4",
+ "destr": "^2.0.5",
+ "errx": "^0.1.0",
+ "exsolve": "^1.0.8",
+ "ignore": "^7.0.5",
+ "jiti": "^2.6.1",
+ "klona": "^2.0.6",
+ "knitwork": "^1.3.0",
+ "mlly": "^1.8.1",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "pkg-types": "^2.3.0",
+ "rc9": "^3.0.0",
+ "scule": "^1.3.0",
+ "semver": "^7.7.4",
+ "tinyglobby": "^0.2.15",
+ "ufo": "^1.6.3",
+ "unctx": "^2.5.0",
+ "untyped": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/@primeuix/styled": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz",
+ "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@primeuix/utils": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=12.11.0"
+ }
+ },
+ "node_modules/@primeuix/styles": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.3.tgz",
+ "integrity": "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@primeuix/styled": "^0.7.4"
+ }
+ },
+ "node_modules/@primeuix/themes": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-2.0.3.tgz",
+ "integrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==",
+ "license": "MIT",
+ "dependencies": {
+ "@primeuix/styled": "^0.7.4"
+ }
+ },
+ "node_modules/@primeuix/utils": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz",
+ "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.11.0"
+ }
+ },
+ "node_modules/@primevue/core": {
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.5.5.tgz",
+ "integrity": "sha512-JpkXhq1ddc70JdsC3CC4dM+UbeeWuCW/8DpS9dNBfrOk824TLSlRlMEGFyVKqRMn5WPQvYLiy3xXfLQeNdSqhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@primeuix/styled": "^0.7.4",
+ "@primeuix/utils": "^0.6.2"
+ },
+ "engines": {
+ "node": ">=12.11.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@primevue/icons": {
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.5.5.tgz",
+ "integrity": "sha512-eteOhTdAOXEYE9qW1AOrBBgDxQ2szHJxSkEK1XVdV2TKxGM5FQf03Ovms0VDyZTc16XBIgvwYjXJQS0BPbhPaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@primeuix/utils": "^0.6.2",
+ "@primevue/core": "4.5.5"
+ },
+ "engines": {
+ "node": ">=12.11.0"
+ }
+ },
+ "node_modules/@primevue/themes": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.5.4.tgz",
+ "integrity": "sha512-rUFZxMHLanTZdvZq4zgZPk+KRBZ3s7fE3bBK32OrZBkHQhEJmkJ7Ftd4w4QFlXyz1B7c+k5invZiOOCjwHXg9Q==",
+ "deprecated": "Deprecated. This package is no longer maintained. Please migrate to @primeuix/themes: https://www.npmjs.com/package/@primeuix/themes",
+ "license": "MIT",
+ "dependencies": {
+ "@primeuix/styled": "^0.7.4",
+ "@primeuix/themes": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=12.11.0"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz",
+ "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.2",
+ "@vue/shared": "3.5.33",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-core/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz",
+ "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.33",
+ "@vue/shared": "3.5.33"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz",
+ "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.2",
+ "@vue/compiler-core": "3.5.33",
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/compiler-ssr": "3.5.33",
+ "@vue/shared": "3.5.33",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.10",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz",
+ "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/shared": "3.5.33"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz",
+ "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.33"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz",
+ "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.33",
+ "@vue/shared": "3.5.33"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz",
+ "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.33",
+ "@vue/runtime-core": "3.5.33",
+ "@vue/shared": "3.5.33",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz",
+ "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.33",
+ "@vue/shared": "3.5.33"
+ },
+ "peerDependencies": {
+ "vue": "3.5.33"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz",
+ "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz",
+ "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "13.9.0",
+ "@vueuse/shared": "13.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz",
+ "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/motion": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@vueuse/motion/-/motion-3.0.3.tgz",
+ "integrity": "sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vueuse/core": "^13.0.0",
+ "@vueuse/shared": "^13.0.0",
+ "defu": "^6.1.4",
+ "framesync": "^6.1.2",
+ "popmotion": "^11.0.5",
+ "style-value-types": "^5.1.2"
+ },
+ "optionalDependencies": {
+ "@nuxt/kit": "^3.13.0"
+ },
+ "peerDependencies": {
+ "vue": ">=3.0.0"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz",
+ "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/c12": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
+ "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "chokidar": "^5.0.0",
+ "confbox": "^0.2.4",
+ "defu": "^6.1.6",
+ "dotenv": "^17.3.1",
+ "exsolve": "^1.0.8",
+ "giget": "^3.2.0",
+ "jiti": "^2.6.1",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^2.1.0",
+ "pkg-types": "^2.3.0",
+ "rc9": "^3.0.1"
+ },
+ "peerDependencies": {
+ "magicast": "*"
+ },
+ "peerDependenciesMeta": {
+ "magicast": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/citty": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
+ "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "consola": "^3.2.3"
+ }
+ },
+ "node_modules/confbox": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "license": "MIT"
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/errx": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz",
+ "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/framesync": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
+ "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.4.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/giget": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
+ "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "giget": "dist/cli.mjs"
+ }
+ },
+ "node_modules/hey-listen": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
+ "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
+ "license": "MIT"
+ },
+ "node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/klona": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
+ "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/knitwork": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.3.0.tgz",
+ "integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mlly": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+ "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.3"
+ }
+ },
+ "node_modules/mlly/node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/mlly/node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/perfect-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmmirror.com/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.13.0.tgz",
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
+ "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "confbox": "^0.2.4",
+ "exsolve": "^1.0.8",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/popmotion": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz",
+ "integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==",
+ "license": "MIT",
+ "dependencies": {
+ "framesync": "6.1.2",
+ "hey-listen": "^1.0.8",
+ "style-value-types": "5.1.2",
+ "tslib": "2.4.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
+ "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/primeicons": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
+ "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
+ "license": "MIT"
+ },
+ "node_modules/primevue": {
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.5.tgz",
+ "integrity": "sha512-Kv5REIewCdP806QaoU+4nBXfmpzOGFKkZ9qH4KsL6MjiAQVc4PUzypt8erl4r3Vzh3nr3aWZIxkxYRRsLGiX2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@primeuix/styled": "^0.7.4",
+ "@primeuix/styles": "^2.0.3",
+ "@primeuix/utils": "^0.6.2",
+ "@primevue/core": "4.5.5",
+ "@primevue/icons": "4.5.5"
+ },
+ "engines": {
+ "node": ">=12.11.0"
+ }
+ },
+ "node_modules/rc9": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz",
+ "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "defu": "^6.1.6",
+ "destr": "^2.0.5"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+ "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scule": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/style-value-types": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz",
+ "integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "hey-listen": "^1.0.8",
+ "tslib": "2.4.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
+ "license": "0BSD"
+ },
+ "node_modules/ufo": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
+ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/unctx": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.5.0.tgz",
+ "integrity": "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21",
+ "unplugin": "^2.3.11"
+ }
+ },
+ "node_modules/unplugin": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
+ "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "acorn": "^8.15.0",
+ "picomatch": "^4.0.3",
+ "webpack-virtual-modules": "^0.6.2"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/untyped": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz",
+ "integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "citty": "^0.1.6",
+ "defu": "^6.1.4",
+ "jiti": "^2.4.2",
+ "knitwork": "^1.2.0",
+ "scule": "^1.3.0"
+ },
+ "bin": {
+ "untyped": "dist/cli.mjs"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz",
+ "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/compiler-sfc": "3.5.33",
+ "@vue/runtime-dom": "3.5.33",
+ "@vue/server-renderer": "3.5.33",
+ "@vue/shared": "3.5.33"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-chartjs": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz",
+ "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "vue": "^3.0.0-0 || ^2.7.0"
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
+ "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/webpack-virtual-modules": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/web/package.json b/web/package.json
index 59a3318..f9ac1d7 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,25 +1,25 @@
-{
- "name": "x-financial-reimbursement-admin",
- "private": true,
- "version": "0.1.0",
- "type": "module",
- "scripts": {
- "start": "vite --host 0.0.0.0",
- "dev": "vite --host 0.0.0.0",
- "build": "vite build",
- "preview": "vite preview --host 0.0.0.0"
- },
- "dependencies": {
- "@primevue/themes": "^4.5.4",
- "@vitejs/plugin-vue": "^5.2.4",
- "@vueuse/motion": "^3.0.3",
- "chart.js": "^4.5.1",
- "pg": "^8.13.1",
- "primeicons": "^7.0.0",
- "primevue": "^4.5.5",
- "vite": "^5.4.19",
- "vue": "^3.5.13",
- "vue-chartjs": "^5.3.3",
- "vue-router": "^4.5.1"
- }
-}
+{
+ "name": "x-financial-reimbursement-admin",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "start": "vite --host 0.0.0.0",
+ "dev": "vite --host 0.0.0.0",
+ "build": "vite build",
+ "preview": "vite preview --host 0.0.0.0"
+ },
+ "dependencies": {
+ "@primevue/themes": "^4.5.4",
+ "@vitejs/plugin-vue": "^5.2.4",
+ "@vueuse/motion": "^3.0.3",
+ "chart.js": "^4.5.1",
+ "pg": "^8.13.1",
+ "primeicons": "^7.0.0",
+ "primevue": "^4.5.5",
+ "vite": "^5.4.19",
+ "vue": "^3.5.13",
+ "vue-chartjs": "^5.3.3",
+ "vue-router": "^4.5.1"
+ }
+}
diff --git a/web/src/assets/styles/views/settings-view.css b/web/src/assets/styles/views/settings-view.css
index 89598a7..60ae7f5 100644
--- a/web/src/assets/styles/views/settings-view.css
+++ b/web/src/assets/styles/views/settings-view.css
@@ -1,697 +1,697 @@
-.settings-page {
- height: 100%;
- min-height: 0;
- animation: fadeUp 220ms var(--ease) both;
-}
-
-.settings-shell {
- height: 100%;
- min-height: 0;
- display: grid;
- grid-template-columns: 248px minmax(0, 1fr);
- overflow: hidden;
- border-radius: 24px;
- background: linear-gradient(180deg, #ffffff 0%, #fbfefd 100%);
-}
-
-.settings-nav {
- min-width: 0;
- min-height: 0;
- display: grid;
- grid-template-rows: auto minmax(0, 1fr);
- gap: 10px;
- padding: 22px 16px 18px;
- border-right: 1px solid #e7edf3;
- background: linear-gradient(180deg, #fcfffd 0%, #f5fbf8 58%, #ffffff 100%);
-}
-
-.settings-nav-head {
- display: grid;
- gap: 8px;
- padding: 4px 10px 18px;
- border-bottom: 1px solid #eef3f7;
-}
-
-.nav-kicker {
- color: #10b981;
- font-size: 11px;
- font-weight: 800;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.settings-nav-head h2 {
- color: #0f172a;
- font-size: 24px;
- font-weight: 860;
- line-height: 1.1;
-}
-
-.settings-nav-head p {
- color: #64748b;
- font-size: 12px;
- line-height: 1.6;
-}
-
-.settings-nav-list {
- min-height: 0;
- display: grid;
- align-content: start;
- gap: 8px;
- overflow: auto;
- padding-right: 4px;
-}
-
-.settings-nav-item {
- width: 100%;
- min-height: 74px;
- display: block;
- padding: 14px 14px 14px 16px;
- border: 1px solid transparent;
- border-radius: 18px;
- background: transparent;
- color: #334155;
- text-align: left;
- transition:
- background 180ms var(--ease),
- border-color 180ms var(--ease),
- box-shadow 180ms var(--ease),
- color 180ms var(--ease),
- transform 180ms var(--ease);
-}
-
-.settings-nav-item:hover {
- transform: translateY(-1px);
- border-color: rgba(16, 185, 129, 0.14);
- background: rgba(255, 255, 255, 0.9);
-}
-
-.settings-nav-item.active {
- border-color: rgba(16, 185, 129, 0.16);
- background: linear-gradient(135deg, rgba(16, 185, 129, 0.14), rgba(16, 185, 129, 0.04));
- box-shadow: inset 3px 0 0 #10b981;
- color: #047857;
-}
-
-.nav-item-copy {
- min-width: 0;
- display: grid;
- gap: 4px;
-}
-
-.nav-item-copy strong {
- color: inherit;
- font-size: 14px;
- font-weight: 820;
- line-height: 1.25;
-}
-
-.nav-item-copy small {
- color: #64748b;
- font-size: 12px;
- line-height: 1.45;
-}
-
-.settings-body {
- min-width: 0;
- min-height: 0;
- display: grid;
- grid-template-rows: auto minmax(0, 1fr);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.96) 100%);
-}
-
-.settings-toolbar {
- position: sticky;
- top: 0;
- z-index: 2;
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 18px;
- padding: 24px 28px 20px;
- border-bottom: 1px solid #eef2f7;
- background: rgba(255, 255, 255, 0.92);
- backdrop-filter: blur(14px);
-}
-
-.settings-toolbar-copy {
- min-width: 0;
-}
-
-.settings-breadcrumb {
- display: inline-flex;
- align-items: center;
- min-height: 28px;
- padding: 0 12px;
- border-radius: 999px;
- background: #eef8f2;
- color: #047857;
- font-size: 12px;
- font-weight: 800;
-}
-
-.settings-toolbar-copy h3 {
- margin-top: 14px;
- color: #0f172a;
- font-size: 28px;
- font-weight: 860;
- line-height: 1.15;
-}
-
-.settings-toolbar-copy p {
- margin-top: 10px;
- max-width: 760px;
- color: #64748b;
- font-size: 14px;
- line-height: 1.7;
-}
-
-.settings-toolbar-actions {
- display: grid;
- justify-items: end;
- gap: 12px;
-}
-
-.save-button {
- min-height: 42px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 0 18px;
- border: 0;
- border-radius: 14px;
- background: linear-gradient(135deg, #13b87b, #0a9d68);
- color: #fff;
- font-size: 13px;
- font-weight: 820;
- box-shadow: 0 12px 26px rgba(5, 150, 105, 0.2);
- transition:
- transform 180ms var(--ease),
- box-shadow 180ms var(--ease),
- filter 180ms var(--ease);
-}
-
-.save-button:hover {
- transform: translateY(-1px);
- box-shadow: 0 16px 30px rgba(5, 150, 105, 0.22);
- filter: saturate(1.04);
-}
-
-.settings-content {
- min-height: 0;
- overflow: auto;
- display: grid;
- align-content: start;
- gap: 18px;
- padding: 24px 28px 28px;
-}
-
-.model-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 18px;
-}
-
-.settings-card {
- padding: 22px 22px 24px;
- border: 1px solid #e8eef3;
- border-radius: 22px;
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 251, 255, 0.94));
-}
-
-.card-head {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 14px;
- margin-bottom: 18px;
-}
-
-.card-head-actions {
- display: flex;
- align-items: center;
- gap: 10px;
- flex: 0 0 auto;
-}
-
-.card-head h4 {
- color: #0f172a;
- font-size: 18px;
- font-weight: 840;
- line-height: 1.2;
-}
-
-.card-head p {
- margin-top: 6px;
- color: #64748b;
- font-size: 13px;
- line-height: 1.65;
-}
-
-.test-button {
- min-height: 38px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 0 14px;
- border: 1px solid rgba(16, 185, 129, 0.18);
- border-radius: 12px;
- background: #f7fffb;
- color: #047857;
- font-size: 13px;
- font-weight: 800;
- transition:
- border-color 180ms var(--ease),
- background 180ms var(--ease),
- color 180ms var(--ease),
- transform 180ms var(--ease);
-}
-
-.test-button:hover:not(:disabled) {
- transform: translateY(-1px);
- border-color: rgba(16, 185, 129, 0.34);
- background: #ecfdf5;
-}
-
-.test-button:disabled {
- cursor: wait;
- opacity: 0.78;
-}
-
-.form-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 18px 20px;
- align-items: start;
-}
-
-.profile-grid {
- grid-template-columns: 96px repeat(2, minmax(0, 1fr));
-}
-
-.compact-grid {
- margin-bottom: 18px;
-}
-
-.field {
- display: grid;
- gap: 8px;
-}
-
-.field-wide {
- grid-column: span 2;
-}
-
-.field-full {
- grid-column: 1 / -1;
-}
-
-.field span {
- color: #334155;
- font-size: 12px;
- font-weight: 800;
- line-height: 1.2;
-}
-
-.field em {
- margin-right: 4px;
- color: #ef4444;
- font-style: normal;
-}
-
-.field input,
-.field select {
- width: 100%;
- min-height: 44px;
- padding: 0 14px;
- border: 1px solid #d7e0ea;
- border-radius: 16px;
- background: #fff;
- color: #0f172a;
- font-size: 13px;
- line-height: 1.45;
- transition:
- border-color 180ms var(--ease),
- box-shadow 180ms var(--ease),
- background 180ms var(--ease);
-}
-
-.field input::placeholder {
- color: #94a3b8;
-}
-
-.field input:focus,
-.field select:focus {
- outline: none;
- border-color: rgba(16, 185, 129, 0.55);
- box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.12);
-}
-
-.secret-bound-state {
- min-height: 24px;
- display: inline-flex;
- align-items: center;
- gap: 7px;
- color: #047857;
- font-size: 12px;
- font-weight: 750;
- line-height: 1.45;
-}
-
-.secret-bound-state i {
- font-size: 15px;
-}
-
-.test-feedback {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- margin-top: 16px;
- padding: 12px 14px;
- border-radius: 14px;
- font-size: 12px;
- font-weight: 700;
- line-height: 1.6;
-}
-
-.test-feedback i {
- margin-top: 2px;
- font-size: 15px;
-}
-
-.test-feedback.is-success {
- background: #ecfdf5;
- color: #047857;
-}
-
-.test-feedback.is-error {
- background: #fef2f2;
- color: #b91c1c;
-}
-
-.test-feedback.is-testing {
- background: #eff6ff;
- color: #1d4ed8;
-}
-
-.logo-field {
- align-self: stretch;
-}
-
-.logo-tile {
- width: 96px;
- height: 96px;
- display: grid;
- place-items: center;
- border: 1px dashed #cbd5e1;
- border-radius: 22px;
- background:
- linear-gradient(45deg, #f8fafc 25%, transparent 25%, transparent 75%, #f8fafc 75%, #f8fafc),
- linear-gradient(45deg, #f8fafc 25%, transparent 25%, transparent 75%, #f8fafc 75%, #f8fafc);
- background-position: 0 0, 9px 9px;
- background-size: 18px 18px;
- color: #10b981;
- font-size: 36px;
-}
-
-.preview-card {
- display: grid;
- grid-template-columns: 78px minmax(0, 1fr) auto;
- align-items: center;
- gap: 18px;
- padding: 22px;
- border: 1px solid rgba(16, 185, 129, 0.14);
- border-radius: 24px;
- background: linear-gradient(135deg, rgba(16, 185, 129, 0.08), rgba(59, 130, 246, 0.05));
-}
-
-.preview-icon {
- width: 78px;
- height: 78px;
- display: grid;
- place-items: center;
- border-radius: 22px;
- background: linear-gradient(135deg, #10b981, #0f766e);
- color: #fff;
- font-size: 34px;
- box-shadow: 0 14px 28px rgba(16, 185, 129, 0.18);
-}
-
-.preview-copy strong {
- display: block;
- color: #0f172a;
- font-size: 18px;
- font-weight: 840;
-}
-
-.preview-copy p {
- margin-top: 6px;
- color: #334155;
- font-size: 14px;
- font-weight: 700;
-}
-
-.preview-copy small {
- display: block;
- margin-top: 8px;
- color: #64748b;
- font-size: 12px;
- line-height: 1.55;
-}
-
-.preview-badge {
- min-height: 30px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0 12px;
- border-radius: 999px;
- background: #ecfdf5;
- color: #059669;
- font-size: 12px;
- font-weight: 820;
- white-space: nowrap;
-}
-
-.chip-row {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- margin-bottom: 18px;
-}
-
-.level-chip {
- min-width: 78px;
- min-height: 36px;
- padding: 0 14px;
- border: 1px solid #d7e0ea;
- border-radius: 999px;
- background: #fff;
- color: #475569;
- font-size: 12px;
- font-weight: 820;
- transition:
- border-color 160ms ease,
- background 160ms ease,
- box-shadow 160ms ease,
- color 160ms ease;
-}
-
-.level-chip.active {
- border-color: #10b981;
- background: #10b981;
- color: #fff;
- box-shadow: 0 8px 18px rgba(16, 185, 129, 0.18);
-}
-
-.range-shell {
- min-height: 44px;
- display: flex;
- align-items: center;
- gap: 14px;
- padding: 0 14px;
- border: 1px solid #d7e0ea;
- border-radius: 16px;
- background: #fff;
-}
-
-.range-shell input[type='range'] {
- flex: 1 1 auto;
- accent-color: #10b981;
-}
-
-.range-shell strong {
- min-width: 28px;
- color: #0f172a;
- font-size: 13px;
- font-weight: 800;
- text-align: right;
-}
-
-.switch-group {
- display: grid;
- gap: 12px;
-}
-
-.switch-row {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- padding: 15px 16px;
- border: 1px solid #e5eaf0;
- border-radius: 18px;
- background: #fbfdff;
- text-align: left;
- transition:
- border-color 180ms var(--ease),
- background 180ms var(--ease),
- transform 180ms var(--ease);
-}
-
-.switch-row:hover {
- transform: translateY(-1px);
- border-color: rgba(16, 185, 129, 0.18);
- background: #f7fffb;
-}
-
-.switch-copy {
- min-width: 0;
- display: grid;
- gap: 4px;
-}
-
-.switch-copy strong {
- color: #0f172a;
- font-size: 14px;
- font-weight: 800;
-}
-
-.switch-copy small {
- color: #64748b;
- font-size: 12px;
- line-height: 1.55;
-}
-
-.switch {
- position: relative;
- flex: 0 0 auto;
- width: 48px;
- height: 28px;
- display: inline-flex;
- align-items: center;
- padding: 3px;
- border-radius: 999px;
- background: #dbe4ee;
- transition: background 180ms var(--ease);
-}
-
-.switch i {
- width: 22px;
- height: 22px;
- border-radius: 999px;
- background: #fff;
- box-shadow: 0 2px 6px rgba(15, 23, 42, 0.14);
- transition: transform 180ms var(--ease);
-}
-
-.switch.active {
- background: #10b981;
-}
-
-.switch.active i {
- transform: translateX(20px);
-}
-
-@media (max-width: 1260px) {
- .settings-shell {
- grid-template-columns: 226px minmax(0, 1fr);
- }
-
- .settings-toolbar {
- flex-direction: column;
- align-items: stretch;
- }
-
- .settings-toolbar-actions {
- justify-items: stretch;
- }
-
- .save-button {
- justify-content: center;
- }
-}
-
-@media (max-width: 960px) {
- .settings-shell {
- grid-template-columns: 1fr;
- }
-
- .settings-nav {
- grid-template-rows: auto auto auto;
- border-right: 0;
- border-bottom: 1px solid #e7edf3;
- }
-
- .settings-nav-list {
- display: flex;
- gap: 8px;
- overflow-x: auto;
- padding-right: 0;
- }
-
- .settings-nav-item {
- min-width: 208px;
- }
-
- .settings-toolbar,
- .settings-content {
- padding-inline: 20px;
- }
-
- .model-grid,
- .form-grid,
- .profile-grid {
- grid-template-columns: 1fr;
- }
-
- .field-wide,
- .field-full {
- grid-column: span 1;
- }
-
- .logo-field {
- width: fit-content;
- }
-
- .preview-card {
- grid-template-columns: 1fr;
- justify-items: start;
- }
-}
-
-@media (max-width: 640px) {
- .settings-toolbar {
- padding: 18px 16px;
- }
-
- .settings-toolbar-copy h3 {
- font-size: 24px;
- }
-
- .settings-content {
- padding: 16px;
- }
-
- .settings-card {
- padding: 18px 16px;
- border-radius: 18px;
- }
-
- .settings-nav {
- padding: 18px 12px 14px;
- }
-}
+.settings-page {
+ height: 100%;
+ min-height: 0;
+ animation: fadeUp 220ms var(--ease) both;
+}
+
+.settings-shell {
+ height: 100%;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: 248px minmax(0, 1fr);
+ overflow: hidden;
+ border-radius: 24px;
+ background: linear-gradient(180deg, #ffffff 0%, #fbfefd 100%);
+}
+
+.settings-nav {
+ min-width: 0;
+ min-height: 0;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ gap: 10px;
+ padding: 22px 16px 18px;
+ border-right: 1px solid #e7edf3;
+ background: linear-gradient(180deg, #fcfffd 0%, #f5fbf8 58%, #ffffff 100%);
+}
+
+.settings-nav-head {
+ display: grid;
+ gap: 8px;
+ padding: 4px 10px 18px;
+ border-bottom: 1px solid #eef3f7;
+}
+
+.nav-kicker {
+ color: #10b981;
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.settings-nav-head h2 {
+ color: #0f172a;
+ font-size: 24px;
+ font-weight: 860;
+ line-height: 1.1;
+}
+
+.settings-nav-head p {
+ color: #64748b;
+ font-size: 12px;
+ line-height: 1.6;
+}
+
+.settings-nav-list {
+ min-height: 0;
+ display: grid;
+ align-content: start;
+ gap: 8px;
+ overflow: auto;
+ padding-right: 4px;
+}
+
+.settings-nav-item {
+ width: 100%;
+ min-height: 74px;
+ display: block;
+ padding: 14px 14px 14px 16px;
+ border: 1px solid transparent;
+ border-radius: 18px;
+ background: transparent;
+ color: #334155;
+ text-align: left;
+ transition:
+ background 180ms var(--ease),
+ border-color 180ms var(--ease),
+ box-shadow 180ms var(--ease),
+ color 180ms var(--ease),
+ transform 180ms var(--ease);
+}
+
+.settings-nav-item:hover {
+ transform: translateY(-1px);
+ border-color: rgba(16, 185, 129, 0.14);
+ background: rgba(255, 255, 255, 0.9);
+}
+
+.settings-nav-item.active {
+ border-color: rgba(16, 185, 129, 0.16);
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.14), rgba(16, 185, 129, 0.04));
+ box-shadow: inset 3px 0 0 #10b981;
+ color: #047857;
+}
+
+.nav-item-copy {
+ min-width: 0;
+ display: grid;
+ gap: 4px;
+}
+
+.nav-item-copy strong {
+ color: inherit;
+ font-size: 14px;
+ font-weight: 820;
+ line-height: 1.25;
+}
+
+.nav-item-copy small {
+ color: #64748b;
+ font-size: 12px;
+ line-height: 1.45;
+}
+
+.settings-body {
+ min-width: 0;
+ min-height: 0;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.96) 100%);
+}
+
+.settings-toolbar {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 18px;
+ padding: 24px 28px 20px;
+ border-bottom: 1px solid #eef2f7;
+ background: rgba(255, 255, 255, 0.92);
+ backdrop-filter: blur(14px);
+}
+
+.settings-toolbar-copy {
+ min-width: 0;
+}
+
+.settings-breadcrumb {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ padding: 0 12px;
+ border-radius: 999px;
+ background: #eef8f2;
+ color: #047857;
+ font-size: 12px;
+ font-weight: 800;
+}
+
+.settings-toolbar-copy h3 {
+ margin-top: 14px;
+ color: #0f172a;
+ font-size: 28px;
+ font-weight: 860;
+ line-height: 1.15;
+}
+
+.settings-toolbar-copy p {
+ margin-top: 10px;
+ max-width: 760px;
+ color: #64748b;
+ font-size: 14px;
+ line-height: 1.7;
+}
+
+.settings-toolbar-actions {
+ display: grid;
+ justify-items: end;
+ gap: 12px;
+}
+
+.save-button {
+ min-height: 42px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 0 18px;
+ border: 0;
+ border-radius: 14px;
+ background: linear-gradient(135deg, #13b87b, #0a9d68);
+ color: #fff;
+ font-size: 13px;
+ font-weight: 820;
+ box-shadow: 0 12px 26px rgba(5, 150, 105, 0.2);
+ transition:
+ transform 180ms var(--ease),
+ box-shadow 180ms var(--ease),
+ filter 180ms var(--ease);
+}
+
+.save-button:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 16px 30px rgba(5, 150, 105, 0.22);
+ filter: saturate(1.04);
+}
+
+.settings-content {
+ min-height: 0;
+ overflow: auto;
+ display: grid;
+ align-content: start;
+ gap: 18px;
+ padding: 24px 28px 28px;
+}
+
+.model-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 18px;
+}
+
+.settings-card {
+ padding: 22px 22px 24px;
+ border: 1px solid #e8eef3;
+ border-radius: 22px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 251, 255, 0.94));
+}
+
+.card-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 14px;
+ margin-bottom: 18px;
+}
+
+.card-head-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex: 0 0 auto;
+}
+
+.card-head h4 {
+ color: #0f172a;
+ font-size: 18px;
+ font-weight: 840;
+ line-height: 1.2;
+}
+
+.card-head p {
+ margin-top: 6px;
+ color: #64748b;
+ font-size: 13px;
+ line-height: 1.65;
+}
+
+.test-button {
+ min-height: 38px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 0 14px;
+ border: 1px solid rgba(16, 185, 129, 0.18);
+ border-radius: 12px;
+ background: #f7fffb;
+ color: #047857;
+ font-size: 13px;
+ font-weight: 800;
+ transition:
+ border-color 180ms var(--ease),
+ background 180ms var(--ease),
+ color 180ms var(--ease),
+ transform 180ms var(--ease);
+}
+
+.test-button:hover:not(:disabled) {
+ transform: translateY(-1px);
+ border-color: rgba(16, 185, 129, 0.34);
+ background: #ecfdf5;
+}
+
+.test-button:disabled {
+ cursor: wait;
+ opacity: 0.78;
+}
+
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 18px 20px;
+ align-items: start;
+}
+
+.profile-grid {
+ grid-template-columns: 96px repeat(2, minmax(0, 1fr));
+}
+
+.compact-grid {
+ margin-bottom: 18px;
+}
+
+.field {
+ display: grid;
+ gap: 8px;
+}
+
+.field-wide {
+ grid-column: span 2;
+}
+
+.field-full {
+ grid-column: 1 / -1;
+}
+
+.field span {
+ color: #334155;
+ font-size: 12px;
+ font-weight: 800;
+ line-height: 1.2;
+}
+
+.field em {
+ margin-right: 4px;
+ color: #ef4444;
+ font-style: normal;
+}
+
+.field input,
+.field select {
+ width: 100%;
+ min-height: 44px;
+ padding: 0 14px;
+ border: 1px solid #d7e0ea;
+ border-radius: 16px;
+ background: #fff;
+ color: #0f172a;
+ font-size: 13px;
+ line-height: 1.45;
+ transition:
+ border-color 180ms var(--ease),
+ box-shadow 180ms var(--ease),
+ background 180ms var(--ease);
+}
+
+.field input::placeholder {
+ color: #94a3b8;
+}
+
+.field input:focus,
+.field select:focus {
+ outline: none;
+ border-color: rgba(16, 185, 129, 0.55);
+ box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.12);
+}
+
+.secret-bound-state {
+ min-height: 24px;
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ color: #047857;
+ font-size: 12px;
+ font-weight: 750;
+ line-height: 1.45;
+}
+
+.secret-bound-state i {
+ font-size: 15px;
+}
+
+.test-feedback {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ margin-top: 16px;
+ padding: 12px 14px;
+ border-radius: 14px;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1.6;
+}
+
+.test-feedback i {
+ margin-top: 2px;
+ font-size: 15px;
+}
+
+.test-feedback.is-success {
+ background: #ecfdf5;
+ color: #047857;
+}
+
+.test-feedback.is-error {
+ background: #fef2f2;
+ color: #b91c1c;
+}
+
+.test-feedback.is-testing {
+ background: #eff6ff;
+ color: #1d4ed8;
+}
+
+.logo-field {
+ align-self: stretch;
+}
+
+.logo-tile {
+ width: 96px;
+ height: 96px;
+ display: grid;
+ place-items: center;
+ border: 1px dashed #cbd5e1;
+ border-radius: 22px;
+ background:
+ linear-gradient(45deg, #f8fafc 25%, transparent 25%, transparent 75%, #f8fafc 75%, #f8fafc),
+ linear-gradient(45deg, #f8fafc 25%, transparent 25%, transparent 75%, #f8fafc 75%, #f8fafc);
+ background-position: 0 0, 9px 9px;
+ background-size: 18px 18px;
+ color: #10b981;
+ font-size: 36px;
+}
+
+.preview-card {
+ display: grid;
+ grid-template-columns: 78px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 18px;
+ padding: 22px;
+ border: 1px solid rgba(16, 185, 129, 0.14);
+ border-radius: 24px;
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.08), rgba(59, 130, 246, 0.05));
+}
+
+.preview-icon {
+ width: 78px;
+ height: 78px;
+ display: grid;
+ place-items: center;
+ border-radius: 22px;
+ background: linear-gradient(135deg, #10b981, #0f766e);
+ color: #fff;
+ font-size: 34px;
+ box-shadow: 0 14px 28px rgba(16, 185, 129, 0.18);
+}
+
+.preview-copy strong {
+ display: block;
+ color: #0f172a;
+ font-size: 18px;
+ font-weight: 840;
+}
+
+.preview-copy p {
+ margin-top: 6px;
+ color: #334155;
+ font-size: 14px;
+ font-weight: 700;
+}
+
+.preview-copy small {
+ display: block;
+ margin-top: 8px;
+ color: #64748b;
+ font-size: 12px;
+ line-height: 1.55;
+}
+
+.preview-badge {
+ min-height: 30px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 12px;
+ border-radius: 999px;
+ background: #ecfdf5;
+ color: #059669;
+ font-size: 12px;
+ font-weight: 820;
+ white-space: nowrap;
+}
+
+.chip-row {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-bottom: 18px;
+}
+
+.level-chip {
+ min-width: 78px;
+ min-height: 36px;
+ padding: 0 14px;
+ border: 1px solid #d7e0ea;
+ border-radius: 999px;
+ background: #fff;
+ color: #475569;
+ font-size: 12px;
+ font-weight: 820;
+ transition:
+ border-color 160ms ease,
+ background 160ms ease,
+ box-shadow 160ms ease,
+ color 160ms ease;
+}
+
+.level-chip.active {
+ border-color: #10b981;
+ background: #10b981;
+ color: #fff;
+ box-shadow: 0 8px 18px rgba(16, 185, 129, 0.18);
+}
+
+.range-shell {
+ min-height: 44px;
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 0 14px;
+ border: 1px solid #d7e0ea;
+ border-radius: 16px;
+ background: #fff;
+}
+
+.range-shell input[type='range'] {
+ flex: 1 1 auto;
+ accent-color: #10b981;
+}
+
+.range-shell strong {
+ min-width: 28px;
+ color: #0f172a;
+ font-size: 13px;
+ font-weight: 800;
+ text-align: right;
+}
+
+.switch-group {
+ display: grid;
+ gap: 12px;
+}
+
+.switch-row {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 15px 16px;
+ border: 1px solid #e5eaf0;
+ border-radius: 18px;
+ background: #fbfdff;
+ text-align: left;
+ transition:
+ border-color 180ms var(--ease),
+ background 180ms var(--ease),
+ transform 180ms var(--ease);
+}
+
+.switch-row:hover {
+ transform: translateY(-1px);
+ border-color: rgba(16, 185, 129, 0.18);
+ background: #f7fffb;
+}
+
+.switch-copy {
+ min-width: 0;
+ display: grid;
+ gap: 4px;
+}
+
+.switch-copy strong {
+ color: #0f172a;
+ font-size: 14px;
+ font-weight: 800;
+}
+
+.switch-copy small {
+ color: #64748b;
+ font-size: 12px;
+ line-height: 1.55;
+}
+
+.switch {
+ position: relative;
+ flex: 0 0 auto;
+ width: 48px;
+ height: 28px;
+ display: inline-flex;
+ align-items: center;
+ padding: 3px;
+ border-radius: 999px;
+ background: #dbe4ee;
+ transition: background 180ms var(--ease);
+}
+
+.switch i {
+ width: 22px;
+ height: 22px;
+ border-radius: 999px;
+ background: #fff;
+ box-shadow: 0 2px 6px rgba(15, 23, 42, 0.14);
+ transition: transform 180ms var(--ease);
+}
+
+.switch.active {
+ background: #10b981;
+}
+
+.switch.active i {
+ transform: translateX(20px);
+}
+
+@media (max-width: 1260px) {
+ .settings-shell {
+ grid-template-columns: 226px minmax(0, 1fr);
+ }
+
+ .settings-toolbar {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .settings-toolbar-actions {
+ justify-items: stretch;
+ }
+
+ .save-button {
+ justify-content: center;
+ }
+}
+
+@media (max-width: 960px) {
+ .settings-shell {
+ grid-template-columns: 1fr;
+ }
+
+ .settings-nav {
+ grid-template-rows: auto auto auto;
+ border-right: 0;
+ border-bottom: 1px solid #e7edf3;
+ }
+
+ .settings-nav-list {
+ display: flex;
+ gap: 8px;
+ overflow-x: auto;
+ padding-right: 0;
+ }
+
+ .settings-nav-item {
+ min-width: 208px;
+ }
+
+ .settings-toolbar,
+ .settings-content {
+ padding-inline: 20px;
+ }
+
+ .model-grid,
+ .form-grid,
+ .profile-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .field-wide,
+ .field-full {
+ grid-column: span 1;
+ }
+
+ .logo-field {
+ width: fit-content;
+ }
+
+ .preview-card {
+ grid-template-columns: 1fr;
+ justify-items: start;
+ }
+}
+
+@media (max-width: 640px) {
+ .settings-toolbar {
+ padding: 18px 16px;
+ }
+
+ .settings-toolbar-copy h3 {
+ font-size: 24px;
+ }
+
+ .settings-content {
+ padding: 16px;
+ }
+
+ .settings-card {
+ padding: 18px 16px;
+ border-radius: 18px;
+ }
+
+ .settings-nav {
+ padding: 18px 12px 14px;
+ }
+}
diff --git a/web/src/assets/styles/views/setup-view.css b/web/src/assets/styles/views/setup-view.css
index 3980bfd..ba0de3d 100644
--- a/web/src/assets/styles/views/setup-view.css
+++ b/web/src/assets/styles/views/setup-view.css
@@ -1,781 +1,781 @@
-.setup-page {
- min-height: 100dvh;
- display: grid;
- grid-template-columns: minmax(320px, 392px) minmax(0, 1fr);
- background:
- radial-gradient(circle at top left, rgba(16, 185, 129, 0.24), transparent 24rem),
- radial-gradient(circle at 36% 14%, rgba(16, 185, 129, 0.16), transparent 18rem),
- linear-gradient(135deg, #04110d 0%, #0b1f18 26%, #10281f 26%, #eef5f1 26%, #f6fbf8 100%);
-}
-
-.setup-context {
- padding: 42px 28px 32px;
- color: rgba(255, 255, 255, 0.92);
- display: grid;
- align-content: start;
- gap: 22px;
- border-right: 1px solid rgba(110, 231, 183, 0.08);
- background: linear-gradient(180deg, rgba(4, 17, 13, 0.92), rgba(16, 40, 31, 0.9));
-}
-
-.setup-brand {
- display: flex;
- gap: 18px;
- align-items: flex-start;
-}
-
-.setup-brand-mark {
- position: relative;
- flex: 0 0 64px;
- width: 64px;
- height: 64px;
- display: grid;
- place-items: center;
-}
-
-.setup-brand-ring {
- position: absolute;
- inset: 0;
- border-radius: 18px;
- background:
- linear-gradient(145deg, rgba(209, 250, 229, 0.96), rgba(52, 211, 153, 0.88)),
- linear-gradient(145deg, rgba(16, 185, 129, 0.4), rgba(5, 150, 105, 0.6));
- box-shadow:
- 0 18px 36px rgba(16, 185, 129, 0.2),
- inset 0 1px 0 rgba(255, 255, 255, 0.46);
- transform: rotate(-8deg);
-}
-
-.setup-brand-ring::before {
- content: '';
- position: absolute;
- inset: 7px;
- border-radius: 14px;
- border: 1px solid rgba(4, 120, 87, 0.22);
-}
-
-.setup-brand-core {
- position: relative;
- z-index: 1;
- width: 42px;
- height: 42px;
- border-radius: 12px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- background: rgba(3, 32, 24, 0.92);
- color: #d1fae5;
- font-size: 15px;
- font-weight: 800;
- letter-spacing: 0.14em;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
-}
-
-.setup-kicker {
- margin-bottom: 8px;
- font-size: 12px;
- font-weight: 700;
- letter-spacing: 0.12em;
- text-transform: uppercase;
- color: rgba(167, 243, 208, 0.86);
-}
-
-.setup-kicker-light {
- color: rgba(209, 250, 229, 0.82);
-}
-
-.setup-context h1 {
- color: #f4fff8;
- font-size: clamp(1.9rem, 2.4vw, 2.5rem);
- line-height: 1.08;
- text-shadow: 0 12px 36px rgba(2, 12, 8, 0.34);
-}
-
-.setup-lead {
- color: rgba(220, 252, 231, 0.84);
- font-size: 14px;
- line-height: 1.8;
-}
-
-.setup-nav {
- display: grid;
- gap: 10px;
-}
-
-.setup-nav-item {
- width: 100%;
- padding: 14px 14px 14px 12px;
- border: 1px solid rgba(110, 231, 183, 0.12);
- border-radius: 8px;
- display: grid;
- grid-template-columns: 44px minmax(0, 1fr) 18px;
- align-items: center;
- gap: 12px;
- background: linear-gradient(160deg, rgba(10, 23, 18, 0.82), rgba(15, 39, 31, 0.72));
- color: inherit;
- text-align: left;
- transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
-}
-
-.setup-nav-item:hover {
- transform: translateY(-1px);
- border-color: rgba(110, 231, 183, 0.22);
-}
-
-.setup-nav-item.is-active {
- border-color: rgba(16, 185, 129, 0.4);
- box-shadow: 0 14px 28px rgba(3, 10, 7, 0.22);
-}
-
-.setup-nav-item.is-complete {
- background: linear-gradient(160deg, rgba(8, 31, 23, 0.96), rgba(12, 58, 44, 0.86));
-}
-
-.setup-nav-index {
- width: 40px;
- height: 40px;
- border-radius: 8px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- background: rgba(16, 185, 129, 0.16);
- color: #d1fae5;
- font-size: 12px;
- font-weight: 700;
- letter-spacing: 0.08em;
-}
-
-.setup-nav-copy {
- display: grid;
- gap: 4px;
-}
-
-.setup-nav-copy strong {
- color: #f0fdf4;
- font-size: 14px;
-}
-
-.setup-nav-copy small {
- color: rgba(209, 250, 229, 0.72);
- font-size: 12px;
- line-height: 1.55;
-}
-
-.setup-nav-check {
- color: #6ee7b7;
- font-size: 14px;
-}
-
-.setup-progress {
- margin-top: 8px;
- padding: 16px 18px;
- border: 1px solid rgba(110, 231, 183, 0.14);
- border-radius: 8px;
- background: rgba(7, 33, 25, 0.76);
-}
-
-.setup-progress strong {
- color: #f0fdf4;
- font-size: 15px;
-}
-
-.setup-progress p {
- margin-top: 8px;
- color: rgba(209, 250, 229, 0.72);
- font-size: 13px;
- line-height: 1.65;
-}
-
-.setup-complete {
- margin-top: auto;
- padding: 16px 18px 0;
- border-top: 1px solid rgba(110, 231, 183, 0.12);
- display: grid;
- gap: 12px;
-}
-
-.setup-complete p {
- color: rgba(209, 250, 229, 0.76);
- font-size: 13px;
- line-height: 1.6;
-}
-
-.setup-complete-btn {
- width: 100%;
-}
-
-.setup-complete-progress {
- display: flex;
- align-items: center;
- gap: 8px;
- color: rgba(209, 250, 229, 0.86);
- font-size: 13px;
- line-height: 1.5;
-}
-
-.setup-panel {
- padding: 36px;
- display: grid;
- align-content: start;
- gap: 24px;
- background:
- radial-gradient(circle at top left, rgba(16, 185, 129, 0.08), transparent 16rem),
- linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0));
-}
-
-.setup-panel-head {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 16px;
- padding: 20px 22px;
- border: 1px solid rgba(16, 185, 129, 0.14);
- border-radius: 8px;
- background: linear-gradient(135deg, #063b2e, #0f5f49);
- box-shadow: 0 16px 34px rgba(6, 59, 46, 0.16);
-}
-
-.setup-panel-head h2 {
- color: #ffffff;
- font-size: 28px;
-}
-
-.setup-panel-desc {
- margin-top: 10px;
- color: rgba(236, 253, 245, 0.82);
- font-size: 14px;
- line-height: 1.65;
-}
-
-.setup-chip {
- display: inline-flex;
- align-items: center;
- min-height: 32px;
- padding: 0 12px;
- border-radius: 999px;
- background: rgba(240, 253, 244, 0.14);
- color: #d1fae5;
- font-size: 12px;
- font-weight: 700;
- border: 1px solid rgba(209, 250, 229, 0.18);
-}
-
-.setup-chip.is-success {
- background: rgba(16, 185, 129, 0.22);
-}
-
-.setup-form {
- padding: 30px 32px;
- border: 1px solid rgba(16, 185, 129, 0.18);
- border-radius: 8px;
- background: linear-gradient(180deg, rgba(244, 255, 248, 0.98), rgba(255, 255, 255, 0.94));
- box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12), 0 1px 0 rgba(255, 255, 255, 0.6) inset;
- backdrop-filter: blur(14px);
-}
-
-.setup-stage {
- display: grid;
- gap: 22px;
-}
-
-.section-head {
- display: grid;
- gap: 6px;
-}
-
-.section-head h3 {
- color: #065f46;
- font-size: 18px;
-}
-
-.section-head p {
- color: #5b6f67;
- font-size: 13px;
- line-height: 1.7;
-}
-
-.field-grid {
- display: grid;
- gap: 16px;
-}
-
-.field-grid-2 {
- grid-template-columns: repeat(2, minmax(0, 1fr));
-}
-
-.field {
- display: grid;
- gap: 8px;
-}
-
-.field span {
- color: #244239;
- font-size: 13px;
- font-weight: 600;
-}
-
-.field-note {
- color: #5f7c72;
- font-size: 12px;
- line-height: 1.5;
-}
-
-.field-group-note {
- margin-top: -6px;
- color: #5f7c72;
- font-size: 12px;
- line-height: 1.6;
-}
-
-.optional-block {
- padding: 18px 18px 0;
- border: 1px dashed rgba(16, 185, 129, 0.22);
- border-radius: 8px;
- background: rgba(240, 253, 244, 0.52);
-}
-
-.optional-block-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 14px;
-}
-
-.optional-block-head strong {
- color: #065f46;
- font-size: 14px;
-}
-
-.optional-block-head span {
- min-height: 24px;
- padding: 0 10px;
- border-radius: 999px;
- display: inline-flex;
- align-items: center;
- background: rgba(16, 185, 129, 0.12);
- color: #047857;
- font-size: 12px;
- font-weight: 700;
-}
-
-.field input {
- width: 100%;
- min-height: 46px;
- padding: 0 14px;
- border: 1px solid rgba(148, 163, 184, 0.78);
- border-radius: 8px;
- background: rgba(255, 255, 255, 0.92);
- color: #0f172a;
- transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
-}
-
-.field input:hover {
- transform: translateY(-1px);
-}
-
-.field input:focus {
- border-color: #10b981;
- box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.12);
-}
-
-.field-span-2 {
- grid-column: span 2;
-}
-
-.setup-runtime {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px;
-}
-
-.setup-runtime article {
- position: relative;
- overflow: hidden;
- padding: 16px 18px;
- border-radius: 8px;
- border: 1px solid rgba(110, 231, 183, 0.14);
- display: grid;
- gap: 10px;
- box-shadow: 0 14px 32px rgba(3, 10, 7, 0.2);
-}
-
-.setup-runtime article::before {
- content: '';
- position: absolute;
- inset: 0 auto auto 0;
- width: 100%;
- height: 3px;
- background: linear-gradient(90deg, rgba(209, 250, 229, 0.92), rgba(16, 185, 129, 0.48));
-}
-
-.setup-runtime article:nth-child(1) {
- background: linear-gradient(150deg, rgba(7, 33, 25, 0.96), rgba(16, 67, 52, 0.9));
-}
-
-.setup-runtime article:nth-child(2) {
- background: linear-gradient(150deg, rgba(7, 28, 26, 0.96), rgba(11, 59, 61, 0.9));
-}
-
-.setup-runtime article:nth-child(3) {
- background: linear-gradient(150deg, rgba(16, 28, 19, 0.96), rgba(48, 74, 36, 0.9));
-}
-
-.setup-runtime span {
- font-size: 12px;
- text-transform: uppercase;
- color: rgba(167, 243, 208, 0.7);
-}
-
-.setup-runtime strong {
- color: #f8fffb;
- font-size: 14px;
- line-height: 1.5;
- word-break: break-word;
- text-shadow: 0 2px 16px rgba(4, 9, 7, 0.22);
-}
-
-.setup-error {
- margin-top: 22px;
- padding: 14px 16px;
- border: 1px solid rgba(239, 68, 68, 0.18);
- border-radius: 8px;
- background: #fef2f2;
- color: #b91c1c;
- white-space: pre-line;
-}
-
-.setup-status {
- margin-top: 22px;
- padding: 14px 16px;
- border-radius: 8px;
- white-space: pre-line;
-}
-
-.setup-status.is-success {
- border: 1px solid rgba(16, 185, 129, 0.18);
- background: #ecfdf5;
- color: #047857;
-}
-
-.setup-status.is-danger {
- border: 1px solid rgba(239, 68, 68, 0.18);
- background: #fef2f2;
- color: #b91c1c;
-}
-
-.setup-gate {
- margin-top: 14px;
- padding: 12px 14px;
- border: 1px solid rgba(245, 158, 11, 0.2);
- border-radius: 8px;
- background: #fffbeb;
- color: #b45309;
-}
-
-.setup-actions {
- margin-top: 28px;
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 12px;
-}
-
-.setup-actions-right {
- display: flex;
- gap: 12px;
- flex-wrap: wrap;
-}
-
-.primary-btn,
-.secondary-btn {
- min-height: 46px;
- padding: 0 18px;
- border-radius: 8px;
- border: 1px solid transparent;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- font-weight: 700;
- transition: transform 160ms ease, box-shadow 160ms ease, opacity 160ms ease;
-}
-
-.primary-btn {
- background: linear-gradient(135deg, #10b981, #0f766e);
- color: #fff;
- box-shadow: 0 14px 28px rgba(16, 185, 129, 0.24);
-}
-
-.secondary-btn {
- background: rgba(240, 253, 244, 0.94);
- color: #1f4f41;
- border-color: rgba(16, 185, 129, 0.18);
-}
-
-.secondary-btn-strong {
- background: linear-gradient(135deg, rgba(16, 185, 129, 0.14), rgba(5, 150, 105, 0.12));
- color: #065f46;
-}
-
-.primary-btn:hover,
-.secondary-btn:hover {
- transform: translateY(-1px);
-}
-
-.primary-btn:disabled,
-.secondary-btn:disabled {
- opacity: 0.55;
- cursor: not-allowed;
- box-shadow: none;
- transform: none;
-}
-
-.setup-modal-backdrop {
- position: fixed;
- inset: 0;
- z-index: 50;
- padding: 24px;
- display: grid;
- place-items: center;
- background: rgba(3, 20, 15, 0.72);
- backdrop-filter: blur(10px);
-}
-
-.setup-startup-modal {
- width: min(1120px, 100%);
- max-height: calc(100vh - 48px);
- overflow: hidden;
- padding: 24px;
- border: 1px solid rgba(110, 231, 183, 0.22);
- border-radius: 8px;
- display: grid;
- grid-template-rows: auto minmax(0, 1fr);
- gap: 20px;
- background:
- radial-gradient(circle at top right, rgba(16, 185, 129, 0.16), transparent 18rem),
- linear-gradient(180deg, #05251d 0%, #081611 100%);
- box-shadow: 0 30px 80px rgba(0, 0, 0, 0.42);
-}
-
-.setup-startup-head {
- display: flex;
- justify-content: space-between;
- gap: 18px;
- align-items: flex-start;
-}
-
-.setup-startup-head h2 {
- margin-top: 4px;
- color: #ffffff;
- font-size: 22px;
-}
-
-.setup-startup-head span {
- display: block;
- margin-top: 8px;
- color: rgba(209, 250, 229, 0.78);
- font-size: 13px;
- line-height: 1.6;
-}
-
-.setup-startup-spinner {
- width: 54px;
- height: 54px;
- border: 1px solid rgba(110, 231, 183, 0.26);
- border-radius: 50%;
- display: grid;
- place-items: center;
- flex: 0 0 auto;
- color: #a7f3d0;
- background: rgba(6, 78, 59, 0.34);
-}
-
-.setup-startup-spinner .pi {
- font-size: 22px;
-}
-
-.setup-startup-spinner strong {
- color: #ffffff;
- font-size: 20px;
-}
-
-.setup-startup-body {
- min-height: 0;
- display: grid;
- grid-template-columns: minmax(330px, 0.86fr) minmax(460px, 1.14fr);
- gap: 18px;
-}
-
-.setup-startup-steps {
- min-height: 0;
- overflow: auto;
- padding-right: 2px;
- display: grid;
- align-content: start;
- gap: 10px;
-}
-
-.setup-startup-step {
- padding: 14px;
- border: 1px solid rgba(148, 163, 184, 0.16);
- border-radius: 8px;
- display: grid;
- grid-template-columns: 24px 1fr;
- gap: 12px;
- background: rgba(15, 23, 42, 0.24);
-}
-
-.setup-startup-step .pi {
- margin-top: 2px;
- color: rgba(209, 250, 229, 0.46);
-}
-
-.setup-startup-step strong {
- color: #f8fffb;
- font-size: 14px;
-}
-
-.setup-startup-step span {
- display: block;
- margin-top: 4px;
- color: rgba(209, 250, 229, 0.68);
- font-size: 12px;
- line-height: 1.5;
-}
-
-.setup-startup-step.is-running {
- border-color: rgba(59, 130, 246, 0.34);
-}
-
-.setup-startup-step.is-running .pi {
- color: #93c5fd;
-}
-
-.setup-startup-step.is-success {
- border-color: rgba(16, 185, 129, 0.32);
-}
-
-.setup-startup-step.is-success .pi {
- color: #34d399;
-}
-
-.setup-startup-step.is-error {
- border-color: rgba(248, 113, 113, 0.36);
-}
-
-.setup-startup-step.is-error .pi {
- color: #f87171;
-}
-
-.setup-startup-console {
- min-height: 0;
- border: 1px solid rgba(148, 163, 184, 0.16);
- border-radius: 8px;
- overflow: hidden;
- display: grid;
- grid-template-rows: auto minmax(0, 1fr);
- background: rgba(2, 6, 23, 0.42);
-}
-
-.setup-startup-console-head {
- min-height: 44px;
- padding: 0 14px;
- border-bottom: 1px solid rgba(148, 163, 184, 0.14);
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- background: rgba(15, 23, 42, 0.5);
-}
-
-.setup-startup-console-head strong {
- color: #e2e8f0;
- font-size: 13px;
-}
-
-.setup-startup-console-head span {
- color: rgba(148, 163, 184, 0.9);
- font-size: 12px;
-}
-
-.setup-startup-log {
- min-height: 0;
- overflow: auto;
- padding: 14px;
- color: rgba(226, 232, 240, 0.84);
- font-size: 12px;
- line-height: 1.5;
- white-space: pre-wrap;
- word-break: break-word;
-}
-
-@media (max-width: 1180px) {
- .setup-page {
- grid-template-columns: 1fr;
- background:
- radial-gradient(circle at top right, rgba(16, 185, 129, 0.2), transparent 22rem),
- linear-gradient(180deg, #04110d 0%, #10281f 36%, #eef5f1 36%, #f6fbf8 100%);
- }
-
- .setup-context,
- .setup-panel {
- padding: 28px 24px;
- }
-
- .setup-complete {
- padding-inline: 0;
- }
-}
-
-@media (max-width: 820px) {
- .field-grid-2,
- .setup-runtime {
- grid-template-columns: 1fr;
- }
-
- .setup-modal-backdrop {
- padding: 14px;
- }
-
- .setup-startup-modal {
- max-height: calc(100vh - 28px);
- padding: 18px;
- }
-
- .setup-startup-head {
- align-items: center;
- }
-
- .setup-startup-body {
- grid-template-columns: 1fr;
- }
-
- .setup-startup-steps,
- .setup-startup-console {
- max-height: none;
- }
-
- .setup-startup-log {
- max-height: 260px;
- }
-
- .field-span-2 {
- grid-column: auto;
- }
-
- .setup-actions {
- flex-direction: column;
- align-items: stretch;
- }
-
- .setup-actions-right {
- width: 100%;
- flex-direction: column;
- }
-
- .primary-btn,
- .secondary-btn {
- width: 100%;
- }
-}
+.setup-page {
+ min-height: 100dvh;
+ display: grid;
+ grid-template-columns: minmax(320px, 392px) minmax(0, 1fr);
+ background:
+ radial-gradient(circle at top left, rgba(16, 185, 129, 0.24), transparent 24rem),
+ radial-gradient(circle at 36% 14%, rgba(16, 185, 129, 0.16), transparent 18rem),
+ linear-gradient(135deg, #04110d 0%, #0b1f18 26%, #10281f 26%, #eef5f1 26%, #f6fbf8 100%);
+}
+
+.setup-context {
+ padding: 42px 28px 32px;
+ color: rgba(255, 255, 255, 0.92);
+ display: grid;
+ align-content: start;
+ gap: 22px;
+ border-right: 1px solid rgba(110, 231, 183, 0.08);
+ background: linear-gradient(180deg, rgba(4, 17, 13, 0.92), rgba(16, 40, 31, 0.9));
+}
+
+.setup-brand {
+ display: flex;
+ gap: 18px;
+ align-items: flex-start;
+}
+
+.setup-brand-mark {
+ position: relative;
+ flex: 0 0 64px;
+ width: 64px;
+ height: 64px;
+ display: grid;
+ place-items: center;
+}
+
+.setup-brand-ring {
+ position: absolute;
+ inset: 0;
+ border-radius: 18px;
+ background:
+ linear-gradient(145deg, rgba(209, 250, 229, 0.96), rgba(52, 211, 153, 0.88)),
+ linear-gradient(145deg, rgba(16, 185, 129, 0.4), rgba(5, 150, 105, 0.6));
+ box-shadow:
+ 0 18px 36px rgba(16, 185, 129, 0.2),
+ inset 0 1px 0 rgba(255, 255, 255, 0.46);
+ transform: rotate(-8deg);
+}
+
+.setup-brand-ring::before {
+ content: '';
+ position: absolute;
+ inset: 7px;
+ border-radius: 14px;
+ border: 1px solid rgba(4, 120, 87, 0.22);
+}
+
+.setup-brand-core {
+ position: relative;
+ z-index: 1;
+ width: 42px;
+ height: 42px;
+ border-radius: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(3, 32, 24, 0.92);
+ color: #d1fae5;
+ font-size: 15px;
+ font-weight: 800;
+ letter-spacing: 0.14em;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
+}
+
+.setup-kicker {
+ margin-bottom: 8px;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: rgba(167, 243, 208, 0.86);
+}
+
+.setup-kicker-light {
+ color: rgba(209, 250, 229, 0.82);
+}
+
+.setup-context h1 {
+ color: #f4fff8;
+ font-size: clamp(1.9rem, 2.4vw, 2.5rem);
+ line-height: 1.08;
+ text-shadow: 0 12px 36px rgba(2, 12, 8, 0.34);
+}
+
+.setup-lead {
+ color: rgba(220, 252, 231, 0.84);
+ font-size: 14px;
+ line-height: 1.8;
+}
+
+.setup-nav {
+ display: grid;
+ gap: 10px;
+}
+
+.setup-nav-item {
+ width: 100%;
+ padding: 14px 14px 14px 12px;
+ border: 1px solid rgba(110, 231, 183, 0.12);
+ border-radius: 8px;
+ display: grid;
+ grid-template-columns: 44px minmax(0, 1fr) 18px;
+ align-items: center;
+ gap: 12px;
+ background: linear-gradient(160deg, rgba(10, 23, 18, 0.82), rgba(15, 39, 31, 0.72));
+ color: inherit;
+ text-align: left;
+ transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
+}
+
+.setup-nav-item:hover {
+ transform: translateY(-1px);
+ border-color: rgba(110, 231, 183, 0.22);
+}
+
+.setup-nav-item.is-active {
+ border-color: rgba(16, 185, 129, 0.4);
+ box-shadow: 0 14px 28px rgba(3, 10, 7, 0.22);
+}
+
+.setup-nav-item.is-complete {
+ background: linear-gradient(160deg, rgba(8, 31, 23, 0.96), rgba(12, 58, 44, 0.86));
+}
+
+.setup-nav-index {
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(16, 185, 129, 0.16);
+ color: #d1fae5;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+}
+
+.setup-nav-copy {
+ display: grid;
+ gap: 4px;
+}
+
+.setup-nav-copy strong {
+ color: #f0fdf4;
+ font-size: 14px;
+}
+
+.setup-nav-copy small {
+ color: rgba(209, 250, 229, 0.72);
+ font-size: 12px;
+ line-height: 1.55;
+}
+
+.setup-nav-check {
+ color: #6ee7b7;
+ font-size: 14px;
+}
+
+.setup-progress {
+ margin-top: 8px;
+ padding: 16px 18px;
+ border: 1px solid rgba(110, 231, 183, 0.14);
+ border-radius: 8px;
+ background: rgba(7, 33, 25, 0.76);
+}
+
+.setup-progress strong {
+ color: #f0fdf4;
+ font-size: 15px;
+}
+
+.setup-progress p {
+ margin-top: 8px;
+ color: rgba(209, 250, 229, 0.72);
+ font-size: 13px;
+ line-height: 1.65;
+}
+
+.setup-complete {
+ margin-top: auto;
+ padding: 16px 18px 0;
+ border-top: 1px solid rgba(110, 231, 183, 0.12);
+ display: grid;
+ gap: 12px;
+}
+
+.setup-complete p {
+ color: rgba(209, 250, 229, 0.76);
+ font-size: 13px;
+ line-height: 1.6;
+}
+
+.setup-complete-btn {
+ width: 100%;
+}
+
+.setup-complete-progress {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: rgba(209, 250, 229, 0.86);
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.setup-panel {
+ padding: 36px;
+ display: grid;
+ align-content: start;
+ gap: 24px;
+ background:
+ radial-gradient(circle at top left, rgba(16, 185, 129, 0.08), transparent 16rem),
+ linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0));
+}
+
+.setup-panel-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 16px;
+ padding: 20px 22px;
+ border: 1px solid rgba(16, 185, 129, 0.14);
+ border-radius: 8px;
+ background: linear-gradient(135deg, #063b2e, #0f5f49);
+ box-shadow: 0 16px 34px rgba(6, 59, 46, 0.16);
+}
+
+.setup-panel-head h2 {
+ color: #ffffff;
+ font-size: 28px;
+}
+
+.setup-panel-desc {
+ margin-top: 10px;
+ color: rgba(236, 253, 245, 0.82);
+ font-size: 14px;
+ line-height: 1.65;
+}
+
+.setup-chip {
+ display: inline-flex;
+ align-items: center;
+ min-height: 32px;
+ padding: 0 12px;
+ border-radius: 999px;
+ background: rgba(240, 253, 244, 0.14);
+ color: #d1fae5;
+ font-size: 12px;
+ font-weight: 700;
+ border: 1px solid rgba(209, 250, 229, 0.18);
+}
+
+.setup-chip.is-success {
+ background: rgba(16, 185, 129, 0.22);
+}
+
+.setup-form {
+ padding: 30px 32px;
+ border: 1px solid rgba(16, 185, 129, 0.18);
+ border-radius: 8px;
+ background: linear-gradient(180deg, rgba(244, 255, 248, 0.98), rgba(255, 255, 255, 0.94));
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12), 0 1px 0 rgba(255, 255, 255, 0.6) inset;
+ backdrop-filter: blur(14px);
+}
+
+.setup-stage {
+ display: grid;
+ gap: 22px;
+}
+
+.section-head {
+ display: grid;
+ gap: 6px;
+}
+
+.section-head h3 {
+ color: #065f46;
+ font-size: 18px;
+}
+
+.section-head p {
+ color: #5b6f67;
+ font-size: 13px;
+ line-height: 1.7;
+}
+
+.field-grid {
+ display: grid;
+ gap: 16px;
+}
+
+.field-grid-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.field {
+ display: grid;
+ gap: 8px;
+}
+
+.field span {
+ color: #244239;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.field-note {
+ color: #5f7c72;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.field-group-note {
+ margin-top: -6px;
+ color: #5f7c72;
+ font-size: 12px;
+ line-height: 1.6;
+}
+
+.optional-block {
+ padding: 18px 18px 0;
+ border: 1px dashed rgba(16, 185, 129, 0.22);
+ border-radius: 8px;
+ background: rgba(240, 253, 244, 0.52);
+}
+
+.optional-block-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+}
+
+.optional-block-head strong {
+ color: #065f46;
+ font-size: 14px;
+}
+
+.optional-block-head span {
+ min-height: 24px;
+ padding: 0 10px;
+ border-radius: 999px;
+ display: inline-flex;
+ align-items: center;
+ background: rgba(16, 185, 129, 0.12);
+ color: #047857;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.field input {
+ width: 100%;
+ min-height: 46px;
+ padding: 0 14px;
+ border: 1px solid rgba(148, 163, 184, 0.78);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.92);
+ color: #0f172a;
+ transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
+}
+
+.field input:hover {
+ transform: translateY(-1px);
+}
+
+.field input:focus {
+ border-color: #10b981;
+ box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.12);
+}
+
+.field-span-2 {
+ grid-column: span 2;
+}
+
+.setup-runtime {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.setup-runtime article {
+ position: relative;
+ overflow: hidden;
+ padding: 16px 18px;
+ border-radius: 8px;
+ border: 1px solid rgba(110, 231, 183, 0.14);
+ display: grid;
+ gap: 10px;
+ box-shadow: 0 14px 32px rgba(3, 10, 7, 0.2);
+}
+
+.setup-runtime article::before {
+ content: '';
+ position: absolute;
+ inset: 0 auto auto 0;
+ width: 100%;
+ height: 3px;
+ background: linear-gradient(90deg, rgba(209, 250, 229, 0.92), rgba(16, 185, 129, 0.48));
+}
+
+.setup-runtime article:nth-child(1) {
+ background: linear-gradient(150deg, rgba(7, 33, 25, 0.96), rgba(16, 67, 52, 0.9));
+}
+
+.setup-runtime article:nth-child(2) {
+ background: linear-gradient(150deg, rgba(7, 28, 26, 0.96), rgba(11, 59, 61, 0.9));
+}
+
+.setup-runtime article:nth-child(3) {
+ background: linear-gradient(150deg, rgba(16, 28, 19, 0.96), rgba(48, 74, 36, 0.9));
+}
+
+.setup-runtime span {
+ font-size: 12px;
+ text-transform: uppercase;
+ color: rgba(167, 243, 208, 0.7);
+}
+
+.setup-runtime strong {
+ color: #f8fffb;
+ font-size: 14px;
+ line-height: 1.5;
+ word-break: break-word;
+ text-shadow: 0 2px 16px rgba(4, 9, 7, 0.22);
+}
+
+.setup-error {
+ margin-top: 22px;
+ padding: 14px 16px;
+ border: 1px solid rgba(239, 68, 68, 0.18);
+ border-radius: 8px;
+ background: #fef2f2;
+ color: #b91c1c;
+ white-space: pre-line;
+}
+
+.setup-status {
+ margin-top: 22px;
+ padding: 14px 16px;
+ border-radius: 8px;
+ white-space: pre-line;
+}
+
+.setup-status.is-success {
+ border: 1px solid rgba(16, 185, 129, 0.18);
+ background: #ecfdf5;
+ color: #047857;
+}
+
+.setup-status.is-danger {
+ border: 1px solid rgba(239, 68, 68, 0.18);
+ background: #fef2f2;
+ color: #b91c1c;
+}
+
+.setup-gate {
+ margin-top: 14px;
+ padding: 12px 14px;
+ border: 1px solid rgba(245, 158, 11, 0.2);
+ border-radius: 8px;
+ background: #fffbeb;
+ color: #b45309;
+}
+
+.setup-actions {
+ margin-top: 28px;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12px;
+}
+
+.setup-actions-right {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.primary-btn,
+.secondary-btn {
+ min-height: 46px;
+ padding: 0 18px;
+ border-radius: 8px;
+ border: 1px solid transparent;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ font-weight: 700;
+ transition: transform 160ms ease, box-shadow 160ms ease, opacity 160ms ease;
+}
+
+.primary-btn {
+ background: linear-gradient(135deg, #10b981, #0f766e);
+ color: #fff;
+ box-shadow: 0 14px 28px rgba(16, 185, 129, 0.24);
+}
+
+.secondary-btn {
+ background: rgba(240, 253, 244, 0.94);
+ color: #1f4f41;
+ border-color: rgba(16, 185, 129, 0.18);
+}
+
+.secondary-btn-strong {
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.14), rgba(5, 150, 105, 0.12));
+ color: #065f46;
+}
+
+.primary-btn:hover,
+.secondary-btn:hover {
+ transform: translateY(-1px);
+}
+
+.primary-btn:disabled,
+.secondary-btn:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+ box-shadow: none;
+ transform: none;
+}
+
+.setup-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 50;
+ padding: 24px;
+ display: grid;
+ place-items: center;
+ background: rgba(3, 20, 15, 0.72);
+ backdrop-filter: blur(10px);
+}
+
+.setup-startup-modal {
+ width: min(1120px, 100%);
+ max-height: calc(100vh - 48px);
+ overflow: hidden;
+ padding: 24px;
+ border: 1px solid rgba(110, 231, 183, 0.22);
+ border-radius: 8px;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ gap: 20px;
+ background:
+ radial-gradient(circle at top right, rgba(16, 185, 129, 0.16), transparent 18rem),
+ linear-gradient(180deg, #05251d 0%, #081611 100%);
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.42);
+}
+
+.setup-startup-head {
+ display: flex;
+ justify-content: space-between;
+ gap: 18px;
+ align-items: flex-start;
+}
+
+.setup-startup-head h2 {
+ margin-top: 4px;
+ color: #ffffff;
+ font-size: 22px;
+}
+
+.setup-startup-head span {
+ display: block;
+ margin-top: 8px;
+ color: rgba(209, 250, 229, 0.78);
+ font-size: 13px;
+ line-height: 1.6;
+}
+
+.setup-startup-spinner {
+ width: 54px;
+ height: 54px;
+ border: 1px solid rgba(110, 231, 183, 0.26);
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+ flex: 0 0 auto;
+ color: #a7f3d0;
+ background: rgba(6, 78, 59, 0.34);
+}
+
+.setup-startup-spinner .pi {
+ font-size: 22px;
+}
+
+.setup-startup-spinner strong {
+ color: #ffffff;
+ font-size: 20px;
+}
+
+.setup-startup-body {
+ min-height: 0;
+ display: grid;
+ grid-template-columns: minmax(330px, 0.86fr) minmax(460px, 1.14fr);
+ gap: 18px;
+}
+
+.setup-startup-steps {
+ min-height: 0;
+ overflow: auto;
+ padding-right: 2px;
+ display: grid;
+ align-content: start;
+ gap: 10px;
+}
+
+.setup-startup-step {
+ padding: 14px;
+ border: 1px solid rgba(148, 163, 184, 0.16);
+ border-radius: 8px;
+ display: grid;
+ grid-template-columns: 24px 1fr;
+ gap: 12px;
+ background: rgba(15, 23, 42, 0.24);
+}
+
+.setup-startup-step .pi {
+ margin-top: 2px;
+ color: rgba(209, 250, 229, 0.46);
+}
+
+.setup-startup-step strong {
+ color: #f8fffb;
+ font-size: 14px;
+}
+
+.setup-startup-step span {
+ display: block;
+ margin-top: 4px;
+ color: rgba(209, 250, 229, 0.68);
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.setup-startup-step.is-running {
+ border-color: rgba(59, 130, 246, 0.34);
+}
+
+.setup-startup-step.is-running .pi {
+ color: #93c5fd;
+}
+
+.setup-startup-step.is-success {
+ border-color: rgba(16, 185, 129, 0.32);
+}
+
+.setup-startup-step.is-success .pi {
+ color: #34d399;
+}
+
+.setup-startup-step.is-error {
+ border-color: rgba(248, 113, 113, 0.36);
+}
+
+.setup-startup-step.is-error .pi {
+ color: #f87171;
+}
+
+.setup-startup-console {
+ min-height: 0;
+ border: 1px solid rgba(148, 163, 184, 0.16);
+ border-radius: 8px;
+ overflow: hidden;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ background: rgba(2, 6, 23, 0.42);
+}
+
+.setup-startup-console-head {
+ min-height: 44px;
+ padding: 0 14px;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.14);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ background: rgba(15, 23, 42, 0.5);
+}
+
+.setup-startup-console-head strong {
+ color: #e2e8f0;
+ font-size: 13px;
+}
+
+.setup-startup-console-head span {
+ color: rgba(148, 163, 184, 0.9);
+ font-size: 12px;
+}
+
+.setup-startup-log {
+ min-height: 0;
+ overflow: auto;
+ padding: 14px;
+ color: rgba(226, 232, 240, 0.84);
+ font-size: 12px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+@media (max-width: 1180px) {
+ .setup-page {
+ grid-template-columns: 1fr;
+ background:
+ radial-gradient(circle at top right, rgba(16, 185, 129, 0.2), transparent 22rem),
+ linear-gradient(180deg, #04110d 0%, #10281f 36%, #eef5f1 36%, #f6fbf8 100%);
+ }
+
+ .setup-context,
+ .setup-panel {
+ padding: 28px 24px;
+ }
+
+ .setup-complete {
+ padding-inline: 0;
+ }
+}
+
+@media (max-width: 820px) {
+ .field-grid-2,
+ .setup-runtime {
+ grid-template-columns: 1fr;
+ }
+
+ .setup-modal-backdrop {
+ padding: 14px;
+ }
+
+ .setup-startup-modal {
+ max-height: calc(100vh - 28px);
+ padding: 18px;
+ }
+
+ .setup-startup-head {
+ align-items: center;
+ }
+
+ .setup-startup-body {
+ grid-template-columns: 1fr;
+ }
+
+ .setup-startup-steps,
+ .setup-startup-console {
+ max-height: none;
+ }
+
+ .setup-startup-log {
+ max-height: 260px;
+ }
+
+ .field-span-2 {
+ grid-column: auto;
+ }
+
+ .setup-actions {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .setup-actions-right {
+ width: 100%;
+ flex-direction: column;
+ }
+
+ .primary-btn,
+ .secondary-btn {
+ width: 100%;
+ }
+}
diff --git a/web/src/components/business/PersonalWorkbench.vue b/web/src/components/business/PersonalWorkbench.vue
index 2cf65ed..0eb7f29 100644
--- a/web/src/components/business/PersonalWorkbench.vue
+++ b/web/src/components/business/PersonalWorkbench.vue
@@ -833,5 +833,5 @@ const policyItems = [
}
}
-
-
+
+
diff --git a/web/src/components/layout/TopBar.vue b/web/src/components/layout/TopBar.vue
index eb66219..6cacee0 100644
--- a/web/src/components/layout/TopBar.vue
+++ b/web/src/components/layout/TopBar.vue
@@ -1,113 +1,113 @@
-
-