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与自动化运营看板

-
- -
- -
-
- -
- -
- - -
- -
- -
-
- - - -
-
- - -
- - - -
-
- - -
- -
- -
-
-
-

待审批单据

-

128

-
-
- -
-
-
- - 12.5% - - 较昨日 -18 单 -
-
- - -
-
-
-

待处理金额

-

¥361,600

-
-
- -
-
-
- - 8.3% - - 较昨日 +¥27,400 -
-
- - -
-
-
-

平均审批时长

-

6.8 h

-
-
- -
-
-
- - 14.8% - - 较昨日 -1.2h -
-
- - -
-
-
-

自动审单通过率

-

78 %

-
-
- -
-
-
- - 6.2% - - 较昨日 +4.6% -
-
- - -
-
-
-

异常预警单

-

14

-
-
- -
-
-
- - 16.7% - - 较昨日 +2 单 -
-
- - -
-
-
-

SLA达成率

-

96 %

-
-
- -
-
-
- - 3.1% - - 较昨日 +2.9% -
-
-
- - -
- -
-
-

报销申请与审批趋势

- -
-
- -
-
- - -
-
-

费用结构

-
-
- -
-

* 百分比为占待处理金额比例

-
- - -
-
-

风险异常分布

-
-
- -
-

* 近30天数据

-
-
- - -
- -
-
-

部门报销排行(待处理金额)

- -
-
-
- 1 - 销售部 -
-
-
- ¥182,000 -
-
- 2 - 研发中心 -
-
-
- ¥146,000 -
-
- 3 - 市场部 -
-
-
- ¥96,000 -
-
- 4 - 运营部 -
-
-
- ¥68,600 -
-
- 5 - 行政部 -
-
-
- ¥48,300 -
-
-
- - -
-
-

审批瓶颈(平均处理时长)

-
-
-
-
- 李文静 -
-

李文静

-

财务经理

-
-
-
-

12.4 h

- 较慢 -
-
-
-
- 王志强 -
-

王志强

-

财务专员

-
-
-
-

8.7 h

- 偏慢 -
-
-
-
- 刘思雨 -
-

刘思雨

-

费用审核员

-
-
-
-

5.2 h

- 正常 -
-
-
- -
- - -
-
-

预算执行率(本月)

-
-
- -
-
-
-

预算总额

-

¥2,800,000

-
-
-

已执行

-

¥2,128,000

-
-
-

剩余可用

-

¥672,000

-
-
- -
-
-
-
-
- - - + + + + + + 企业报销智能运营台 + + + + + + + + + + +
+ + + + +
+ +
+
+
+
Smart Expense Operations
+

企业报销智能运营台

+

面向财务共享中心的审批、风控、SLA与自动化运营看板

+
+ +
+ +
+
+ +
+ +
+ + +
+ +
+ +
+
+ + + +
+
+ + +
+ + + +
+
+ + +
+ +
+ +
+
+
+

待审批单据

+

128

+
+
+ +
+
+
+ + 12.5% + + 较昨日 -18 单 +
+
+ + +
+
+
+

待处理金额

+

¥361,600

+
+
+ +
+
+
+ + 8.3% + + 较昨日 +¥27,400 +
+
+ + +
+
+
+

平均审批时长

+

6.8 h

+
+
+ +
+
+
+ + 14.8% + + 较昨日 -1.2h +
+
+ + +
+
+
+

自动审单通过率

+

78 %

+
+
+ +
+
+
+ + 6.2% + + 较昨日 +4.6% +
+
+ + +
+
+
+

异常预警单

+

14

+
+
+ +
+
+
+ + 16.7% + + 较昨日 +2 单 +
+
+ + +
+
+
+

SLA达成率

+

96 %

+
+
+ +
+
+
+ + 3.1% + + 较昨日 +2.9% +
+
+
+ + +
+ +
+
+

报销申请与审批趋势

+ +
+
+ +
+
+ + +
+
+

费用结构

+
+
+ +
+

* 百分比为占待处理金额比例

+
+ + +
+
+

风险异常分布

+
+
+ +
+

* 近30天数据

+
+
+ + +
+ +
+
+

部门报销排行(待处理金额)

+ +
+
+
+ 1 + 销售部 +
+
+
+ ¥182,000 +
+
+ 2 + 研发中心 +
+
+
+ ¥146,000 +
+
+ 3 + 市场部 +
+
+
+ ¥96,000 +
+
+ 4 + 运营部 +
+
+
+ ¥68,600 +
+
+ 5 + 行政部 +
+
+
+ ¥48,300 +
+
+
+ + +
+
+

审批瓶颈(平均处理时长)

+
+
+
+
+ 李文静 +
+

李文静

+

财务经理

+
+
+
+

12.4 h

+ 较慢 +
+
+
+
+ 王志强 +
+

王志强

+

财务专员

+
+
+
+

8.7 h

+ 偏慢 +
+
+
+
+ 刘思雨 +
+

刘思雨

+

费用审核员

+
+
+
+

5.2 h

+ 正常 +
+
+
+ +
+ + +
+
+

预算执行率(本月)

+
+
+ +
+
+
+

预算总额

+

¥2,800,000

+
+
+

已执行

+

¥2,128,000

+
+
+

剩余可用

+

¥672,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 @@ -