commit 0a6f048c0de724beb89b7d6d67a082e8e57230f6 Author: jif2.zhang Date: Sat Sep 26 22:46:15 2026 +0800 chore: Storage 初版 — 家庭共享物品管理 (React+TS+Vite+Express+DuckDB) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9632add --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +data +.git +.gitea +ota-package +*.log \ No newline at end of file diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..70f1e19 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,58 @@ +name: Build and Deploy + +# 每次主分支提交: 构建前端 + Docker 镜像 → 在 runner (baihu) 上重建容器自动部署 +# 端口 3003 (与 Wardrobe :3002 / iboard :3001 同机共存, 按部署环境分配, 见 部署环境.md) +# 数据持久化: 具名卷 storage-data 挂载 /app/data (DuckDB + 图片), 重建容器不丢数据 +on: + push: + branches: + - main + - master + +jobs: + build-image: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: https://gitea.com/actions/checkout@v4 + + - name: Setup Node + uses: https://gitea.com/actions/setup-node@v4 + with: + node-version: 22 + + - name: Install deps + run: npm ci --no-audit --no-fund + + - name: Typecheck & build + run: | + npm run check + npm run build + + - name: Build Docker image + run: docker build -t storage:latest . + + - name: Remove old container + run: docker rm -vf storage || true + + - name: Run container + # 一键镜像: 静态前端 + API + 图片; 端口 3003 + run: docker run -d --name storage -p 3003:3003 \ + -v storage-data:/app/data \ + storage:latest + + - name: Verify health + run: | + for i in $(seq 1 20); do + if curl -sf http://127.0.0.1:3003/healthz >/dev/null 2>&1; then + echo "storage healthy after ${i}s" + exit 0 + fi + sleep 1 + done + echo "ERROR: storage did not become healthy" >&2 + docker logs storage --tail 50 2>&1 + exit 1 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..670a94d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +data/ +*.local +.DS_Store +ota-package/ \ No newline at end of file diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..84db9e7 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,3 @@ +[tools] +node = "24.21.0" +rust = "stable" \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..2d78846 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,427 @@ +# Storage 个人物品管理 App —— 设计文档 + +> 版本:v0.1(草稿) +> 日期:2026-09 +> 关联:REQUIREMENTS.md + +--- + +## 1. 架构总览 + +沿用 Wardrobe 已验证的 **三层薄客户端架构**: + +``` +┌──────────────────────────────────────────────┐ +│ 客户端(Tauri 2 壳 + React SPA) │ +│ pages / stores(Zustand) / api(fetch) / types │ +└──────────────────────┬───────────────────────┘ + │ HTTP REST (Bearer Token) +┌──────────────────────▼───────────────────────┐ +│ 服务端(Express Node 22) │ +│ routes/app.js → 校验 + 权限(家庭角色) │ +│ │ │ +│ ▼ │ +│ repository(server/db.js)DuckDB DAO + 迁移 │ +└──────────────────────┬───────────────────────┘ + │ 单机上写串行化(enqueue) +┌──────────────────────▼───────────────────────┐ +│ DuckDB 文件(data/storage.duckdb) │ +│ 用户/会话/家庭/成员/盒子/物品/图片/日志 │ +└──────────────────────────────────────────────┘ +``` + +- 客户端无本地数据库,仅缓存当前数据到 `localStorage`(沿用 Wardrobe 的 `RouterGate` 恢复方案)。 +- 服务端为唯一事实来源;所有写操作经家庭角色权限校验。 +- 客户端 UI 仅做展示层隐藏/禁用,不承载鉴权。 + +--- + +## 2. 技术选型 + +| 层 | 技术 | 说明 | +|---|---|---| +| 前端 | React 18 + TypeScript 5 + Vite 5 | 沿用 Wardrobe | +| 状态管理 | Zustand | stores/family.ts、stores/data.ts 等 | +| 路由 | React Router v6 | `/home` `/boxes` `/items` `/families` `/profile` | +| 桌面壳 | Tauri 2(Rust),hotswap 热更新可选 | 沿用 Wardrobe | +| 后端 | Express 4(Node ≥ 18,`--experimental-strip-types`) | 沿用 | +| 数据库 | DuckDB(@duckdb/node-api)单写串行化 | 沿用 `enqueue` | +| 图片 | 本地文件存储 + 鉴权读取代理 | 新建(Wardrobe 未实现) | +| 构建部署 | 单 Docker 镜像 + Gitea Actions | 沿用 Wardrobe | + +--- + +## 3. 目录结构 + +``` +D:\workbench\Storage\ +├── index.html / vite.config.ts / tsconfig.json / package.json +├── REQUIREMENTS.md / DESIGN.md +├── Dockerfile / .mise.toml +├── scripts/ # 构建、签名、上传(沿用) +├── server/ +│ ├── index.js # 启动、静态、SPA fallback +│ ├── db.js # 连接 + 迁移 + DAO + MCP +│ ├── auth.js # 验证码/会话 +│ └── routes/ +│ ├── app.js # 业务 REST API +│ ├── images.js # 图片上传/读取(鉴权) +│ └── ota.js # 热更新(沿用) +│ └── data/uploads/ # 图片文件(数据卷) +├── src-tauri/ # Tauri 壳(沿用) +└── src/ + ├── main.tsx / App.tsx / version.ts + ├── api/ # config.ts / index.ts(类型化 fetch)/ images.ts + ├── stores/ # auth / family / boxes / items(Zustand) + ├── types/index.ts # 全部领域类型(单一来源,避免 Wardrobe 重复定义问题) + ├── components/ # Layout, Breadcrumb, BoxCanvas, BoxNode, ItemCard, FamilySwitcher... + ├── pages/ # Home, Boxes(树+画布), BoxDetail, ItemList, ItemDetail, ItemEdit, FamilyManage, Profile + └── styles/global.css # 设计令牌(沿用 Wardrobe 暖色系) +``` + +> 改进点(相对 Wardrobe):类型集中到 `src/types/index.ts`,服务端用 `server/types.ts` 与之一致;新增正式迁移机制 §5。 + +--- + +## 4. 领域模型(ER) + +``` +users ──< family_members >── families +family_id 归属 │ + ┌───────────────────────┼─────────────────────┐ + ▼ ▼ ▼ + categories boxes ──` fetch 封装与 Bearer Token。所有写接口经 `requireFamilyAdmin(familyId)` 校验角色(管理员);读取接口 `requireFamilyMember` 校验成员身份。 + +### 6.1 路由总表 + +| 方法 | 路径 | 权限 | 说明 | +|---|---|---|---| +| POST | `/api/auth/send-code` `/register` `/login` `/logout` | 公开 | 沿用 Wardrobe | +| GET | `/api/auth/me` | 登录 | | +| GET | `/api/families` | 登录 | 我加入的家庭列表 | +| POST | `/api/families` | 登录 | 创建家庭(成为管理员) | +| POST | `/api/families/:id/join` | 登录 | 凭 invite_code 加入 | +| GET | `/api/families/:id` | 成员 | 家庭详情 + 成员列表 | +| PUT | `/api/families/:id` `DELETE` | 管理员/创建者 | 改名;解散 | +| POST | `/api/families/:id/invite` | 管理员 | 生成/刷新/失效邀请码 | +| POST | `/api/families/:id/members/:userId/role` | 管理员 | 改角色 admin/member | +| DELETE | `/api/families/:id/members/:userId` | 管理员 | 移除成员;离开家庭为 `DELETE /members/me` | +| GET | `/api/families/:id/categories` | 成员 | | +| POST | `/api/families/:id/categories` | 管理员 | | +| PUT/DELETE | `/api/families/:id/categories/:catId` | 管理员 | | +| GET | `/api/families/:id/boxes?parent=null\|:boxId` | 成员 | 某容器的直接子盒(根层=parent 缺省) | +| GET | `/api/families/:id/boxes/:boxId` | 成员 | 盒子详情 + 面包屑路径 | +| POST | `/api/families/:id/boxes` | 管理员 | 新建盒子(parent_box_id + 位置) | +| PUT | `/api/families/:id/boxes/:boxId` | 管理员 | 改名/图标/颜色 | +| DELETE | `/api/families/:id/boxes/:boxId` | 管理员 | 删除(含子树,二次确认) | +| PUT | `/api/families/:id/boxes/:boxId/move` | 管理员 | `{parent_box_id, pos_x, pos_y}` 整盒移动(跨容器) | +| PUT | `/api/families/:id/boxes/:boxId/position` | 管理员 | 仅调整位置(拖拽保存) | +| GET | `/api/families/:id/items?box=...&category=...&tag=...&q=&favorite=&expiring=` | 成员 | 列表/搜索/筛选;`box` 支持递归子树 | +| POST | `/api/families/:id/items` | 管理员 | | +| GET | `/api/families/:id/items/:itemId` | 成员 | 含所在路径 | +| PUT | `/api/families/:id/items/:itemId` | 管理员 | 全字段更新 | +| DELETE | `/api/families/:id/items/:itemId` | 管理员 | | +| PUT | `/api/families/:id/items/:itemId/move` | 管理员 | `{box_id\|null}` 移动盒/根层 | +| POST | `/api/families/:id/transfer` | 管理员 | `{type:'item'\|'box', entityId, dstFamilyId, dstParentBoxId?, pos?}` 跨家庭转移 | +| GET | `/api/families/:id/stats` | 成员 | 概览统计 | +| POST | `/api/images` | 登录 | multipart 上传,返回路径 | +| GET | `/api/images/*` | 成员 | 鉴权读取(校验所在家庭成员) | +| GET | `/api/transfer-logs?familyId=` | 管理员 | P1 | +| GET/POST | `/api/mcp/tools` `/call` | 登录 | P1,沿用 Wardrobe | + +### 6.2 盒子树读取与递归 + +- **单容器展开**(懒加载):`GET boxes?parent=X` 只返回直接子盒,前端按需展开——避免一次拉整棵树。 +- **全树**(用于画布俯视/统计):`GET boxes?tree=1`,服务端用递归 CTE 一次性返回子树(深度 <100 校验防环)。 + +```sql +WITH RECURSIVE subtree AS ( + SELECT id, parent_box_id, name FROM boxes WHERE id = ? AND family_id = ? + UNION ALL + SELECT b.id, b.parent_box_id, b.name + FROM boxes b JOIN subtree s ON b.parent_box_id = s.id + WHERE b.family_id = ? +) +SELECT * FROM subtree; +``` + +- **子树的物品计数 / 汇总**:`items JOIN subtree` 聚合,一个查询得到整棵子树物品数与价值合计。 + +### 6.3 权限校验(中间件) + +```js +async function requireFamilyMember(req, res, next) { + // 1) 认证会话 → req.user + // 2) 查 family_members → 非成员 403 +} +async function requireFamilyAdmin(familyId) { + // 成员且 role==='admin',否则 403 +} +``` + +- `requireFamilyMember` 用于读取/列表/搜索;`requireFamilyAdmin` 用于一切写操作。 +- `transfer` 需同时满足:操作者是 **源家庭管理员**,且是 **目标家庭成员**(防越权散落)。 + +--- + +## 7. 盒子位置模型(画布) + +### 7.1 坐标系 + +- 每个父容器(父盒子或家庭根层)的内容区 = 一张 **归一化虚拟画布**,坐标 `x,y ∈ [0,1000]`(整数)。 +- 子盒子渲染为其内部的一张方块,方块尺寸 v1 取「自动」(如 根据内容大小 / 固定网格),`width/height` 字段为预留尺寸。 +- 位置允许重叠;「自动整理」按钮将同一容器的直接子盒按蛇形序、以统一网格间距重新分配坐标(P1)。 + +### 7.2 交互 + +- **画布视图**:`BoxCanvas` 将父容器绘制为比例画布,子盒为绝对定位 `div`(`left=x/1000*100%`),支持拖拽更新 `pos_x/pos_y`;松手后 `PUT /boxes/:id/position` 保存。 +- **进入多层**:点击子盒 → 进入其内部(面包屑 `家庭 / 客厅 / 柜子1 / 收纳盒x`);根层面包屑第一项即家庭。 +- **列表视图**:同容器内容按位置/创建时间扁平排列,与画布视图同一数据源、可自由切换。 +- **整盒移动**:画布/列表中选择「移动到…」,弹窗选择目标容器(懒加载树选择器)→ `PUT /boxes/:id/move`(同时提交目标容器内新位置)。 + +### 7.3 约束 + +- `parent_box_id` 不允许设置成自身或自身后代(防环):服务端在 move 时先递归校验。 +- 移动/转移后清空并重载当前容器缓存。 + +--- + +## 8. 关键业务流程 + +### 8.1 跨家庭转移(整盒含物品) + +``` +1) 管理员对 box B(或 item I)发起 transfer,目标家庭 F2,目标容器 parent(可选) +2) 服务端校验:源家庭管理员 + 目标家庭成员 +3) 防环校验(如为盒子) +4) 事务中:递归取出 B 子树;源框 items 与 boxes 全部重写 family_id=F2; + 根盒置位 parent(或 F2 根层)+ 新位置;物品同步迁移 +5) 写 transfer_logs 审计 +6) 返回新实体;客户端切换当前家庭后可见 +``` + +### 8.2 权限矩阵 + +| 操作 | 管理员 | 成员 | +|---|---|---| +| 查看家成员/盒子树/物品/搜索/统计 | ✅ | ✅ | +| 新建/编辑/删除 盒子、物品、分类 | ✅ | ❌ 403 | +| 整盒移动、物品移动 | ✅ | ❌ | +| 邀请/移员/改角色 | ✅ | ❌ | +| 跨家庭转移 | ✅(需为目标家成员) | ❌ | +| 解散家庭 | 仅创建者 | ❌ | + +--- + +## 9. 前端设计 + +### 9.1 状态(Zustand) + +- `stores/auth.ts`:账号/验证码表单(沿用)。 +- `stores/app.ts`:`user`, `token`, `currentFamilyId`, `families[]`, `boxesById`, `items[]`, `stats`, `breadcrumb[]`;`logout` 清空;`RouterGate` 持久化到 `localStorage["storage_user"]`。 +- 数据加载沿用「页面 useEffect 拉取写 store」模式;切换家庭时清空该家庭缓存。 + +### 9.2 页面与导航 + +- 底部导航 5 项:**首页** · **盒子**(树+画布)· **+**(新增物品 FAB)· **物品**(列表/搜索)· **我的**(家庭管理/退出)。 +- 顶部:家庭切换器(下拉)— 切换即重载全部数据。 +- 页面:`Home.tsx`(统计+待办)、`Boxes.tsx`(树+画布俯视)、`BoxDetail.tsx`(面包屑+子盒+物品)、`ItemList.tsx`、`ItemDetail.tsx`、`ItemEdit.tsx`、`FamilyManage.tsx`(成员/邀请码/分类)、`Profile.tsx`。 + +### 9.3 关键组件 + +- `BoxCanvas`:归一化画布 + 绝对定位子盒 + 拖拽(pointer events)+ 自动整理。 +- `BoxTreePicker`:懒加载树选择器(选择目标容器)。 +- `Breadcrumb`:位置路径导航。 +- `FamilySwitcher`:当前家庭下拉。 +- `PermissionGuard`:按 `role` 隐藏/禁用写操作控件(仅展示层,服务端仍校验)。 + +### 9.4 图片 + +- 上传:`POST /api/images`(multipart)→ 存 `server/data/uploads/.`,返回相对路径。 +- 读取:`GET /api/images/.`,中间件解析 Token 并校验当前用户为文件所属家庭(文件名前缀含 family_id 便于校验,或读库校验)成员。 +- 前端:`` 直接引用 `/api/images/...`(带 Token 的 header 方式对 `` 不便 → 用 fetch 转 blob 或 cookie 会话,二选一,见 §11 风险)。 + +--- + +## 10. 统计实现(首页) + +沿用 Wardrobe 的服务器端 SQL 聚合思路(`GET /stats`): + +```sql +-- 总物品数(当前家庭) +SELECT COUNT(*) FROM items WHERE family_id = ?; +-- 分类分布 +SELECT COALESCE(c.name,'未分类'), COUNT(i.id) +FROM periods... -- 简略:LEFT JOIN categories c, GROUP BY 零填充 6 项 → 交给前端补零 +-- 盒子占用 Top N(含嵌套子盒物品数,递归 CTE) +-- 临期提醒:expiry_date BETWEEN today AND today+30 +-- 深度:递归 CTE 求 family_id 下每盒子最大 depth,取 MAX +``` + +--- + +## 11. 安全、并发与风险 + +| 主题 | 方案 | +|---|---| +| 权限 | 服务端 `requireFamilyMember/Admin` 为唯一权威;客户端仅隐藏 UI | +| 防环 | move/transfer 前递归校验目标不得为自身或后代 | +| 并发写 | DuckDB 单写 → 全局 `enqueue` 串行化(沿用);事务包裹转移 | +| 图片鉴权 | 经 `/api/images` 代理读取,未授权 403;不上传落地任何静态目录 | +| 会话 | Bearer Token + 服务端 sessions 表,可撤销 | +| 验证码 | 开发=固定码 1111;预留 provider 抽象,上线换真实短信 | +| Token 与 `` | 若 header 不便,改用 SSE 前先采用「短时签名 URL」或 cookie;评审时定 | +| 迁移失败 | 启动中止 + 明确报错,禁止隐式降级 | + +--- + +## 12. 迁移与部署 + +- **本地开发**:`npm run dev`(Vite:1420)→ `npm run tauri dev`。 +- **产物**:`vite-plugin-singlefile` 单文件 + 服务端;`docker build` 单镜像 `node:22-slim`,`DB_PATH` 默认 `./data/storage.duckdb`(数据卷 `storage-data`),图片存在 `data/uploads`(同卷),端口沿用习惯(建议 3002 或另定)。 +- **CI**:沿用 Wardrobe 的 Gitea Actions —— `docker-build.yml`(主分支)、`android-build.yml`(APK 签名发布)、可选 `ota-build.yml`(hotswap 热更新,P1 接入)。 +- **OEM 差异**:首发可只做 Web + Docker + Android APK,桌面壳作为增量。 + +--- + +## 13. 分阶段实施计划 + +| 阶段 | 内容 | 交付物 | +|---|---|---| +| M1 | 骨架:auth、家庭创建/切换、`schema_version` 迁移、types 集中 | 登录 + 家庭空壳可运行 | +| M2 | 盒子树 + 画布位置 + 面包屑;物品 CRUD + 移动 | 核心收纳闭环 | +| M3 | 搜索/筛选/定位;分类/标签;图片上传鉴权 | 日常可用 | +| M4 | 权限矩阵(admin/member 中间件);跨家庭转移 + 审计 | 家庭协作闭环 | +| M5 | 统计首页;邀请码/成员管理;MCP;打包部署(Docker/APK) | v0.1 发布 | +| P1+ | 操作日志 UI、导出、备份、真实短信、使用记录 | 迭代 | + +--- + +## 14. 待评审决策点 + +1. 图片鉴权采用短时签名 URL。 +2. 目标家庭转移权限仅能转移到自己拥有管理员的家庭。 +3. 盒子尺寸(width/height)支持自定义。 +4. 端口根据部署环境分配。 +5. 部署环境请参考部署环境.md文件 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cffc442 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# =============================================================== +# Storage 一体化镜像: 静态前端 (dist/) + Express/DuckDB 服务端 (server/) 同进程 +# 构建: docker build -t storage:latest . +# 可选 --build-arg VITE_API_BASE=https://api.example.com 让前端走绝对地址 (默认同源 /api) +# 运行: docker run -d --name storage -p 3003:3003 -v storage-data:/app/data storage:latest +# 端口: 与 Wardrobe(:3002) / iboard(:3001) 同机共存, 按部署环境分配 (见 部署环境.md) +# 数据: DuckDB (data/storage.duckdb) + 图片 (data/uploads) 都在 /app/data 卷, 部署不丢数据 +# =============================================================== +FROM node:22-slim AS builder + +WORKDIR /app + +# 国内构建时可换源 +RUN npm config set registry https://registry.npmmirror.com + +# 先拷贝 package 文件, 利用 Docker 缓存 +COPY package*.json ./ + +RUN npm install + +# 拷贝源码 +COPY . . + +# 构建前端 (输出到 dist/); 留空 = 同源 /api +ARG VITE_API_BASE= +ENV VITE_API_BASE=$VITE_API_BASE +RUN npm run build + +# 运行时阶段 (slim: @duckdb/node-api 用 glibc 预编译, 不能用 alpine musl) +FROM node:22-slim + +WORKDIR /app + +RUN npm config set registry https://registry.npmmirror.com + +# 只装运行时依赖 +COPY package*.json ./ +RUN npm install --omit=dev + +# 业务数据目录: DuckDB + 图片上传, 运行时挂 -v storage-data:/app/data 持久化 +RUN mkdir -p /app/data +ENV DB_PATH=/app/data/storage.duckdb +ENV UPLOAD_DIR=/app/data/uploads + +# 拷贝构建产物 (server/ 为纯 JS, 直接 node 运行, 无需 strip-types) +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/server ./server + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3003/healthz || exit 1 + +ENV NODE_ENV=production +ENV PORT=3003 +EXPOSE 3003 + +CMD ["node", "server/index.js"] \ No newline at end of file diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..5140b9e --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,209 @@ +# Storage 个人物品管理 App —— 需求文档 + +> 版本:v0.1(草稿) +> 日期:2026-09 +> 状态:待评审 +> 参考实现:D:\workbench\Wardrobe(Tauri 2 + React + TS + Express + DuckDB 架构) + +--- + +## 1. 项目背景与目标 + +Wardrobe 是一款只管理「衣服」的衣柜 App,其记录、整理、统计分析的能力已被验证。 +Storage 将沿用 Wardrobe 的架构与产品理念,但把管理对象从「衣服」扩展到「个人所有物品」。 + +典型使用场景: + +- 搬进新家后,把客厅、卧室、厨房的每件物品登记下来,想找东西时快速定位「在哪个柜子的哪个收纳盒里」。 +- 给每一件物品拍照、打标签、记录购买信息与保质期,临期自动提醒。 +- 全家共用一份物品账本,谁都想把东西登记进去,但只有管理员能改,避免误删。 +- 把物品从「A 家庭(如父母家)」搬到「B 家庭(自己的家)」,盒子整体搬走。 + +### 1.1 与 Wardrobe 的核心差异 + +| 维度 | Wardrobe | Storage | +|---|---|---| +| 管理对象 | 仅衣物 | 所有物品 | +| 归属单位 | 个人(user_id 隔离) | 家庭(family_id 隔离,家庭内共享) | +| 收纳体系 | 无(物品平铺) | 无限层级的「盒子」树 + 盒内相对位置 | +| 分类方式 | 固定 6 类枚举 | 家庭可自定义分类 + 自由标签 | +| 权限 | 仅本人 | 管理员(可改)/ 成员(只读) | +| 跨域移动 | 不适用 | 物品与盒子可整体跨家庭转移 | + +--- + +## 2. 产品定位与范围 + +- **形态**:个人/家庭物品台账 + 收纳可视化定位系统。 +- **核心价值**:(1) 分类、定位、一目了然;(2) 家庭共享协作;(3) 物品生命周期记录(购买、保质期、保养)。 +- **范围外**(本期不包含): + - 购物、求购、二手交易等电商功能; + - 相机的 AR 识别、AI 自动分类(预留接口,不作为本期承诺); + - 物品维修/送洗的线下对接; + - 多端实时协同编辑(采用服务端为准、前端请求式刷新,不做 CRDT)。 + +--- + +## 3. 术语定义 + +| 术语 | 含义 | +|---|---| +| 家庭(Family) | 一组用户共享的协作空间,是所有物品、盒子、分类的归属单位。物品与盒子在家庭内共享。 | +| 家庭根层(Root) | 家庭内除盒子外的「顶层空间」。物品可以散放在根层,不必须放入盒子。 | +| 盒子(Box) | 收纳容器,可无限嵌套。盒子从属一个家庭,可整体移动/转移。 | +| 子盒(Sub-Box) | 某盒子内部的下一层盒子。 | +| 位置(Position) | 盒子在「父容器」内部的相对坐标(归一化 0~1000),用于将其绘制为父容器内的一块方块。 | +| 物品(Item) | 被管理的具体物件。强绑定一个家庭;可放入任意盒子,也可只在根层。 | +| 管理员 / 成员 | 家庭内的两种角色。管理员可修改,成员只读。 | +| 跨家庭转移(Transfer) | 将物品或整个盒子(含全部子盒与物品)从 A 家庭移动到 B 家庭。 | + +--- + +## 4. 用户与权限 + +### 4.1 账号体系 + +- 沿用 Wardrobe:手机号/邮箱 + 验证码登录,Bearer Token 会话(`/api/auth/*`)。 +- 验证码:开发期使用固定码 `1111`;上线前接入真实短信/邮件通道(见非功能需求)。 +- 一个用户可属于 **多个家庭**,任意时刻「当前家庭」为全局上下文。 + +### 4.2 家庭 + +- 用户可 **创建家庭**,创建者为「创建者」并自动成为管理员。 +- 用户可 **加入已有家庭**:通过邀请码或管理员邀请(先做邀请码)。 +- 用户可 **离开家庭** / 被移除;**创建者可解散家庭**。 +- 家庭名、家庭头像(首页家庭切换器展示)。 + +### 4.3 角色与权限(关键规则) + +- 角色仅两级且不限数量: + - **管理员**:可增/删/改家庭内所有物品、盒子、分类;可管理成员(邀请、移除、改角色);可把物品/盒子转移到其他家庭;可解散家庭(仅创建者)。 + - **成员**:**只读**。可查看、搜索、筛选、进入盒子查看;不可新增/编辑/删除/转移任何数据。 +- 权限 **以服务端校验为准**(每个写接口按 family 角色校验),前端仅做展示层隐藏/禁用。 +- 创建者为「超级管理员」,不可被降级或移除;可转让主持权(P2)。 +- 成员操作越权时返回 `403`,前端给出「无权限」提示。 + +--- + +## 5. 功能需求 + +优先级标记:`P0` 首版必须 / `P1` 次版 / `P2` 远期可选。 + +### 5.1 家庭管理(P0) + +- FR-家庭-1 创建/加入/离开/解散家庭。 +- FR-家庭-2 家庭切换器:全局顶部展示当前家庭,点击切换,切换后所有数据按新家庭重载。 +- FR-家庭-3 成员列表与角色管理(管理员操作)。 +- FR-家庭-4 邀请码:生成/刷新/失效,凭码加入即成为成员。 +- FR-家庭-5 各家庭数据完全隔离:列表、搜索、统计均只返回当前家庭数据。 + +### 5.2 盒子与收纳(P0) + +- FR-盒-1 **无限层级嵌套**:盒子内可再建盒子,深度不设上限(如 客厅 → 柜子1 → 收纳盒x → 内胆)。 +- FR-盒-2 盒子的基本属性:名称、图标(emoji)、颜色(自由自定义,可重复)。 +- FR-盒-3 **相对位置**:每个盒子在其「父容器」(父盒子或家庭根层)内拥有一个归一化坐标 (x, y),渲染为父容器内的一块方块(如把「柜子1」画在「客厅」左上角)。 +- FR-盒-4 进入/退出盒子:通过面包屑导航(家庭 / 客厅 / 柜子1 / 收纳盒x),逐层查看。 +- FR-盒-5 盒子内两种视图:**画布视图**(俯视布局,方块按位置摆放,可拖拽改位置)与 **列表视图**(按创建顺序平铺,适合快速浏览)。切换自由。 +- FR-盒-6 盒子的增删改、改名;删除盒子时要求确认(提示将删除其中全部物品,P2 提供「合并到上级」选项)。 +- FR-盒-7 **整盒移动**:将某盒子(含子树)拖拽/移至另一盒子的内部,或直接移到家庭根层;位置在目标容器内重新指定。 +- FR-盒-8 盒子的嵌套统计:每个盒子显示「物品总数(含嵌套)」与「深度」。 +- FR-盒-9 画布中允许方块重叠,位置由用户自由摆放;提供「自动整理」按钮(按蛇形序排布,P1)。 + +### 5.3 物品管理(P0) + +- FR-物-1 新增/编辑/删除物品,字段见「物品字段定义」。 +- FR-物-2 物品必须属于某家庭;可放入盒子(box_id)或散放在家庭根层(box_id 为空)。 +- FR-物-3 物品从一盒移至另一盒 / 移至根层。 +- FR-物-4 物品图片:每个物品 0~9 张;拍照与相册上传(桌面/移动端);详情页缩略图 + 大图查看。 +- FR-物-5 搜索与筛选(P0): + - 关键字:名称、品牌、型号、备注; + - 筛选:分类、标签、所在盒子(含子树)、是否收藏、数量>0、临期提醒; + - 结果可一键「定位所在位置」(逐层路径,如 客厅>柜子1>收纳盒x)。 +- FR-物-6 收藏夹(P0),常用物品置顶。 +- FR-物-7 物品生命周期:数量、购买日期/价格、保质期(临期提醒)、开始使用/最近使用(P1)、保养/清洁备注(P1)、借出记录(P2)。 + +### 5.4 分类与标签(P0) + +- FR-类-1 分类为家庭内可自定义列表(管理所有东西,无法预置固定枚举)。允许用户自建分类层级(P1 支持两级,P0 平铺列表即可)。 +- FR-类-2 标签(tags):自由多选,一个物品可打多个标签;标签全局共享、由管理员维护。 +- FR-类-3 分类/标签的增删改由管理员操作;删除时物品回到「未分类」。 + +### 5.5 跨家庭移动(P0) + +- FR-转移-1 **物品转移**:源家庭管理员可将物品转移到另一个家庭(前置条件:操作者同时是目标家庭成员,防越权散落;精确规则见设计文档 §6.3、§8.1)。 +- FR-转移-2 **整盒转移**:含全部子盒与物品,整体移动到目标家庭的某盒子或根层。 +- FR-转移-3 转移带审计:记录操作人、时间、源/目标家庭(P1 提供操作日志查询)。 +- FR-转移-4 转移后:源家庭不可再看到该项;如无权限目标家庭则不可见。 + +### 5.6 首页与统计(P0) + +- FR-统-1 当前家庭概览:总物品数、盒子总数、直接子盒数量、最大嵌套深度。 +- FR-统-2 分类分布(数量、占比);盒子占用 Top N(物品最多的盒子)。 +- FR-统-3 待办提醒:即将过期物品(30 天内)、无图物品、散放在根层未整理物品。 +- FR-统-4 价值合计(存在 purchase_price 时求和)。 +- FR-统-5 最近新增/最近操作列表。 + +### 5.7 其他 + +- FR-他-1 全局搜索入口(跨家庭可选,P1)。 +- FR-他-2 导出数据(JSON/CSV,P1)。 +- FR-他-3 深色模式(P2)。 +- FR-他-4 数据备份(服务端磁盘备份 + 可下载快照,P1)。 +- FR-他-5 MCP 能力:沿用 Wardrobe 的 MCP 服务,供 AI 助手查询物品位置(P1)。 + +--- + +## 6. 物品字段定义 + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| 名称 | string | 是 | 用户可见名字 | +| 分类 | category_id | 否 | 家庭自定义分类;为空表示未分类 | +| 标签 | string[] | 否 | 自由标签 | +| 数量 | int | 否 | 默认 1 | +| 品牌/型号 | string | 否 | 便于搜索 | +| 序列号 | string | 否 | P1 | +| 购买日期/价格 | date / number | 否 | 用于统计与价值合计 | +| 保质期/有效期 | date | 否 | 临期提醒依据(可为「无期限」) | +| 图片 | string[] | 否 | 文件路径列表(最多 9 张) | +| 备注 | string | 否 | 自由文本 | +| 是否收藏 | bool | 否 | 默认 false | +| 使用记录 | 见 5.3-FR-物-7 | 否 | P1 | +| 创建/更新时间 | datetime | 否 | 系统生成 | + +--- + +## 7. 非功能需求 + +| 编号 | 要求 | +|---|---| +| NFR-1 | 性能:家庭内物品 ≤5 万时,列表/搜索/盒子树展开响应 <300ms;盒子树(递归)加载 <500ms。 | +| NFR-2 | 数据安全:服务端为准,所有写接口必须通过家庭角色校验;不得在客户端以任何方式绕过权限。 | +| NFR-3 | 可靠性:DuckDB 单机文件持久化 + 启动迁移机制(schema_version),预留备份。 | +| NFR-4 | 跨端:Windows/macOS 桌面(Tauri)、Android(Tauri)、Web(浏览器),共享同一服务端与前端代码。 | +| NFR-5 | 可部署性:单镜像 Docker 部署(前端静态 + Express API + DuckDB 数据卷),沿用 Wardrobe 的 Gitea Actions 流水线。 | +| NFR-6 | 安全:会话 Token 存储于客户端(localStorage),Token 可撤销;图片路径做鉴权(经 /api 代理读取)。 | +| NFR-7 | 验证码:P0 开发期固定码 1111;上线前接入真实短信 API(预留 channel 抽象)。 | +| NFR-8 | 迁移友好:任何表结构变更通过迁移脚本顺序执行,禁止裸 `CREATE TABLE IF NOT EXISTS` 演进。 | +| NFR-9 | 时区:统一以 UTC 存储,前端本地化展示。 | + +--- + +## 8. 需求优先级汇总 + +| 优先级 | 范围 | +|---|---| +| **P0(首版)** | 家庭创建/加入/切换;角色权限(管理/只读);物品 CRUD + 图片;盒子无限嵌套 + 相对位置 + 画布/列表视图 + 整盒移动;分类与标签;搜索/筛选/定位;跨家庭转移(物品 + 整盒);首页统计;验证码登录。 | +| **P1(次版)** | 操作日志;自动整理;导出;备份快照;真实短信;使用/保养记录;全局跨家庭搜索;MCP。 | +| **P2(远期)** | 借出记录;分类层级深化;深色模式;转让家庭主持权;AI 自动分类。 | + +--- + +## 9. 验收要点(供评审确认) + +1. 未登录不可访问任何业务接口;非家庭成员访问返回 403。 +2. 成员对物品/盒子的任何写操作均被服务端拒绝(非仅前端隐藏)。 +3. 盒子嵌套(含 3+ 层)下,物品定位路径显示正确;整盒移动后子树物品随迁。 +4. 画布中拖拽盒子位置并刷新后位置保持;归一化坐标在 0~1000 内。 +5. 物品跨家庭转移后,仅目标家庭可见,源家庭不可见,并留下审计记录。 +6. 搜索可命中名称/品牌/标签;「定位」点击后可逐层导航到物品所在盒子。 \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..31de4b8 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Storage 收纳 + + +
+ + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fb91cdd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5055 @@ +{ + "name": "storage", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "storage", + "version": "0.1.0", + "dependencies": { + "@duckdb/node-api": "1.5.5-r.5", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.2", + "multer": "^1.4.5-lts.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.0", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.14.0", + "@typescript-eslint/parser": "^7.14.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "typescript": "^5.5.3", + "vite": "^5.3.3", + "vite-plugin-singlefile": "^2.0.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@duckdb/node-api": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-api/-/node-api-1.5.5-r.5.tgz", + "integrity": "sha512-ZMX+sel3DjSKbGuQ44Y5Tp5LwYjlJkhB5faKr+9sSMcxixDMSW+8vaTOD1uRDuN9o0udUoCY/XX9PI1haEZuZg==", + "license": "MIT", + "dependencies": { + "@duckdb/node-bindings": "1.5.5-r.5" + } + }, + "node_modules/@duckdb/node-bindings": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings/-/node-bindings-1.5.5-r.5.tgz", + "integrity": "sha512-sR7x91TfBunLqLbdvrcdiPCU+VGEQFPicxh1STZM402fuHW2ituPTAo2bY3IlntsvJShltrKF4R6B+JYPlAPZg==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.1.2" + }, + "optionalDependencies": { + "@duckdb/node-bindings-darwin-arm64": "1.5.5-r.5", + "@duckdb/node-bindings-darwin-x64": "1.5.5-r.5", + "@duckdb/node-bindings-linux-arm64": "1.5.5-r.5", + "@duckdb/node-bindings-linux-arm64-musl": "1.5.5-r.5", + "@duckdb/node-bindings-linux-x64": "1.5.5-r.5", + "@duckdb/node-bindings-linux-x64-musl": "1.5.5-r.5", + "@duckdb/node-bindings-win32-arm64": "1.5.5-r.5", + "@duckdb/node-bindings-win32-x64": "1.5.5-r.5" + } + }, + "node_modules/@duckdb/node-bindings-darwin-arm64": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-darwin-arm64/-/node-bindings-darwin-arm64-1.5.5-r.5.tgz", + "integrity": "sha512-WJdeLTFy4pRg60rkbp9smtlP1iIQ8ErtIJkxS+2LQtM1ZZMSByyW+3WPpNoKe7IwPtVl2fLlPw7tDmQW4Z974g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@duckdb/node-bindings-darwin-x64": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-darwin-x64/-/node-bindings-darwin-x64-1.5.5-r.5.tgz", + "integrity": "sha512-rN0KjX13MwVQ5V5ufykolty/tf4SaafOpGWcUA1Cegf98jPeRdsJsEE1IMXn8n3u6WvMSZEVJmHJ3ma4Hr7Vzg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@duckdb/node-bindings-linux-arm64": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-linux-arm64/-/node-bindings-linux-arm64-1.5.5-r.5.tgz", + "integrity": "sha512-CafH2AIvXtpwDynG3KB/4uEOHRGlFoB7auCRMIwH3BKNIobsANV5LSpDJ3tfyRCFIsK6e8AT1eseWVnu2Dv3cw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@duckdb/node-bindings-linux-arm64-musl": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-linux-arm64-musl/-/node-bindings-linux-arm64-musl-1.5.5-r.5.tgz", + "integrity": "sha512-00z3mYHUBAucp3L73EoftKDdaGOaV8ENELejiT+PK7dFjTzdXxqhJwZ1LaQV1/zEhIecz+x23f6WvFhWw+7XXQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@duckdb/node-bindings-linux-x64": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-linux-x64/-/node-bindings-linux-x64-1.5.5-r.5.tgz", + "integrity": "sha512-kCyq6BXhn6QXfwF8yVdVKriSSHP+rfVWcJ56cG8qZzlOZpxyvT5WGm6jMshxg4ditLlOsHsIN/D47RYrcjXuHQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@duckdb/node-bindings-linux-x64-musl": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-linux-x64-musl/-/node-bindings-linux-x64-musl-1.5.5-r.5.tgz", + "integrity": "sha512-S5qEc7F+GjXt85T2xArjjvN5oc/IjtH6zsLJsuo8N3fQolheBCefemIMdZoK2Jik8a0sBxidiS6Ld4qprMPPDw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@duckdb/node-bindings-win32-arm64": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-win32-arm64/-/node-bindings-win32-arm64-1.5.5-r.5.tgz", + "integrity": "sha512-L3Mroc23xhqOowWVElni8LgEmsm5e9DdG1y2pLka/1ug3Pl2vpQMl1Phxwz4UEMF8DOBbMgN5BBNrYwG2BFo+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@duckdb/node-bindings-win32-x64": { + "version": "1.5.5-r.5", + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-win32-x64/-/node-bindings-win32-x64-1.5.5-r.5.tgz", + "integrity": "sha512-mBGFn8netT/AAhK4TOG6Fw47+Db68CxCVtQ9qfxZc1kZAjds+aYrh17Fd/u5CbTDdxILsrcyuF8q9cUcm54kQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", + "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", + "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz", + "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz", + "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz", + "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz", + "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz", + "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz", + "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz", + "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz", + "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz", + "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz", + "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz", + "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz", + "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz", + "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz", + "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz", + "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz", + "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz", + "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz", + "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz", + "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz", + "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz", + "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz", + "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz", + "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz", + "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.5.tgz", + "integrity": "sha512-YbeJ6tctNoo40purO4u3Eeo3RVqmsjJr0NoKQRu+4pvO4gMLzkjta7x0k96SxSLmx+rQ+8um1ThOlw+HLpgFug==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.5", + "@tauri-apps/cli-darwin-x64": "2.11.5", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.5", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.5", + "@tauri-apps/cli-linux-arm64-musl": "2.11.5", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.5", + "@tauri-apps/cli-linux-x64-gnu": "2.11.5", + "@tauri-apps/cli-linux-x64-musl": "2.11.5", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.5", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.5", + "@tauri-apps/cli-win32-x64-msvc": "2.11.5" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.5.tgz", + "integrity": "sha512-tggOiVOohjIHdiElbjeBfB41s6cmfsq+ZQ0PX0fEtCdsDdZ9cCi54e5gNK7tdPbR6JbdDe7RA4kbQdyyBUUk/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.5.tgz", + "integrity": "sha512-SrO+KCbqvG1IvVPdlzQX4GEGZRQDISrmDY9bGcW68DLVFBCC06H+Loz6oyUZETUxsSMR6B4oDfvbQVgD4hxsqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.5.tgz", + "integrity": "sha512-qLrOaCa8hLD5QVPQRjAgOXvhlTFKXmTliVF773GgvADYcWZSdmncnF5zBFAkdu1uQK8KfQbDlBUwqQYZqBfBRQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.5.tgz", + "integrity": "sha512-Tq32xpQjiQEdIrvwDq7lwHhXemn070CXWdbCvr9rdi9rsv9NFTbQ6izVSIfNE+59QaerxuwtUtZ5IxDNWaoYBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.5.tgz", + "integrity": "sha512-+OdWKj5Aq+C8HXypQ4t926V8cpVnc0ZcJIdhTXWjVAJHkaNuo8EuQR6KRZKHWIbJJgKdMPCDosvwzIFaBBqHKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.5.tgz", + "integrity": "sha512-Q0V2hjpllyDW8dWQZSsITuQDfcY9BpBZuhXkMa2T5AFLZAn+FJPXQSyvnxWNVxRowdQ59Xn0QVtGm6HEHNUEvw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.5.tgz", + "integrity": "sha512-Xz1s87gFjZJZMQKAhLa77YeiCJM6yVog3f9OxDgVd/KxDIoB1dA5V2xOnMM+SRkOGk9juwIBqesVAjV0kUB0DA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.5.tgz", + "integrity": "sha512-6A88wqAZXZHgMaBrGiy/Bpa4GgUnftXM7uquKOeOXCuK78/XdBxEECicK/1oHgrT179XBFOaA6lQEHysjRmGig==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.5.tgz", + "integrity": "sha512-sRGgF/ObRfLcS3PtUdyTbJtkGq3E+PNZTknvAURwMNUtHw9DU/BF6S5si43y9lS5yEO9bWJCA+GGYvnn6cphQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.5.tgz", + "integrity": "sha512-bHOv/yxqfKpI7ioIFQi5wOfZ7jkbPzJD/UhXY52I/hrmE3qNh71cohIPyqZB5GQn3LZeJWdCUOqmIg/w6XX+Wg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.5.tgz", + "integrity": "sha512-vaaOmavqdCeS7oMib2banBZemgrC5v54tSkwoBV+bxrXQZhQZrOwE2EO2i4x+CHNtvQUM/6bSs3iymZMAsycWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", + "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.438", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.438.tgz", + "integrity": "sha512-AN9xMU1hJiT65LkCUPL1DZm5TCbOPb2Qsm5pYwFLEXp6/qj9SdT4yMiqs7Qqstwvkn1zF7l36SRGt+s6XcJ0FA==", + "dev": true, + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.13", + "proxy-addr": "~2.0.7", + "qs": "~6.16.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "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.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", + "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", + "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4", + "react-router": "6.30.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", + "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.4", + "@rollup/rollup-android-arm64": "4.63.4", + "@rollup/rollup-darwin-arm64": "4.63.4", + "@rollup/rollup-darwin-x64": "4.63.4", + "@rollup/rollup-freebsd-arm64": "4.63.4", + "@rollup/rollup-freebsd-x64": "4.63.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", + "@rollup/rollup-linux-arm-musleabihf": "4.63.4", + "@rollup/rollup-linux-arm64-gnu": "4.63.4", + "@rollup/rollup-linux-arm64-musl": "4.63.4", + "@rollup/rollup-linux-loong64-gnu": "4.63.4", + "@rollup/rollup-linux-loong64-musl": "4.63.4", + "@rollup/rollup-linux-ppc64-gnu": "4.63.4", + "@rollup/rollup-linux-ppc64-musl": "4.63.4", + "@rollup/rollup-linux-riscv64-gnu": "4.63.4", + "@rollup/rollup-linux-riscv64-musl": "4.63.4", + "@rollup/rollup-linux-s390x-gnu": "4.63.4", + "@rollup/rollup-linux-x64-gnu": "4.63.4", + "@rollup/rollup-linux-x64-musl": "4.63.4", + "@rollup/rollup-openbsd-x64": "4.63.4", + "@rollup/rollup-openharmony-arm64": "4.63.4", + "@rollup/rollup-win32-arm64-msvc": "4.63.4", + "@rollup/rollup-win32-ia32-msvc": "4.63.4", + "@rollup/rollup-win32-x64-gnu": "4.63.4", + "@rollup/rollup-win32-x64-msvc": "4.63.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "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/vite-plugin-singlefile": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", + "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">18.0.0" + }, + "peerDependencies": { + "rollup": "^4.59.0", + "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d697d60 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "storage", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri", + "android:dev": "tauri android dev", + "android:build": "tauri android build", + "check": "tsc --noEmit", + "lint": "eslint src --ext .ts,.tsx", + "dev:server": "node --watch --experimental-strip-types --no-warnings server/index.js", + "start": "node --experimental-strip-types --no-warnings server/index.js", + "build:apk": "powershell -ExecutionPolicy Bypass -File scripts/build-apk.ps1" + }, + "dependencies": { + "@duckdb/node-api": "1.5.5-r.5", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.2", + "multer": "^1.4.5-lts.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.0", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.14.0", + "@typescript-eslint/parser": "^7.14.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "typescript": "^5.5.3", + "vite": "^5.3.3", + "vite-plugin-singlefile": "^2.0.1" + } +} \ No newline at end of file diff --git a/scripts/build-apk.ps1 b/scripts/build-apk.ps1 new file mode 100644 index 0000000..ce8700f --- /dev/null +++ b/scripts/build-apk.ps1 @@ -0,0 +1,32 @@ +# Storage Android APK 构建 (Tauri 2) +# 前置: Android SDK + JVM + Rust + src-tauri/ +# 用法: npm run build:apk (或 powershell -ExecutionPolicy Bypass -File scripts/build-apk.ps1) +$ErrorActionPreference = "Stop" + +$root = Split-Path -Parent $PSScriptRoot +$tauriDir = Join-Path $root "src-tauri" +if (-not (Test-Path $tauriDir)) { + Write-Host @" +[build-apk] 缺少 src-tauri/ 目录。Storage 的 Tauri 壳尚未初始化, 先执行: + npm run tauri init +然后重新运行本脚本。 +"@ -ForegroundColor Yellow + exit 1 +} + +# 前端打内嵌资源 (Android 端相对 /api 会打到内嵌服务, 需指向远端后端) +# 生产后端地址由环境决定 (与 部署环境.md 一致): baihu.vps.honor3.com:3003 +# (Android APP 页面源是 http://tauri.localhost, 访问外网明文 HTTP 需放行 usesCleartextTraffic) +$VITE_API_BASE = if ($env:VITE_API_BASE) { $env:VITE_API_BASE } else { "http://baihu.vps.honor3.com:3003" } +$env:VITE_API_BASE = $VITE_API_BASE + +Write-Host "[build-apk] VITE_API_BASE=$VITE_API_BASE" + +Push-Location $root +try { + npx tauri android build --apk +} finally { + Pop-Location +} + +Write-Host "[build-apk] 完成 (release APK 见 src-tauri/gen/android/... )" \ No newline at end of file diff --git a/server/auth.js b/server/auth.js new file mode 100644 index 0000000..711779d --- /dev/null +++ b/server/auth.js @@ -0,0 +1,66 @@ +// 登录/注册: 验证码(服务端一次性校验) + 服务端签发会话 token +// 注意: 短信/邮件发送仍是假通道 (验证码固定 1111), 后续按 NFR-7 接真实短信 +import { uu, dbCreateUser, dbGetUserByEmail, dbGetUserByPhone, dbGetUserById, dbGetSession, dbCreateSession, dbDeleteSession } from './db.js' + +const VERIFICATION_CODE = '1111' +const CODE_EXPIRY_SECONDS = 300 +const codes = new Map() + +export function sendVerification(target) { + codes.set(target, { code: VERIFICATION_CODE, expiresAt: Date.now() + CODE_EXPIRY_SECONDS * 1000 }) + console.log(`[auth] verification code for ${target}: ${VERIFICATION_CODE}`) + return `验证码已发送至 ${target}` +} + +function verifyCode(target, code) { + const v = codes.get(target) + if (!v) return false + if (v.code !== code || Date.now() >= v.expiresAt) return false + codes.delete(target) + return true +} + +export async function register({ email, phone, code, nickname }) { + const target = email || phone + if (!target) return { success: false, message: '请提供邮箱或手机号', user: null, token: null } + if (!verifyCode(target, code)) { + return { success: false, message: '验证码无效或已过期', user: null, token: null } + } + if (email && (await dbGetUserByEmail(email))) { + return { success: false, message: '该邮箱已注册', user: null, token: null } + } + if (phone && (await dbGetUserByPhone(phone))) { + return { success: false, message: '该手机号已注册', user: null, token: null } + } + const id = uu() + const user = await dbCreateUser(id, email || null, phone || null, nickname || '收纳达人') + const token = 'tok_' + uu().replaceAll('-', '') + await dbCreateSession(token, id) + console.log(`[auth] user registered: ${id}`) + return { success: true, message: '注册成功', user, token } +} + +export async function login({ email, phone, code }) { + const target = email || phone + if (!target) return { success: false, message: '请提供邮箱或手机号', user: null, token: null } + if (!verifyCode(target, code)) { + return { success: false, message: '验证码无效或已过期', user: null, token: null } + } + const user = email ? await dbGetUserByEmail(email) : await dbGetUserByPhone(phone) + if (!user) return { success: false, message: '账号不存在,请先注册', user: null, token: null } + const token = 'tok_' + uu().replaceAll('-', '') + await dbCreateSession(token, user.id) + console.log(`[auth] user login: ${user.id}`) + return { success: true, message: '登录成功', user, token } +} + +export async function logout(token) { + if (token) await dbDeleteSession(token) +} + +export async function resolveSession(token) { + const s = token ? await dbGetSession(token) : null + if (!s) return null + const user = await dbGetUserById(s.user_id) + return user +} \ No newline at end of file diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..82afb2b --- /dev/null +++ b/server/db.js @@ -0,0 +1,841 @@ +// Storage 服务端数据层: DuckDB (@duckdb/node-api) +// 业务数据全部存储在服务端数据库文件 (DB_PATH, 生产建议挂持久化数据卷), +// 客户端不再持有数据库, 仅保留最近数据的读缓存。 +// 采用版本化迁移机制 (schema_version), 禁止用裸 CREATE TABLE 演进。 +// +// 重要约定: 由于 DuckDB 单写入者 + 全局 enqueue 串行化, 任何"enqueue"的公共函数 +// 内部不得再调用其它 enqueue 函数 (会自锁)。因此所有底层读操作都实现为 +// 内部函数 (直接 conn.run), 公共 API 一律是薄 enqueue 包装。 +import { DuckDBInstance } from '@duckdb/node-api' +import path from 'node:path' +import fs from 'node:fs' + +let instance = null +let conn = null + +function dbFile() { + const p = process.env.DB_PATH || path.resolve(process.cwd(), 'data', 'storage.duckdb') + fs.mkdirSync(path.dirname(p), { recursive: true }) + return p +} + +let chain = Promise.resolve() +export function enqueue(fn) { + const p = chain.then(fn) + chain = p.catch(() => {}) + return p +} + +// ── 内部底层读 (非 enqueue) ── +async function rows(sql, params = []) { + const r = await conn.run(sql, params) + return (await r.getRowObjects()) ?? [] +} +async function one(sql, params = []) { + const r = await rows(sql, params) + return r.length ? r[0] : null +} +async function count(sql, params = []) { + const r = await rows(sql, params) + return r.length ? Number(r[0].c) : 0 +} + +const nowIso = () => new Date().toISOString() +export const uu = () => globalThis.crypto.randomUUID() + +function parseArr(v) { + if (Array.isArray(v)) return v + if (typeof v === 'string') { + try { + const p = JSON.parse(v) + return Array.isArray(p) ? p : [] + } catch { + return [] + } + } + return [] +} + +function mapBox(x) { + if (!x) return null + return { + ...x, + pos_x: Number(x.pos_x) || 0, + pos_y: Number(x.pos_y) || 0, + width: x.width === null || x.width === undefined ? null : Number(x.width), + height: x.height === null || x.height === undefined ? null : Number(x.height), + direct_item_count: x.direct_item_count === undefined ? 0 : Number(x.direct_item_count || 0), + } +} + +function mapItem(x) { + if (!x) return null + return { + ...x, + tags: parseArr(x.tags), + images: parseArr(x.images), + quantity: Number(x.quantity || 1), + purchase_price: x.purchase_price === null || x.purchase_price === undefined ? null : Number(x.purchase_price), + is_favorite: !!x.is_favorite, + used_count: Number(x.used_count || 0), + } +} + +// ── 迁移机制 ── +const MIGRATIONS = [ + { + version: 1, + up: async (c) => { + await c.run(`CREATE TABLE IF NOT EXISTS users ( + id VARCHAR PRIMARY KEY, + email VARCHAR UNIQUE, + phone VARCHAR UNIQUE, + nickname VARCHAR NOT NULL DEFAULT '', + avatar_path VARCHAR, + created_at VARCHAR NOT NULL + )`) + await c.run(`CREATE TABLE IF NOT EXISTS sessions ( + token VARCHAR PRIMARY KEY, + user_id VARCHAR NOT NULL, + created_at VARCHAR NOT NULL + )`) + await c.run(`CREATE TABLE IF NOT EXISTS families ( + id VARCHAR PRIMARY KEY, + name VARCHAR NOT NULL, + icon VARCHAR DEFAULT '🏠', + owner_user_id VARCHAR NOT NULL, + invite_code VARCHAR, + created_at VARCHAR NOT NULL, + updated_at VARCHAR NOT NULL + )`) + await c.run(`CREATE TABLE IF NOT EXISTS family_members ( + family_id VARCHAR NOT NULL, + user_id VARCHAR NOT NULL, + role VARCHAR NOT NULL CHECK (role IN ('admin','member')), + joined_at VARCHAR NOT NULL, + PRIMARY KEY (family_id, user_id) + )`) + await c.run(`CREATE TABLE IF NOT EXISTS categories ( + id VARCHAR PRIMARY KEY, + family_id VARCHAR NOT NULL, + name VARCHAR NOT NULL, + sort INTEGER DEFAULT 0, + created_at VARCHAR NOT NULL + )`) + await c.run(`CREATE TABLE IF NOT EXISTS boxes ( + id VARCHAR PRIMARY KEY, + family_id VARCHAR NOT NULL, + parent_box_id VARCHAR, + name VARCHAR NOT NULL, + icon VARCHAR DEFAULT '📦', + color VARCHAR DEFAULT '#E8C4A0', + pos_x INTEGER DEFAULT 0 CHECK (pos_x BETWEEN 0 AND 1000), + pos_y INTEGER DEFAULT 0 CHECK (pos_y BETWEEN 0 AND 1000), + width INTEGER, + height INTEGER, + created_at VARCHAR NOT NULL, + updated_at VARCHAR NOT NULL + )`) + await c.run(`CREATE TABLE IF NOT EXISTS items ( + id VARCHAR PRIMARY KEY, + family_id VARCHAR NOT NULL, + box_id VARCHAR, + name VARCHAR NOT NULL, + category_id VARCHAR, + tags VARCHAR DEFAULT '[]', + quantity INTEGER DEFAULT 1, + brand VARCHAR, + model VARCHAR, + serial_no VARCHAR, + purchase_date VARCHAR, + purchase_price DOUBLE, + expiry_date VARCHAR, + images VARCHAR DEFAULT '[]', + notes VARCHAR, + is_favorite BOOLEAN DEFAULT FALSE, + used_count INTEGER DEFAULT 0, + last_used VARCHAR, + created_at VARCHAR NOT NULL, + updated_at VARCHAR NOT NULL + )`) + await c.run(`CREATE TABLE IF NOT EXISTS transfer_logs ( + id VARCHAR PRIMARY KEY, + entity_type VARCHAR NOT NULL, + entity_id VARCHAR NOT NULL, + src_family_id VARCHAR NOT NULL, + dst_family_id VARCHAR NOT NULL, + operator_user_id VARCHAR NOT NULL, + created_at VARCHAR NOT NULL + )`) + await c.run(`CREATE INDEX IF NOT EXISTS idx_members_user ON family_members(user_id)`) + await c.run(`CREATE INDEX IF NOT EXISTS idx_boxes_family ON boxes(family_id)`) + await c.run(`CREATE INDEX IF NOT EXISTS idx_boxes_parent ON boxes(parent_box_id)`) + await c.run(`CREATE INDEX IF NOT EXISTS idx_items_family ON items(family_id)`) + await c.run(`CREATE INDEX IF NOT EXISTS idx_items_box ON items(box_id)`) + await c.run(`CREATE INDEX IF NOT EXISTS idx_items_family_box ON items(family_id, box_id)`) + }, + }, +] + +export async function initDb() { + const file = dbFile() + instance = await DuckDBInstance.create(file) + conn = await instance.connect() + await conn.run(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY, applied_at VARCHAR NOT NULL)`) + const applied = await rows('SELECT version FROM schema_version ORDER BY version') + const appliedSet = new Set(applied.map((r) => Number(r.version))) + for (const m of MIGRATIONS) { + if (appliedSet.has(m.version)) continue + console.log(`[db] applying migration v${m.version} ...`) + await conn.run('BEGIN TRANSACTION') + try { + await m.up(conn) + await conn.run('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)', [m.version, nowIso()]) + await conn.run('COMMIT') + console.log(`[db] migration v${m.version} ok`) + } catch (e) { + await conn.run('ROLLBACK') + throw e + } + } + console.log(`[db] DuckDB ready at ${file} (schema v${applied.length ? Math.max(...applied.map((r) => Number(r.version))) : 0})`) +} + +async function tx(fn) { + await conn.run('BEGIN TRANSACTION') + try { + const r = await fn() + await conn.run('COMMIT') + return r + } catch (e) { + try { await conn.run('ROLLBACK') } catch { /* ignore */ } + throw e + } +} + +// ══════════════ users / sessions ══════════════ + +export function dbCreateUser(id, email, phone, nickname) { + return enqueue(async () => { + await conn.run('INSERT INTO users (id, email, phone, nickname, created_at) VALUES (?,?,?,?,?)', [id, email, phone, nickname, nowIso()]) + return { id, email, phone, nickname, avatar_path: null, created_at: nowIso() } + }) +} + +export const dbGetUserByEmail = (email) => enqueue(() => + one('SELECT id, email, phone, nickname, avatar_path, created_at FROM users WHERE email = ?', [email])) + +export const dbGetUserByPhone = (phone) => enqueue(() => + one('SELECT id, email, phone, nickname, avatar_path, created_at FROM users WHERE phone = ?', [phone])) + +export const dbGetUserById = (id) => enqueue(() => + one('SELECT id, email, phone, nickname, avatar_path, created_at FROM users WHERE id = ?', [id])) + +export const dbUpdateUserNickname = (id, nickname) => enqueue(async () => { + await conn.run('UPDATE users SET nickname = ? WHERE id = ?', [nickname, id]) +}) + +export const dbCreateSession = (token, userId) => enqueue(async () => { + await conn.run('INSERT INTO sessions (token, user_id, created_at) VALUES (?,?,?)', [token, userId, nowIso()]) +}) + +export const dbGetSession = (token) => enqueue(() => + one('SELECT token, user_id, created_at FROM sessions WHERE token = ?', [token])) + +export const dbDeleteSession = (token) => enqueue(async () => { + await conn.run('DELETE FROM sessions WHERE token = ?', [token]) +}) + +// ══════════════ families / members ══════════════ + +function genInviteCode() { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' + let s = '' + for (let i = 0; i < 8; i++) s += chars[Math.floor(Math.random() * chars.length)] + return s +} + +export function dbCreateFamily(ownerId, name, icon) { + return enqueue(async () => { + const family = { id: uu(), name: name || '我的家庭', icon: icon || '🏠', owner_user_id: ownerId, invite_code: genInviteCode(), created_at: nowIso(), updated_at: nowIso() } + await tx(async () => { + await conn.run('INSERT INTO families (id, name, icon, owner_user_id, invite_code, created_at, updated_at) VALUES (?,?,?,?,?,?,?)', + [family.id, family.name, family.icon, family.owner_user_id, family.invite_code, family.created_at, family.updated_at]) + await conn.run('INSERT INTO family_members (family_id, user_id, role, joined_at) VALUES (?,?,?,?)', + [family.id, ownerId, 'admin', nowIso()]) + }) + return family + }) +} + +export const dbListFamiliesForUser = (userId) => enqueue(async () => { + return rows( + `SELECT f.id, f.name, f.icon, f.owner_user_id, f.invite_code, f.created_at, m.role + FROM family_members m JOIN families f ON f.id = m.family_id + WHERE m.user_id = ? ORDER BY f.created_at`, + [userId], + ) +}) + +export const dbGetFamily = (familyId) => enqueue(() => + one('SELECT id, name, icon, owner_user_id, invite_code, created_at, updated_at FROM families WHERE id = ?', [familyId])) + +export const dbUpdateFamily = (familyId, data) => enqueue(async () => { + await conn.run('UPDATE families SET name = ?, icon = ?, updated_at = ? WHERE id = ?', + [data.name, data.icon || '🏠', nowIso(), familyId]) +}) + +export const dbDeleteFamily = (familyId) => enqueue(async () => { + await tx(async () => { + await conn.run('DELETE FROM family_members WHERE family_id = ?', [familyId]) + await conn.run('DELETE FROM categories WHERE family_id = ?', [familyId]) + const boxIds = (await rows('SELECT id FROM boxes WHERE family_id = ?', [familyId])).map((r) => r.id) + if (boxIds.length) { + const placeholders = boxIds.map(() => '?').join(',') + await conn.run(`DELETE FROM items WHERE family_id = ? OR box_id IN (${placeholders})`, [familyId, ...boxIds]) + await conn.run(`DELETE FROM boxes WHERE family_id = ?`, [familyId]) + } else { + await conn.run('DELETE FROM items WHERE family_id = ?', [familyId]) + } + await conn.run('DELETE FROM transfer_logs WHERE src_family_id = ? OR dst_family_id = ?', [familyId, familyId]) + await conn.run('DELETE FROM families WHERE id = ?', [familyId]) + }) +}) + +export const dbGetRole = (familyId, userId) => enqueue(async () => { + const r = await one('SELECT role FROM family_members WHERE family_id = ? AND user_id = ?', [familyId, userId]) + return r ? r.role : null +}) + +export const dbAddMember = (familyId, userId, role) => enqueue(async () => { + await conn.run('INSERT INTO family_members (family_id, user_id, role, joined_at) VALUES (?,?,?,?)', [familyId, userId, role || 'member', nowIso()]) +}) + +export const dbRemoveMember = (familyId, userId) => enqueue(async () => { + await conn.run('DELETE FROM family_members WHERE family_id = ? AND user_id = ?', [familyId, userId]) +}) + +export const dbSetRole = (familyId, userId, role) => enqueue(async () => { + await conn.run('UPDATE family_members SET role = ? WHERE family_id = ? AND user_id = ?', [role, familyId, userId]) +}) + +export const dbSetInviteCode = (familyId, code) => enqueue(async () => { + await conn.run('UPDATE families SET invite_code = ?, updated_at = ? WHERE id = ?', [code, nowIso(), familyId]) +}) + +export const dbJoinByCode = (code, userId) => enqueue(async () => { + const family = await one('SELECT id FROM families WHERE invite_code = ?', [code]) + if (!family) return { success: false, message: '邀请码无效' } + const existing = await one('SELECT role FROM family_members WHERE family_id = ? AND user_id = ?', [family.id, userId]) + if (existing) return { success: false, message: '你已在该家庭中' } + await conn.run('INSERT INTO family_members (family_id, user_id, role, joined_at) VALUES (?,?,?,?)', [family.id, userId, 'member', nowIso()]) + return { success: true, message: '加入成功', familyId: family.id } +}) + +export const dbListMembers = (familyId) => enqueue(async () => { + return rows( + `SELECT m.family_id, m.user_id, m.role, m.joined_at, u.nickname, u.email, u.phone + FROM family_members m JOIN users u ON u.id = m.user_id + WHERE m.family_id = ? ORDER BY m.joined_at`, + [familyId], + ) +}) + +// ══════════════ categories ══════════════ + +export const dbListCategories = (familyId) => enqueue(async () => { + return rows('SELECT id, family_id, name, sort, created_at FROM categories WHERE family_id = ? ORDER BY sort, created_at', [familyId]) +}) + +export const dbCreateCategory = (familyId, name) => enqueue(async () => { + const cat = { id: uu(), family_id: familyId, name, sort: 0, created_at: nowIso() } + await conn.run('INSERT INTO categories (id, family_id, name, sort, created_at) VALUES (?,?,?,?,?)', + [cat.id, cat.family_id, cat.name, cat.sort, cat.created_at]) + return cat +}) + +export const dbUpdateCategory = (familyId, catId, name) => enqueue(async () => { + await conn.run('UPDATE categories SET name = ? WHERE id = ? AND family_id = ?', [name, catId, familyId]) +}) + +export const dbDeleteCategory = (familyId, catId) => enqueue(async () => { + await tx(async () => { + await conn.run('UPDATE items SET category_id = NULL WHERE category_id = ? AND family_id = ?', [catId, familyId]) + await conn.run('DELETE FROM categories WHERE id = ? AND family_id = ?', [catId, familyId]) + }) +}) + +// ══════════════ 盒子树内部工具 (非 enqueue) ══════════════ + +async function allBoxes(familyId) { + const r = await rows( + `SELECT b.*, ( + SELECT COUNT(*) FROM items i WHERE i.box_id = b.id + ) AS direct_item_count + FROM boxes b WHERE b.family_id = ?`, + [familyId], + ) + return r.map(mapBox) +} + +function buildPath(boxes, boxId) { + const byId = new Map(boxes.map((b) => [b.id, b])) + const path = [] + let cur = byId.get(boxId) + const guard = new Set() + while (cur && !guard.has(cur.id)) { + guard.add(cur.id) + path.unshift({ id: cur.id, name: cur.name }) + cur = cur.parent_box_id ? byId.get(cur.parent_box_id) : null + } + return path +} + +function directChildren(boxes, parentId) { + return boxes.filter((b) => (b.parent_box_id ?? null) === (parentId ?? null)).sort((a, b) => a.pos_y - b.pos_y || a.pos_x - b.pos_x) +} + +async function isDescendant(familyId, boxId, targetId) { + if (boxId === targetId) return true + const all = await rows('SELECT id, parent_box_id FROM boxes WHERE family_id = ?', [familyId]) + const parentOf = new Map(all.map((b) => [b.id, b.parent_box_id])) + let cur = parentOf.get(targetId) + while (cur) { + if (cur === boxId) return true + cur = parentOf.get(cur) + } + return false +} + +async function subtreeIds(familyId, boxId) { + const all = await rows('SELECT id, parent_box_id FROM boxes WHERE family_id = ?', [familyId]) + const childrenOf = new Map() + for (const b of all) { + if (!childrenOf.has(b.parent_box_id)) childrenOf.set(b.parent_box_id, []) + childrenOf.get(b.parent_box_id).push(b.id) + } + const out = [] + const stack = [boxId] + while (stack.length) { + const cur = stack.pop() + out.push(cur) + for (const c of childrenOf.get(cur) || []) stack.push(c) + } + return out +} + +function subtreeItemCount(boxes, boxId) { + const childrenOf = new Map() + for (const b of boxes) { + const p = b.parent_box_id ?? 'ROOT' + if (!childrenOf.has(p)) childrenOf.set(p, []) + childrenOf.get(p).push(b) + } + let total = 0 + const stack = [boxId] + const seen = new Set() + while (stack.length) { + const cur = stack.pop() + if (seen.has(cur)) continue + seen.add(cur) + const b = boxes.find((x) => x.id === cur) + if (b) total += b.direct_item_count || 0 + for (const c of childrenOf.get(cur) || []) stack.push(c.id) + } + return total +} + +function childBoxes(boxes) { + const childCounts = new Map() + for (const b of boxes) { + const key = b.parent_box_id ?? 'ROOT' + if (!childCounts.has(key)) childCounts.set(key, []) + childCounts.get(key).push(b) + } + return childCounts +} + +async function buildTree(familyId) { + const boxes = await allBoxes(familyId) + const childrenOf = childBoxes(boxes) + return boxes.map((box) => { + const path = buildPath(boxes, box.id) + return { + ...box, + depth: path.length, + path: path.map((p) => ({ id: p.id, name: p.name })), + total_item_count: subtreeItemCount(boxes, box.id), + } + }) +} + +// ══════════════ boxes (公共 enqueue API) ══════════════ + +export const dbAllBoxes = (familyId) => enqueue(() => allBoxes(familyId)) + +export const dbGetBox = (familyId, boxId) => enqueue(async () => { + const b = await one('SELECT * FROM boxes WHERE id = ? AND family_id = ?', [boxId, familyId]) + return mapBox(b) +}) + +export const dbBoxPath = (familyId, boxId) => enqueue(async () => { + const boxes = await allBoxes(familyId) + return buildPath(boxes, boxId) +}) + +const clampPos = (v) => { + const n = Number(v) + if (!Number.isFinite(n)) return 0 + return Math.max(0, Math.min(1000, Math.round(n))) +} + +export function dbCreateBox(familyId, data) { + return enqueue(async () => { + const t = nowIso() + const box = { + id: uu(), + family_id: familyId, + parent_box_id: data.parent_box_id ?? null, + name: data.name, + icon: data.icon || '📦', + color: data.color || '#E8C4A0', + pos_x: clampPos(data.pos_x), + pos_y: clampPos(data.pos_y), + width: data.width ?? null, + height: data.height ?? null, + created_at: t, + updated_at: t, + } + if (box.parent_box_id) { + const parent = await one('SELECT id, family_id FROM boxes WHERE id = ?', [box.parent_box_id]) + if (!parent || parent.family_id !== familyId) throw { status: 400, message: '父盒子不存在' } + } + await conn.run( + 'INSERT INTO boxes (id, family_id, parent_box_id, name, icon, color, pos_x, pos_y, width, height, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', + [box.id, box.family_id, box.parent_box_id, box.name, box.icon, box.color, box.pos_x, box.pos_y, box.width, box.height, box.created_at, box.updated_at], + ) + return mapBox({ ...box, direct_item_count: 0 }) + }) +} + +export function dbUpdateBox(familyId, boxId, data) { + return enqueue(async () => { + const sets = [] + const params = [] + for (const f of ['name', 'icon', 'color']) { + if (data[f] !== undefined) { sets.push(`${f} = ?`); params.push(data[f]) } + } + if (data.width !== undefined) { sets.push('width = ?'); params.push(data.width) } + if (data.height !== undefined) { sets.push('height = ?'); params.push(data.height) } + sets.push('updated_at = ?'); params.push(nowIso()) + params.push(boxId, familyId) + await conn.run(`UPDATE boxes SET ${sets.join(', ')} WHERE id = ? AND family_id = ?`, params) + }) +} + +export function dbMoveBox(familyId, boxId, parentBoxId, posX, posY) { + return enqueue(async () => { + const box = await one('SELECT id FROM boxes WHERE id = ? AND family_id = ?', [boxId, familyId]) + if (!box) throw { status: 404, message: '盒子不存在' } + if (parentBoxId) { + const parent = await one('SELECT id, family_id FROM boxes WHERE id = ?', [parentBoxId]) + if (!parent || parent.family_id !== familyId) throw { status: 400, message: '目标父盒子不存在' } + if (await isDescendant(familyId, boxId, parentBoxId)) throw { status: 400, message: '不能移动到自身或其子盒中' } + } + await conn.run('UPDATE boxes SET parent_box_id = ?, pos_x = ?, pos_y = ?, updated_at = ? WHERE id = ?', + [parentBoxId ?? null, clampPos(posX), clampPos(posY), nowIso(), boxId]) + }) +} + +export function dbSetBoxPosition(familyId, boxId, posX, posY) { + return enqueue(async () => { + await conn.run('UPDATE boxes SET pos_x = ?, pos_y = ?, updated_at = ? WHERE id = ? AND family_id = ?', + [clampPos(posX), clampPos(posY), nowIso(), boxId, familyId]) + }) +} + +export function dbDeleteBox(familyId, boxId) { + return enqueue(async () => { + const ids = await subtreeIds(familyId, boxId) + await tx(async () => { + const ph = ids.map(() => '?').join(',') + await conn.run(`DELETE FROM items WHERE family_id = ? AND box_id IN (${ph})`, [familyId, ...ids]) + await conn.run(`DELETE FROM boxes WHERE id IN (${ph})`, ids) + }) + }) +} + +export function dbListChildBoxes(familyId, parentBoxId) { + return enqueue(async () => { + const boxes = await allBoxes(familyId) + return directChildren(boxes, parentBoxId).map((box) => ({ + ...box, + total_item_count: subtreeItemCount(boxes, box.id), + })) + }) +} + +export const dbBuildTree = (familyId) => enqueue(() => buildTree(familyId)) + +export const dbSubtreeIds = (familyId, boxId) => enqueue(() => subtreeIds(familyId, boxId)) + +// ══════════════ items ══════════════ + +export function dbCreateItem(familyId, data) { + return enqueue(async () => { + const t = nowIso() + const item = { + id: uu(), + family_id: familyId, + box_id: data.box_id ?? null, + name: data.name, + category_id: data.category_id ?? null, + tags: data.tags ?? [], + quantity: data.quantity ?? 1, + brand: data.brand ?? null, + model: data.model ?? null, + serial_no: data.serial_no ?? null, + purchase_date: data.purchase_date ?? null, + purchase_price: data.purchase_price === '' ? null : (data.purchase_price ?? null), + expiry_date: data.expiry_date ?? null, + images: data.images ?? [], + notes: data.notes ?? null, + is_favorite: !!data.is_favorite, + used_count: 0, + last_used: null, + created_at: t, + updated_at: t, + } + if (item.box_id && (await one('SELECT id FROM boxes WHERE id = ? AND family_id = ?', [item.box_id, familyId])) === null) throw { status: 400, message: '盒子不存在' } + await conn.run( + `INSERT INTO items (id, family_id, box_id, name, category_id, tags, quantity, brand, model, serial_no, purchase_date, purchase_price, expiry_date, images, notes, is_favorite, used_count, last_used, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + [ + item.id, item.family_id, item.box_id, item.name, item.category_id, + JSON.stringify(item.tags), item.quantity, item.brand, item.model, item.serial_no, + item.purchase_date, item.purchase_price, item.expiry_date, + JSON.stringify(item.images), item.notes, item.is_favorite, + item.used_count, item.last_used, item.created_at, item.updated_at, + ], + ) + return mapItem(item) + }) +} + +export const dbGetItem = (familyId, itemId) => enqueue(async () => { + const it = await one('SELECT * FROM items WHERE id = ? AND family_id = ?', [itemId, familyId]) + return mapItem(it) +}) + +// 内部 listItems: 建议在 enqueue 内使用 (不嵌套 enqueue) +async function listItems(familyId, filters = {}) { + let sql = 'SELECT * FROM items WHERE family_id = ?' + const params = [familyId] + if (filters.boxId) { + if (filters.recursive) { + const ids = await subtreeIds(familyId, filters.boxId) + if (!ids.length) return [] + sql += ` AND box_id IN (${ids.map(() => '?').join(',')})` + params.push(...ids) + } else { + sql += ' AND box_id = ?' + params.push(filters.boxId) + } + } else if (filters.looseOnly) { + sql += ' AND box_id IS NULL' + } + if (filters.categoryId) { sql += ' AND category_id = ?'; params.push(filters.categoryId) } + if (filters.tag) { + // tags 存为 JSON 数组字符串: 去掉 [ ] " 后按逗号拆为列表再判断包含 + sql += " AND list_contains(string_split(regexp_replace(tags, '[\\[\\]\" ]', '', 'g'), ','), ?)" + params.push(filters.tag) + } + if (filters.q) { + sql += ' AND (name ILIKE ? OR COALESCE(brand, \'\') ILIKE ? OR COALESCE(model, \'\') ILIKE ? OR COALESCE(notes, \'\') ILIKE ?)' + const like = `%${filters.q}%` + params.push(like, like, like, like) + } + if (filters.favorite) sql += ' AND is_favorite = TRUE' + if (filters.expiring) { + sql += " AND expiry_date IS NOT NULL AND CAST(expiry_date AS DATE) >= CURRENT_DATE AND CAST(expiry_date AS DATE) < CURRENT_DATE + INTERVAL '30 days'" + } + sql += ' ORDER BY (is_favorite = TRUE) DESC, updated_at DESC' + const limit = Math.min(Number(filters.limit) || 200, 1000) + sql += ` LIMIT ${limit}` + const r = await rows(sql, params) + return r.map(mapItem) +} + +export const dbListItems = (familyId, filters = {}) => enqueue(() => listItems(familyId, filters)) + +const ITEM_SET_FIELDS = ['name', 'category_id', 'quantity', 'brand', 'model', 'serial_no', 'purchase_date', 'purchase_price', 'expiry_date', 'notes', 'is_favorite'] + +export function dbUpdateItem(familyId, itemId, data) { + return enqueue(async () => { + const item = await one('SELECT id FROM items WHERE id = ? AND family_id = ?', [itemId, familyId]) + if (!item) throw { status: 404, message: '物品不存在' } + const sets = [] + const params = [] + for (const f of ITEM_SET_FIELDS) { + if (data[f] === undefined) continue + if (f === 'quantity') { sets.push('quantity = ?'); params.push(Number(data[f]) || 1) } + else if (f === 'is_favorite') { sets.push('is_favorite = ?'); params.push(!!data[f]) } + else if (f === 'category_id' || f === 'brand' || f === 'model' || f === 'serial_no' || f === 'purchase_date' || f === 'purchase_price' || f === 'expiry_date' || f === 'notes') { + sets.push(`${f} = ?`); params.push(data[f] === '' ? null : data[f]) + } + else { sets.push(`${f} = ?`); params.push(data[f]) } + } + if (data.box_id !== undefined) { sets.push('box_id = ?'); params.push(data.box_id || null) } + if (data.tags !== undefined) { sets.push('tags = ?'); params.push(JSON.stringify(data.tags ?? [])) } + if (data.images !== undefined) { sets.push('images = ?'); params.push(JSON.stringify(data.images ?? [])) } + if (!sets.length) { sets.push('name = name') } + sets.push('updated_at = ?'); params.push(nowIso()) + params.push(itemId, familyId) + await conn.run(`UPDATE items SET ${sets.join(', ')} WHERE id = ? AND family_id = ?`, params) + }) +} + +export function dbDeleteItem(familyId, itemId) { + return enqueue(async () => { + await conn.run('DELETE FROM items WHERE id = ? AND family_id = ?', [itemId, familyId]) + }) +} + +export function dbMoveItem(familyId, itemId, boxId) { + return enqueue(async () => { + const item = await one('SELECT id FROM items WHERE id = ? AND family_id = ?', [itemId, familyId]) + if (!item) throw { status: 404, message: '物品不存在' } + if (boxId) { + const box = await one('SELECT id FROM boxes WHERE id = ? AND family_id = ?', [boxId, familyId]) + if (!box) throw { status: 400, message: '目标盒子不存在' } + } + await conn.run('UPDATE items SET box_id = ?, updated_at = ? WHERE id = ?', [boxId ?? null, nowIso(), itemId]) + }) +} + +// ══════════════ transfer (跨家庭转移) ══════════════ + +export function dbTransfer(familyId, data) { + return enqueue(async () => { + const { type, entityId, dstFamilyId, dstParentBoxId, posX, posY, operatorUserId } = data + if (!['item', 'box'].includes(type)) throw { status: 400, message: 'type 非法' } + if (familyId === dstFamilyId) throw { status: 400, message: '目标家庭不能是源家庭' } + + return tx(async () => { + if (type === 'item') { + const item = await one('SELECT * FROM items WHERE id = ? AND family_id = ?', [entityId, familyId]) + if (!item) throw { status: 404, message: '物品不存在' } + if (dstParentBoxId && (await one('SELECT id FROM boxes WHERE id = ? AND family_id = ?', [dstParentBoxId, dstFamilyId])) === null) throw { status: 400, message: '目标盒子不存在于目标家庭' } + await conn.run('UPDATE items SET family_id = ?, box_id = ?, updated_at = ? WHERE id = ?', [dstFamilyId, dstParentBoxId ?? null, nowIso(), entityId]) + await logTransfer('item', entityId, familyId, dstFamilyId, operatorUserId) + return { type, entityId, dstFamilyId, ok: true } + } + + const roots = await rows('SELECT id, parent_box_id, pos_x, pos_y FROM boxes WHERE id = ? AND family_id = ?', [entityId, familyId]) + if (!roots.length) throw { status: 404, message: '盒子不存在' } + if (dstParentBoxId && (await one('SELECT id FROM boxes WHERE id = ? AND family_id = ?', [dstParentBoxId, dstFamilyId])) === null) throw { status: 400, message: '目标盒子不存在于目标家庭' } + const ids = await subtreeIds(familyId, entityId) + const ph = ids.map(() => '?').join(',') + await conn.run(`UPDATE boxes SET family_id = ? WHERE id IN (${ph})`, [dstFamilyId, ...ids]) + await conn.run('UPDATE boxes SET parent_box_id = ?, pos_x = ?, pos_y = ?, updated_at = ? WHERE id = ?', + [dstParentBoxId ?? null, clampPos(posX), clampPos(posY), nowIso(), entityId]) + await conn.run(`UPDATE items SET family_id = ? WHERE family_id = ? AND box_id IN (${ph})`, [dstFamilyId, familyId, ...ids]) + await logTransfer('box', entityId, familyId, dstFamilyId, operatorUserId) + return { type, entityId, dstFamilyId, ok: true } + }) + }) +} + +async function logTransfer(type, entityId, srcFamilyId, dstFamilyId, operatorUserId) { + await conn.run('INSERT INTO transfer_logs (id, entity_type, entity_id, src_family_id, dst_family_id, operator_user_id, created_at) VALUES (?,?,?,?,?,?,?)', + [uu(), type, entityId, srcFamilyId, dstFamilyId, operatorUserId, nowIso()]) +} + +export const dbListTransferLogs = (familyId) => enqueue(async () => { + return rows('SELECT * FROM transfer_logs WHERE src_family_id = ? OR dst_family_id = ? ORDER BY created_at DESC LIMIT 100', [familyId, familyId]) +}) + +// ══════════════ stats ══════════════ + +export function dbGetFamilyStats(familyId) { + return enqueue(async () => { + const [totalItems, totalBoxes] = await Promise.all([ + count('SELECT COUNT(*) AS c FROM items WHERE family_id = ?', [familyId]), + count('SELECT COUNT(*) AS c FROM boxes WHERE family_id = ?', [familyId]), + ]) + const boxes = await allBoxes(familyId) + const rootBoxes = directChildren(boxes, null) + const valueRow = await one('SELECT COALESCE(SUM(purchase_price), 0) AS v FROM items WHERE family_id = ? AND purchase_price IS NOT NULL', [familyId]) + const looseCount = await count('SELECT COUNT(*) AS c FROM items WHERE family_id = ? AND box_id IS NULL', [familyId]) + const noImageCount = await count("SELECT COUNT(*) AS c FROM items WHERE family_id = ? AND images = '[]'", [familyId]) + const maxDepth = boxes.reduce((m, b) => Math.max(m, buildPath(boxes, b.id).length), 0) + const topBoxes = boxes + .map((b) => ({ id: b.id, name: b.name, icon: b.icon, count: subtreeItemCount(boxes, b.id), path: buildPath(boxes, b.id) })) + .filter((b) => b.count > 0) + .sort((a, b) => b.count - a.count) + .slice(0, 5) + const catRows = await rows( + `SELECT COALESCE(c.name, '未分类') AS name, COUNT(i.id) AS c + FROM items i LEFT JOIN categories c ON c.id = i.category_id + WHERE i.family_id = ? GROUP BY COALESCE(c.name, '未分类') ORDER BY c DESC`, + [familyId], + ) + const expiring = (await listItems(familyId, { expiring: true, limit: 50 })) + const recent = (await listItems(familyId, { limit: 8 })) + return { + total_items: totalItems, + total_boxes: totalBoxes, + root_boxes: rootBoxes.length, + max_depth: maxDepth, + total_value: Number(valueRow?.v || 0), + expiring_count: expiring.length, + expiring_items: expiring, + loose_items: looseCount, + no_image_count: noImageCount, + category_distribution: catRows.map((r) => ({ name: r.name, count: Number(r.c) })), + top_boxes: topBoxes, + recent_items: recent, + } + }) +} + +// ══════════════ MCP (P1: AI 助手查询) ══════════════ + +export function mcpTools() { + return [ + { name: 'list_my_families', description: '列出我加入的所有家庭', input_schema: '{"type":"object","properties":{},"required":[]}' }, + { name: 'search_items', description: '按关键字或标签搜索物品(需 family_id), 返回物品及所在位置', input_schema: '{"type":"object","properties":{"family_id":{"type":"string"},"q":{"type":"string"},"tag":{"type":"string"}},"required":["family_id"]}' }, + { name: 'get_item', description: '根据 item_id 和 family_id 获取物品详情与所在路径', input_schema: '{"type":"object","properties":{"item_id":{"type":"string"},"family_id":{"type":"string"}},"required":["item_id","family_id"]}' }, + { name: 'get_family_stats', description: '获取家庭物品统计概览', input_schema: '{"type":"object","properties":{"family_id":{"type":"string"}},"required":["family_id"]}' }, + ] +} + +export async function mcpCall(toolName, args = {}, userId) { + switch (toolName) { + case 'list_my_families': + return { ok: true, value: await dbListFamiliesForUser(userId) } + case 'search_items': { + const { family_id, q, tag } = args + const role = await dbGetRole(family_id, userId) + if (!role) throw { status: 403, message: '非该家庭成员' } + return { ok: true, value: await dbListItems(family_id, { q, tag, limit: 100 }) } + } + case 'get_item': { + const { item_id, family_id } = args + const role = await dbGetRole(family_id, userId) + if (!role) throw { status: 403, message: '非该家庭成员' } + const item = await dbGetItem(family_id, item_id) + if (!item) throw { status: 404, message: '物品不存在' } + const boxes = await allBoxes(family_id) + return { ok: true, value: { ...item, box_path: item.box_id ? buildPath(boxes, item.box_id) : [] } } + } + case 'get_family_stats': { + const { family_id } = args + const role = await dbGetRole(family_id, userId) + if (!role) throw { status: 403, message: '非该家庭成员' } + return { ok: true, value: await dbGetFamilyStats(family_id) } + } + default: + throw { status: 400, message: `Unknown tool: ${toolName}` } + } +} \ No newline at end of file diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..291bab0 --- /dev/null +++ b/server/index.js @@ -0,0 +1,55 @@ +// Storage 服务端入口: 业务 API + 图片 API + 静态托管 (SPA) +// 端口由部署环境决定 (PORT env), 本地缺省 3002。见 部署环境.md +import 'dotenv/config' +import path from 'node:path' +import fs from 'node:fs' +import express from 'express' +import cors from 'cors' +import appRouter from './routes/app.js' +import imagesRouter from './routes/images.js' +import { initDb } from './db.js' + +const PORT = Number(process.env.PORT) || 3002 +const DIST_DIR = path.join(process.cwd(), 'dist') +const HAS_DIST = fs.existsSync(DIST_DIR) + +const app = express() +app.use(cors()) +app.use(express.json({ limit: '2mb' })) + +app.get('/healthz', (_req, res) => res.json({ ok: true, ts: Date.now() })) +app.get('/api/health', (_req, res) => res.json({ ok: true, ts: Date.now() })) + +// 业务接口 +app.use('/api', appRouter) +// 图片接口 (签名 URL 鉴权), 需在 SPA fallback 之前挂载 +app.use('/api', imagesRouter) + +if (HAS_DIST) { + app.use(express.static(DIST_DIR, { index: false, maxAge: '1h', etag: true })) + app.get(/^\/(?!api\/).*/, (_req, res, next) => { + const indexHtml = path.join(DIST_DIR, 'index.html') + if (!fs.existsSync(indexHtml)) return next() + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') + res.sendFile(indexHtml) + }) +} + +app.use((err, _req, res, _next) => { + console.error('[api error]', err) + const code = err && err.status ? Number(err.status) : 500 + if (code >= 500) console.error(err.stack || err) + res.status(code).json({ error: (err && err.message) || '服务器内部错误' }) +}) + +initDb() + .then(() => { + app.listen(PORT, () => { + console.log(`[storage-api] listening on http://127.0.0.1:${PORT}`) + console.log(`[storage-api] static dist: ${HAS_DIST ? DIST_DIR : '(not built, API only)'}`) + }) + }) + .catch((e) => { + console.error('[db] failed to init DuckDB:', e) + process.exit(1) + }) \ No newline at end of file diff --git a/server/routes/app.js b/server/routes/app.js new file mode 100644 index 0000000..ed542bd --- /dev/null +++ b/server/routes/app.js @@ -0,0 +1,356 @@ +// Storage 业务 REST API: 家庭 / 盒子 / 物品 / 分类 / 转移 / 统计 / MCP +// 权限模型: 管理员(admin)可写, 成员(member)只读。服务端为唯一权威。 +import { Router } from 'express' +import * as auth from '../auth.js' +import { finalizeImages, deleteImageFiles } from './images.js' +import { + // users/sessions + dbGetUserById, dbUpdateUserNickname, + // families + dbCreateFamily, dbListFamiliesForUser, dbGetFamily, dbUpdateFamily, dbDeleteFamily, + dbGetRole, dbAddMember, dbRemoveMember, dbSetRole, dbSetInviteCode, dbJoinByCode, + dbListMembers, + // categories + dbListCategories, dbCreateCategory, dbUpdateCategory, dbDeleteCategory, + // boxes + dbCreateBox, dbGetBox, dbUpdateBox, dbMoveBox, dbSetBoxPosition, dbDeleteBox, + dbListChildBoxes, dbBuildTree, dbBoxPath, + // items + dbCreateItem, dbGetItem, dbListItems, dbUpdateItem, dbDeleteItem, dbMoveItem, + // transfer + dbTransfer, dbListTransferLogs, + // stats / mcp + dbGetFamilyStats, mcpTools, mcpCall, + uu, +} from '../db.js' + +const router = Router() +const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next) + +// ── 鉴权中间件: Authorization: Bearer ── +async function requireAuth(req, res, next) { + try { + const h = req.headers.authorization || '' + const token = h.startsWith('Bearer ') ? h.slice(7).trim() : null + const user = await auth.resolveSession(token) + if (!user) return res.status(401).json({ error: '未登录或登录已过期' }) + req.user = user + req.token = token + next() + } catch (e) { + next(e) + } +} + +// 家庭成员校验: familyId 取 URL 中的 :id +async function requireMember(req, res, next) { + const role = await dbGetRole(req.params.id, req.user.id) + if (!role) return res.status(403).json({ error: '你不是该家庭成员' }) + req.role = role + next() +} + +// 家庭管理员校验 (所有写操作) +async function requireAdmin(req, res, next) { + const role = await dbGetRole(req.params.id, req.user.id) + if (role !== 'admin') return res.status(403).json({ error: '仅管理员可执行此操作' }) + req.role = role + next() +} + +// ── Auth ── +router.post('/auth/send-code', asyncH(async (req, res) => { + const target = String(req.body?.target || '').trim() + if (!target) return res.status(400).json({ error: '请提供邮箱或手机号' }) + res.json({ message: auth.sendVerification(target) }) +})) + +router.post('/auth/register', asyncH(async (req, res) => { + const { email, phone, code, nickname } = req.body || {} + res.json(await auth.register({ email: email || null, phone: phone || null, code, nickname })) +})) + +router.post('/auth/login', asyncH(async (req, res) => { + const { email, phone, code } = req.body || {} + res.json(await auth.login({ email: email || null, phone: phone || null, code })) +})) + +router.post('/auth/logout', requireAuth, asyncH(async (req, res) => { + await auth.logout(req.token) + res.json({ success: true }) +})) + +router.get('/auth/me', requireAuth, asyncH(async (req, res) => { + res.json({ user: await dbGetUserById(req.user.id) }) +})) + +router.put('/auth/me', requireAuth, asyncH(async (req, res) => { + const nickname = String(req.body?.nickname || '').trim() + if (!nickname) return res.status(400).json({ error: '昵称不能为空' }) + await dbUpdateUserNickname(req.user.id, nickname) + res.json({ user: await dbGetUserById(req.user.id) }) +})) + +// ── Families ── +router.get('/families', requireAuth, asyncH(async (req, res) => { + res.json(await dbListFamiliesForUser(req.user.id)) +})) + +router.post('/families', requireAuth, asyncH(async (req, res) => { + const name = String(req.body?.name || '').trim() + if (!name) return res.status(400).json({ error: '请输入家庭名称' }) + res.json(await dbCreateFamily(req.user.id, name, req.body?.icon || '🏠')) +})) + +router.get('/families/:id', requireAuth, requireMember, asyncH(async (req, res) => { + res.json({ + family: await dbGetFamily(req.params.id), + members: await dbListMembers(req.params.id), + my_role: req.role, + }) +})) + +router.put('/families/:id', requireAuth, requireAdmin, asyncH(async (req, res) => { + const name = String(req.body?.name || '').trim() + if (!name) return res.status(400).json({ error: '请输入家庭名称' }) + await dbUpdateFamily(req.params.id, { name, icon: req.body?.icon || '🏠' }) + res.json({ success: true, family: await dbGetFamily(req.params.id) }) +})) + +router.delete('/families/:id', requireAuth, requireAdmin, asyncH(async (req, res) => { + const family = await dbGetFamily(req.params.id) + if (family.owner_user_id !== req.user.id) return res.status(403).json({ error: '仅创建者可解散家庭' }) + await dbDeleteFamily(req.params.id) + res.json({ success: true }) +})) + +// 通过邀请码加入 +router.post('/families/:id/join', requireAuth, asyncH(async (req, res) => { + const code = String(req.body?.invite_code || '').trim().toUpperCase() + if (!code) return res.status(400).json({ error: '请输入邀请码' }) + res.json(await dbJoinByCode(code, req.user.id)) +})) + +// 邀请码管理 (管理员) +router.post('/families/:id/invite', requireAuth, requireAdmin, asyncH(async (req, res) => { + const action = req.body?.action || 'refresh' + if (action === 'clear') { + await dbSetInviteCode(req.params.id, null) + return res.json({ invite_code: null }) + } + let code + do { code = 'ST-' + uu().replaceAll('-', '').slice(0, 8).toUpperCase() } while (code.includes('0') || code.includes('O')) + await dbSetInviteCode(req.params.id, code) + res.json({ invite_code: code }) +})) + +router.post('/families/:id/members/:userId/role', requireAuth, requireAdmin, asyncH(async (req, res) => { + const role = req.body?.role + if (!['admin', 'member'].includes(role)) return res.status(400).json({ error: 'role 非法' }) + const family = await dbGetFamily(req.params.id) + const targetExists = (await dbListMembers(req.params.id)).some((m) => m.user_id === req.params.userId) + if (!targetExists) return res.status(404).json({ error: '成员不存在' }) + if (req.params.userId === family.owner_user_id) return res.status(403).json({ error: '不能修改创建者角色' }) + await dbSetRole(req.params.id, req.params.userId, role) + res.json({ success: true }) +})) + +router.post('/families/:id/members/:userId/add', requireAuth, requireAdmin, asyncH(async (req, res) => { + await dbAddMember(req.params.id, req.params.userId, req.body?.role || 'member') + res.json({ success: true }) +})) + +// 移除成员; userId === 'me' 表示自己离开 +router.delete('/families/:id/members/:userId', requireAuth, requireAdmin, asyncH(async (req, res) => { + const family = await dbGetFamily(req.params.id) + const target = req.params.userId === 'me' ? req.user.id : req.params.userId + if (target === family.owner_user_id) return res.status(403).json({ error: '创建者不能离开/被移除, 请先解散家庭' }) + if (target === req.user.id && req.params.userId === 'me') { + await dbRemoveMember(req.params.id, target) + return res.json({ success: true, left: true }) + } + await dbRemoveMember(req.params.id, target) + res.json({ success: true }) +})) + +// ── Categories ── +router.get('/families/:id/categories', requireAuth, requireMember, asyncH(async (req, res) => { + res.json(await dbListCategories(req.params.id)) +})) + +router.post('/families/:id/categories', requireAuth, requireAdmin, asyncH(async (req, res) => { + const name = String(req.body?.name || '').trim() + if (!name) return res.status(400).json({ error: '请输入分类名称' }) + res.json(await dbCreateCategory(req.params.id, name)) +})) + +router.put('/families/:id/categories/:catId', requireAuth, requireAdmin, asyncH(async (req, res) => { + const name = String(req.body?.name || '').trim() + if (!name) return res.status(400).json({ error: '请输入分类名称' }) + await dbUpdateCategory(req.params.id, req.params.catId, name) + res.json({ success: true }) +})) + +router.delete('/families/:id/categories/:catId', requireAuth, requireAdmin, asyncH(async (req, res) => { + await dbDeleteCategory(req.params.id, req.params.catId) + res.json({ success: true }) +})) + +// ── Boxes ── +// 直接子盒 (根层 = 不带 parent 参数) +router.get('/families/:id/boxes', requireAuth, requireMember, asyncH(async (req, res) => { + if (req.query.tree === '1') return res.json(await dbBuildTree(req.params.id)) + const parentId = req.query.parent && req.query.parent !== 'root' ? String(req.query.parent) : null + res.json(await dbListChildBoxes(req.params.id, parentId)) +})) + +router.get('/families/:id/boxes/:boxId', requireAuth, requireMember, asyncH(async (req, res) => { + const box = await dbGetBox(req.params.id, req.params.boxId) + if (!box) return res.status(404).json({ error: '盒子不存在' }) + const [path, children] = await Promise.all([ + dbBoxPath(req.params.id, req.params.boxId), + dbListChildBoxes(req.params.id, req.params.boxId), + ]) + res.json({ box, path, children }) +})) + +router.post('/families/:id/boxes', requireAuth, requireAdmin, asyncH(async (req, res) => { + const b = req.body || {} + if (!b.name) return res.status(400).json({ error: '请输入盒子名称' }) + res.json(await dbCreateBox(req.params.id, { + name: String(b.name).trim(), + parent_box_id: b.parent_box_id || null, + icon: b.icon, color: b.color, + pos_x: b.pos_x, pos_y: b.pos_y, + width: b.width ?? null, height: b.height ?? null, + })) +})) + +router.put('/families/:id/boxes/:boxId', requireAuth, requireAdmin, asyncH(async (req, res) => { + const b = req.body || {} + const patch = {} + if (b.name !== undefined) patch.name = String(b.name).trim() + if (b.icon !== undefined) patch.icon = b.icon + if (b.color !== undefined) patch.color = b.color + if (b.width !== undefined) patch.width = b.width + if (b.height !== undefined) patch.height = b.height + if (!Object.keys(patch).length) return res.status(400).json({ error: '没有可更新的字段' }) + await dbUpdateBox(req.params.id, req.params.boxId, patch) + res.json({ success: true, box: await dbGetBox(req.params.id, req.params.boxId) }) +})) + +// 整盒移动 (跨容器 / 根层) +router.put('/families/:id/boxes/:boxId/move', requireAuth, requireAdmin, asyncH(async (req, res) => { + const b = req.body || {} + const parent = b.parent_box_id && b.parent_box_id !== 'root' ? String(b.parent_box_id) : null + await dbMoveBox(req.params.id, req.params.boxId, parent, b.pos_x ?? 0, b.pos_y ?? 0) + res.json({ success: true, box: await dbGetBox(req.params.id, req.params.boxId) }) +})) + +// 仅调整位置 (画布拖拽) +router.put('/families/:id/boxes/:boxId/position', requireAuth, requireAdmin, asyncH(async (req, res) => { + const b = req.body || {} + await dbSetBoxPosition(req.params.id, req.params.boxId, b.pos_x ?? 0, b.pos_y ?? 0) + res.json({ success: true }) +})) + +router.delete('/families/:id/boxes/:boxId', requireAuth, requireAdmin, asyncH(async (req, res) => { + await dbDeleteBox(req.params.id, req.params.boxId) + res.json({ success: true }) +})) + +// ── Items ── +router.get('/families/:id/items', requireAuth, requireMember, asyncH(async (req, res) => { + const q = req.query + const filters = { + boxId: q.box && q.box !== 'root' ? String(q.box) : null, + recursive: q.recursive === '1', + looseOnly: q.loose === '1', + categoryId: q.category || null, + tag: q.tag || null, + q: q.q ? String(q.q) : null, + favorite: q.favorite === '1', + expiring: q.expiring === '1', + limit: q.limit, + } + res.json(await dbListItems(req.params.id, filters)) +})) + +router.post('/families/:id/items', requireAuth, requireAdmin, asyncH(async (req, res) => { + const b = req.body || {} + if (!b.name) return res.status(400).json({ error: '请输入物品名称' }) + b.images = finalizeImages(b.images || [], req.params.id) + res.json(await dbCreateItem(req.params.id, b)) +})) + +router.get('/families/:id/items/:itemId', requireAuth, requireMember, asyncH(async (req, res) => { + const item = await dbGetItem(req.params.id, req.params.itemId) + if (!item) return res.status(404).json({ error: '物品不存在' }) + const path = item.box_id ? await dbBoxPath(req.params.id, item.box_id) : [] + res.json({ item, box_path: path }) +})) + +router.put('/families/:id/items/:itemId', requireAuth, requireAdmin, asyncH(async (req, res) => { + const b = req.body || {} + if (b.images !== undefined) b.images = finalizeImages(b.images || [], req.params.id) + await dbUpdateItem(req.params.id, req.params.itemId, b) + res.json({ success: true, item: await dbGetItem(req.params.id, req.params.itemId) }) +})) + +router.delete('/families/:id/items/:itemId', requireAuth, requireAdmin, asyncH(async (req, res) => { + const item = await dbGetItem(req.params.id, req.params.itemId) + if (item?.images?.length) deleteImageFiles(item.images) + await dbDeleteItem(req.params.id, req.params.itemId) + res.json({ success: true }) +})) + +// 物品移动 (换盒 / 根层) +router.put('/families/:id/items/:itemId/move', requireAuth, requireAdmin, asyncH(async (req, res) => { + const b = req.body || {} + const boxId = b.box_id && b.box_id !== 'root' ? String(b.box_id) : null + await dbMoveItem(req.params.id, req.params.itemId, boxId) + res.json({ success: true, box_id: boxId }) +})) + +// ── Transfer (跨家庭转移) ── +// 权限: 操作者必须同时是 源家庭管理员 与 目标家庭管理员 (见 §14 决策点 2) +router.post('/families/:id/transfer', requireAuth, requireAdmin, asyncH(async (req, res) => { + const b = req.body || {} + const dstFamilyId = String(b.dstFamilyId || '') + if (!dstFamilyId) return res.status(400).json({ error: '缺少目标家庭' }) + const dstRole = await dbGetRole(dstFamilyId, req.user.id) + if (dstRole !== 'admin') return res.status(403).json({ error: '仅能转移到自己拥有管理权限的家庭' }) + const dstParent = b.dstParentBoxId && b.dstParentBoxId !== 'root' ? String(b.dstParentBoxId) : null + const r = await dbTransfer(req.params.id, { + type: b.type, + entityId: String(b.entityId || ''), + dstFamilyId, + dstParentBoxId: dstParent, + posX: b.pos_x, posY: b.pos_y, + operatorUserId: req.user.id, + }) + res.json(r) +})) + +router.get('/families/:id/transfer-logs', requireAuth, requireAdmin, asyncH(async (req, res) => { + res.json(await dbListTransferLogs(req.params.id)) +})) + +// ── Stats ── +router.get('/families/:id/stats', requireAuth, requireMember, asyncH(async (req, res) => { + res.json(await dbGetFamilyStats(req.params.id)) +})) + +// ── MCP (P1: AI 助手查询) ── +router.get('/mcp/tools', requireAuth, asyncH(async (req, res) => { + res.json(mcpTools()) +})) + +router.post('/mcp/call', requireAuth, asyncH(async (req, res) => { + const { tool_name, args } = req.body || {} + if (!tool_name) return res.status(400).json({ error: '缺少 tool_name' }) + const r = await mcpCall(tool_name, args || {}, req.user.id) + res.json(r.value) +})) + +export default router \ No newline at end of file diff --git a/server/routes/images.js b/server/routes/images.js new file mode 100644 index 0000000..17e8e36 --- /dev/null +++ b/server/routes/images.js @@ -0,0 +1,169 @@ +// 图片: multipart 上传 + 短时签名 URL 鉴权 (决策点 1) +// - 上传: POST /api/images (登录即可, 归属家庭由后续挂到物品时确定) +// - 签名: POST /api/images/sign {path} -> {url}, 15 分钟内有效, 校验家庭成员 +// - 读取: GET /api/images/:familyId/:file?exp=&sig=, 校验签名与成员身份 +// - 物理文件: //., 不落入任何静态目录 +import { Router } from 'express' +import fs from 'node:fs' +import path from 'node:path' +import crypto from 'node:crypto' +import multer from 'multer' +import * as auth from '../auth.js' +import { dbGetRole, dbListFamiliesForUser } from '../db.js' + +const router = Router() +const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next) + +const UPLOAD_DIR = process.env.UPLOAD_DIR || path.resolve(process.cwd(), 'data', 'uploads') +fs.mkdirSync(UPLOAD_DIR, { recursive: true }) +const SIGN_TTL_MS = 15 * 60 * 1000 +const MAX_SIZE = 8 * 1024 * 1024 + +// 签名密钥: 优先环境变量; 否则持久化到 data/.image-secret, 保证重启后旧签名仍可校验 +function secret() { + if (process.env.IMAGE_SECRET) return process.env.IMAGE_SECRET + const f = path.resolve(process.cwd(), 'data', '.image-secret') + fs.mkdirSync(path.dirname(f), { recursive: true }) + if (fs.existsSync(f)) return fs.readFileSync(f, 'utf8').trim() + const s = crypto.randomBytes(32).toString('hex') + fs.writeFileSync(f, s, { mode: 0o600 }) + return s +} + +const extWhitelist = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']) + +async function requireAuth(req, res, next) { + try { + const h = req.headers.authorization || '' + const token = h.startsWith('Bearer ') ? h.slice(7).trim() : null + const user = await auth.resolveSession(token) + if (!user) return res.status(401).json({ error: '未登录或登录已过期' }) + req.user = user + next() + } catch (e) { + next(e) + } +} + +const upload = multer({ + storage: multer.diskStorage({ + destination: (_req, _file, cb) => cb(null, UPLOAD_DIR), + filename: (_req, file, cb) => cb(null, crypto.randomUUID() + path.extname(file.originalname).toLowerCase()), + }), + limits: { fileSize: MAX_SIZE }, + fileFilter: (_req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase() + cb(extWhitelist.has(ext) ? null : new Error('仅支持 jpg/png/webp/gif'), extWhitelist.has(ext)) + }, +}) + +function sign(fileKey, exp) { + return crypto.createHmac('sha256', secret()).update(`${exp}:${fileKey}`).digest('hex') +} + +// 上传: 存到 staging 目录, 返回临时文件名 (带 familyId 前缀由客户端后续挂载时确定) +router.post('/images', requireAuth, upload.single('file'), asyncH(async (req, res) => { + if (!req.file) return res.status(400).json({ error: '未收到文件' }) + const name = req.file.filename + res.json({ path: name, name, size: req.file.size }) +})) + +// 生成短时签名 URL; path 形如 / 或 +router.post('/images/sign', requireAuth, asyncH(async (req, res) => { + const rawPath = String(req.body?.path || '').trim() + if (!rawPath) return res.status(400).json({ error: '缺少 path' }) + const parts = rawPath.split('/') + const fileName = parts[parts.length - 1] + const familyId = parts.length >= 2 ? parts[0] : null + // 旧格式 (无 familyId 前缀): 从用户所属家庭中匹配该文件 + let fileKey + if (familyId) { + const role = await dbGetRole(familyId, req.user.id) + if (role) fileKey = `${familyId}/${fileName}` + } else { + const families = await dbListFamiliesForUser(req.user.id) + for (const f of families) { + const p = path.join(UPLOAD_DIR, f.id, fileName) + if (fs.existsSync(p)) { fileKey = `${f.id}/${fileName}`; break } + const p2 = path.join(UPLOAD_DIR, fileName) + if (fs.existsSync(p2)) { fileKey = fileName; break } + } + } + if (!fileKey) return res.status(403).json({ error: '无权访问该图片' }) + res.json({ url: signedUrl(fileKey) }) +})) + +function signedUrl(fileKey) { + const exp = Date.now() + SIGN_TTL_MS + const sig = sign(fileKey, exp) + return `/api/images/${fileKey}?exp=${exp}&sig=${sig}` +} + +// 读取: 验签即鉴权 (签名只能由服务端在成员校验通过后签发, 15 分钟过期) +// 不要求 Bearer 头, 以便 直接加载 (决策点 1: 短时签名 URL 免登录浏览) +router.get('/images/:familyId/:file', asyncH(async (req, res) => { + const { familyId, file } = req.params + const { exp, sig } = req.query + if (!exp || !sig) return res.status(403).json({ error: '缺少签名参数' }) + const expNum = Number(exp) + if (!Number.isFinite(expNum) || expNum < Date.now()) return res.status(403).json({ error: '链接已过期' }) + const fileKey = `${familyId}/${file}` + const expect = sign(fileKey, expNum) + const sigBuf = Buffer.from(String(sig), 'utf8') + const expectBuf = Buffer.from(expect, 'utf8') + if (sigBuf.length !== expectBuf.length || !crypto.timingSafeEqual(sigBuf, expectBuf)) { + return res.status(403).json({ error: '签名无效' }) + } + const full = path.join(UPLOAD_DIR, familyId, file) + if (!fs.existsSync(full)) return res.status(404).json({ error: '文件不存在' }) + res.sendFile(full) +})) + +// 旧格式兼容: /api/images/:file (无家庭前缀, 文件名反查; 同样验签) +router.get('/images/:file([a-zA-Z0-9-]+[.][a-zA-Z0-9]+)', asyncH(async (req, res) => { + const { file } = req.params + const { exp, sig } = req.query + if (!exp || !sig) return res.status(403).json({ error: '缺少签名参数' }) + const expNum = Number(exp) + if (!Number.isFinite(expNum) || expNum < Date.now()) return res.status(403).json({ error: '链接已过期' }) + const fileKey = file + const expect = sign(fileKey, expNum) + const sigBuf = Buffer.from(String(sig), 'utf8') + const expectBuf = Buffer.from(expect, 'utf8') + if (sigBuf.length !== expectBuf.length || !crypto.timingSafeEqual(sigBuf, expectBuf)) { + return res.status(403).json({ error: '签名无效' }) + } + const full = path.join(UPLOAD_DIR, file) + if (!fs.existsSync(full)) return res.status(404).json({ error: '文件不存在' }) + res.sendFile(full) +})) + +// 物品保存时把暂存的上传文件归入家庭目录, 得到规范路径 / +// 返回清理后的路径数组; 已是本家庭路径的保持不变 +export function finalizeImages(images, familyId) { + const dir = path.join(UPLOAD_DIR, familyId) + fs.mkdirSync(dir, { recursive: true }) + const out = [] + for (const img of images || []) { + const s = String(img).trim() + if (!s) continue + if (s.startsWith(familyId + '/')) { out.push(s); continue } + const fileName = s.includes('/') ? s.split('/').pop() : s + const staging = path.join(UPLOAD_DIR, fileName) + if (!fs.existsSync(staging)) continue + const dest = path.join(dir, fileName) + if (!fs.existsSync(dest)) fs.copyFileSync(staging, dest) + try { fs.unlink(staging) } catch { /* ignore */ } + out.push(`${familyId}/${fileName}`) + } + return out +} + +// 删除物品时顺带清理其图片文件 +export function deleteImageFiles(images) { + for (const img of images || []) { + try { fs.unlinkSync(path.join(UPLOAD_DIR, img)) } catch { /* ignore */ } + } +} + +export default router \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..c7d4b2f --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,99 @@ +import React, { useEffect } from "react"; +import { BrowserRouter, Routes, Route, Navigate, useNavigate } from "react-router-dom"; +import { useAppStore } from "./stores/app"; +import { onToast } from "./lib/toast"; +import Login from "./pages/Login"; +import Home from "./pages/Home"; +import Boxes from "./pages/Boxes"; +import BoxDetail from "./pages/BoxDetail"; +import ItemList from "./pages/ItemList"; +import ItemDetail from "./pages/ItemDetail"; +import ItemEdit from "./pages/ItemEdit"; +import FamilyManage from "./pages/FamilyManage"; +import Profile from "./pages/Profile"; +import Layout from "./components/Layout"; + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const user = useAppStore((s) => s.user); + if (!user) return ; + return <>{children}; +} + +function getStoredUser(): unknown { + try { + const saved = localStorage.getItem("storage_user"); + if (!saved) return null; + const p = JSON.parse(saved); + return p?.user ?? null; + } catch { + return null; + } +} + +function RootGate() { + const user = useAppStore((s) => s.user); + const effective = user ?? getStoredUser(); + return ; +} + +function ToastHost() { + const [msg, setMsg] = React.useState(null); + useEffect(() => { + let timer: ReturnType; + const off = onToast((m) => { + setMsg(m); + clearTimeout(timer); + timer = setTimeout(() => setMsg(null), 2400); + }); + return off; + }, []); + if (!msg) return null; + return
{msg}
; +} + +function RouterGate() { + const user = useAppStore((s) => s.user); + const navigate = useNavigate(); + + useEffect(() => { + const saved = localStorage.getItem("storage_user"); + if (saved && !user) { + try { + const parsed = JSON.parse(saved); + useAppStore.getState().setUser(parsed.user, parsed.token); + navigate("/home", { replace: true }); + } catch { /* ignore */ } + } else if (user) { + localStorage.setItem("storage_user", JSON.stringify(useAppStore.getState())); + } + }, [user, navigate]); + + return ( + <> + + + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); +} + +export default function App() { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/api/config.ts b/src/api/config.ts new file mode 100644 index 0000000..8b6936c --- /dev/null +++ b/src/api/config.ts @@ -0,0 +1,2 @@ +// 统一 API 基址: 生产同源 (/api), 也可用 VITE_API_BASE 强制绝对地址 +export const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? ""; \ No newline at end of file diff --git a/src/api/images.ts b/src/api/images.ts new file mode 100644 index 0000000..1555407 --- /dev/null +++ b/src/api/images.ts @@ -0,0 +1,64 @@ +// 图片上传与短时签名 URL (与 server/routes/images.js 对应) +import { API_BASE } from "./config"; +import { getToken } from "./index"; + +function base(p: string) { + return API_BASE ? API_BASE + p : p; +} + +function authHeaders(): Record { + const t = getToken(); + return t ? { Authorization: `Bearer ${t}` } : {}; +} + +async function request(method: string, path: string, body?: unknown): Promise { + const headers: Record = { "Content-Type": "application/json", ...authHeaders() }; + let res: Response; + try { + res = await fetch(base(path), { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); + } catch { + throw new Error("网络错误,请检查网络连接"); + } + let data: unknown = null; + try { + data = await res.json(); + } catch { /* no body */ } + if (!res.ok) { + const msg = (data as { error?: string } | null)?.error ?? `请求失败 (${res.status})`; + if (res.status === 401) localStorage.removeItem("storage_user"); + throw new Error(msg); + } + return data as T; +} + +// 上传: multipart/form-data, 返回暂存文件名 (挂到物品后由服务端归入家庭目录) +export async function uploadImage(file: File): Promise { + const fd = new FormData(); + fd.append("file", file); + let res: Response; + try { + res = await fetch(base("/api/images"), { method: "POST", headers: authHeaders(), body: fd }); + } catch { + throw new Error("网络错误,请检查网络连接"); + } + let data: unknown = null; + try { + data = await res.json(); + } catch { /* no body */ } + if (!res.ok) { + const msg = (data as { error?: string } | null)?.error ?? `上传失败 (${res.status})`; + throw new Error(msg); + } + return (data as { path: string }).path; +} + +// 生成短时签名 URL (15 分钟), 供 直接使用 +export function signImagePath(path: string) { + return request<{ url: string }>("POST", "/api/images/sign", { path }); +} + +// 带鉴权的输入框支持 (web 浏览器限制多, 移动端/桌面经 Tauri shell 无缝) +export function imageAuthHeader(): Record { + const t = getToken(); + return t ? { Authorization: `Bearer ${t}` } : {}; +} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..bf4b104 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,173 @@ +// 数据层统一走 HTTP API (服务端 DuckDB 是唯一数据源) +// token 从 localStorage["storage_user"] 快照取; 401 自动清理 +import { API_BASE } from "./config"; +import type { + User, AuthResult, Family, FamilySummary, FamilyDetail, Category, + Box, BoxTreeNode, Item, ItemDetail, TransferLog, FamilyStats, McpTool, Role, +} from "../types"; + +export function getToken(): string | null { + try { + const saved = localStorage.getItem("storage_user"); + if (!saved) return null; + const p = JSON.parse(saved); + return p?.token ?? null; + } catch { + return null; + } +} + +function base(p: string) { + return API_BASE ? API_BASE + p : p; +} + +async function request(method: string, path: string, body?: unknown): Promise { + const headers: Record = { "Content-Type": "application/json" }; + const token = getToken(); + if (token) headers.Authorization = `Bearer ${token}`; + + let res: Response; + try { + res = await fetch(base(path), { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + }); + } catch { + throw new Error("网络错误,请检查网络连接"); + } + + let data: unknown = null; + try { + data = await res.json(); + } catch { + /* 无 body */ + } + + if (!res.ok) { + const msg = (data as { error?: string; message?: string } | null)?.error + ?? (data as { message?: string } | null)?.message + ?? `请求失败 (${res.status})`; + if (res.status === 401) localStorage.removeItem("storage_user"); + throw new Error(msg); + } + return data as T; +} + +// ── Auth ── +export const sendCode = (target: string) => + request<{ message: string }>("POST", "/api/auth/send-code", { target }).then((r) => r.message); +export const register = (email: string | null, phone: string | null, code: string, nickname: string) => + request("POST", "/api/auth/register", { email, phone, code, nickname }); +export const login = (email: string | null, phone: string | null, code: string) => + request("POST", "/api/auth/login", { email, phone, code }); +export const logout = () => + request<{ success: boolean }>("POST", "/api/auth/logout").then(() => undefined); +export const fetchMe = () => + request<{ user: User }>("GET", "/api/auth/me").then((r) => r.user); +export const updateMe = (nickname: string) => + request<{ user: User }>("PUT", "/api/auth/me", { nickname }).then((r) => r.user); + +// ── Families ── +export const listFamilies = () => + request("GET", "/api/families"); +export const createFamily = (name: string, icon?: string) => + request("POST", "/api/families", { name, icon }); +export const getFamilyDetail = (familyId: string) => + request("GET", `/api/families/${familyId}`); +export const updateFamily = (familyId: string, name: string, icon?: string) => + request<{ success: boolean; family: Family }>("PUT", `/api/families/${familyId}`, { name, icon }); +export const deleteFamily = (familyId: string) => + request<{ success: boolean }>("DELETE", `/api/families/${familyId}`); +export const joinFamily = (familyId: string, inviteCode: string) => + request<{ success: boolean; message: string; familyId?: string }>("POST", `/api/families/${familyId}/join`, { invite_code: inviteCode }); +export const refreshInviteCode = (familyId: string) => + request<{ invite_code: string | null }>("POST", `/api/families/${familyId}/invite`, { action: "refresh" }); +export const clearInviteCode = (familyId: string) => + request<{ invite_code: string | null }>("POST", `/api/families/${familyId}/invite`, { action: "clear" }); +export const setMemberRole = (familyId: string, userId: string, role: Role) => + request<{ success: boolean }>("POST", `/api/families/${familyId}/members/${userId}/role`, { role }); +export const removeMember = (familyId: string, userId: string) => + request<{ success: boolean }>("DELETE", `/api/families/${familyId}/members/${userId}`); + +// ── Categories ── +export const listCategories = (familyId: string) => + request("GET", `/api/families/${familyId}/categories`); +export const createCategory = (familyId: string, name: string) => + request("POST", `/api/families/${familyId}/categories`, { name }); +export const updateCategory = (familyId: string, catId: string, name: string) => + request<{ success: boolean }>("PUT", `/api/families/${familyId}/categories/${catId}`, { name }); +export const deleteCategory = (familyId: string, catId: string) => + request<{ success: boolean }>("DELETE", `/api/families/${familyId}/categories/${catId}`); + +// ── Boxes ── +export const listChildBoxes = (familyId: string, parentBoxId?: string | null) => + request("GET", `/api/families/${familyId}/boxes${parentBoxId ? `?parent=${parentBoxId}` : ""}`); +export const fetchBoxTree = (familyId: string) => + request("GET", `/api/families/${familyId}/boxes?tree=1`); +export const getBoxDetail = (familyId: string, boxId: string) => + request<{ box: Box; path: { id: string; name: string }[]; children: Box[] }>( + "GET", `/api/families/${familyId}/boxes/${boxId}`); +export const createBox = (familyId: string, data: { name: string; parent_box_id?: string | null; icon?: string; color?: string; pos_x?: number; pos_y?: number; width?: number | null; height?: number | null }) => + request("POST", `/api/families/${familyId}/boxes`, data); +export const updateBox = (familyId: string, boxId: string, data: { name?: string; icon?: string; color?: string; width?: number | null; height?: number | null }) => + request<{ success: boolean; box: Box }>("PUT", `/api/families/${familyId}/boxes/${boxId}`, data); +export const moveBox = (familyId: string, boxId: string, parent_box_id: string | null, pos_x?: number, pos_y?: number) => + request<{ success: boolean; box: Box }>("PUT", `/api/families/${familyId}/boxes/${boxId}/move`, { parent_box_id, pos_x, pos_y }); +export const setBoxPosition = (familyId: string, boxId: string, pos_x: number, pos_y: number) => + request<{ success: boolean }>("PUT", `/api/families/${familyId}/boxes/${boxId}/position`, { pos_x, pos_y }); +export const deleteBox = (familyId: string, boxId: string) => + request<{ success: boolean }>("DELETE", `/api/families/${familyId}/boxes/${boxId}`); + +// ── Items ── +export interface ItemFilters { + box?: string | null; + recursive?: boolean; + loose?: boolean; + category?: string; + tag?: string; + q?: string; + favorite?: boolean; + expiring?: boolean; + limit?: number; +} +export const listItems = (familyId: string, f: ItemFilters = {}) => { + const p = new URLSearchParams(); + if (f.box) p.set("box", f.box); + if (f.recursive) p.set("recursive", "1"); + if (f.loose) p.set("loose", "1"); + if (f.category) p.set("category", f.category); + if (f.tag) p.set("tag", f.tag); + if (f.q) p.set("q", f.q); + if (f.favorite) p.set("favorite", "1"); + if (f.expiring) p.set("expiring", "1"); + if (f.limit) p.set("limit", String(f.limit)); + const qs = p.toString(); + return request("GET", `/api/families/${familyId}/items${qs ? `?${qs}` : ""}`); +}; +export const createItem = (familyId: string, data: Partial) => + request("POST", `/api/families/${familyId}/items`, data); +export const getItemDetail = (familyId: string, itemId: string) => + request("GET", `/api/families/${familyId}/items/${itemId}`); +export const updateItem = (familyId: string, itemId: string, data: Partial) => + request<{ success: boolean; item: Item }>("PUT", `/api/families/${familyId}/items/${itemId}`, data); +export const deleteItem = (familyId: string, itemId: string) => + request<{ success: boolean }>("DELETE", `/api/families/${familyId}/items/${itemId}`); +export const moveItem = (familyId: string, itemId: string, box_id: string | null) => + request<{ success: boolean; box_id: string | null }>("PUT", `/api/families/${familyId}/items/${itemId}/move`, { box_id }); + +// ── Transfer ── +export const transferEntity = (familyId: string, data: { type: "item" | "box"; entityId: string; dstFamilyId: string; dstParentBoxId?: string | null; pos_x?: number; pos_y?: number }) => + request<{ ok: boolean; type: string; entityId: string; dstFamilyId: string }>("POST", `/api/families/${familyId}/transfer`, data); +export const listTransferLogs = (familyId: string) => + request("GET", `/api/families/${familyId}/transfer-logs`); + +// ── Stats ── +export const getFamilyStats = (familyId: string) => + request("GET", `/api/families/${familyId}/stats`); + +// ── MCP ── +export const mcpListTools = () => + request("GET", "/api/mcp/tools"); +export const mcpCallTool = (toolName: string, args: Record) => + request("POST", "/api/mcp/call", { tool_name: toolName, args }); \ No newline at end of file diff --git a/src/components/BoxCanvas.tsx b/src/components/BoxCanvas.tsx new file mode 100644 index 0000000..2657ee3 --- /dev/null +++ b/src/components/BoxCanvas.tsx @@ -0,0 +1,115 @@ +import { useRef, useState, type PointerEvent } from "react"; +import type { BoxNode } from "../lib/boxTree"; + +export const NORM = 1000; // 归一化坐标系上限 + +// 盒子画布: 父容器内容区 = 0~1000 归一化坐标系, 子盒按 pos_x/pos_y 绝对定位 +// v1 尺寸: 未设置 width/height 时用固定瓦片; 设置了则按宽度比例缩放 (width/10 = %) +export default function BoxCanvas({ + boxes, canWrite, onOpen, onEdit, onPositionChange, onAutoArrange, containerKey, +}: { + boxes: BoxNode[]; + canWrite: boolean; + onOpen: (id: string) => void; + onEdit?: (id: string) => void; + onPositionChange?: (id: string, x: number, y: number) => void; + onAutoArrange?: (positions: { id: string; x: number; y: number }[]) => void; + containerKey: string; +}) { + const containerRef = useRef(null); + const [dragPos, setDragPos] = useState>({}); + const drag = useRef<{ id: string; startX: number; startY: number; moved: boolean } | null>(null); + + const toNorm = (clientX: number, clientY: number) => { + const rect = containerRef.current?.getBoundingClientRect(); + if (!rect || !rect.width || !rect.height) return { x: 0, y: 0 }; + const x = Math.max(0, Math.min(NORM, Math.round(((clientX - rect.left) / rect.width) * NORM))); + const y = Math.max(0, Math.min(NORM, Math.round(((clientY - rect.top) / rect.height) * NORM))); + return { x, y }; + }; + + const onPointerDown = (e: PointerEvent, boxId: string) => { + if (!canWrite) return; + (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId); + drag.current = { id: boxId, startX: e.clientX, startY: e.clientY, moved: false }; + }; + + const onPointerMove = (e: PointerEvent) => { + const d = drag.current; + if (!d || !canWrite) return; + if (Math.abs(e.clientX - d.startX) > 6 || Math.abs(e.clientY - d.startY) > 6) d.moved = true; + if (!d.moved) return; + const pos = toNorm(e.clientX, e.clientY); + setDragPos((prev) => ({ ...prev, [d.id]: pos })); + }; + + const onPointerUp = () => { + const d = drag.current; + drag.current = null; + if (!d || !d.moved) return; + const pos = dragPos[d.id]; + if (pos) onPositionChange?.(d.id, pos.x, pos.y); + }; + + const arrange = () => { + if (!onAutoArrange) return; + const positions: { id: string; x: number; y: number }[] = []; + const cols = Math.max(1, Math.ceil(Math.sqrt(boxes.length * 1.5))); + boxes.forEach((b, i) => { + positions.push({ id: b.id, x: (i % cols) * 240, y: Math.floor(i / cols) * 240 }); + }); + onAutoArrange(positions); + }; + + return ( +
+ {onAutoArrange && boxes.length > 1 && ( +
+ +
+ )} +
+ {boxes.length === 0 && ( +
暂无盒子{canWrite ? " · 点击下方新建" : ""}
+ )} + {boxes.map((b) => { + const dp = dragPos[b.id]; + const x = dp?.x ?? b.pos_x ?? 0; + const y = dp?.y ?? b.pos_y ?? 0; + const sizeW = b.width && b.width > 0 ? `${Math.min(70, b.width / 10)}%` : 96; + const sizeH = b.height && b.height > 0 ? `${Math.min(70, b.height / 10)}%` : 96; + return ( +
onPointerDown(e, b.id)} + onClick={() => { if (!drag.current?.moved) onOpen(b.id); }} + > + {b.icon} + {b.name} + {b.total_item_count ?? 0} 件 + {canWrite && onEdit && ( + + )} +
+ ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/BoxFormModal.tsx b/src/components/BoxFormModal.tsx new file mode 100644 index 0000000..435017e --- /dev/null +++ b/src/components/BoxFormModal.tsx @@ -0,0 +1,98 @@ +import { useState, useEffect } from "react"; +import Modal from "./Modal"; + +const COLORS = ["#E8C4A0", "#D9E8C4", "#C4DCE8", "#E8C4D9", "#E0D6F0", "#F0E0B8", "#C4E8E0"]; + +// 新建 / 编辑盒子: 名称、图标、颜色、尺寸(width/height 归一化 0~1000, 可选) +export default function BoxFormModal({ + open, title, initial, onClose, onSubmit, +}: { + open: boolean; + title?: string; + initial?: { name: string; icon: string; color: string; width?: number | null; height?: number | null }; + onClose?: () => void; + onSubmit?: (data: { name: string; icon: string; color: string; width?: number | null; height?: number | null }) => void; +}) { + const [name, setName] = useState(""); + const [icon, setIcon] = useState("📦"); + const [color, setColor] = useState(COLORS[0]); + const [width, setWidth] = useState(""); + const [height, setHeight] = useState(""); + const [busy, setBusy] = useState(false); + + useEffect(() => { + if (open) { + setName(initial?.name ?? ""); + setIcon(initial?.icon ?? "📦"); + setColor(initial?.color ?? COLORS[0]); + setWidth(initial?.width ? String(initial.width) : ""); + setHeight(initial?.height ? String(initial.height) : ""); + } + }, [open, initial]); + + if (!open) return null; + + const icons = ["📦", "🗄️", "🚪", "🛏️", "🛋️", "🪑", "🍳", "🚿", "👔", "📚", "🎮", "🧰", "📺", "🧺", "👟", "🍷"]; + + return ( + + + + + } + > +
+ + setName(e.target.value)} /> +
+
+ +
+ {icons.map((i) => ( + + ))} +
+
+
+ +
+ {COLORS.map((c) => ( +
+
+
+ +
+ setWidth(e.target.value)} /> + setHeight(e.target.value)} /> +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Breadcrumb.tsx b/src/components/Breadcrumb.tsx new file mode 100644 index 0000000..2e2050e --- /dev/null +++ b/src/components/Breadcrumb.tsx @@ -0,0 +1,17 @@ +import type { PathEntry } from "../types"; +import { useNavigate } from "react-router-dom"; + +export default function Breadcrumb({ path }: { path: PathEntry[] }) { + const navigate = useNavigate(); + return ( +
+ navigate("/boxes")}>🏠 客厅 + {path.map((p) => ( + + / + navigate(`/boxes/${p.id}`)}>{p.name} + + ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx new file mode 100644 index 0000000..d47d54b --- /dev/null +++ b/src/components/ConfirmModal.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import Modal from "./Modal"; + +export default function ConfirmModal({ + open, title = "确认操作", message, danger = true, onCancel, onConfirm, +}: { + open: boolean; + title?: string; + message: string; + danger?: boolean; + onCancel?: () => void; + onConfirm?: () => void | Promise; +}) { + const [loading, setLoading] = useState(false); + return ( + +

{message}

+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/DestinationPicker.tsx b/src/components/DestinationPicker.tsx new file mode 100644 index 0000000..602b91b --- /dev/null +++ b/src/components/DestinationPicker.tsx @@ -0,0 +1,61 @@ +import type { ReactNode } from "react"; +import Modal from "./Modal"; +import { useAppStore } from "../stores/app"; +import { indexBoxes } from "../lib/boxTree"; + +// 盒子目标选择器: 根层 / 家庭盒子树 (可选择"放进某盒子内部") +// excludeBoxId: 移动盒子时, 排除自身及其全部后代 +export default function DestinationPicker({ + open, title = "选择位置", excludeBoxId, onClose, onSelect, +}: { + open: boolean; + title?: string; + excludeBoxId?: string; + onClose?: () => void; + onSelect?: (parentId: string | null) => void; +}) { + const boxes = useAppStore((s) => s.boxes); + if (!open) return null; + + const index = indexBoxes(boxes); + const excluded = new Set(); + if (excludeBoxId) { + excluded.add(excludeBoxId); + const stack = [excludeBoxId]; + while (stack.length) { + const cur = stack.pop()!; + for (const c of index.byParent.get(cur) ?? []) { + excluded.add(c.id); + stack.push(c.id); + } + } + } + + const options = index.roots.filter((n) => !excluded.has(n.id)); + + const renderNodes = (nodes: typeof options, depth: number): ReactNode => { + return nodes.map((n) => ( +
+ + {renderNodes((index.byParent.get(n.id) ?? []).filter((c) => !excluded.has(c.id)), depth + 1)} +
+ )); + }; + + return ( + + + {options.length === 0 &&

暂无可用的盒子

} +
{renderNodes(options, 0)}
+
+ ); +} \ No newline at end of file diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..bb8761b --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,108 @@ +import { useEffect, useRef } from "react"; +import { Outlet, useNavigate, useLocation } from "react-router-dom"; +import { useAppStore, selectCurrentFamily } from "../stores/app"; +import { listFamilies, fetchBoxTree, listCategories } from "../api"; +import { showToast } from "../lib/toast"; + +export default function Layout() { + const navigate = useNavigate(); + const location = useLocation(); + const user = useAppStore((s) => s.user); + const families = useAppStore((s) => s.families); + const currentFamily = useAppStore(selectCurrentFamily); + const setFamilies = useAppStore((s) => s.setFamilies); + const setCurrentFamily = useAppStore((s) => s.setCurrentFamily); + const setBoxes = useAppStore((s) => s.setBoxes); + const setCategories = useAppStore((s) => s.setCategories); + const contentRef = useRef(null); + const loadingRef = useRef(false); + + // 加载用户加入的家庭; 无家庭时进入 /families 引导创建 + useEffect(() => { + if (!user || families.length) return; + if (loadingRef.current) return; + loadingRef.current = true; + listFamilies() + .then((fs) => setFamilies(fs)) + .catch((e) => showToast(String(e))) + .finally(() => { loadingRef.current = false; }); + }, [user, families.length, setFamilies]); + + // 家庭切换后: 重载该家庭的全量盒子树与分类 (页面各自再拉统计/列表) + useEffect(() => { + if (!user || !currentFamily?.id) return; + fetchBoxTree(currentFamily.id).then(setBoxes).catch(() => setBoxes([])); + listCategories(currentFamily.id).then(setCategories).catch(() => setCategories([])); + }, [user, currentFamily?.id, setBoxes, setCategories]); + + useEffect(() => { + contentRef.current?.scrollTo({ top: 0 }); + }, [location.pathname]); + + if (families.length === 0 && location.pathname !== "/families" && location.pathname !== "/profile") { + return ( +
+
📦
+

还没有家庭

+

创建一个家庭,开始整理你的所有物品

+ +
+ ); + } + + const isActive = (path: string) => + path === "/home" ? location.pathname === "/home" : location.pathname.startsWith(path); + + const goAdd = () => { + if (!currentFamily) { showToast("请先创建或加入家庭"); return; } + if (currentFamily.role === "member") { showToast("只读成员不可新增物品"); return; } + navigate("/items/new"); + }; + + return ( +
+
+ + {families.length > 1 && ( + + )} +
+ +
+
+ +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..5ccea5c --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +export default function Modal({ + open, title, onClose, children, footer, +}: { + open: boolean; + title?: string; + onClose?: () => void; + children: React.ReactNode; + footer?: React.ReactNode; +}) { + if (!open) return null; + return ( +
+
e.stopPropagation()}> + {title &&
{title}
} +
{children}
+ {footer &&
{footer}
} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/SignedImg.tsx b/src/components/SignedImg.tsx new file mode 100644 index 0000000..1bec907 --- /dev/null +++ b/src/components/SignedImg.tsx @@ -0,0 +1,33 @@ +import { useEffect, useState } from "react"; +import { signImagePath } from "../api/images"; + +// 图片 URL 内存缓存, 避免同一张图反复请求签名 +const urlCache = new Map(); + +export default function SignedImg({ + path, alt = "", className, style, +}: { + path: string; + alt?: string; + className?: string; + style?: React.CSSProperties; +}) { + const [url, setUrl] = useState(urlCache.get(path) ?? null); + + useEffect(() => { + let alive = true; + const cached = urlCache.get(path); + if (cached) { setUrl(cached); return; } + signImagePath(path) + .then((r) => { + if (!alive) return; + urlCache.set(path, r.url); + setUrl(r.url); + }) + .catch(() => { /* 签名失败不展示 */ }); + return () => { alive = false; }; + }, [path]); + + if (!url) return
; + return {alt}; +} \ No newline at end of file diff --git a/src/components/TransferModal.tsx b/src/components/TransferModal.tsx new file mode 100644 index 0000000..2e33a5b --- /dev/null +++ b/src/components/TransferModal.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import Modal from "./Modal"; +import { useAppStore } from "../stores/app"; +import { transferEntity } from "../api"; +import { showToast } from "../lib/toast"; + +// 跨家庭转移: 将物品/整盒转移到"自己拥有管理权限"的另一个家庭 (决策点 2) +export default function TransferModal({ + open, type, entityId, onClose, onDone, +}: { + open: boolean; + type: "item" | "box"; + entityId: string; + onClose?: () => void; + onDone?: () => void; +}) { + const sourceFamilyId = useAppStore((s) => s.currentFamilyId); + const families = useAppStore((s) => s.families); + const [dst, setDst] = useState(""); + const [loading, setLoading] = useState(false); + + if (!open) return null; + + const eligible = families.filter((f) => f.id !== sourceFamilyId && f.role === "admin"); + + const doTransfer = async () => { + if (!dst || !sourceFamilyId) { showToast("请选择目标家庭"); return; } + setLoading(true); + try { + await transferEntity(sourceFamilyId, { + type, entityId, dstFamilyId: dst, dstParentBoxId: null, + }); + showToast(`已转移到目标家庭`); + onDone?.(); + onClose?.(); + } catch (e) { + showToast(String(e)); + } finally { + setLoading(false); + } + }; + + return ( + + + +
+ } + > + {eligible.length === 0 ? ( +

没有可转移的家庭(需要你拥有该家庭的管理权限)

+ ) : ( +
+ {eligible.map((f) => ( + + ))} +
+ )} + {dst &&

转移后将从当前家庭移除,转移到目标家庭根层。

} + + ); +} \ No newline at end of file diff --git a/src/lib/boxTree.ts b/src/lib/boxTree.ts new file mode 100644 index 0000000..dbe14b8 --- /dev/null +++ b/src/lib/boxTree.ts @@ -0,0 +1,89 @@ +// 盒子树计算工具: 由扁平的 boxes 数组构建层级、路径、计数 +// 前端服务端共用同一套"扁平 + JS 建树"的算法思路 +import type { Box, PathEntry } from "../types"; + +export interface BoxNode extends Box { + depth: number; + path: PathEntry[]; + childCount: number; + totalItemCount: number; +} + +export interface BoxIndex { + byId: Map; + roots: BoxNode[]; + byParent: Map; // key: 父盒 id 或 "ROOT" +} + +export function indexBoxes(boxes: Box[]): BoxIndex { + const byParent = new Map(); + + const flattened = boxes.map((b) => { + const path: PathEntry[] = []; + let curId: string | undefined = b.id; + const guard = new Set(); + while (curId && !guard.has(curId)) { + guard.add(curId); + const cur = boxes.find((x) => x.id === curId); + if (!cur) break; + path.unshift({ id: cur.id, name: cur.name }); + curId = cur.parent_box_id ?? undefined; + } + return { box: b, path }; + }); + + const nodeMap = new Map(); + for (const { box, path } of flattened) { + const node: BoxNode = { + ...box, + depth: path.length, + path, + childCount: 0, + totalItemCount: box.total_item_count ?? box.direct_item_count ?? 0, + }; + nodeMap.set(box.id, node); + } + for (const box of boxes) { + const parentKey = box.parent_box_id ?? "ROOT"; + if (!byParent.has(parentKey)) byParent.set(parentKey, []); + byParent.get(parentKey)!.push(nodeMap.get(box.id)!); + if (box.parent_box_id && nodeMap.has(box.parent_box_id)) { + nodeMap.get(box.parent_box_id)!.childCount += 1; + } + } + // 递归汇总物品数 + const counted = new Set(); + const sumTotal = (id: string): number => { + if (counted.has(id)) return nodeMap.get(id)!.totalItemCount; + counted.add(id); + for (const child of byParent.get(id) ?? []) { + nodeMap.get(id)!.totalItemCount += sumTotal(child.id); + } + return nodeMap.get(id)!.totalItemCount; + }; + for (const box of boxes) sumTotal(box.id); + + return { byId: nodeMap, roots: byParent.get("ROOT") ?? [], byParent }; +} + +export function childrenOf(index: BoxIndex, parentId: string | null): BoxNode[] { + return index.byParent.get(parentId ?? "ROOT") ?? []; +} + +export function pathOf(index: BoxIndex, boxId: string): PathEntry[] { + return index.byId.get(boxId)?.path ?? []; +} + +// 供表单/树选择器使用的扁平树状选项 +export function flattenTree(nodes: BoxNode[], depth = 0): { id: string; label: string; depth: number }[] { + const out: { id: string; label: string; depth: number }[] = []; + for (const n of nodes) { + out.push({ id: n.id, label: n.name, depth }); + out.push(...flattenTree(childrenOf2(n.id, nodes), depth + 1)); + } + return out; +} + +function childrenOf2(parentId: string, nodes: BoxNode[]): BoxNode[] { + return nodes.filter((n) => (n.parent_box_id ?? null) === parentId); +} \ No newline at end of file diff --git a/src/lib/toast.ts b/src/lib/toast.ts new file mode 100644 index 0000000..c393289 --- /dev/null +++ b/src/lib/toast.ts @@ -0,0 +1,12 @@ +// 全局轻量 Toast (模块级发布订阅, 无需 Context) +type Listener = (msg: string) => void; +const listeners = new Set(); + +export function showToast(msg: string) { + listeners.forEach((l) => l(msg)); +} + +export function onToast(l: Listener): () => void { + listeners.add(l); + return () => listeners.delete(l); +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..a3e352b --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles/global.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + +); \ No newline at end of file diff --git a/src/pages/BoxDetail.tsx b/src/pages/BoxDetail.tsx new file mode 100644 index 0000000..821c061 --- /dev/null +++ b/src/pages/BoxDetail.tsx @@ -0,0 +1,211 @@ +import { useEffect, useState, useMemo } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAppStore } from "../stores/app"; +import { getBoxDetail, createBox, updateBox, moveBox, deleteBox, setBoxPosition, fetchBoxTree, listItems } from "../api"; +import type { Item } from "../types"; +import { indexBoxes, childrenOf } from "../lib/boxTree"; +import { showToast } from "../lib/toast"; +import type { PathEntry } from "../types"; +import BoxCanvas from "../components/BoxCanvas"; +import Breadcrumb from "../components/Breadcrumb"; +import BoxFormModal from "../components/BoxFormModal"; +import DestinationPicker from "../components/DestinationPicker"; +import ConfirmModal from "../components/ConfirmModal"; +import TransferModal from "../components/TransferModal"; +import SignedImg from "../components/SignedImg"; +import Modal from "../components/Modal"; + +export default function BoxDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const familyId = useAppStore((s) => s.currentFamilyId); + const role = useAppStore((s) => s.families.find((f) => f.id === s.currentFamilyId)?.role); + const boxes = useAppStore((s) => s.boxes); + const setBoxes = useAppStore((s) => s.setBoxes); + const [path, setPath] = useState([]); + const [items, setItems] = useState([]); + const [formOpen, setFormOpen] = useState(false); + const [editBox, setEditBox] = useState<{ id: string; name: string; icon: string; color: string; width: number | null; height: number | null } | null>(null); + const [actionBoxId, setActionBoxId] = useState(null); + const [moveTarget, setMoveTarget] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + const [transferTarget, setTransferTarget] = useState(null); + const [view, setView] = useState<"canvas" | "list">("canvas"); + + const canWrite = role === "admin"; + + const refresh = () => { + if (!familyId || !id) return; + Promise.all([ + getBoxDetail(familyId, id), + fetchBoxTree(familyId), + listItems(familyId, { box: id }), + ]).then(([d, tree, its]) => { + setPath(d.path); + setBoxes(tree); + setItems(its); + }).catch((e) => showToast(String(e))); + }; + + useEffect(() => { + refresh(); // eslint-disable-line react-hooks/exhaustive-deps + }, [familyId, id]); + + const index = useMemo(() => indexBoxes(boxes), [boxes]); + const childNodes = useMemo(() => (id ? childrenOf(index, id) : []), [index, id]); + const current = id ? index.byId.get(id) : null; + + const nextPos = () => { + const cols = 3; + return { x: (childNodes.length % cols) * 280, y: Math.floor(childNodes.length / cols) * 280 }; + }; + + const handleSubmitForm = async (data: { name: string; icon: string; color: string; width?: number | null; height?: number | null }) => { + if (!familyId) return; + if (editBox) { + await updateBox(familyId, editBox.id, data); + showToast("盒子已更新"); + } else { + const pos = nextPos(); + await createBox(familyId, { ...data, parent_box_id: id, pos_x: pos.x, pos_y: pos.y }); + showToast("盒子已创建"); + } + refresh(); + }; + + const handleMove = async (parentId: string | null) => { + if (!familyId || !moveTarget) return; + try { + await moveBox(familyId, moveTarget, parentId); + showToast("已移动"); + refresh(); + } catch (e) { + showToast(String(e)); + } finally { + setMoveTarget(null); + } + }; + + const handleDelete = async () => { + if (!familyId || !deleteTarget) return; + try { + await deleteBox(familyId, deleteTarget); + showToast("已删除"); + if (deleteTarget === id) navigate("/boxes"); + refresh(); + } catch (e) { + showToast(String(e)); + } finally { + setDeleteTarget(null); + } + }; + + const handlePosition = async (bid: string, x: number, y: number) => { + if (!familyId) return; + try { + await setBoxPosition(familyId, bid, x, y); + refresh(); + } catch { /* ignore */ } + }; + + const handleAutoArrange = async (positions: { id: string; x: number; y: number }[]) => { + if (!familyId) return; + try { + for (const p of positions) await setBoxPosition(familyId, p.id, p.x, p.y); + showToast("已自动整理"); + refresh(); + } catch (e) { showToast(String(e)); } + }; + + const openEdit = (bid: string) => { + const b = index.byId.get(bid); + if (b) setEditBox({ id: b.id, name: b.name, icon: b.icon, color: b.color, width: b.width ?? null, height: b.height ?? null }); + setActionBoxId(null); + setFormOpen(true); + }; + + const actionBox = actionBoxId ? index.byId.get(actionBoxId) : null; + + return ( +
+
+
+

{current?.icon} {current?.name ?? "…"}

+ {canWrite && ( + + )} +
+ +
+ +
+ + +
+ + {view === "canvas" ? ( + navigate(`/boxes/${bid}`)} + onEdit={canWrite ? (bid) => setActionBoxId(bid) : undefined} + onPositionChange={handlePosition} + onAutoArrange={canWrite ? handleAutoArrange : undefined} + /> + ) : ( +
+ {childNodes.length === 0 &&
该容器内还没有子盒
} + {childNodes.map((n) => ( +
navigate(`/boxes/${n.id}`)}> + {n.icon} {n.name} + {n.total_item_count} 件 +
+ ))} +
+ )} + + {canWrite && ( + + )} + +
+
本层物品({items.length})
+ +
+ + {items.length === 0 &&
这里还没有物品
} +
+ {items.map((it) => ( +
navigate(`/items/${it.id}`)}> + {it.images[0] ? ( + + ) : ( +
📦
+ )} +
{it.name}
+ {it.is_favorite && ♥} +
+ ))} +
+ + setActionBoxId(null)}> +
+ + + + +
+
+ + setFormOpen(false)} onSubmit={handleSubmitForm} /> + setMoveTarget(null)} onSelect={handleMove} /> + setDeleteTarget(null)} onConfirm={handleDelete} /> + setTransferTarget(null)} onDone={refresh} /> +
+ ); +} \ No newline at end of file diff --git a/src/pages/Boxes.tsx b/src/pages/Boxes.tsx new file mode 100644 index 0000000..755d158 --- /dev/null +++ b/src/pages/Boxes.tsx @@ -0,0 +1,168 @@ +import { useEffect, useState, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAppStore } from "../stores/app"; +import { fetchBoxTree, createBox, updateBox, moveBox, deleteBox, setBoxPosition } from "../api"; +import { indexBoxes } from "../lib/boxTree"; +import { showToast } from "../lib/toast"; +import BoxCanvas from "../components/BoxCanvas"; +import BoxFormModal from "../components/BoxFormModal"; +import DestinationPicker from "../components/DestinationPicker"; +import ConfirmModal from "../components/ConfirmModal"; +import TransferModal from "../components/TransferModal"; +import Modal from "../components/Modal"; + +export default function Boxes() { + const navigate = useNavigate(); + const familyId = useAppStore((s) => s.currentFamilyId); + const role = useAppStore((s) => s.families.find((f) => f.id === s.currentFamilyId)?.role); + const boxes = useAppStore((s) => s.boxes); + const setBoxes = useAppStore((s) => s.setBoxes); + const [formOpen, setFormOpen] = useState(false); + const [editBox, setEditBox] = useState<{ id: string; name: string; icon: string; color: string; width: number | null; height: number | null } | null>(null); + const [actionBoxId, setActionBoxId] = useState(null); + const [moveTarget, setMoveTarget] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + const [transferTarget, setTransferTarget] = useState(null); + + const canWrite = role === "admin"; + + const refresh = () => { + if (!familyId) return; + fetchBoxTree(familyId).then(setBoxes).catch((e) => showToast(String(e))); + }; + + useEffect(() => { + refresh(); // eslint-disable-line react-hooks/exhaustive-deps + }, [familyId]); + + const index = useMemo(() => indexBoxes(boxes), [boxes]); + const rootChildren = index.roots; + + const nextPos = () => { + const cols = 3; + return { + x: (rootChildren.length % cols) * 280, + y: Math.floor(rootChildren.length / cols) * 280, + }; + }; + + const handleSubmitForm = async (data: { name: string; icon: string; color: string; width?: number | null; height?: number | null }) => { + if (!familyId) return; + if (editBox) { + await updateBox(familyId, editBox.id, data); + showToast("盒子已更新"); + } else { + const pos = nextPos(); + await createBox(familyId, { ...data, parent_box_id: null, pos_x: pos.x, pos_y: pos.y }); + showToast("盒子已创建"); + } + refresh(); + }; + + const handleMove = async (parentId: string | null) => { + if (!familyId || !moveTarget) return; + try { + await moveBox(familyId, moveTarget, parentId); + showToast("已移动"); + refresh(); + } catch (e) { + showToast(String(e)); + } finally { + setMoveTarget(null); + } + }; + + const handleDelete = async () => { + if (!familyId || !deleteTarget) return; + try { + await deleteBox(familyId, deleteTarget); + showToast("盒子已删除"); + refresh(); + } catch (e) { + showToast(String(e)); + } finally { + setDeleteTarget(null); + } + }; + + const handlePosition = async (id: string, x: number, y: number) => { + if (!familyId) return; + try { + await setBoxPosition(familyId, id, x, y); + refresh(); + } catch { /* 忽略拖拽保存失败 */ } + }; + + const handleAutoArrange = async (positions: { id: string; x: number; y: number }[]) => { + if (!familyId) return; + try { + for (const p of positions) await setBoxPosition(familyId, p.id, p.x, p.y); + showToast("已自动整理"); + refresh(); + } catch (e) { + showToast(String(e)); + } + }; + + const openEdit = (id: string) => { + const b = index.byId.get(id); + if (b) setEditBox({ id: b.id, name: b.name, icon: b.icon, color: b.color, width: b.width ?? null, height: b.height ?? null }); + setActionBoxId(null); + setFormOpen(true); + }; + + const actionBox = actionBoxId ? index.byId.get(actionBoxId) : null; + + return ( +
+
+

盒子

+ {canWrite && ( + + )} +
+ + navigate(`/boxes/${id}`)} + onEdit={canWrite ? (id) => setActionBoxId(id) : undefined} + onPositionChange={handlePosition} + onAutoArrange={canWrite ? handleAutoArrange : undefined} + /> + +
+
根层备忘
+
    +
  • 盒子可无限嵌套,整盒拖动可调整相对位置(画布 0~1000 坐标)。
  • +
  • 点击盒子进入内部;管理员点击「…」可编辑、移动、转移、删除。
  • +
+
+ + setActionBoxId(null)} + footer={
} + > +
+ + + {canWrite && } + +
+
+ + setFormOpen(false)} onSubmit={handleSubmitForm} /> + setMoveTarget(null)} onSelect={handleMove} /> + setDeleteTarget(null)} + onConfirm={handleDelete} + /> + setTransferTarget(null)} onDone={refresh} /> +
+ ); +} \ No newline at end of file diff --git a/src/pages/FamilyManage.tsx b/src/pages/FamilyManage.tsx new file mode 100644 index 0000000..db980d5 --- /dev/null +++ b/src/pages/FamilyManage.tsx @@ -0,0 +1,273 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAppStore } from "../stores/app"; +import { + listFamilies, createFamily, updateFamily, deleteFamily, joinFamily, + refreshInviteCode, clearInviteCode, getFamilyDetail, setMemberRole, removeMember, + listCategories, createCategory, updateCategory, deleteCategory, listTransferLogs, +} from "../api"; +import type { FamilyDetail, TransferLog } from "../types"; +import { showToast } from "../lib/toast"; +import Modal from "../components/Modal"; +import ConfirmModal from "../components/ConfirmModal"; + +export default function FamilyManage() { + const navigate = useNavigate(); + const families = useAppStore((s) => s.families); + const currentFamilyId = useAppStore((s) => s.currentFamilyId); + const setFamilies = useAppStore((s) => s.setFamilies); + const setCurrentFamily = useAppStore((s) => s.setCurrentFamily); + const [detail, setDetail] = useState(null); + const [cats, setCats] = useState>>([]); + const [logs, setLogs] = useState([]); + const [newFamilyName, setNewFamilyName] = useState(""); + const [joinCode, setJoinCode] = useState(""); + const [renameOpen, setRenameOpen] = useState(false); + const [renameName, setRenameName] = useState(""); + const [disbandOpen, setDisbandOpen] = useState(false); + + const current = families.find((f) => f.id === currentFamilyId); + const isOwner = current?.id ? current.owner_user_id === useAppStore.getState().user?.id : false; + const isAdmin = current?.role === "admin"; + + const refreshFamilies = async () => { + const list = await listFamilies(); + setFamilies(list); + }; + + const loadDetail = () => { + if (!currentFamilyId) { setDetail(null); return; } + getFamilyDetail(currentFamilyId).then(setDetail).catch(() => setDetail(null)); + listCategories(currentFamilyId).then(setCats).catch(() => setCats([])); + listTransferLogs(currentFamilyId).then(setLogs).catch(() => setLogs([])); + }; + + useEffect(() => { loadDetail(); }, [currentFamilyId]); // eslint-disable-line react-hooks/exhaustive-deps + + const create = async () => { + if (!newFamilyName.trim()) return; + try { + await createFamily(newFamilyName.trim()); + setNewFamilyName(""); + await refreshFamilies(); + showToast("家庭已创建"); + } catch (e) { showToast(String(e)); } + }; + + const join = async () => { + if (!joinCode.trim() || !currentFamilyId) { showToast("请输入邀请码"); return; } + try { + const r = await joinFamily(currentFamilyId, joinCode.trim().toUpperCase()); + showToast(r.message); + if (r.success) { + setJoinCode(""); + await refreshFamilies(); + setCurrentFamily(r.familyId!); + } + } catch (e) { showToast(String(e)); } + }; + + const rename = async () => { + if (!currentFamilyId) return; + try { + await updateFamily(currentFamilyId, renameName.trim(), current?.icon); + showToast("已保存"); + setRenameOpen(false); + await refreshFamilies(); + } catch (e) { showToast(String(e)); } + }; + + const disband = async () => { + if (!currentFamilyId) return; + try { + await deleteFamily(currentFamilyId); + showToast("家庭已解散"); + setDisbandOpen(false); + await refreshFamilies(); + } catch (e) { showToast(String(e)); } + }; + + const catAdd = async (name: string) => { + if (!currentFamilyId) return; + try { await createCategory(currentFamilyId, name); loadDetail(); } catch (e) { showToast(String(e)); } + }; + + const addCatName = () => { + const n = window.prompt("分类名称"); + if (n) catAdd(n.trim()); + }; + + const renameCat = (catId: string, old: string) => { + const n = window.prompt("重命名分类", old); + if (n && currentFamilyId) updateCategory(currentFamilyId, catId, n.trim()).then(loadDetail).catch((e) => showToast(String(e))); + }; + + const delCat = (catId: string) => { + if (!currentFamilyId) return; + if (!confirm("删除分类后,该分类下物品将变为未分类。确定?")) return; + deleteCategory(currentFamilyId, catId).then(loadDetail).catch((e) => showToast(String(e))); + }; + + const changeRole = (userId: string, role: string) => { + if (!currentFamilyId) return; + setMemberRole(currentFamilyId, userId, role as "admin" | "member").then(loadDetail).catch((e) => showToast(String(e))); + }; + + const remove = (userId: string) => { + if (!currentFamilyId) return; + if (!confirm("确定移除该成员?")) return; + removeMember(currentFamilyId, userId).then(loadDetail).catch((e) => showToast(String(e))); + }; + + const leaveMyself = async () => { + if (!currentFamilyId) return; + try { + await removeMember(currentFamilyId, "me"); + showToast("已离开家庭"); + await refreshFamilies(); + navigate("/home"); + } catch (e) { showToast(String(e)); } + }; + + const showInvite = async () => { + if (!currentFamilyId) return; + try { + const r = await refreshInviteCode(currentFamilyId); + await refreshFamilies(); + alert(`邀请码:${r.invite_code}`); + } catch (e) { showToast(String(e)); } + }; + + return ( +
+
+

家庭

+
+ + {/* 我的家庭列表(切换) */} +
+
我的家庭
+ {families.length === 0 &&
还没有家庭,创建一个吧
} + {families.map((f) => ( +
setCurrentFamily(f.id)}> + {f.icon} {f.name} + {f.role === "admin" ? "管理员" : "成员"} +
+ ))} +
+ +
+
创建家庭
+
+ setNewFamilyName(e.target.value)} /> + +
+
通过邀请码加入
+
+ setJoinCode(e.target.value)} /> + +
+
+ + {current && ( +
+
+
{current.icon} {current.name}
+ {isAdmin && } +
+

身份:{isAdmin ? "管理员(可修改)" : "成员(只读)"}

+ + {isAdmin && ( +
+
邀请码
+
+ {detail?.family.invite_code ? ( + {detail.family.invite_code} + ) : ( + 未生成 + )} + + {detail?.family.invite_code && ( + + )} +
+
+ )} +
+ )} + + {detail && ( +
+
成员({detail.members.length})
+ {detail.members.map((m) => ( +
+
+
{m.nickname}{m.user_id === detail.family.owner_user_id ? "(创建者)" : ""}
+ {m.email &&
{m.email}
} +
+ {m.user_id === detail.family.owner_user_id ? ( + 创建者 + ) : isAdmin ? ( +
+ + +
+ ) : ( + {m.role === "admin" ? "管理员" : "成员"} + )} +
+ ))} +
+ )} + + {isAdmin && ( +
+
分类管理
+ {cats.length === 0 &&
暂无分类
} + {cats.map((c) => ( +
+ {c.name} + + +
+ ))} + +
+ )} + + {logs.length > 0 && ( +
+
转移记录
+ {logs.map((l) => ( +
+ {l.entity_type === "box" ? "📦 盒子" : "📄 物品"} + {new Date(l.created_at).toLocaleString()} +
+ ))} +
+ )} + +
+
+ {!isOwner && } + {isOwner && } +
+

解散家庭将删除全部物品与盒子,不可恢复。

+
+ + setRenameOpen(false)} + footer={
+ + +
} + > + setRenameName(e.target.value)} /> +
+ + setDisbandOpen(false)} onConfirm={disband} /> +
+ ); +} \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..08886ac --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,142 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAppStore } from "../stores/app"; +import { getFamilyStats } from "../api"; +import { showToast } from "../lib/toast"; +import SignedImg from "../components/SignedImg"; + +export default function Home() { + const navigate = useNavigate(); + const familyId = useAppStore((s) => s.currentFamilyId); + const currentFamily = useAppStore((s) => s.families.find((f) => f.id === s.currentFamilyId)); + const stats = useAppStore((s) => s.stats); + const setStats = useAppStore((s) => s.setStats); + + const load = () => { + if (!familyId) return; + getFamilyStats(familyId).then(setStats).catch((e) => showToast(String(e))); + }; + + useEffect(load, [familyId]); // eslint-disable-line react-hooks/exhaustive-deps + + if (!familyId) { + return ( +
+

还没有家庭

+
+ ); + } + + const hasContent = stats && stats.total_items > 0; + + return ( +
+
+

首页

+ +
+ +
+
+ {currentFamily?.icon} +
+
{currentFamily?.name}
+
{currentFamily?.role === "admin" ? "你是管理员(可修改)" : "你是成员(只读)"}
+
+
+ {!hasContent && ( +
+ 还没有物品。去 navigate("/families")}>家庭管理 或直接开始: +
+ )} +
+ + {stats && ( +
+
navigate("/items")}> +
{stats.total_items}
+
物品总数
+
+
navigate("/boxes")}> +
{stats.total_boxes}
+
盒子总数
+
+
+
{stats.max_depth}
+
最深嵌套
+
+
+
¥{stats.total_value.toLocaleString()}
+
物品估价
+
+
+ )} + + {stats && stats.expiring_count > 0 && ( +
+
⏰ 即将过期({stats.expiring_count})
+ {stats.expiring_items.slice(0, 5).map((it) => ( +
navigate(`/items/${it.id}`)}> + {it.name} + {it.expiry_date} +
+ ))} +
+ )} + + {stats && ( +
+
分类分布
+ {stats.category_distribution.length === 0 &&
暂无分类数据
} + {stats.category_distribution.map((c) => ( +
+ {c.name} +
+ {c.count} +
+ ))} +
+ )} + + {stats && stats.top_boxes.length > 0 && ( +
+
物品最多的盒子
+ {stats.top_boxes.map((b) => ( +
navigate(`/boxes/${b.id}`)}> + {b.icon} {b.path.map((p) => p.name).join(" / ")} + {b.count} 件 +
+ ))} +
+ )} + + {stats && stats.recent_items.length > 0 && ( +
+
最近新增
+
+ {stats.recent_items.map((it) => ( +
navigate(`/items/${it.id}`)}> + {it.images[0] ? ( + + ) : ( +
📦
+ )} +
{it.name}
+
+ ))} +
+
+ )} + + {stats && stats.loose_items > 0 && ( +
+
待整理提醒
+
navigate(`/items?loose=1`)}> + 散放在家庭根层未归类到盒子的物品 + {stats.loose_items} 件 › +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/ItemDetail.tsx b/src/pages/ItemDetail.tsx new file mode 100644 index 0000000..c9eeb53 --- /dev/null +++ b/src/pages/ItemDetail.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAppStore } from "../stores/app"; +import { getItemDetail, updateItem, deleteItem, moveItem } from "../api"; +import { showToast } from "../lib/toast"; +import Breadcrumb from "../components/Breadcrumb"; +import SignedImg from "../components/SignedImg"; +import ConfirmModal from "../components/ConfirmModal"; +import DestinationPicker from "../components/DestinationPicker"; +import TransferModal from "../components/TransferModal"; + +export default function ItemDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const familyId = useAppStore((s) => s.currentFamilyId); + const role = useAppStore((s) => s.families.find((f) => f.id === s.currentFamilyId)?.role); + const categories = useAppStore((s) => s.categories); + const [item, setItem] = useState> | null>(null); + const [moveOpen, setMoveOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const [transferOpen, setTransferOpen] = useState(false); + + const canWrite = role === "admin"; + + const load = () => { + if (!familyId || !id) return; + getItemDetail(familyId, id).then(setItem).catch((e) => showToast(String(e))); + }; + + useEffect(load, [familyId, id]); // eslint-disable-line react-hooks/exhaustive-deps + + if (!item) return
加载中…
; + + const { item: it, box_path: boxPath } = item; + const catName = categories.find((c) => c.id === it.category_id)?.name; + + const toggleFavorite = async () => { + if (!familyId || !canWrite) return; + try { + await updateItem(familyId, it.id, { is_favorite: !it.is_favorite }); + load(); + } catch (e) { + showToast(String(e)); + } + }; + + const doMove = async (parentId: string | null) => { + if (!familyId) return; + try { + await moveItem(familyId, it.id, parentId); + showToast("已移动"); + setMoveOpen(false); + load(); + navigate(`/items/${it.id}`); + } catch (e) { + showToast(String(e)); + } + }; + + const doDelete = async () => { + if (!familyId) return; + try { + await deleteItem(familyId, it.id); + showToast("已删除"); + navigate("/items"); + } catch (e) { + showToast(String(e)); + } + }; + + const field = (label: string, value?: string | number | null) => + value !== undefined && value !== null && value !== "" ? ( +
+ {label} + {value} +
+ ) : null; + + return ( +
+
+

{it.name}

+
+ +
+
+ + {it.images.length > 0 && ( +
+ {it.images.map((p) => )} +
+ )} + +
+ {field("数量", it.quantity)} + {field("分类", catName)} + {it.tags.length > 0 && ( +
+ 标签 + {it.tags.join("、")} +
+ )} + {field("品牌", it.brand)} + {field("型号", it.model)} + {field("序列号", it.serial_no)} + {field("购买日期", it.purchase_date)} + {field("购买价格", it.purchase_price != null ? `¥${it.purchase_price}` : null)} + {field("保质期", it.expiry_date ? `${it.expiry_date}${isExpiring(it.expiry_date) ? "(临期)" : ""}` : null)} + {field("使用次数", it.used_count)} + {field("最后使用", it.last_used)} + {field("备注", it.notes)} +
+ +
+ {canWrite && ( + <> + + + + + + )} + +
+ + setMoveOpen(false)} onSelect={doMove} /> + setDeleteOpen(false)} onConfirm={doDelete} /> + setTransferOpen(false)} onDone={() => { load(); navigate("/items"); }} /> +
+ ); +} + +function isExpiring(exp: string): boolean { + const d = new Date(exp + "T00:00:00"); + const now = new Date(); + return d.getTime() < now.getTime() + 30 * 864e5; +} \ No newline at end of file diff --git a/src/pages/ItemEdit.tsx b/src/pages/ItemEdit.tsx new file mode 100644 index 0000000..2154eff --- /dev/null +++ b/src/pages/ItemEdit.tsx @@ -0,0 +1,240 @@ +import { useEffect, useState, useRef } from "react"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { useAppStore } from "../stores/app"; +import { createItem, updateItem, getItemDetail, listCategories } from "../api"; +import { indexBoxes } from "../lib/boxTree"; +import { uploadImage } from "../api/images"; +import { showToast } from "../lib/toast"; +import SignedImg from "../components/SignedImg"; + +const MAX_IMAGES = 9; + +export default function ItemEdit() { + const { id } = useParams<{ id?: string }>(); + const [params] = useSearchParams(); + const navigate = useNavigate(); + const familyId = useAppStore((s) => s.currentFamilyId); + const role = useAppStore((s) => s.families.find((f) => f.id === s.currentFamilyId)?.role); + const categories = useAppStore((s) => s.categories); + const setCategories = useAppStore((s) => s.setCategories); + const boxes = useAppStore((s) => s.boxes); + const isEdit = !!id; + + const [name, setName] = useState(""); + const [categoryId, setCategoryId] = useState(""); + const [tags, setTags] = useState(""); + const [quantity, setQuantity] = useState("1"); + const [brand, setBrand] = useState(""); + const [model, setModel] = useState(""); + const [serialNo, setSerialNo] = useState(""); + const [purchaseDate, setPurchaseDate] = useState(""); + const [purchasePrice, setPurchasePrice] = useState(""); + const [expiryDate, setExpiryDate] = useState(""); + const [notes, setNotes] = useState(""); + const [images, setImages] = useState([]); + const [boxId, setBoxId] = useState(""); + const [busy, setBusy] = useState(false); + const [uploading, setUploading] = useState(false); + const fileRef = useRef(null); + + useEffect(() => { + if (!familyId) return; + if (categories.length === 0) listCategories(familyId).then(setCategories).catch(() => undefined); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [familyId]); + + useEffect(() => { + if (!isEdit) { + setBoxId(params.get("box") ?? ""); + return; + } + if (!familyId) return; + getItemDetail(familyId, id!).then((d) => { + const it = d.item; + setName(it.name); + setCategoryId(it.category_id ?? ""); + setTags(it.tags.join(", ")); + setQuantity(String(it.quantity ?? 1)); + setBrand(it.brand ?? ""); + setModel(it.model ?? ""); + setSerialNo(it.serial_no ?? ""); + setPurchaseDate(it.purchase_date ?? ""); + setPurchasePrice(it.purchase_price != null ? String(it.purchase_price) : ""); + setExpiryDate(it.expiry_date ?? ""); + setNotes(it.notes ?? ""); + setImages(it.images); + setBoxId(it.box_id ?? ""); + }).catch((e) => showToast(String(e))); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEdit, id, familyId]); + + if (role === "member") { + return ( +
+
你当前只读,不能编辑物品。
+ +
+ ); + } + + const boxOptions = (() => { + const index = indexBoxes(boxes); + return [...index.byId.values()].sort((a, b) => a.depth - b.depth); + })(); + + const handleFiles = async (files: FileList | null) => { + if (!files || !files.length) return; + if (images.length + files.length > MAX_IMAGES) { showToast(`最多 ${MAX_IMAGES} 张图片`); return; } + setUploading(true); + try { + const uploaded: string[] = []; + for (const f of Array.from(files)) { + const p = await uploadImage(f); + uploaded.push(p); + } + setImages((prev) => [...prev, ...uploaded]); + } catch (e) { + showToast(String(e)); + } finally { + setUploading(false); + if (fileRef.current) fileRef.current.value = ""; + } + }; + + const removeImage = (idx: number) => setImages((prev) => prev.filter((_, i) => i !== idx)); + + const save = async () => { + if (!familyId) return; + if (!name.trim()) { showToast("请输入物品名称"); return; } + setBusy(true); + const payload = { + name: name.trim(), + category_id: categoryId || null, + tags: tags.split(/[,,]/).map((t) => t.trim()).filter(Boolean), + quantity: Number(quantity) || 1, + brand: brand.trim() || null, + model: model.trim() || null, + serial_no: serialNo.trim() || null, + purchase_date: purchaseDate || null, + purchase_price: purchasePrice ? Number(purchasePrice) : null, + expiry_date: expiryDate || null, + notes: notes.trim() || null, + images, + box_id: boxId || null, + }; + try { + const saved = isEdit + ? (await updateItem(familyId, id!, payload)).item + : await createItem(familyId, payload); + showToast(isEdit ? "已保存" : "已添加"); + navigate(`/items/${saved.id}`); + } catch (e) { + showToast(String(e)); + } finally { + setBusy(false); + } + }; + + return ( +
+
+

{isEdit ? "编辑物品" : "新增物品"}

+
+ +
+
+ + setName(e.target.value)} /> +
+ +
+ + + {categories.length === 0 &&

暂无分类,可在「我的 › 家庭管理」中添加

} +
+ +
+ + +
+ +
+ + setTags(e.target.value)} /> +
+ +
+ + setQuantity(e.target.value)} /> +
+ +
+
+ + setBrand(e.target.value)} /> +
+
+ + setModel(e.target.value)} /> +
+
+ +
+ + setSerialNo(e.target.value)} /> +
+ +
+
+ + setPurchaseDate(e.target.value)} /> +
+
+ + setPurchasePrice(e.target.value)} /> +
+
+ +
+ + setExpiryDate(e.target.value)} /> +
+ +
+ +