From a188ee1426d13b4cdcbbd89965a138623414e9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E6=B5=B7?= Date: Sat, 6 Jun 2026 10:40:48 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20init:=20=E5=B0=8F=E9=BE=99?= =?UTF-8?q?=E7=9A=84=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .benchmark/appointment/arch.json | 1 + .benchmark/appointment/backend/.gitignore | 7 + .benchmark/appointment/backend/README.md | 118 + .benchmark/appointment/backend/package.json | 27 + .../src/__tests__/appointments.test.ts | 121 + .../backend/src/__tests__/approvals.test.ts | 121 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/clients.test.ts | 119 + .../backend/src/__tests__/contracts.test.ts | 124 + .../backend/src/__tests__/customers.test.ts | 122 + .../backend/src/__tests__/items.test.ts | 118 + .../backend/src/__tests__/reminders.test.ts | 121 + .../backend/src/__tests__/services.test.ts | 120 + .../backend/src/__tests__/stats.test.ts | 121 + .../backend/src/__tests__/users.test.ts | 118 + .../appointment/backend/src/db/client.ts | 93 + .../appointment/backend/src/db/schema.ts | 20 + .benchmark/appointment/backend/src/index.ts | 79 + .../backend/src/middleware/auth.ts | 41 + .../backend/src/routes/appointments.ts | 51 + .../backend/src/routes/approvals.ts | 51 + .../appointment/backend/src/routes/auth.ts | 121 + .../appointment/backend/src/routes/clients.ts | 51 + .../backend/src/routes/contracts.ts | 51 + .../backend/src/routes/customers.ts | 51 + .../appointment/backend/src/routes/items.ts | 51 + .../backend/src/routes/reminders.ts | 51 + .../backend/src/routes/services.ts | 51 + .../appointment/backend/src/routes/stats.ts | 51 + .../appointment/backend/src/routes/users.ts | 51 + .../backend/src/services/appointment.ts | 56 + .../backend/src/services/approval.ts | 56 + .../backend/src/services/client.ts | 54 + .../backend/src/services/contract.ts | 59 + .../backend/src/services/customer.ts | 57 + .../appointment/backend/src/services/item.ts | 55 + .../backend/src/services/reminder.ts | 56 + .../backend/src/services/service.ts | 55 + .../appointment/backend/src/services/stat.ts | 56 + .../appointment/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + .../appointment/backend/src/types/index.ts | 322 + .../appointment/backend/src/types/sql.js.d.ts | 27 + .benchmark/appointment/backend/tsconfig.json | 27 + .benchmark/appointment/electron/README.md | 82 + .../appointment/electron/assets/icon.png | Bin 0 -> 66 bytes .../appointment/electron/electron-builder.yml | 100 + .../appointment/electron/electron/ipc.ts | 77 + .../appointment/electron/electron/main.ts | 238 + .../appointment/electron/electron/preload.ts | 92 + .../appointment/electron/electron/tray.ts | 61 + .../appointment/electron/electron/updater.ts | 87 + .../appointment/electron/electron/utils.ts | 80 + .../electron/entitlements.mac.plist | 18 + .benchmark/appointment/electron/package.json | 41 + .benchmark/appointment/electron/tsconfig.json | 32 + .../frontend/app/appointments/page.tsx | 51 + .../appointment/frontend/app/clients/page.tsx | 51 + .../appointment/frontend/app/feature/page.tsx | 29 + .../appointment/frontend/app/globals.css | 8 + .../appointment/frontend/app/layout.tsx | 30 + .benchmark/appointment/frontend/app/page.tsx | 50 + .../appointment/frontend/app/profile/page.tsx | 29 + .../frontend/app/services/page.tsx | 51 + .../appointment/frontend/app/stats/page.tsx | 51 + .../frontend/app/客户通知/page.tsx | 51 + .../frontend/app/时间段预约/page.tsx | 51 + .../frontend/app/服务项目/page.tsx | 51 + .../frontend/app/预约统计/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/ui/Button.tsx | 31 + .../frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + .../frontend/hooks/useAppointments.ts | 35 + .../appointment/frontend/hooks/useAuth.ts | 35 + .../appointment/frontend/hooks/useClients.ts | 35 + .../appointment/frontend/hooks/useDebounce.ts | 14 + .../appointment/frontend/hooks/useForm.ts | 33 + .../appointment/frontend/hooks/useHealth.ts | 35 + .../appointment/frontend/hooks/useItem.ts | 35 + .../appointment/frontend/hooks/useServices.ts | 35 + .../appointment/frontend/hooks/useStats.ts | 35 + .../appointment/frontend/hooks/useUsers.ts | 35 + .benchmark/appointment/frontend/next-env.d.ts | 6 + .../appointment/frontend/next.config.ts | 7 + .benchmark/appointment/frontend/package.json | 25 + .../appointment/frontend/postcss.config.js | 6 + .../appointment/frontend/services/api.ts | 47 + .../frontend/services/appointments.ts | 10 + .../appointment/frontend/services/auth.ts | 10 + .../appointment/frontend/services/clients.ts | 10 + .../appointment/frontend/services/health.ts | 6 + .../appointment/frontend/services/services.ts | 10 + .../appointment/frontend/services/stats.ts | 10 + .../appointment/frontend/services/users.ts | 10 + .../appointment/frontend/services/客户通知.ts | 10 + .../frontend/services/时间段预约.ts | 10 + .../appointment/frontend/services/服务项目.ts | 10 + .../appointment/frontend/services/预约统计.ts | 10 + .../appointment/frontend/tailwind.config.ts | 10 + .benchmark/appointment/frontend/tsconfig.json | 40 + .../appointment/frontend/types/index.ts | 132 + .benchmark/appointment/fullstack/.gitignore | 8 + .benchmark/appointment/fullstack/README.md | 125 + .../appointment/fullstack/apps/api/.gitignore | 7 + .../appointment/fullstack/apps/api/README.md | 118 + .../fullstack/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/items.test.ts | 118 + .../apps/api/src/__tests__/users.test.ts | 118 + .../fullstack/apps/api/src/db/client.ts | 93 + .../fullstack/apps/api/src/db/schema.ts | 20 + .../fullstack/apps/api/src/index.ts | 79 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../fullstack/apps/api/src/routes/auth.ts | 121 + .../fullstack/apps/api/src/routes/items.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../fullstack/apps/api/src/services/item.ts | 55 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../fullstack/apps/api/src/types/index.ts | 223 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../fullstack/apps/api/tsconfig.json | 27 + .../fullstack/apps/web/app/feature/page.tsx | 29 + .../fullstack/apps/web/app/globals.css | 8 + .../fullstack/apps/web/app/layout.tsx | 30 + .../fullstack/apps/web/app/page.tsx | 50 + .../fullstack/apps/web/app/profile/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../fullstack/apps/web/hooks/useForm.ts | 33 + .../fullstack/apps/web/hooks/useHealth.ts | 35 + .../fullstack/apps/web/hooks/useUsers.ts | 35 + .../fullstack/apps/web/next.config.ts | 7 + .../fullstack/apps/web/package.json | 27 + .../fullstack/apps/web/postcss.config.js | 6 + .../fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/health.ts | 6 + .../fullstack/apps/web/services/users.ts | 10 + .../fullstack/apps/web/tailwind.config.ts | 10 + .../fullstack/apps/web/tsconfig.json | 40 + .../fullstack/apps/web/types/index.ts | 132 + .benchmark/appointment/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 134 + .../packages/shared-types/tsconfig.json | 15 + .../appointment/fullstack/scripts/build.mjs | 26 + .../appointment/fullstack/scripts/dev.mjs | 32 + .benchmark/appointment/prd.json | 1 + .../appointment/release/release/README.md | 33 + .../release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/myproject-0.1.0.AppImage | 3 + .../release/linux/myproject_0.1.0_amd64.deb | 3 + .../release/macos/myproject-0.1.0-arm64.dmg | 3 + .../release/release/macos/myproject-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 60 + .../appointment/release/release/version.json | 10 + .../windows/myproject-0.1.0-portable.exe | 3 + .../release/windows/myproject-setup-0.1.0.exe | 3 + .benchmark/asset/arch.json | 1 + .benchmark/asset/backend/.gitignore | 7 + .benchmark/asset/backend/README.md | 111 + .benchmark/asset/backend/package.json | 27 + .../backend/src/__tests__/approvals.test.ts | 121 + .../backend/src/__tests__/assets.test.ts | 122 + .../asset/backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/contracts.test.ts | 124 + .../backend/src/__tests__/customers.test.ts | 122 + .../asset/backend/src/__tests__/items.test.ts | 119 + .../backend/src/__tests__/reminders.test.ts | 121 + .../asset/backend/src/__tests__/stats.test.ts | 119 + .../asset/backend/src/__tests__/users.test.ts | 118 + .benchmark/asset/backend/src/db/client.ts | 93 + .benchmark/asset/backend/src/db/schema.ts | 19 + .benchmark/asset/backend/src/index.ts | 77 + .../asset/backend/src/middleware/auth.ts | 41 + .../asset/backend/src/routes/approvals.ts | 51 + .benchmark/asset/backend/src/routes/assets.ts | 51 + .benchmark/asset/backend/src/routes/auth.ts | 121 + .../asset/backend/src/routes/contracts.ts | 51 + .../asset/backend/src/routes/customers.ts | 51 + .benchmark/asset/backend/src/routes/items.ts | 51 + .../asset/backend/src/routes/reminders.ts | 51 + .benchmark/asset/backend/src/routes/stats.ts | 51 + .benchmark/asset/backend/src/routes/users.ts | 51 + .../asset/backend/src/services/approval.ts | 56 + .../asset/backend/src/services/asset.ts | 57 + .../asset/backend/src/services/contract.ts | 59 + .../asset/backend/src/services/customer.ts | 57 + .benchmark/asset/backend/src/services/item.ts | 54 + .../asset/backend/src/services/reminder.ts | 56 + .benchmark/asset/backend/src/services/stat.ts | 54 + .benchmark/asset/backend/src/services/user.ts | 56 + .../asset/backend/src/types/fastify.d.ts | 8 + .benchmark/asset/backend/src/types/index.ts | 292 + .../asset/backend/src/types/sql.js.d.ts | 27 + .benchmark/asset/backend/tsconfig.json | 27 + .benchmark/asset/electron/README.md | 82 + .benchmark/asset/electron/assets/icon.png | Bin 0 -> 66 bytes .../asset/electron/electron-builder.yml | 100 + .benchmark/asset/electron/electron/ipc.ts | 77 + .benchmark/asset/electron/electron/main.ts | 238 + .benchmark/asset/electron/electron/preload.ts | 92 + .benchmark/asset/electron/electron/tray.ts | 61 + .benchmark/asset/electron/electron/updater.ts | 87 + .benchmark/asset/electron/electron/utils.ts | 80 + .../asset/electron/entitlements.mac.plist | 18 + .benchmark/asset/electron/package.json | 41 + .benchmark/asset/electron/tsconfig.json | 32 + .benchmark/asset/frontend/app/assets/page.tsx | 51 + .../asset/frontend/app/feature/page.tsx | 29 + .benchmark/asset/frontend/app/globals.css | 8 + .benchmark/asset/frontend/app/items/page.tsx | 51 + .benchmark/asset/frontend/app/layout.tsx | 30 + .benchmark/asset/frontend/app/page.tsx | 50 + .../asset/frontend/app/profile/page.tsx | 29 + .benchmark/asset/frontend/app/stats/page.tsx | 51 + .../asset/frontend/app/折旧计算/page.tsx | 51 + .../asset/frontend/app/盘点统计/page.tsx | 51 + .../asset/frontend/app/资产登记/page.tsx | 51 + .../asset/frontend/app/领用归还/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../asset/frontend/components/ui/Button.tsx | 31 + .../asset/frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../asset/frontend/components/ui/Input.tsx | 22 + .../asset/frontend/components/ui/Modal.tsx | 33 + .benchmark/asset/frontend/hooks/useAssets.ts | 35 + .benchmark/asset/frontend/hooks/useAuth.ts | 35 + .../asset/frontend/hooks/useDebounce.ts | 14 + .benchmark/asset/frontend/hooks/useForm.ts | 33 + .benchmark/asset/frontend/hooks/useHealth.ts | 35 + .benchmark/asset/frontend/hooks/useItem.ts | 35 + .benchmark/asset/frontend/hooks/useItems.ts | 35 + .benchmark/asset/frontend/hooks/useStats.ts | 35 + .benchmark/asset/frontend/hooks/useUsers.ts | 35 + .benchmark/asset/frontend/next-env.d.ts | 6 + .benchmark/asset/frontend/next.config.ts | 7 + .benchmark/asset/frontend/package.json | 25 + .benchmark/asset/frontend/postcss.config.js | 6 + .benchmark/asset/frontend/services/api.ts | 47 + .benchmark/asset/frontend/services/assets.ts | 10 + .benchmark/asset/frontend/services/auth.ts | 10 + .benchmark/asset/frontend/services/health.ts | 6 + .benchmark/asset/frontend/services/items.ts | 18 + .benchmark/asset/frontend/services/stats.ts | 10 + .benchmark/asset/frontend/services/users.ts | 10 + .../asset/frontend/services/折旧计算.ts | 10 + .../asset/frontend/services/盘点统计.ts | 10 + .../asset/frontend/services/资产登记.ts | 10 + .../asset/frontend/services/领用归还.ts | 10 + .benchmark/asset/frontend/tailwind.config.ts | 10 + .benchmark/asset/frontend/tsconfig.json | 40 + .benchmark/asset/frontend/types/index.ts | 120 + .benchmark/asset/fullstack/.gitignore | 8 + .benchmark/asset/fullstack/README.md | 118 + .../asset/fullstack/apps/api/.gitignore | 7 + .benchmark/asset/fullstack/apps/api/README.md | 111 + .../asset/fullstack/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/items.test.ts | 119 + .../apps/api/src/__tests__/users.test.ts | 118 + .../asset/fullstack/apps/api/src/db/client.ts | 93 + .../asset/fullstack/apps/api/src/db/schema.ts | 19 + .../asset/fullstack/apps/api/src/index.ts | 77 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../fullstack/apps/api/src/routes/auth.ts | 121 + .../fullstack/apps/api/src/routes/items.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../fullstack/apps/api/src/services/item.ts | 54 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../fullstack/apps/api/src/types/index.ts | 203 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../asset/fullstack/apps/api/tsconfig.json | 27 + .../fullstack/apps/web/app/feature/page.tsx | 29 + .../asset/fullstack/apps/web/app/globals.css | 8 + .../asset/fullstack/apps/web/app/layout.tsx | 30 + .../asset/fullstack/apps/web/app/page.tsx | 50 + .../fullstack/apps/web/app/profile/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../asset/fullstack/apps/web/hooks/useForm.ts | 33 + .../fullstack/apps/web/hooks/useHealth.ts | 35 + .../fullstack/apps/web/hooks/useUsers.ts | 35 + .../asset/fullstack/apps/web/next.config.ts | 7 + .../asset/fullstack/apps/web/package.json | 27 + .../fullstack/apps/web/postcss.config.js | 6 + .../asset/fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/health.ts | 6 + .../fullstack/apps/web/services/users.ts | 10 + .../fullstack/apps/web/tailwind.config.ts | 10 + .../asset/fullstack/apps/web/tsconfig.json | 40 + .../asset/fullstack/apps/web/types/index.ts | 120 + .benchmark/asset/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 122 + .../packages/shared-types/tsconfig.json | 15 + .benchmark/asset/fullstack/scripts/build.mjs | 26 + .benchmark/asset/fullstack/scripts/dev.mjs | 32 + .benchmark/asset/prd.json | 1 + .benchmark/asset/release/release/README.md | 33 + .../asset/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/myproject-0.1.0.AppImage | 3 + .../release/linux/myproject_0.1.0_amd64.deb | 3 + .../release/macos/myproject-0.1.0-arm64.dmg | 3 + .../release/release/macos/myproject-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 58 + .benchmark/asset/release/release/version.json | 10 + .../windows/myproject-0.1.0-portable.exe | 3 + .../release/windows/myproject-setup-0.1.0.exe | 3 + .benchmark/blog-cms/arch.json | 1 + .benchmark/blog-cms/backend/.gitignore | 7 + .benchmark/blog-cms/backend/README.md | 118 + .benchmark/blog-cms/backend/package.json | 27 + .../backend/src/__tests__/approvals.test.ts | 121 + .../backend/src/__tests__/articles.test.ts | 123 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/comments.test.ts | 120 + .../backend/src/__tests__/contracts.test.ts | 124 + .../backend/src/__tests__/customers.test.ts | 122 + .../backend/src/__tests__/items.test.ts | 118 + .../backend/src/__tests__/media.test.ts | 120 + .../backend/src/__tests__/reminders.test.ts | 121 + .../backend/src/__tests__/tags.test.ts | 118 + .../backend/src/__tests__/users.test.ts | 118 + .benchmark/blog-cms/backend/src/db/client.ts | 93 + .benchmark/blog-cms/backend/src/db/schema.ts | 20 + .benchmark/blog-cms/backend/src/index.ts | 79 + .../blog-cms/backend/src/middleware/auth.ts | 41 + .../blog-cms/backend/src/routes/approvals.ts | 51 + .../blog-cms/backend/src/routes/articles.ts | 51 + .../blog-cms/backend/src/routes/auth.ts | 121 + .../blog-cms/backend/src/routes/comments.ts | 51 + .../blog-cms/backend/src/routes/contracts.ts | 51 + .../blog-cms/backend/src/routes/customers.ts | 51 + .../blog-cms/backend/src/routes/items.ts | 51 + .../blog-cms/backend/src/routes/media.ts | 51 + .../blog-cms/backend/src/routes/reminders.ts | 51 + .../blog-cms/backend/src/routes/tags.ts | 51 + .../blog-cms/backend/src/routes/users.ts | 51 + .../blog-cms/backend/src/services/approval.ts | 56 + .../blog-cms/backend/src/services/article.ts | 58 + .../blog-cms/backend/src/services/comment.ts | 55 + .../blog-cms/backend/src/services/contract.ts | 59 + .../blog-cms/backend/src/services/customer.ts | 57 + .../blog-cms/backend/src/services/item.ts | 55 + .../blog-cms/backend/src/services/media.ts | 55 + .../blog-cms/backend/src/services/reminder.ts | 56 + .../blog-cms/backend/src/services/tag.ts | 53 + .../blog-cms/backend/src/services/user.ts | 56 + .../blog-cms/backend/src/types/fastify.d.ts | 8 + .../blog-cms/backend/src/types/index.ts | 320 + .../blog-cms/backend/src/types/sql.js.d.ts | 27 + .benchmark/blog-cms/backend/tsconfig.json | 27 + .benchmark/blog-cms/electron/README.md | 82 + .benchmark/blog-cms/electron/assets/icon.png | Bin 0 -> 66 bytes .../blog-cms/electron/electron-builder.yml | 100 + .benchmark/blog-cms/electron/electron/ipc.ts | 77 + .benchmark/blog-cms/electron/electron/main.ts | 238 + .../blog-cms/electron/electron/preload.ts | 92 + .benchmark/blog-cms/electron/electron/tray.ts | 61 + .../blog-cms/electron/electron/updater.ts | 87 + .../blog-cms/electron/electron/utils.ts | 80 + .../blog-cms/electron/entitlements.mac.plist | 18 + .benchmark/blog-cms/electron/package.json | 41 + .benchmark/blog-cms/electron/tsconfig.json | 32 + .../blog-cms/frontend/app/articles/page.tsx | 51 + .../blog-cms/frontend/app/comments/page.tsx | 51 + .benchmark/blog-cms/frontend/app/globals.css | 8 + .benchmark/blog-cms/frontend/app/layout.tsx | 30 + .../blog-cms/frontend/app/media/page.tsx | 51 + .../blog-cms/frontend/app/messages/page.tsx | 29 + .benchmark/blog-cms/frontend/app/page.tsx | 50 + .../frontend/app/profile/[userId]/page.tsx | 29 + .../blog-cms/frontend/app/publish/page.tsx | 29 + .../blog-cms/frontend/app/tags/page.tsx | 51 + .../blog-cms/frontend/app/分类标签/page.tsx | 51 + .../blog-cms/frontend/app/媒体库/page.tsx | 51 + .../blog-cms/frontend/app/文章发布/page.tsx | 51 + .../blog-cms/frontend/app/评论管理/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/ui/Button.tsx | 31 + .../blog-cms/frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../blog-cms/frontend/components/ui/Input.tsx | 22 + .../blog-cms/frontend/components/ui/Modal.tsx | 33 + .../blog-cms/frontend/hooks/useArticles.ts | 35 + .benchmark/blog-cms/frontend/hooks/useAuth.ts | 35 + .../blog-cms/frontend/hooks/useComments.ts | 35 + .../blog-cms/frontend/hooks/useDebounce.ts | 14 + .benchmark/blog-cms/frontend/hooks/useFeed.ts | 35 + .benchmark/blog-cms/frontend/hooks/useForm.ts | 33 + .benchmark/blog-cms/frontend/hooks/useItem.ts | 35 + .../blog-cms/frontend/hooks/useMedia.ts | 35 + .../blog-cms/frontend/hooks/usePosts.ts | 35 + .benchmark/blog-cms/frontend/hooks/useTags.ts | 35 + .../blog-cms/frontend/hooks/useUsers.ts | 35 + .benchmark/blog-cms/frontend/next-env.d.ts | 6 + .benchmark/blog-cms/frontend/next.config.ts | 7 + .benchmark/blog-cms/frontend/package.json | 25 + .../blog-cms/frontend/postcss.config.js | 6 + .benchmark/blog-cms/frontend/services/api.ts | 47 + .../blog-cms/frontend/services/articles.ts | 10 + .benchmark/blog-cms/frontend/services/auth.ts | 10 + .../blog-cms/frontend/services/comments.ts | 10 + .benchmark/blog-cms/frontend/services/feed.ts | 6 + .../blog-cms/frontend/services/media.ts | 10 + .../blog-cms/frontend/services/posts.ts | 10 + .benchmark/blog-cms/frontend/services/tags.ts | 10 + .../blog-cms/frontend/services/users.ts | 6 + .../blog-cms/frontend/services/分类标签.ts | 10 + .../blog-cms/frontend/services/媒体库.ts | 10 + .../blog-cms/frontend/services/文章发布.ts | 10 + .../blog-cms/frontend/services/评论管理.ts | 10 + .../blog-cms/frontend/tailwind.config.ts | 10 + .benchmark/blog-cms/frontend/tsconfig.json | 40 + .benchmark/blog-cms/frontend/types/index.ts | 130 + .benchmark/blog-cms/fullstack/.gitignore | 8 + .benchmark/blog-cms/fullstack/README.md | 125 + .../blog-cms/fullstack/apps/api/.gitignore | 7 + .../blog-cms/fullstack/apps/api/README.md | 118 + .../blog-cms/fullstack/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/items.test.ts | 118 + .../apps/api/src/__tests__/users.test.ts | 118 + .../fullstack/apps/api/src/db/client.ts | 93 + .../fullstack/apps/api/src/db/schema.ts | 20 + .../blog-cms/fullstack/apps/api/src/index.ts | 79 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../fullstack/apps/api/src/routes/auth.ts | 121 + .../fullstack/apps/api/src/routes/items.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../fullstack/apps/api/src/services/item.ts | 55 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../fullstack/apps/api/src/types/index.ts | 223 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../blog-cms/fullstack/apps/api/tsconfig.json | 27 + .../fullstack/apps/web/app/globals.css | 8 + .../fullstack/apps/web/app/layout.tsx | 30 + .../fullstack/apps/web/app/messages/page.tsx | 29 + .../blog-cms/fullstack/apps/web/app/page.tsx | 50 + .../apps/web/app/profile/[userId]/page.tsx | 29 + .../fullstack/apps/web/app/publish/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useComments.ts | 35 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../fullstack/apps/web/hooks/useFeed.ts | 35 + .../fullstack/apps/web/hooks/useForm.ts | 33 + .../fullstack/apps/web/hooks/usePosts.ts | 35 + .../fullstack/apps/web/hooks/useUsers.ts | 35 + .../fullstack/apps/web/next.config.ts | 7 + .../blog-cms/fullstack/apps/web/package.json | 27 + .../fullstack/apps/web/postcss.config.js | 6 + .../fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/comments.ts | 10 + .../fullstack/apps/web/services/feed.ts | 6 + .../fullstack/apps/web/services/posts.ts | 10 + .../fullstack/apps/web/services/users.ts | 6 + .../fullstack/apps/web/tailwind.config.ts | 10 + .../blog-cms/fullstack/apps/web/tsconfig.json | 40 + .../fullstack/apps/web/types/index.ts | 130 + .benchmark/blog-cms/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 132 + .../packages/shared-types/tsconfig.json | 15 + .../blog-cms/fullstack/scripts/build.mjs | 26 + .benchmark/blog-cms/fullstack/scripts/dev.mjs | 32 + .benchmark/blog-cms/prd.json | 1 + .benchmark/blog-cms/release/release/README.md | 33 + .../blog-cms/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/socialapp-0.1.0.AppImage | 3 + .../release/linux/socialapp_0.1.0_amd64.deb | 3 + .../release/macos/socialapp-0.1.0-arm64.dmg | 3 + .../release/release/macos/socialapp-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 60 + .../blog-cms/release/release/version.json | 10 + .../windows/socialapp-0.1.0-portable.exe | 3 + .../release/windows/socialapp-setup-0.1.0.exe | 3 + .benchmark/contract-mgmt/arch.json | 1 + .benchmark/contract-mgmt/backend/.gitignore | 7 + .benchmark/contract-mgmt/backend/README.md | 104 + .benchmark/contract-mgmt/backend/package.json | 27 + .../backend/src/__tests__/approvals.test.ts | 121 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/contracts.test.ts | 124 + .../backend/src/__tests__/customers.test.ts | 122 + .../src/__tests__/operation_logs.test.ts | 119 + .../backend/src/__tests__/reminders.test.ts | 121 + .../backend/src/__tests__/stats.test.ts | 119 + .../contract-mgmt/backend/src/db/client.ts | 93 + .../contract-mgmt/backend/src/db/schema.ts | 18 + .benchmark/contract-mgmt/backend/src/index.ts | 75 + .../backend/src/middleware/auth.ts | 41 + .../backend/src/routes/approvals.ts | 51 + .../contract-mgmt/backend/src/routes/auth.ts | 121 + .../backend/src/routes/contracts.ts | 51 + .../backend/src/routes/customers.ts | 51 + .../backend/src/routes/operation_logs.ts | 51 + .../backend/src/routes/reminders.ts | 51 + .../contract-mgmt/backend/src/routes/stats.ts | 51 + .../backend/src/services/approval.ts | 56 + .../backend/src/services/contract.ts | 59 + .../backend/src/services/customer.ts | 57 + .../backend/src/services/operation_log.ts | 54 + .../backend/src/services/reminder.ts | 56 + .../backend/src/services/stat.ts | 54 + .../backend/src/types/fastify.d.ts | 8 + .../contract-mgmt/backend/src/types/index.ts | 258 + .../backend/src/types/sql.js.d.ts | 27 + .../contract-mgmt/backend/tsconfig.json | 27 + .benchmark/contract-mgmt/electron/README.md | 82 + .../contract-mgmt/electron/assets/icon.png | Bin 0 -> 66 bytes .../electron/electron-builder.yml | 100 + .../contract-mgmt/electron/electron/ipc.ts | 77 + .../contract-mgmt/electron/electron/main.ts | 238 + .../electron/electron/preload.ts | 92 + .../contract-mgmt/electron/electron/tray.ts | 61 + .../electron/electron/updater.ts | 87 + .../contract-mgmt/electron/electron/utils.ts | 80 + .../electron/entitlements.mac.plist | 18 + .../contract-mgmt/electron/package.json | 41 + .../contract-mgmt/electron/tsconfig.json | 32 + .../frontend/app/approvals/page.tsx | 35 + .../frontend/app/contracts/page.tsx | 51 + .../frontend/app/customers/page.tsx | 51 + .../contract-mgmt/frontend/app/globals.css | 8 + .../contract-mgmt/frontend/app/layout.tsx | 30 + .../frontend/app/operation_logs/page.tsx | 29 + .../contract-mgmt/frontend/app/page.tsx | 50 + .../frontend/app/reminders/page.tsx | 51 + .../contract-mgmt/frontend/app/stats/page.tsx | 29 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/ui/Button.tsx | 31 + .../frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + .../frontend/hooks/useApprovals.ts | 35 + .../contract-mgmt/frontend/hooks/useAuth.ts | 35 + .../frontend/hooks/useContracts.ts | 35 + .../frontend/hooks/useCustomers.ts | 35 + .../frontend/hooks/useDebounce.ts | 14 + .../contract-mgmt/frontend/hooks/useForm.ts | 33 + .../frontend/hooks/useReminders.ts | 35 + .../contract-mgmt/frontend/next.config.ts | 7 + .../contract-mgmt/frontend/package.json | 25 + .../contract-mgmt/frontend/postcss.config.js | 6 + .../contract-mgmt/frontend/services/api.ts | 47 + .../frontend/services/approvals.ts | 10 + .../contract-mgmt/frontend/services/auth.ts | 10 + .../frontend/services/contracts.ts | 10 + .../frontend/services/customers.ts | 10 + .../frontend/services/reminders.ts | 10 + .../contract-mgmt/frontend/tailwind.config.ts | 10 + .../contract-mgmt/frontend/tsconfig.json | 40 + .../contract-mgmt/frontend/types/index.ts | 106 + .benchmark/contract-mgmt/prd.json | 1 + .../contract-mgmt/release/release/README.md | 33 + .../release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/oaflow-0.1.0.AppImage | 3 + .../release/linux/oaflow_0.1.0_amd64.deb | 3 + .../release/macos/oaflow-0.1.0-arm64.dmg | 3 + .../release/release/macos/oaflow-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 56 + .../release/release/version.json | 10 + .../release/windows/oaflow-0.1.0-portable.exe | 3 + .../release/windows/oaflow-setup-0.1.0.exe | 3 + .benchmark/contract-mgmt/summary.json | 43 + .benchmark/course/arch.json | 1 + .benchmark/course/backend/.gitignore | 7 + .benchmark/course/backend/README.md | 90 + .benchmark/course/backend/package.json | 27 + .../backend/src/__tests__/assignments.test.ts | 120 + .../course/backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/chapters.test.ts | 119 + .../backend/src/__tests__/courses.test.ts | 122 + .../backend/src/__tests__/exercises.test.ts | 120 + .../backend/src/__tests__/lessons.test.ts | 122 + .../backend/src/__tests__/students.test.ts | 118 + .../src/__tests__/user_progress.test.ts | 122 + .../backend/src/__tests__/users.test.ts | 118 + .benchmark/course/backend/src/db/client.ts | 93 + .benchmark/course/backend/src/db/schema.ts | 16 + .benchmark/course/backend/src/index.ts | 71 + .../course/backend/src/middleware/auth.ts | 41 + .../course/backend/src/routes/assignments.ts | 51 + .benchmark/course/backend/src/routes/auth.ts | 121 + .../course/backend/src/routes/chapters.ts | 51 + .../course/backend/src/routes/courses.ts | 51 + .../course/backend/src/routes/exercises.ts | 51 + .../course/backend/src/routes/lessons.ts | 51 + .../course/backend/src/routes/students.ts | 51 + .../backend/src/routes/user_progress.ts | 51 + .benchmark/course/backend/src/routes/users.ts | 51 + .../course/backend/src/services/assignment.ts | 55 + .../course/backend/src/services/chapter.ts | 54 + .../course/backend/src/services/cours.ts | 57 + .../course/backend/src/services/exercis.ts | 56 + .../course/backend/src/services/lesson.ts | 57 + .../course/backend/src/services/student.ts | 53 + .../course/backend/src/services/user.ts | 56 + .../backend/src/services/user_progress.ts | 56 + .../course/backend/src/types/fastify.d.ts | 8 + .benchmark/course/backend/src/types/index.ts | 185 + .../course/backend/src/types/sql.js.d.ts | 27 + .benchmark/course/backend/tsconfig.json | 27 + .benchmark/course/electron/README.md | 82 + .benchmark/course/electron/assets/icon.png | Bin 0 -> 66 bytes .../course/electron/electron-builder.yml | 100 + .benchmark/course/electron/electron/ipc.ts | 77 + .benchmark/course/electron/electron/main.ts | 238 + .../course/electron/electron/preload.ts | 92 + .benchmark/course/electron/electron/tray.ts | 61 + .../course/electron/electron/updater.ts | 87 + .benchmark/course/electron/electron/utils.ts | 80 + .../course/electron/entitlements.mac.plist | 18 + .benchmark/course/electron/package.json | 41 + .benchmark/course/electron/tsconfig.json | 32 + .../course/frontend/app/assignments/page.tsx | 51 + .../course/frontend/app/chapters/page.tsx | 51 + .../course/frontend/app/course/[id]/page.tsx | 26 + .../course/frontend/app/courses/page.tsx | 26 + .../course/frontend/app/exercises/page.tsx | 10 + .benchmark/course/frontend/app/globals.css | 8 + .benchmark/course/frontend/app/layout.tsx | 30 + .../app/learn/[courseId]/[lessonId]/page.tsx | 26 + .benchmark/course/frontend/app/live/page.tsx | 10 + .benchmark/course/frontend/app/page.tsx | 50 + .../course/frontend/app/profile/page.tsx | 29 + .../course/frontend/app/students/page.tsx | 51 + .../course/frontend/app/作业批改/page.tsx | 51 + .../course/frontend/app/学员进度/page.tsx | 51 + .../course/frontend/app/章节管理/page.tsx | 51 + .../course/frontend/app/课程发布/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../course/frontend/components/ui/Button.tsx | 31 + .../course/frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../course/frontend/components/ui/Input.tsx | 22 + .../course/frontend/components/ui/Modal.tsx | 33 + .../course/frontend/hooks/useAssignments.ts | 35 + .benchmark/course/frontend/hooks/useAuth.ts | 35 + .../course/frontend/hooks/useChapters.ts | 35 + .../course/frontend/hooks/useCourses.ts | 35 + .../course/frontend/hooks/useDebounce.ts | 14 + .../course/frontend/hooks/useExercises.ts | 35 + .benchmark/course/frontend/hooks/useForm.ts | 33 + .benchmark/course/frontend/hooks/useItem.ts | 35 + .../course/frontend/hooks/useLessons.ts | 35 + .../course/frontend/hooks/useProgress.ts | 35 + .../course/frontend/hooks/useStudents.ts | 35 + .benchmark/course/frontend/next-env.d.ts | 6 + .benchmark/course/frontend/next.config.ts | 7 + .benchmark/course/frontend/package.json | 25 + .benchmark/course/frontend/postcss.config.js | 6 + .benchmark/course/frontend/services/api.ts | 47 + .../course/frontend/services/assignments.ts | 10 + .benchmark/course/frontend/services/auth.ts | 10 + .../course/frontend/services/chapters.ts | 10 + .../course/frontend/services/courses.ts | 10 + .../course/frontend/services/exercises.ts | 10 + .../course/frontend/services/lessons.ts | 6 + .../course/frontend/services/progress.ts | 6 + .../course/frontend/services/students.ts | 10 + .../course/frontend/services/作业批改.ts | 10 + .../course/frontend/services/学员进度.ts | 10 + .../course/frontend/services/章节管理.ts | 10 + .../course/frontend/services/课程发布.ts | 10 + .benchmark/course/frontend/tailwind.config.ts | 10 + .benchmark/course/frontend/tsconfig.json | 40 + .benchmark/course/frontend/types/index.ts | 79 + .benchmark/course/fullstack/.gitignore | 8 + .benchmark/course/fullstack/README.md | 97 + .../course/fullstack/apps/api/.gitignore | 7 + .../course/fullstack/apps/api/README.md | 90 + .../course/fullstack/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/courses.test.ts | 122 + .../apps/api/src/__tests__/exercises.test.ts | 120 + .../apps/api/src/__tests__/lessons.test.ts | 122 + .../api/src/__tests__/user_progress.test.ts | 122 + .../apps/api/src/__tests__/users.test.ts | 118 + .../fullstack/apps/api/src/db/client.ts | 93 + .../fullstack/apps/api/src/db/schema.ts | 16 + .../course/fullstack/apps/api/src/index.ts | 71 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../fullstack/apps/api/src/routes/auth.ts | 121 + .../fullstack/apps/api/src/routes/courses.ts | 51 + .../apps/api/src/routes/exercises.ts | 51 + .../fullstack/apps/api/src/routes/lessons.ts | 51 + .../apps/api/src/routes/user_progress.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../fullstack/apps/api/src/services/cours.ts | 57 + .../apps/api/src/services/exercis.ts | 56 + .../fullstack/apps/api/src/services/lesson.ts | 57 + .../fullstack/apps/api/src/services/user.ts | 56 + .../apps/api/src/services/user_progress.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../fullstack/apps/api/src/types/index.ts | 131 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../course/fullstack/apps/api/tsconfig.json | 27 + .../apps/web/app/course/[id]/page.tsx | 26 + .../fullstack/apps/web/app/exercises/page.tsx | 10 + .../course/fullstack/apps/web/app/globals.css | 8 + .../course/fullstack/apps/web/app/layout.tsx | 30 + .../app/learn/[courseId]/[lessonId]/page.tsx | 26 + .../fullstack/apps/web/app/live/page.tsx | 10 + .../course/fullstack/apps/web/app/page.tsx | 50 + .../fullstack/apps/web/app/profile/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useCourses.ts | 35 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../fullstack/apps/web/hooks/useExercises.ts | 35 + .../fullstack/apps/web/hooks/useForm.ts | 33 + .../fullstack/apps/web/hooks/useLessons.ts | 35 + .../fullstack/apps/web/hooks/useProgress.ts | 35 + .../course/fullstack/apps/web/next.config.ts | 7 + .../course/fullstack/apps/web/package.json | 27 + .../fullstack/apps/web/postcss.config.js | 6 + .../course/fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/courses.ts | 10 + .../fullstack/apps/web/services/exercises.ts | 10 + .../fullstack/apps/web/services/lessons.ts | 6 + .../fullstack/apps/web/services/progress.ts | 6 + .../fullstack/apps/web/tailwind.config.ts | 10 + .../course/fullstack/apps/web/tsconfig.json | 40 + .../course/fullstack/apps/web/types/index.ts | 79 + .benchmark/course/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 81 + .../packages/shared-types/tsconfig.json | 15 + .benchmark/course/fullstack/scripts/build.mjs | 26 + .benchmark/course/fullstack/scripts/dev.mjs | 32 + .benchmark/course/prd.json | 1 + .benchmark/course/release/release/README.md | 33 + .../course/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/eduplatform-0.1.0.AppImage | 3 + .../release/linux/eduplatform_0.1.0_amd64.deb | 3 + .../release/macos/eduplatform-0.1.0-arm64.dmg | 3 + .../release/macos/eduplatform-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 58 + .../course/release/release/version.json | 10 + .../windows/eduplatform-0.1.0-portable.exe | 3 + .../windows/eduplatform-setup-0.1.0.exe | 3 + .benchmark/crm/arch.json | 1 + .benchmark/crm/backend/.gitignore | 7 + .benchmark/crm/backend/README.md | 90 + .benchmark/crm/backend/package.json | 27 + .../crm/backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/customers.test.ts | 121 + .../backend/src/__tests__/dashboard.test.ts | 119 + .../backend/src/__tests__/follow_ups.test.ts | 121 + .../backend/src/__tests__/note_tags.test.ts | 118 + .../crm/backend/src/__tests__/notes.test.ts | 122 + .../src/__tests__/sales_pipeline.test.ts | 121 + .../crm/backend/src/__tests__/tags.test.ts | 116 + .../crm/backend/src/__tests__/users.test.ts | 118 + .benchmark/crm/backend/src/db/client.ts | 93 + .benchmark/crm/backend/src/db/schema.ts | 16 + .benchmark/crm/backend/src/index.ts | 71 + .benchmark/crm/backend/src/middleware/auth.ts | 41 + .benchmark/crm/backend/src/routes/auth.ts | 121 + .../crm/backend/src/routes/customers.ts | 51 + .../crm/backend/src/routes/dashboard.ts | 51 + .../crm/backend/src/routes/follow_ups.ts | 51 + .../crm/backend/src/routes/note_tags.ts | 51 + .benchmark/crm/backend/src/routes/notes.ts | 51 + .../crm/backend/src/routes/sales_pipeline.ts | 51 + .benchmark/crm/backend/src/routes/tags.ts | 51 + .benchmark/crm/backend/src/routes/users.ts | 51 + .../crm/backend/src/services/customer.ts | 56 + .../crm/backend/src/services/dashboard.ts | 54 + .../crm/backend/src/services/follow_up.ts | 56 + .benchmark/crm/backend/src/services/note.ts | 58 + .../crm/backend/src/services/note_tag.ts | 54 + .../backend/src/services/sales_pipeline.ts | 56 + .benchmark/crm/backend/src/services/tag.ts | 54 + .benchmark/crm/backend/src/services/user.ts | 56 + .benchmark/crm/backend/src/types/fastify.d.ts | 8 + .benchmark/crm/backend/src/types/index.ts | 191 + .benchmark/crm/backend/src/types/sql.js.d.ts | 27 + .benchmark/crm/backend/tsconfig.json | 27 + .benchmark/crm/electron/README.md | 82 + .benchmark/crm/electron/assets/icon.png | Bin 0 -> 66 bytes .benchmark/crm/electron/electron-builder.yml | 100 + .benchmark/crm/electron/electron/ipc.ts | 77 + .benchmark/crm/electron/electron/main.ts | 238 + .benchmark/crm/electron/electron/preload.ts | 92 + .benchmark/crm/electron/electron/tray.ts | 61 + .benchmark/crm/electron/electron/updater.ts | 87 + .benchmark/crm/electron/electron/utils.ts | 80 + .../crm/electron/entitlements.mac.plist | 18 + .benchmark/crm/electron/package.json | 41 + .benchmark/crm/electron/tsconfig.json | 32 + .../crm/frontend/app/customers/page.tsx | 51 + .../crm/frontend/app/dashboard/page.tsx | 51 + .../crm/frontend/app/follow_ups/page.tsx | 51 + .benchmark/crm/frontend/app/globals.css | 8 + .benchmark/crm/frontend/app/layout.tsx | 30 + .../crm/frontend/app/note/[id]/page.tsx | 25 + .benchmark/crm/frontend/app/notes/page.tsx | 25 + .benchmark/crm/frontend/app/page.tsx | 50 + .../crm/frontend/app/sales_pipeline/page.tsx | 51 + .benchmark/crm/frontend/app/settings/page.tsx | 14 + .benchmark/crm/frontend/app/tags/page.tsx | 14 + .benchmark/crm/frontend/app/客户管理/page.tsx | 51 + .../crm/frontend/app/数据分析仪表盘/page.tsx | 51 + .benchmark/crm/frontend/app/跟进记录/page.tsx | 51 + .benchmark/crm/frontend/app/销售漏斗/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../crm/frontend/components/note/NoteCard.tsx | 20 + .../crm/frontend/components/ui/Button.tsx | 31 + .../crm/frontend/components/ui/Card.tsx | 18 + .../crm/frontend/components/ui/EmptyState.tsx | 19 + .../crm/frontend/components/ui/Input.tsx | 22 + .../crm/frontend/components/ui/Modal.tsx | 33 + .benchmark/crm/frontend/hooks/useAuth.ts | 35 + .benchmark/crm/frontend/hooks/useCustomers.ts | 35 + .benchmark/crm/frontend/hooks/useDashboard.ts | 35 + .benchmark/crm/frontend/hooks/useDebounce.ts | 14 + .../crm/frontend/hooks/useFollow_ups.ts | 35 + .benchmark/crm/frontend/hooks/useForm.ts | 33 + .benchmark/crm/frontend/hooks/useItem.ts | 35 + .benchmark/crm/frontend/hooks/useNotes.ts | 35 + .../crm/frontend/hooks/useSales_pipeline.ts | 35 + .benchmark/crm/frontend/hooks/useTags.ts | 35 + .benchmark/crm/frontend/next-env.d.ts | 6 + .benchmark/crm/frontend/next.config.ts | 7 + .benchmark/crm/frontend/package.json | 25 + .benchmark/crm/frontend/postcss.config.js | 6 + .benchmark/crm/frontend/services/api.ts | 47 + .benchmark/crm/frontend/services/auth.ts | 10 + .benchmark/crm/frontend/services/customers.ts | 10 + .benchmark/crm/frontend/services/dashboard.ts | 10 + .../crm/frontend/services/follow_ups.ts | 10 + .benchmark/crm/frontend/services/notes.ts | 18 + .../crm/frontend/services/sales_pipeline.ts | 10 + .benchmark/crm/frontend/services/tags.ts | 6 + .benchmark/crm/frontend/services/客户管理.ts | 10 + .../crm/frontend/services/数据分析仪表盘.ts | 10 + .benchmark/crm/frontend/services/跟进记录.ts | 10 + .benchmark/crm/frontend/services/销售漏斗.ts | 10 + .benchmark/crm/frontend/tailwind.config.ts | 10 + .benchmark/crm/frontend/tsconfig.json | 40 + .benchmark/crm/frontend/types/index.ts | 79 + .benchmark/crm/fullstack/.gitignore | 8 + .benchmark/crm/fullstack/README.md | 97 + .benchmark/crm/fullstack/apps/api/.gitignore | 7 + .benchmark/crm/fullstack/apps/api/README.md | 90 + .../crm/fullstack/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/note_tags.test.ts | 118 + .../apps/api/src/__tests__/notes.test.ts | 122 + .../apps/api/src/__tests__/tags.test.ts | 116 + .../apps/api/src/__tests__/users.test.ts | 118 + .../crm/fullstack/apps/api/src/db/client.ts | 93 + .../crm/fullstack/apps/api/src/db/schema.ts | 16 + .../crm/fullstack/apps/api/src/index.ts | 71 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../crm/fullstack/apps/api/src/routes/auth.ts | 121 + .../apps/api/src/routes/note_tags.ts | 51 + .../fullstack/apps/api/src/routes/notes.ts | 51 + .../crm/fullstack/apps/api/src/routes/tags.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../fullstack/apps/api/src/services/note.ts | 58 + .../apps/api/src/services/note_tag.ts | 54 + .../fullstack/apps/api/src/services/tag.ts | 54 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../crm/fullstack/apps/api/src/types/index.ts | 137 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../crm/fullstack/apps/api/tsconfig.json | 27 + .../crm/fullstack/apps/web/app/globals.css | 8 + .../crm/fullstack/apps/web/app/layout.tsx | 30 + .../fullstack/apps/web/app/note/[id]/page.tsx | 25 + .../crm/fullstack/apps/web/app/notes/page.tsx | 25 + .../crm/fullstack/apps/web/app/page.tsx | 50 + .../fullstack/apps/web/app/settings/page.tsx | 14 + .../crm/fullstack/apps/web/app/tags/page.tsx | 14 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/note/NoteCard.tsx | 20 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../crm/fullstack/apps/web/hooks/useForm.ts | 33 + .../crm/fullstack/apps/web/hooks/useNotes.ts | 35 + .../crm/fullstack/apps/web/hooks/useTags.ts | 35 + .../crm/fullstack/apps/web/next.config.ts | 7 + .../crm/fullstack/apps/web/package.json | 27 + .../crm/fullstack/apps/web/postcss.config.js | 6 + .../crm/fullstack/apps/web/services/api.ts | 49 + .../crm/fullstack/apps/web/services/notes.ts | 18 + .../crm/fullstack/apps/web/services/tags.ts | 6 + .../crm/fullstack/apps/web/tailwind.config.ts | 10 + .../crm/fullstack/apps/web/tsconfig.json | 40 + .../crm/fullstack/apps/web/types/index.ts | 79 + .benchmark/crm/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 81 + .../packages/shared-types/tsconfig.json | 15 + .benchmark/crm/fullstack/scripts/build.mjs | 26 + .benchmark/crm/fullstack/scripts/dev.mjs | 32 + .benchmark/crm/prd.json | 1 + .benchmark/crm/release/release/README.md | 33 + .../crm/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/noteapp-0.1.0.AppImage | 3 + .../release/linux/noteapp_0.1.0_amd64.deb | 3 + .../release/macos/noteapp-0.1.0-arm64.dmg | 3 + .../release/release/macos/noteapp-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 58 + .benchmark/crm/release/release/version.json | 10 + .../windows/noteapp-0.1.0-portable.exe | 3 + .../release/windows/noteapp-setup-0.1.0.exe | 3 + .benchmark/hr/arch.json | 1 + .benchmark/hr/backend/.gitignore | 7 + .benchmark/hr/backend/README.md | 118 + .benchmark/hr/backend/package.json | 27 + .../src/__tests__/approval_steps.test.ts | 124 + .../backend/src/__tests__/approvals.test.ts | 121 + .../backend/src/__tests__/attendance.test.ts | 119 + .../hr/backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/contracts.test.ts | 124 + .../backend/src/__tests__/customers.test.ts | 122 + .../backend/src/__tests__/departments.test.ts | 118 + .../backend/src/__tests__/employees.test.ts | 123 + .../hr/backend/src/__tests__/items.test.ts | 119 + .../backend/src/__tests__/performance.test.ts | 119 + .../backend/src/__tests__/reminders.test.ts | 121 + .../hr/backend/src/__tests__/users.test.ts | 118 + .benchmark/hr/backend/src/db/client.ts | 93 + .benchmark/hr/backend/src/db/schema.ts | 20 + .benchmark/hr/backend/src/index.ts | 79 + .benchmark/hr/backend/src/middleware/auth.ts | 41 + .../hr/backend/src/routes/approval_steps.ts | 51 + .benchmark/hr/backend/src/routes/approvals.ts | 51 + .../hr/backend/src/routes/attendance.ts | 51 + .benchmark/hr/backend/src/routes/auth.ts | 121 + .benchmark/hr/backend/src/routes/contracts.ts | 51 + .benchmark/hr/backend/src/routes/customers.ts | 51 + .../hr/backend/src/routes/departments.ts | 51 + .benchmark/hr/backend/src/routes/employees.ts | 51 + .benchmark/hr/backend/src/routes/items.ts | 51 + .../hr/backend/src/routes/performance.ts | 51 + .benchmark/hr/backend/src/routes/reminders.ts | 51 + .benchmark/hr/backend/src/routes/users.ts | 51 + .../hr/backend/src/services/approval.ts | 56 + .../hr/backend/src/services/approval_step.ts | 58 + .../hr/backend/src/services/attendance.ts | 54 + .../hr/backend/src/services/contract.ts | 59 + .../hr/backend/src/services/customer.ts | 57 + .../hr/backend/src/services/department.ts | 55 + .../hr/backend/src/services/employee.ts | 58 + .benchmark/hr/backend/src/services/item.ts | 54 + .../hr/backend/src/services/performance.ts | 54 + .../hr/backend/src/services/reminder.ts | 56 + .benchmark/hr/backend/src/services/user.ts | 56 + .benchmark/hr/backend/src/types/fastify.d.ts | 8 + .benchmark/hr/backend/src/types/index.ts | 319 + .benchmark/hr/backend/src/types/sql.js.d.ts | 27 + .benchmark/hr/backend/tsconfig.json | 27 + .benchmark/hr/electron/README.md | 82 + .benchmark/hr/electron/assets/icon.png | Bin 0 -> 66 bytes .benchmark/hr/electron/electron-builder.yml | 100 + .benchmark/hr/electron/electron/ipc.ts | 77 + .benchmark/hr/electron/electron/main.ts | 238 + .benchmark/hr/electron/electron/preload.ts | 92 + .benchmark/hr/electron/electron/tray.ts | 61 + .benchmark/hr/electron/electron/updater.ts | 87 + .benchmark/hr/electron/electron/utils.ts | 80 + .benchmark/hr/electron/entitlements.mac.plist | 18 + .benchmark/hr/electron/package.json | 41 + .benchmark/hr/electron/tsconfig.json | 32 + .benchmark/hr/frontend/app/approvals/page.tsx | 35 + .../hr/frontend/app/attendance/page.tsx | 20 + .benchmark/hr/frontend/app/contacts/page.tsx | 17 + .benchmark/hr/frontend/app/employees/page.tsx | 51 + .benchmark/hr/frontend/app/globals.css | 8 + .benchmark/hr/frontend/app/items/page.tsx | 51 + .benchmark/hr/frontend/app/layout.tsx | 30 + .benchmark/hr/frontend/app/notices/page.tsx | 51 + .benchmark/hr/frontend/app/page.tsx | 50 + .benchmark/hr/frontend/app/profile/page.tsx | 29 + .benchmark/hr/frontend/app/员工档案/page.tsx | 51 + .benchmark/hr/frontend/app/招聘流程/page.tsx | 51 + .benchmark/hr/frontend/app/绩效评估/page.tsx | 51 + .benchmark/hr/frontend/app/考勤管理/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../hr/frontend/components/layout/Sidebar.tsx | 44 + .../hr/frontend/components/ui/Button.tsx | 31 + .benchmark/hr/frontend/components/ui/Card.tsx | 18 + .../hr/frontend/components/ui/EmptyState.tsx | 19 + .../hr/frontend/components/ui/Input.tsx | 22 + .../hr/frontend/components/ui/Modal.tsx | 33 + .benchmark/hr/frontend/hooks/useApprovals.ts | 35 + .benchmark/hr/frontend/hooks/useAttendance.ts | 35 + .benchmark/hr/frontend/hooks/useAuth.ts | 35 + .benchmark/hr/frontend/hooks/useDebounce.ts | 14 + .../hr/frontend/hooks/useDepartments.ts | 35 + .benchmark/hr/frontend/hooks/useEmployees.ts | 35 + .benchmark/hr/frontend/hooks/useForm.ts | 33 + .benchmark/hr/frontend/hooks/useItem.ts | 35 + .benchmark/hr/frontend/hooks/useItems.ts | 35 + .benchmark/hr/frontend/hooks/useNotices.ts | 35 + .benchmark/hr/frontend/hooks/useUsers.ts | 35 + .benchmark/hr/frontend/next-env.d.ts | 6 + .benchmark/hr/frontend/next.config.ts | 7 + .benchmark/hr/frontend/package.json | 25 + .benchmark/hr/frontend/postcss.config.js | 6 + .benchmark/hr/frontend/services/api.ts | 47 + .benchmark/hr/frontend/services/approvals.ts | 14 + .benchmark/hr/frontend/services/attendance.ts | 10 + .benchmark/hr/frontend/services/auth.ts | 10 + .../hr/frontend/services/departments.ts | 6 + .benchmark/hr/frontend/services/employees.ts | 10 + .benchmark/hr/frontend/services/items.ts | 18 + .benchmark/hr/frontend/services/notices.ts | 6 + .benchmark/hr/frontend/services/users.ts | 6 + .benchmark/hr/frontend/services/员工档案.ts | 10 + .benchmark/hr/frontend/services/招聘流程.ts | 10 + .benchmark/hr/frontend/services/绩效评估.ts | 10 + .benchmark/hr/frontend/services/考勤管理.ts | 10 + .benchmark/hr/frontend/tailwind.config.ts | 10 + .benchmark/hr/frontend/tsconfig.json | 40 + .benchmark/hr/frontend/types/index.ts | 131 + .benchmark/hr/fullstack/.gitignore | 8 + .benchmark/hr/fullstack/README.md | 125 + .benchmark/hr/fullstack/apps/api/.gitignore | 7 + .benchmark/hr/fullstack/apps/api/README.md | 118 + .benchmark/hr/fullstack/apps/api/package.json | 29 + .../api/src/__tests__/approval_steps.test.ts | 124 + .../apps/api/src/__tests__/approvals.test.ts | 121 + .../apps/api/src/__tests__/attendance.test.ts | 119 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../api/src/__tests__/departments.test.ts | 118 + .../apps/api/src/__tests__/users.test.ts | 118 + .../hr/fullstack/apps/api/src/db/client.ts | 93 + .../hr/fullstack/apps/api/src/db/schema.ts | 20 + .benchmark/hr/fullstack/apps/api/src/index.ts | 79 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../apps/api/src/routes/approval_steps.ts | 51 + .../apps/api/src/routes/approvals.ts | 51 + .../apps/api/src/routes/attendance.ts | 51 + .../hr/fullstack/apps/api/src/routes/auth.ts | 121 + .../apps/api/src/routes/departments.ts | 51 + .../hr/fullstack/apps/api/src/routes/users.ts | 51 + .../apps/api/src/services/approval.ts | 56 + .../apps/api/src/services/approval_step.ts | 58 + .../apps/api/src/services/attendance.ts | 54 + .../apps/api/src/services/department.ts | 55 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../hr/fullstack/apps/api/src/types/index.ts | 221 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../hr/fullstack/apps/api/tsconfig.json | 27 + .../fullstack/apps/web/app/approvals/page.tsx | 35 + .../apps/web/app/attendance/page.tsx | 20 + .../fullstack/apps/web/app/contacts/page.tsx | 17 + .../hr/fullstack/apps/web/app/globals.css | 8 + .../hr/fullstack/apps/web/app/layout.tsx | 30 + .../fullstack/apps/web/app/notices/page.tsx | 51 + .benchmark/hr/fullstack/apps/web/app/page.tsx | 50 + .../fullstack/apps/web/app/profile/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useApprovals.ts | 35 + .../fullstack/apps/web/hooks/useAttendance.ts | 35 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../apps/web/hooks/useDepartments.ts | 35 + .../hr/fullstack/apps/web/hooks/useForm.ts | 33 + .../hr/fullstack/apps/web/hooks/useNotices.ts | 35 + .../hr/fullstack/apps/web/hooks/useUsers.ts | 35 + .../hr/fullstack/apps/web/next.config.ts | 7 + .benchmark/hr/fullstack/apps/web/package.json | 27 + .../hr/fullstack/apps/web/postcss.config.js | 6 + .../hr/fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/approvals.ts | 14 + .../fullstack/apps/web/services/attendance.ts | 10 + .../apps/web/services/departments.ts | 6 + .../hr/fullstack/apps/web/services/notices.ts | 6 + .../hr/fullstack/apps/web/services/users.ts | 6 + .../hr/fullstack/apps/web/tailwind.config.ts | 10 + .../hr/fullstack/apps/web/tsconfig.json | 40 + .../hr/fullstack/apps/web/types/index.ts | 131 + .benchmark/hr/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 133 + .../packages/shared-types/tsconfig.json | 15 + .benchmark/hr/fullstack/scripts/build.mjs | 26 + .benchmark/hr/fullstack/scripts/dev.mjs | 32 + .benchmark/hr/prd.json | 1 + .benchmark/hr/release/release/README.md | 33 + .benchmark/hr/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/oaflow-0.1.0.AppImage | 3 + .../release/linux/oaflow_0.1.0_amd64.deb | 3 + .../release/macos/oaflow-0.1.0-arm64.dmg | 3 + .../hr/release/release/macos/oaflow-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 61 + .benchmark/hr/release/release/version.json | 10 + .../release/windows/oaflow-0.1.0-portable.exe | 3 + .../release/windows/oaflow-setup-0.1.0.exe | 3 + .benchmark/inventory/arch.json | 1 + .benchmark/inventory/backend/.gitignore | 7 + .benchmark/inventory/backend/README.md | 97 + .benchmark/inventory/backend/package.json | 27 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/inventory.test.ts | 124 + .../backend/src/__tests__/logistics.test.ts | 122 + .../backend/src/__tests__/order_items.test.ts | 120 + .../backend/src/__tests__/orders.test.ts | 119 + .../backend/src/__tests__/products.test.ts | 121 + .../src/__tests__/stock_alerts.test.ts | 119 + .../backend/src/__tests__/stocktaking.test.ts | 121 + .../backend/src/__tests__/suppliers.test.ts | 121 + .../backend/src/__tests__/users.test.ts | 118 + .benchmark/inventory/backend/src/db/client.ts | 93 + .benchmark/inventory/backend/src/db/schema.ts | 17 + .benchmark/inventory/backend/src/index.ts | 73 + .../inventory/backend/src/middleware/auth.ts | 41 + .../inventory/backend/src/routes/auth.ts | 121 + .../inventory/backend/src/routes/inventory.ts | 51 + .../inventory/backend/src/routes/logistics.ts | 51 + .../backend/src/routes/order_items.ts | 51 + .../inventory/backend/src/routes/orders.ts | 51 + .../inventory/backend/src/routes/products.ts | 51 + .../backend/src/routes/stock_alerts.ts | 51 + .../backend/src/routes/stocktaking.ts | 51 + .../inventory/backend/src/routes/suppliers.ts | 51 + .../inventory/backend/src/routes/users.ts | 51 + .../backend/src/services/inventory.ts | 59 + .../backend/src/services/logistic.ts | 58 + .../inventory/backend/src/services/order.ts | 54 + .../backend/src/services/order_item.ts | 56 + .../inventory/backend/src/services/product.ts | 56 + .../backend/src/services/stock_alert.ts | 54 + .../backend/src/services/stocktaking.ts | 56 + .../backend/src/services/supplier.ts | 56 + .../inventory/backend/src/services/user.ts | 56 + .../inventory/backend/src/types/fastify.d.ts | 8 + .../inventory/backend/src/types/index.ts | 216 + .../inventory/backend/src/types/sql.js.d.ts | 27 + .benchmark/inventory/backend/tsconfig.json | 27 + .benchmark/inventory/electron/README.md | 82 + .benchmark/inventory/electron/assets/icon.png | Bin 0 -> 66 bytes .../inventory/electron/electron-builder.yml | 100 + .benchmark/inventory/electron/electron/ipc.ts | 77 + .../inventory/electron/electron/main.ts | 238 + .../inventory/electron/electron/preload.ts | 92 + .../inventory/electron/electron/tray.ts | 61 + .../inventory/electron/electron/updater.ts | 87 + .../inventory/electron/electron/utils.ts | 80 + .../inventory/electron/entitlements.mac.plist | 18 + .benchmark/inventory/electron/package.json | 41 + .benchmark/inventory/electron/tsconfig.json | 32 + .../inventory/frontend/app/cart/page.tsx | 13 + .benchmark/inventory/frontend/app/globals.css | 8 + .../inventory/frontend/app/inventory/page.tsx | 51 + .benchmark/inventory/frontend/app/layout.tsx | 30 + .../frontend/app/order/[id]/page.tsx | 12 + .../inventory/frontend/app/orders/page.tsx | 12 + .benchmark/inventory/frontend/app/page.tsx | 50 + .../frontend/app/product/[id]/page.tsx | 18 + .../inventory/frontend/app/products/page.tsx | 18 + .../inventory/frontend/app/profile/page.tsx | 43 + .../frontend/app/stock_alerts/page.tsx | 51 + .../frontend/app/stocktaking/page.tsx | 51 + .../inventory/frontend/app/suppliers/page.tsx | 51 + .../frontend/app/供应商管理/page.tsx | 51 + .../frontend/app/商品入库出库/page.tsx | 51 + .../inventory/frontend/app/库存盘点/page.tsx | 51 + .../inventory/frontend/app/库存预警/page.tsx | 51 + .../components/ecommerce/ProductCard.tsx | 24 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/ui/Button.tsx | 31 + .../inventory/frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + .../inventory/frontend/hooks/useAuth.ts | 35 + .../inventory/frontend/hooks/useCoupons.ts | 35 + .../inventory/frontend/hooks/useDebounce.ts | 14 + .../inventory/frontend/hooks/useForm.ts | 33 + .../inventory/frontend/hooks/useInventory.ts | 35 + .../inventory/frontend/hooks/useItem.ts | 35 + .../inventory/frontend/hooks/useLogistics.ts | 35 + .../inventory/frontend/hooks/useOrders.ts | 35 + .../inventory/frontend/hooks/usePayments.ts | 35 + .../inventory/frontend/hooks/useProducts.ts | 35 + .../frontend/hooks/useStock_alerts.ts | 35 + .../frontend/hooks/useStocktaking.ts | 35 + .../inventory/frontend/hooks/useSuppliers.ts | 35 + .benchmark/inventory/frontend/next-env.d.ts | 6 + .benchmark/inventory/frontend/next.config.ts | 7 + .benchmark/inventory/frontend/package.json | 25 + .../inventory/frontend/postcss.config.js | 6 + .benchmark/inventory/frontend/services/api.ts | 47 + .../inventory/frontend/services/auth.ts | 10 + .../inventory/frontend/services/coupons.ts | 6 + .../inventory/frontend/services/inventory.ts | 18 + .../inventory/frontend/services/logistics.ts | 6 + .../inventory/frontend/services/orders.ts | 10 + .../inventory/frontend/services/payments.ts | 6 + .../inventory/frontend/services/products.ts | 10 + .../frontend/services/stock_alerts.ts | 10 + .../frontend/services/stocktaking.ts | 10 + .../inventory/frontend/services/suppliers.ts | 10 + .../inventory/frontend/services/供应商管理.ts | 10 + .../frontend/services/商品入库出库.ts | 10 + .../inventory/frontend/services/库存盘点.ts | 10 + .../inventory/frontend/services/库存预警.ts | 10 + .../inventory/frontend/tailwind.config.ts | 10 + .benchmark/inventory/frontend/tsconfig.json | 40 + .benchmark/inventory/frontend/types/index.ts | 90 + .benchmark/inventory/fullstack/.gitignore | 8 + .benchmark/inventory/fullstack/README.md | 104 + .../inventory/fullstack/apps/api/.gitignore | 7 + .../inventory/fullstack/apps/api/README.md | 97 + .../inventory/fullstack/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/logistics.test.ts | 122 + .../api/src/__tests__/order_items.test.ts | 120 + .../apps/api/src/__tests__/orders.test.ts | 119 + .../apps/api/src/__tests__/products.test.ts | 121 + .../apps/api/src/__tests__/users.test.ts | 118 + .../fullstack/apps/api/src/db/client.ts | 93 + .../fullstack/apps/api/src/db/schema.ts | 17 + .../inventory/fullstack/apps/api/src/index.ts | 73 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../fullstack/apps/api/src/routes/auth.ts | 121 + .../apps/api/src/routes/logistics.ts | 51 + .../apps/api/src/routes/order_items.ts | 51 + .../fullstack/apps/api/src/routes/orders.ts | 51 + .../fullstack/apps/api/src/routes/products.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../apps/api/src/services/logistic.ts | 58 + .../fullstack/apps/api/src/services/order.ts | 54 + .../apps/api/src/services/order_item.ts | 56 + .../apps/api/src/services/product.ts | 56 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../fullstack/apps/api/src/types/index.ts | 153 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../fullstack/apps/api/tsconfig.json | 27 + .../fullstack/apps/web/app/cart/page.tsx | 13 + .../fullstack/apps/web/app/globals.css | 8 + .../fullstack/apps/web/app/layout.tsx | 30 + .../apps/web/app/order/[id]/page.tsx | 12 + .../fullstack/apps/web/app/orders/page.tsx | 12 + .../inventory/fullstack/apps/web/app/page.tsx | 50 + .../apps/web/app/product/[id]/page.tsx | 18 + .../fullstack/apps/web/app/profile/page.tsx | 43 + .../web/components/ecommerce/ProductCard.tsx | 24 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useCoupons.ts | 35 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../fullstack/apps/web/hooks/useForm.ts | 33 + .../fullstack/apps/web/hooks/useLogistics.ts | 35 + .../fullstack/apps/web/hooks/useOrders.ts | 35 + .../fullstack/apps/web/hooks/usePayments.ts | 35 + .../fullstack/apps/web/hooks/useProducts.ts | 35 + .../fullstack/apps/web/next.config.ts | 7 + .../inventory/fullstack/apps/web/package.json | 27 + .../fullstack/apps/web/postcss.config.js | 6 + .../fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/coupons.ts | 6 + .../fullstack/apps/web/services/logistics.ts | 6 + .../fullstack/apps/web/services/orders.ts | 10 + .../fullstack/apps/web/services/payments.ts | 6 + .../fullstack/apps/web/services/products.ts | 10 + .../fullstack/apps/web/tailwind.config.ts | 10 + .../fullstack/apps/web/tsconfig.json | 40 + .../fullstack/apps/web/types/index.ts | 90 + .benchmark/inventory/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 92 + .../packages/shared-types/tsconfig.json | 15 + .../inventory/fullstack/scripts/build.mjs | 26 + .../inventory/fullstack/scripts/dev.mjs | 32 + .benchmark/inventory/prd.json | 1 + .../inventory/release/release/README.md | 33 + .../inventory/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/shopapp-0.1.0.AppImage | 3 + .../release/linux/shopapp_0.1.0_amd64.deb | 3 + .../release/macos/shopapp-0.1.0-arm64.dmg | 3 + .../release/release/macos/shopapp-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 59 + .../inventory/release/release/version.json | 10 + .../windows/shopapp-0.1.0-portable.exe | 3 + .../release/windows/shopapp-setup-0.1.0.exe | 3 + .benchmark/petcare-e2e/arch.json | 1 + .benchmark/petcare-e2e/backend/.gitignore | 7 + .benchmark/petcare-e2e/backend/README.md | 90 + .benchmark/petcare-e2e/backend/package.json | 27 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/daily_logs.test.ts | 120 + .../backend/src/__tests__/pets.test.ts | 126 + .../backend/src/__tests__/schedules.test.ts | 122 + .../backend/src/__tests__/users.test.ts | 118 + .../petcare-e2e/backend/src/db/client.ts | 93 + .../petcare-e2e/backend/src/db/schema.ts | 16 + .benchmark/petcare-e2e/backend/src/index.ts | 67 + .../backend/src/middleware/auth.ts | 41 + .../petcare-e2e/backend/src/routes/auth.ts | 121 + .../backend/src/routes/daily_logs.ts | 51 + .../petcare-e2e/backend/src/routes/pets.ts | 51 + .../backend/src/routes/schedules.ts | 51 + .../petcare-e2e/backend/src/routes/users.ts | 51 + .../backend/src/services/daily_log.ts | 56 + .../petcare-e2e/backend/src/services/pet.ts | 59 + .../backend/src/services/schedule.ts | 57 + .../petcare-e2e/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + .../petcare-e2e/backend/src/types/index.ts | 147 + .../petcare-e2e/backend/src/types/sql.js.d.ts | 27 + .benchmark/petcare-e2e/backend/tsconfig.json | 27 + .benchmark/petcare-e2e/electron/README.md | 82 + .../petcare-e2e/electron/assets/icon.png | Bin 0 -> 66 bytes .../petcare-e2e/electron/electron-builder.yml | 100 + .../petcare-e2e/electron/electron/ipc.ts | 77 + .../petcare-e2e/electron/electron/main.ts | 238 + .../petcare-e2e/electron/electron/preload.ts | 92 + .../petcare-e2e/electron/electron/tray.ts | 61 + .../petcare-e2e/electron/electron/updater.ts | 87 + .../petcare-e2e/electron/electron/utils.ts | 80 + .../electron/entitlements.mac.plist | 18 + .benchmark/petcare-e2e/electron/package.json | 41 + .benchmark/petcare-e2e/electron/tsconfig.json | 32 + .../petcare-e2e/frontend/app/album/page.tsx | 39 + .../frontend/app/daily-log/page.tsx | 79 + .../petcare-e2e/frontend/app/globals.css | 8 + .../frontend/app/hospitals/page.tsx | 52 + .../petcare-e2e/frontend/app/layout.tsx | 30 + .benchmark/petcare-e2e/frontend/app/page.tsx | 50 + .../frontend/app/pet/[id]/page.tsx | 79 + .../petcare-e2e/frontend/app/profile/page.tsx | 43 + .../frontend/app/schedule/page.tsx | 69 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/pet/PetCard.tsx | 28 + .../frontend/components/pet/ScheduleCard.tsx | 34 + .../frontend/components/ui/Button.tsx | 31 + .../frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + .../petcare-e2e/frontend/hooks/useAlbums.ts | 35 + .../frontend/hooks/useDailylogs.ts | 35 + .../petcare-e2e/frontend/hooks/useDebounce.ts | 14 + .../petcare-e2e/frontend/hooks/useForm.ts | 33 + .../frontend/hooks/useHospitals.ts | 35 + .../petcare-e2e/frontend/hooks/usePets.ts | 35 + .../frontend/hooks/useSchedules.ts | 35 + .../petcare-e2e/frontend/next.config.ts | 7 + .benchmark/petcare-e2e/frontend/package.json | 25 + .../petcare-e2e/frontend/postcss.config.js | 6 + .../petcare-e2e/frontend/services/albums.ts | 6 + .../petcare-e2e/frontend/services/api.ts | 47 + .../frontend/services/daily-logs.ts | 10 + .../frontend/services/hospitals.ts | 6 + .../petcare-e2e/frontend/services/pets.ts | 14 + .../frontend/services/schedules.ts | 10 + .../petcare-e2e/frontend/tailwind.config.ts | 10 + .benchmark/petcare-e2e/frontend/tsconfig.json | 40 + .../petcare-e2e/frontend/types/index.ts | 68 + .benchmark/petcare-e2e/prd.json | 1 + .../petcare-e2e/release/release/README.md | 33 + .../release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/petcare-0.1.0.AppImage | 3 + .../release/linux/petcare_0.1.0_amd64.deb | 3 + .../release/macos/petcare-0.1.0-arm64.dmg | 3 + .../release/release/macos/petcare-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 54 + .../petcare-e2e/release/release/version.json | 10 + .../windows/petcare-0.1.0-portable.exe | 3 + .../release/windows/petcare-setup-0.1.0.exe | 3 + .benchmark/petcare/arch.json | 1 + .benchmark/petcare/backend/.gitignore | 7 + .benchmark/petcare/backend/README.md | 111 + .benchmark/petcare/backend/package.json | 27 + .../backend/src/__tests__/albums.test.ts | 120 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/daily-logs.test.ts | 118 + .../backend/src/__tests__/daily_logs.test.ts | 120 + .../backend/src/__tests__/hospitals.test.ts | 122 + .../backend/src/__tests__/items.test.ts | 119 + .../backend/src/__tests__/pets.test.ts | 122 + .../backend/src/__tests__/records.test.ts | 119 + .../backend/src/__tests__/schedules.test.ts | 122 + .../backend/src/__tests__/users.test.ts | 118 + .benchmark/petcare/backend/src/db/client.ts | 93 + .benchmark/petcare/backend/src/db/schema.ts | 19 + .benchmark/petcare/backend/src/index.ts | 77 + .../petcare/backend/src/middleware/auth.ts | 41 + .../petcare/backend/src/routes/albums.ts | 51 + .benchmark/petcare/backend/src/routes/auth.ts | 121 + .../petcare/backend/src/routes/daily-logs.ts | 51 + .../petcare/backend/src/routes/daily_logs.ts | 51 + .../petcare/backend/src/routes/hospitals.ts | 51 + .../petcare/backend/src/routes/items.ts | 51 + .benchmark/petcare/backend/src/routes/pets.ts | 51 + .../petcare/backend/src/routes/records.ts | 51 + .../petcare/backend/src/routes/schedules.ts | 51 + .../petcare/backend/src/routes/users.ts | 51 + .../petcare/backend/src/services/album.ts | 55 + .../petcare/backend/src/services/daily-log.ts | 53 + .../petcare/backend/src/services/daily_log.ts | 56 + .../petcare/backend/src/services/hospital.ts | 57 + .../petcare/backend/src/services/item.ts | 54 + .../petcare/backend/src/services/pet.ts | 57 + .../petcare/backend/src/services/record.ts | 54 + .../petcare/backend/src/services/schedule.ts | 57 + .../petcare/backend/src/services/user.ts | 56 + .../petcare/backend/src/types/fastify.d.ts | 8 + .benchmark/petcare/backend/src/types/index.ts | 273 + .../petcare/backend/src/types/sql.js.d.ts | 27 + .benchmark/petcare/backend/tsconfig.json | 27 + .benchmark/petcare/electron/README.md | 82 + .benchmark/petcare/electron/assets/icon.png | Bin 0 -> 66 bytes .../petcare/electron/electron-builder.yml | 100 + .benchmark/petcare/electron/electron/ipc.ts | 77 + .benchmark/petcare/electron/electron/main.ts | 238 + .../petcare/electron/electron/preload.ts | 92 + .benchmark/petcare/electron/electron/tray.ts | 61 + .../petcare/electron/electron/updater.ts | 87 + .benchmark/petcare/electron/electron/utils.ts | 80 + .../petcare/electron/entitlements.mac.plist | 18 + .benchmark/petcare/electron/package.json | 41 + .benchmark/petcare/electron/tsconfig.json | 32 + .../petcare/frontend/app/album/page.tsx | 39 + .../petcare/frontend/app/albums/page.tsx | 39 + .../petcare/frontend/app/daily-log/page.tsx | 79 + .benchmark/petcare/frontend/app/globals.css | 8 + .../petcare/frontend/app/hospitals/page.tsx | 52 + .../petcare/frontend/app/items/page.tsx | 29 + .benchmark/petcare/frontend/app/layout.tsx | 30 + .benchmark/petcare/frontend/app/page.tsx | 50 + .../petcare/frontend/app/pet/[id]/page.tsx | 79 + .benchmark/petcare/frontend/app/pets/page.tsx | 79 + .../petcare/frontend/app/profile/page.tsx | 43 + .../petcare/frontend/app/records/page.tsx | 29 + .../petcare/frontend/app/schedule/page.tsx | 69 + .../petcare/frontend/app/schedules/page.tsx | 69 + .../petcare/frontend/app/健康日程/page.tsx | 29 + .../petcare/frontend/app/健康百科/page.tsx | 29 + .../petcare/frontend/app/宠物档案/page.tsx | 29 + .../petcare/frontend/app/成长相册/page.tsx | 29 + .../petcare/frontend/app/日常记录/page.tsx | 29 + .../petcare/frontend/app/社区分享/page.tsx | 29 + .../petcare/frontend/app/附近医院/page.tsx | 29 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/pet/PetCard.tsx | 28 + .../frontend/components/pet/ScheduleCard.tsx | 34 + .../petcare/frontend/components/ui/Button.tsx | 31 + .../petcare/frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../petcare/frontend/components/ui/Input.tsx | 22 + .../petcare/frontend/components/ui/Modal.tsx | 33 + .../petcare/frontend/hooks/useAlbums.ts | 35 + .../petcare/frontend/hooks/useDailylogs.ts | 35 + .../petcare/frontend/hooks/useDebounce.ts | 14 + .benchmark/petcare/frontend/hooks/useForm.ts | 33 + .../petcare/frontend/hooks/useHospitals.ts | 35 + .benchmark/petcare/frontend/hooks/usePets.ts | 35 + .../petcare/frontend/hooks/useSchedules.ts | 35 + .benchmark/petcare/frontend/next-env.d.ts | 6 + .benchmark/petcare/frontend/next.config.ts | 7 + .benchmark/petcare/frontend/package.json | 25 + .benchmark/petcare/frontend/postcss.config.js | 6 + .../petcare/frontend/services/albums.ts | 6 + .benchmark/petcare/frontend/services/api.ts | 47 + .../petcare/frontend/services/daily-logs.ts | 10 + .../petcare/frontend/services/hospitals.ts | 6 + .benchmark/petcare/frontend/services/pets.ts | 14 + .../petcare/frontend/services/schedules.ts | 10 + .../petcare/frontend/tailwind.config.ts | 10 + .benchmark/petcare/frontend/tsconfig.json | 40 + .benchmark/petcare/frontend/types/index.ts | 113 + .benchmark/petcare/fullstack/.gitignore | 8 + .benchmark/petcare/fullstack/README.md | 118 + .../petcare/fullstack/apps/api/.gitignore | 7 + .../petcare/fullstack/apps/api/README.md | 111 + .../petcare/fullstack/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/daily_logs.test.ts | 120 + .../apps/api/src/__tests__/pets.test.ts | 122 + .../apps/api/src/__tests__/schedules.test.ts | 122 + .../apps/api/src/__tests__/users.test.ts | 118 + .../fullstack/apps/api/src/db/client.ts | 93 + .../fullstack/apps/api/src/db/schema.ts | 19 + .../petcare/fullstack/apps/api/src/index.ts | 77 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../fullstack/apps/api/src/routes/auth.ts | 121 + .../apps/api/src/routes/daily_logs.ts | 51 + .../fullstack/apps/api/src/routes/pets.ts | 51 + .../apps/api/src/routes/schedules.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../apps/api/src/services/daily_log.ts | 56 + .../fullstack/apps/api/src/services/pet.ts | 57 + .../apps/api/src/services/schedule.ts | 57 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../fullstack/apps/api/src/types/index.ts | 191 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../petcare/fullstack/apps/api/tsconfig.json | 27 + .../fullstack/apps/web/app/album/page.tsx | 39 + .../fullstack/apps/web/app/daily-log/page.tsx | 79 + .../fullstack/apps/web/app/globals.css | 8 + .../fullstack/apps/web/app/hospitals/page.tsx | 52 + .../petcare/fullstack/apps/web/app/layout.tsx | 30 + .../petcare/fullstack/apps/web/app/page.tsx | 50 + .../fullstack/apps/web/app/pet/[id]/page.tsx | 79 + .../fullstack/apps/web/app/profile/page.tsx | 43 + .../fullstack/apps/web/app/schedule/page.tsx | 69 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/pet/PetCard.tsx | 28 + .../apps/web/components/pet/ScheduleCard.tsx | 34 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useAlbums.ts | 35 + .../fullstack/apps/web/hooks/useDailylogs.ts | 35 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../fullstack/apps/web/hooks/useForm.ts | 33 + .../fullstack/apps/web/hooks/useHospitals.ts | 35 + .../fullstack/apps/web/hooks/usePets.ts | 35 + .../fullstack/apps/web/hooks/useSchedules.ts | 35 + .../petcare/fullstack/apps/web/next.config.ts | 7 + .../petcare/fullstack/apps/web/package.json | 27 + .../fullstack/apps/web/postcss.config.js | 6 + .../fullstack/apps/web/services/albums.ts | 6 + .../fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/daily-logs.ts | 10 + .../fullstack/apps/web/services/hospitals.ts | 6 + .../fullstack/apps/web/services/pets.ts | 14 + .../fullstack/apps/web/services/schedules.ts | 10 + .../fullstack/apps/web/tailwind.config.ts | 10 + .../petcare/fullstack/apps/web/tsconfig.json | 40 + .../petcare/fullstack/apps/web/types/index.ts | 113 + .benchmark/petcare/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 115 + .../packages/shared-types/tsconfig.json | 15 + .../petcare/fullstack/scripts/build.mjs | 26 + .benchmark/petcare/fullstack/scripts/dev.mjs | 32 + .benchmark/petcare/prd.json | 1 + .benchmark/petcare/release/release/README.md | 33 + .../petcare/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/petcare-0.1.0.AppImage | 3 + .../release/linux/petcare_0.1.0_amd64.deb | 3 + .../release/macos/petcare-0.1.0-arm64.dmg | 3 + .../release/release/macos/petcare-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 59 + .../petcare/release/release/version.json | 10 + .../windows/petcare-0.1.0-portable.exe | 3 + .../release/windows/petcare-setup-0.1.0.exe | 3 + .benchmark/project-mgmt/arch.json | 1 + .benchmark/project-mgmt/backend/.gitignore | 7 + .benchmark/project-mgmt/backend/README.md | 111 + .benchmark/project-mgmt/backend/package.json | 27 + .../backend/src/__tests__/approvals.test.ts | 121 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/boards.test.ts | 119 + .../backend/src/__tests__/contracts.test.ts | 124 + .../backend/src/__tests__/customers.test.ts | 122 + .../backend/src/__tests__/items.test.ts | 119 + .../backend/src/__tests__/reminders.test.ts | 121 + .../backend/src/__tests__/tasks.test.ts | 121 + .../backend/src/__tests__/users.test.ts | 118 + .../project-mgmt/backend/src/db/client.ts | 93 + .../project-mgmt/backend/src/db/schema.ts | 19 + .benchmark/project-mgmt/backend/src/index.ts | 77 + .../backend/src/middleware/auth.ts | 41 + .../backend/src/routes/approvals.ts | 51 + .../project-mgmt/backend/src/routes/auth.ts | 121 + .../project-mgmt/backend/src/routes/boards.ts | 51 + .../backend/src/routes/contracts.ts | 51 + .../backend/src/routes/customers.ts | 51 + .../project-mgmt/backend/src/routes/items.ts | 51 + .../backend/src/routes/reminders.ts | 51 + .../project-mgmt/backend/src/routes/tasks.ts | 51 + .../project-mgmt/backend/src/routes/users.ts | 51 + .../backend/src/services/approval.ts | 56 + .../backend/src/services/board.ts | 54 + .../backend/src/services/contract.ts | 59 + .../backend/src/services/customer.ts | 57 + .../project-mgmt/backend/src/services/item.ts | 54 + .../backend/src/services/reminder.ts | 56 + .../project-mgmt/backend/src/services/task.ts | 56 + .../project-mgmt/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + .../project-mgmt/backend/src/types/index.ts | 289 + .../backend/src/types/sql.js.d.ts | 27 + .benchmark/project-mgmt/backend/tsconfig.json | 27 + .benchmark/project-mgmt/electron/README.md | 82 + .../project-mgmt/electron/assets/icon.png | Bin 0 -> 66 bytes .../electron/electron-builder.yml | 100 + .../project-mgmt/electron/electron/ipc.ts | 77 + .../project-mgmt/electron/electron/main.ts | 238 + .../project-mgmt/electron/electron/preload.ts | 92 + .../project-mgmt/electron/electron/tray.ts | 61 + .../project-mgmt/electron/electron/updater.ts | 87 + .../project-mgmt/electron/electron/utils.ts | 80 + .../electron/entitlements.mac.plist | 18 + .benchmark/project-mgmt/electron/package.json | 41 + .../project-mgmt/electron/tsconfig.json | 32 + .../project-mgmt/frontend/app/boards/page.tsx | 51 + .../frontend/app/feature/page.tsx | 29 + .../project-mgmt/frontend/app/globals.css | 8 + .../project-mgmt/frontend/app/items/page.tsx | 51 + .../project-mgmt/frontend/app/layout.tsx | 30 + .benchmark/project-mgmt/frontend/app/page.tsx | 50 + .../frontend/app/profile/page.tsx | 29 + .../project-mgmt/frontend/app/tasks/page.tsx | 51 + .../frontend/app/任务分配/page.tsx | 51 + .../frontend/app/团队协作/page.tsx | 51 + .../project-mgmt/frontend/app/甘特图/page.tsx | 51 + .../frontend/app/项目看板/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/ui/Button.tsx | 31 + .../frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + .../project-mgmt/frontend/hooks/useAuth.ts | 35 + .../project-mgmt/frontend/hooks/useBoards.ts | 35 + .../frontend/hooks/useDebounce.ts | 14 + .../project-mgmt/frontend/hooks/useForm.ts | 33 + .../project-mgmt/frontend/hooks/useHealth.ts | 35 + .../project-mgmt/frontend/hooks/useItem.ts | 35 + .../project-mgmt/frontend/hooks/useItems.ts | 35 + .../project-mgmt/frontend/hooks/useTasks.ts | 35 + .../project-mgmt/frontend/hooks/useUsers.ts | 35 + .../project-mgmt/frontend/next-env.d.ts | 6 + .../project-mgmt/frontend/next.config.ts | 7 + .benchmark/project-mgmt/frontend/package.json | 25 + .../project-mgmt/frontend/postcss.config.js | 6 + .../project-mgmt/frontend/services/api.ts | 47 + .../project-mgmt/frontend/services/auth.ts | 10 + .../project-mgmt/frontend/services/boards.ts | 10 + .../project-mgmt/frontend/services/health.ts | 6 + .../project-mgmt/frontend/services/items.ts | 18 + .../project-mgmt/frontend/services/tasks.ts | 10 + .../project-mgmt/frontend/services/users.ts | 10 + .../frontend/services/任务分配.ts | 10 + .../frontend/services/团队协作.ts | 10 + .../project-mgmt/frontend/services/甘特图.ts | 10 + .../frontend/services/项目看板.ts | 10 + .../project-mgmt/frontend/tailwind.config.ts | 10 + .../project-mgmt/frontend/tsconfig.json | 40 + .../project-mgmt/frontend/types/index.ts | 119 + .benchmark/project-mgmt/fullstack/.gitignore | 8 + .benchmark/project-mgmt/fullstack/README.md | 118 + .../fullstack/apps/api/.gitignore | 7 + .../project-mgmt/fullstack/apps/api/README.md | 111 + .../fullstack/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/items.test.ts | 119 + .../apps/api/src/__tests__/users.test.ts | 118 + .../fullstack/apps/api/src/db/client.ts | 93 + .../fullstack/apps/api/src/db/schema.ts | 19 + .../fullstack/apps/api/src/index.ts | 77 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../fullstack/apps/api/src/routes/auth.ts | 121 + .../fullstack/apps/api/src/routes/items.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../fullstack/apps/api/src/services/item.ts | 54 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../fullstack/apps/api/src/types/index.ts | 201 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../fullstack/apps/api/tsconfig.json | 27 + .../fullstack/apps/web/app/feature/page.tsx | 29 + .../fullstack/apps/web/app/globals.css | 8 + .../fullstack/apps/web/app/layout.tsx | 30 + .../fullstack/apps/web/app/page.tsx | 50 + .../fullstack/apps/web/app/profile/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../fullstack/apps/web/hooks/useForm.ts | 33 + .../fullstack/apps/web/hooks/useHealth.ts | 35 + .../fullstack/apps/web/hooks/useUsers.ts | 35 + .../fullstack/apps/web/next.config.ts | 7 + .../fullstack/apps/web/package.json | 27 + .../fullstack/apps/web/postcss.config.js | 6 + .../fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/health.ts | 6 + .../fullstack/apps/web/services/users.ts | 10 + .../fullstack/apps/web/tailwind.config.ts | 10 + .../fullstack/apps/web/tsconfig.json | 40 + .../fullstack/apps/web/types/index.ts | 119 + .../project-mgmt/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 121 + .../packages/shared-types/tsconfig.json | 15 + .../project-mgmt/fullstack/scripts/build.mjs | 26 + .../project-mgmt/fullstack/scripts/dev.mjs | 32 + .benchmark/project-mgmt/prd.json | 1 + .../project-mgmt/release/release/README.md | 33 + .../release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/myproject-0.1.0.AppImage | 3 + .../release/linux/myproject_0.1.0_amd64.deb | 3 + .../release/macos/myproject-0.1.0-arm64.dmg | 3 + .../release/release/macos/myproject-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 58 + .../project-mgmt/release/release/version.json | 10 + .../windows/myproject-0.1.0-portable.exe | 3 + .../release/windows/myproject-setup-0.1.0.exe | 3 + .benchmark/ticket/arch.json | 1 + .benchmark/ticket/backend/.gitignore | 7 + .benchmark/ticket/backend/README.md | 104 + .benchmark/ticket/backend/package.json | 27 + .../src/__tests__/approval_steps.test.ts | 124 + .../backend/src/__tests__/approvals.test.ts | 121 + .../backend/src/__tests__/attendance.test.ts | 124 + .../ticket/backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/contracts.test.ts | 124 + .../backend/src/__tests__/customers.test.ts | 122 + .../backend/src/__tests__/departments.test.ts | 118 + .../backend/src/__tests__/items.test.ts | 119 + .../backend/src/__tests__/reminders.test.ts | 121 + .../backend/src/__tests__/tickets.test.ts | 120 + .../backend/src/__tests__/users.test.ts | 118 + .benchmark/ticket/backend/src/db/client.ts | 93 + .benchmark/ticket/backend/src/db/schema.ts | 18 + .benchmark/ticket/backend/src/index.ts | 75 + .../ticket/backend/src/middleware/auth.ts | 41 + .../backend/src/routes/approval_steps.ts | 51 + .../ticket/backend/src/routes/approvals.ts | 51 + .../ticket/backend/src/routes/attendance.ts | 51 + .benchmark/ticket/backend/src/routes/auth.ts | 121 + .../ticket/backend/src/routes/contracts.ts | 51 + .../ticket/backend/src/routes/customers.ts | 51 + .../ticket/backend/src/routes/departments.ts | 51 + .benchmark/ticket/backend/src/routes/items.ts | 51 + .../ticket/backend/src/routes/reminders.ts | 51 + .../ticket/backend/src/routes/tickets.ts | 51 + .benchmark/ticket/backend/src/routes/users.ts | 51 + .../ticket/backend/src/services/approval.ts | 56 + .../backend/src/services/approval_step.ts | 58 + .../ticket/backend/src/services/attendance.ts | 57 + .../ticket/backend/src/services/contract.ts | 59 + .../ticket/backend/src/services/customer.ts | 57 + .../ticket/backend/src/services/department.ts | 55 + .../ticket/backend/src/services/item.ts | 54 + .../ticket/backend/src/services/reminder.ts | 56 + .../ticket/backend/src/services/ticket.ts | 55 + .../ticket/backend/src/services/user.ts | 56 + .../ticket/backend/src/types/fastify.d.ts | 8 + .benchmark/ticket/backend/src/types/index.ts | 262 + .../ticket/backend/src/types/sql.js.d.ts | 27 + .benchmark/ticket/backend/tsconfig.json | 27 + .benchmark/ticket/electron/README.md | 82 + .benchmark/ticket/electron/assets/icon.png | Bin 0 -> 66 bytes .../ticket/electron/electron-builder.yml | 100 + .benchmark/ticket/electron/electron/ipc.ts | 77 + .benchmark/ticket/electron/electron/main.ts | 238 + .../ticket/electron/electron/preload.ts | 92 + .benchmark/ticket/electron/electron/tray.ts | 61 + .../ticket/electron/electron/updater.ts | 87 + .benchmark/ticket/electron/electron/utils.ts | 80 + .../ticket/electron/entitlements.mac.plist | 18 + .benchmark/ticket/electron/package.json | 41 + .benchmark/ticket/electron/tsconfig.json | 32 + .../ticket/frontend/app/approvals/page.tsx | 35 + .../ticket/frontend/app/attendance/page.tsx | 20 + .../ticket/frontend/app/contacts/page.tsx | 17 + .benchmark/ticket/frontend/app/globals.css | 8 + .benchmark/ticket/frontend/app/items/page.tsx | 51 + .benchmark/ticket/frontend/app/layout.tsx | 30 + .../ticket/frontend/app/notices/page.tsx | 51 + .benchmark/ticket/frontend/app/page.tsx | 50 + .../ticket/frontend/app/profile/page.tsx | 29 + .../ticket/frontend/app/tickets/page.tsx | 51 + .../ticket/frontend/app/优先级管理/page.tsx | 51 + .benchmark/ticket/frontend/app/分配/page.tsx | 51 + .../ticket/frontend/app/处理流程/page.tsx | 51 + .../ticket/frontend/app/工单创建/page.tsx | 51 + .../ticket/frontend/app/工单归档/page.tsx | 29 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../ticket/frontend/components/ui/Button.tsx | 31 + .../ticket/frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../ticket/frontend/components/ui/Input.tsx | 22 + .../ticket/frontend/components/ui/Modal.tsx | 33 + .../ticket/frontend/hooks/useApprovals.ts | 35 + .../ticket/frontend/hooks/useAttendance.ts | 35 + .benchmark/ticket/frontend/hooks/useAuth.ts | 35 + .../ticket/frontend/hooks/useDebounce.ts | 14 + .../ticket/frontend/hooks/useDepartments.ts | 35 + .benchmark/ticket/frontend/hooks/useForm.ts | 33 + .benchmark/ticket/frontend/hooks/useItem.ts | 35 + .benchmark/ticket/frontend/hooks/useItems.ts | 35 + .../ticket/frontend/hooks/useNotices.ts | 35 + .../ticket/frontend/hooks/useTickets.ts | 35 + .benchmark/ticket/frontend/hooks/useUsers.ts | 35 + .benchmark/ticket/frontend/next-env.d.ts | 6 + .benchmark/ticket/frontend/next.config.ts | 7 + .benchmark/ticket/frontend/package.json | 25 + .benchmark/ticket/frontend/postcss.config.js | 6 + .benchmark/ticket/frontend/services/api.ts | 47 + .../ticket/frontend/services/approvals.ts | 14 + .../ticket/frontend/services/attendance.ts | 10 + .benchmark/ticket/frontend/services/auth.ts | 10 + .../ticket/frontend/services/departments.ts | 6 + .benchmark/ticket/frontend/services/items.ts | 26 + .../ticket/frontend/services/notices.ts | 6 + .../ticket/frontend/services/tickets.ts | 10 + .benchmark/ticket/frontend/services/users.ts | 6 + .../ticket/frontend/services/优先级管理.ts | 10 + .benchmark/ticket/frontend/services/分配.ts | 10 + .../ticket/frontend/services/处理流程.ts | 10 + .../ticket/frontend/services/工单创建.ts | 10 + .benchmark/ticket/frontend/tailwind.config.ts | 10 + .benchmark/ticket/frontend/tsconfig.json | 40 + .benchmark/ticket/frontend/types/index.ts | 108 + .benchmark/ticket/fullstack/.gitignore | 8 + .benchmark/ticket/fullstack/README.md | 111 + .../ticket/fullstack/apps/api/.gitignore | 7 + .../ticket/fullstack/apps/api/README.md | 104 + .../ticket/fullstack/apps/api/package.json | 29 + .../api/src/__tests__/approval_steps.test.ts | 124 + .../apps/api/src/__tests__/approvals.test.ts | 121 + .../apps/api/src/__tests__/attendance.test.ts | 124 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../api/src/__tests__/departments.test.ts | 118 + .../apps/api/src/__tests__/users.test.ts | 118 + .../fullstack/apps/api/src/db/client.ts | 93 + .../fullstack/apps/api/src/db/schema.ts | 18 + .../ticket/fullstack/apps/api/src/index.ts | 75 + .../fullstack/apps/api/src/middleware/auth.ts | 41 + .../apps/api/src/routes/approval_steps.ts | 51 + .../apps/api/src/routes/approvals.ts | 51 + .../apps/api/src/routes/attendance.ts | 51 + .../fullstack/apps/api/src/routes/auth.ts | 121 + .../apps/api/src/routes/departments.ts | 51 + .../fullstack/apps/api/src/routes/users.ts | 51 + .../apps/api/src/services/approval.ts | 56 + .../apps/api/src/services/approval_step.ts | 58 + .../apps/api/src/services/attendance.ts | 57 + .../apps/api/src/services/department.ts | 55 + .../fullstack/apps/api/src/services/user.ts | 56 + .../fullstack/apps/api/src/types/fastify.d.ts | 8 + .../fullstack/apps/api/src/types/index.ts | 183 + .../fullstack/apps/api/src/types/sql.js.d.ts | 27 + .../ticket/fullstack/apps/api/tsconfig.json | 27 + .../fullstack/apps/web/app/approvals/page.tsx | 35 + .../apps/web/app/attendance/page.tsx | 20 + .../fullstack/apps/web/app/contacts/page.tsx | 17 + .../ticket/fullstack/apps/web/app/globals.css | 8 + .../ticket/fullstack/apps/web/app/layout.tsx | 30 + .../fullstack/apps/web/app/notices/page.tsx | 51 + .../ticket/fullstack/apps/web/app/page.tsx | 50 + .../fullstack/apps/web/app/profile/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../fullstack/apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../fullstack/apps/web/hooks/useApprovals.ts | 35 + .../fullstack/apps/web/hooks/useAttendance.ts | 35 + .../fullstack/apps/web/hooks/useDebounce.ts | 14 + .../apps/web/hooks/useDepartments.ts | 35 + .../fullstack/apps/web/hooks/useForm.ts | 33 + .../fullstack/apps/web/hooks/useNotices.ts | 35 + .../fullstack/apps/web/hooks/useUsers.ts | 35 + .../ticket/fullstack/apps/web/next.config.ts | 7 + .../ticket/fullstack/apps/web/package.json | 27 + .../fullstack/apps/web/postcss.config.js | 6 + .../ticket/fullstack/apps/web/services/api.ts | 49 + .../fullstack/apps/web/services/approvals.ts | 14 + .../fullstack/apps/web/services/attendance.ts | 10 + .../apps/web/services/departments.ts | 6 + .../fullstack/apps/web/services/notices.ts | 6 + .../fullstack/apps/web/services/users.ts | 6 + .../fullstack/apps/web/tailwind.config.ts | 10 + .../ticket/fullstack/apps/web/tsconfig.json | 40 + .../ticket/fullstack/apps/web/types/index.ts | 108 + .benchmark/ticket/fullstack/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 110 + .../packages/shared-types/tsconfig.json | 15 + .benchmark/ticket/fullstack/scripts/build.mjs | 26 + .benchmark/ticket/fullstack/scripts/dev.mjs | 32 + .benchmark/ticket/prd.json | 1 + .benchmark/ticket/release/release/README.md | 33 + .../ticket/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/oaflow-0.1.0.AppImage | 3 + .../release/linux/oaflow_0.1.0_amd64.deb | 3 + .../release/macos/oaflow-0.1.0-arm64.dmg | 3 + .../release/release/macos/oaflow-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 60 + .../ticket/release/release/version.json | 10 + .../release/windows/oaflow-0.1.0-portable.exe | 3 + .../release/windows/oaflow-setup-0.1.0.exe | 3 + .github/workflows/release-candidate.yml | 182 + .gitignore | 36 + .tmp-pr8-test/b01.jsonl | 2 + .tmp-pr8-test/b02.jsonl | 1 + .tmp-pr8-test/b03-thresholds.json | 1 + .tmp-pr8-test/b03.jsonl | 1 + .tmp-pr8-test/b04.jsonl | 1 + .tmp-pr8-test/b04b-thresholds.json | 1 + .tmp-pr8-test/b04b.jsonl | 1 + .tmp-pr8-test/b05.jsonl | 0 .tmp-pr8-test/c01.json | 1 + .tmp-pr8-test/c01b.json | 1 + .tmp-pr8-test/c03.json | 1 + .tmp-pr8-test/c04.json | 0 .tmp-pr8-test/c05-thresholds.json | 1 + .tmp-pr8-test/c05.json | 1 + .tmp-pr8-test/c05b.json | 1 + AGENTS.md | 110 + CHANGELOG.md | 107 + COMPATIBILITY-STRATEGY.md | 335 + DREAMS.md | 17 + GOVERNANCE.md | 432 + HEARTBEAT.md | 49 + IDENTITY.md | 14 + MEMORY.md | 66 + OpenClaw-Evolution-Plan.md | 793 + PORTFOLIO.md | 520 + PROJECT-COMPLETION-REPORT.md | 159 + README.md | 212 + RELEASE-NOTES-v2.md | 96 + SOUL.md | 29 + SYSTEM-AUDIT.md | 437 + TOOLS.md | 15 + USER.md | 9 + VERSION.md | 48 + architecture-example.json | 443 + audit-report.md | 163 + cases/README.md | 93 + cases/bookshelf-v2/arch.json | 1 + cases/bookshelf-v2/backend/.gitignore | 7 + cases/bookshelf-v2/backend/README.md | 90 + cases/bookshelf-v2/backend/package.json | 27 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/note_tags.test.ts | 118 + .../backend/src/__tests__/notes.test.ts | 122 + .../backend/src/__tests__/tags.test.ts | 116 + .../backend/src/__tests__/users.test.ts | 118 + cases/bookshelf-v2/backend/src/db/client.ts | 93 + cases/bookshelf-v2/backend/src/db/schema.ts | 19 + cases/bookshelf-v2/backend/src/index.ts | 67 + .../backend/src/middleware/auth.ts | 41 + cases/bookshelf-v2/backend/src/routes/auth.ts | 121 + .../backend/src/routes/note_tags.ts | 51 + .../bookshelf-v2/backend/src/routes/notes.ts | 51 + cases/bookshelf-v2/backend/src/routes/tags.ts | 51 + .../bookshelf-v2/backend/src/routes/users.ts | 51 + .../bookshelf-v2/backend/src/services/note.ts | 58 + .../backend/src/services/note_tag.ts | 54 + .../bookshelf-v2/backend/src/services/tag.ts | 54 + .../bookshelf-v2/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + cases/bookshelf-v2/backend/src/types/index.ts | 126 + .../backend/src/types/sql.js.d.ts | 27 + cases/bookshelf-v2/backend/tsconfig.json | 27 + cases/bookshelf-v2/electron/README.md | 82 + cases/bookshelf-v2/electron/assets/icon.png | Bin 0 -> 66 bytes .../electron/electron-builder.yml | 100 + cases/bookshelf-v2/electron/electron/ipc.ts | 77 + cases/bookshelf-v2/electron/electron/main.ts | 238 + .../bookshelf-v2/electron/electron/preload.ts | 92 + cases/bookshelf-v2/electron/electron/tray.ts | 61 + .../bookshelf-v2/electron/electron/updater.ts | 87 + cases/bookshelf-v2/electron/electron/utils.ts | 80 + .../electron/entitlements.mac.plist | 18 + cases/bookshelf-v2/electron/package.json | 41 + cases/bookshelf-v2/electron/tsconfig.json | 32 + cases/bookshelf-v2/frontend/app/globals.css | 8 + cases/bookshelf-v2/frontend/app/layout.tsx | 30 + cases/bookshelf-v2/frontend/app/page.tsx | 50 + .../bookshelf-v2/frontend/app/书籍库/page.tsx | 51 + cases/bookshelf-v2/frontend/app/书评/page.tsx | 29 + .../frontend/app/搜索和筛选/page.tsx | 29 + .../frontend/app/笔记和标注/page.tsx | 51 + .../frontend/app/统计仪表盘/page.tsx | 29 + .../frontend/app/阅读状态/page.tsx | 51 + .../frontend/app/阅读进度/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/note/NoteCard.tsx | 20 + .../frontend/components/ui/Button.tsx | 31 + .../frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + cases/bookshelf-v2/frontend/hooks/useAuth.ts | 35 + .../frontend/hooks/useDebounce.ts | 14 + cases/bookshelf-v2/frontend/hooks/useForm.ts | 33 + cases/bookshelf-v2/frontend/hooks/useItem.ts | 35 + cases/bookshelf-v2/frontend/next.config.ts | 7 + cases/bookshelf-v2/frontend/package.json | 25 + cases/bookshelf-v2/frontend/postcss.config.js | 6 + cases/bookshelf-v2/frontend/services/api.ts | 47 + cases/bookshelf-v2/frontend/services/auth.ts | 10 + .../bookshelf-v2/frontend/services/书籍库.ts | 10 + .../frontend/services/笔记和标注.ts | 10 + .../frontend/services/阅读状态.ts | 10 + .../frontend/services/阅读进度.ts | 10 + .../bookshelf-v2/frontend/tailwind.config.ts | 10 + cases/bookshelf-v2/frontend/tsconfig.json | 40 + cases/bookshelf-v2/frontend/types/index.ts | 46 + cases/bookshelf-v2/prd.json | 1 + cases/bookshelf-v2/release/release/README.md | 33 + .../release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/bookshelf-0.1.0.AppImage | 3 + .../release/linux/bookshelf_0.1.0_amd64.deb | 3 + .../release/macos/bookshelf-0.1.0-arm64.dmg | 3 + .../release/release/macos/bookshelf-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 54 + .../bookshelf-v2/release/release/version.json | 10 + .../windows/bookshelf-0.1.0-portable.exe | 3 + .../release/windows/bookshelf-setup-0.1.0.exe | 3 + cases/bookshelf-v3/arch.json | 1 + cases/bookshelf-v3/backend/.gitignore | 7 + cases/bookshelf-v3/backend/README.md | 132 + cases/bookshelf-v3/backend/package.json | 27 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/books.test.ts | 122 + .../backend/src/__tests__/items.test.ts | 122 + .../backend/src/__tests__/note_tags.test.ts | 118 + .../backend/src/__tests__/notes.test.ts | 122 + .../src/__tests__/reading_progress.test.ts | 122 + .../src/__tests__/reading_status.test.ts | 122 + .../backend/src/__tests__/reviews.test.ts | 122 + .../backend/src/__tests__/stats.test.ts | 122 + .../backend/src/__tests__/tags.test.ts | 116 + .../backend/src/__tests__/users.test.ts | 118 + cases/bookshelf-v3/backend/src/db/client.ts | 93 + cases/bookshelf-v3/backend/src/db/schema.ts | 29 + cases/bookshelf-v3/backend/src/index.ts | 79 + .../backend/src/middleware/auth.ts | 41 + cases/bookshelf-v3/backend/src/routes/auth.ts | 121 + .../bookshelf-v3/backend/src/routes/books.ts | 51 + .../bookshelf-v3/backend/src/routes/items.ts | 51 + .../backend/src/routes/note_tags.ts | 51 + .../bookshelf-v3/backend/src/routes/notes.ts | 51 + .../backend/src/routes/reading_progress.ts | 51 + .../backend/src/routes/reading_status.ts | 51 + .../backend/src/routes/reviews.ts | 51 + .../bookshelf-v3/backend/src/routes/stats.ts | 51 + cases/bookshelf-v3/backend/src/routes/tags.ts | 51 + .../bookshelf-v3/backend/src/routes/users.ts | 51 + .../bookshelf-v3/backend/src/services/book.ts | 58 + .../bookshelf-v3/backend/src/services/item.ts | 58 + .../bookshelf-v3/backend/src/services/note.ts | 58 + .../backend/src/services/note_tag.ts | 54 + .../backend/src/services/reading_progress.ts | 58 + .../backend/src/services/reading_statu.ts | 58 + .../backend/src/services/review.ts | 58 + .../bookshelf-v3/backend/src/services/stat.ts | 58 + .../bookshelf-v3/backend/src/services/tag.ts | 54 + .../bookshelf-v3/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + cases/bookshelf-v3/backend/src/types/index.ts | 288 + .../backend/src/types/sql.js.d.ts | 27 + cases/bookshelf-v3/backend/tsconfig.json | 27 + cases/bookshelf-v3/prd.json | 1 + cases/bookshelf/arch.json | 1 + cases/bookshelf/backend/.gitignore | 7 + cases/bookshelf/backend/README.md | 90 + cases/bookshelf/backend/package.json | 27 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/note_tags.test.ts | 118 + .../backend/src/__tests__/notes.test.ts | 122 + .../backend/src/__tests__/tags.test.ts | 116 + .../backend/src/__tests__/users.test.ts | 118 + cases/bookshelf/backend/src/db/client.ts | 93 + cases/bookshelf/backend/src/db/schema.ts | 19 + cases/bookshelf/backend/src/index.ts | 67 + .../bookshelf/backend/src/middleware/auth.ts | 41 + cases/bookshelf/backend/src/routes/auth.ts | 121 + .../bookshelf/backend/src/routes/note_tags.ts | 51 + cases/bookshelf/backend/src/routes/notes.ts | 51 + cases/bookshelf/backend/src/routes/tags.ts | 51 + cases/bookshelf/backend/src/routes/users.ts | 51 + cases/bookshelf/backend/src/services/note.ts | 58 + .../backend/src/services/note_tag.ts | 54 + cases/bookshelf/backend/src/services/tag.ts | 54 + cases/bookshelf/backend/src/services/user.ts | 56 + .../bookshelf/backend/src/types/fastify.d.ts | 8 + cases/bookshelf/backend/src/types/index.ts | 126 + cases/bookshelf/backend/src/types/sql.js.d.ts | 27 + cases/bookshelf/backend/tsconfig.json | 27 + cases/bookshelf/electron/README.md | 82 + cases/bookshelf/electron/assets/icon.png | Bin 0 -> 66 bytes cases/bookshelf/electron/electron-builder.yml | 100 + cases/bookshelf/electron/electron/ipc.ts | 77 + cases/bookshelf/electron/electron/main.ts | 238 + cases/bookshelf/electron/electron/preload.ts | 92 + cases/bookshelf/electron/electron/tray.ts | 61 + cases/bookshelf/electron/electron/updater.ts | 87 + cases/bookshelf/electron/electron/utils.ts | 80 + .../bookshelf/electron/entitlements.mac.plist | 18 + cases/bookshelf/electron/package.json | 41 + cases/bookshelf/electron/tsconfig.json | 32 + cases/bookshelf/frontend/app/globals.css | 8 + cases/bookshelf/frontend/app/layout.tsx | 30 + .../bookshelf/frontend/app/note/[id]/page.tsx | 25 + cases/bookshelf/frontend/app/notes/page.tsx | 25 + cases/bookshelf/frontend/app/page.tsx | 50 + .../bookshelf/frontend/app/settings/page.tsx | 14 + cases/bookshelf/frontend/app/tags/page.tsx | 14 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/note/NoteCard.tsx | 20 + .../frontend/components/ui/Button.tsx | 31 + .../bookshelf/frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + cases/bookshelf/frontend/hooks/useDebounce.ts | 14 + cases/bookshelf/frontend/hooks/useForm.ts | 33 + cases/bookshelf/frontend/hooks/useNotes.ts | 35 + cases/bookshelf/frontend/hooks/useTags.ts | 35 + cases/bookshelf/frontend/next.config.ts | 7 + cases/bookshelf/frontend/package.json | 25 + cases/bookshelf/frontend/postcss.config.js | 6 + cases/bookshelf/frontend/services/api.ts | 47 + cases/bookshelf/frontend/services/notes.ts | 18 + cases/bookshelf/frontend/services/tags.ts | 6 + cases/bookshelf/frontend/tailwind.config.ts | 10 + cases/bookshelf/frontend/tsconfig.json | 40 + cases/bookshelf/frontend/types/index.ts | 46 + cases/bookshelf/prd.json | 1 + cases/bookshelf/release/release/README.md | 33 + .../bookshelf/release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/noteapp-0.1.0.AppImage | 3 + .../release/linux/noteapp_0.1.0_amd64.deb | 3 + .../release/macos/noteapp-0.1.0-arm64.dmg | 3 + .../release/release/macos/noteapp-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 54 + cases/bookshelf/release/release/version.json | 10 + .../windows/noteapp-0.1.0-portable.exe | 3 + .../release/windows/noteapp-setup-0.1.0.exe | 3 + cases/failure-report.md | 131 + cases/mealprep-v2/arch.json | 1 + cases/mealprep-v2/backend/.gitignore | 7 + cases/mealprep-v2/backend/README.md | 97 + cases/mealprep-v2/backend/package.json | 27 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/logistics.test.ts | 122 + .../backend/src/__tests__/order_items.test.ts | 120 + .../backend/src/__tests__/orders.test.ts | 120 + .../backend/src/__tests__/products.test.ts | 122 + .../backend/src/__tests__/users.test.ts | 118 + cases/mealprep-v2/backend/src/db/client.ts | 93 + cases/mealprep-v2/backend/src/db/schema.ts | 18 + cases/mealprep-v2/backend/src/index.ts | 69 + .../backend/src/middleware/auth.ts | 41 + cases/mealprep-v2/backend/src/routes/auth.ts | 121 + .../backend/src/routes/logistics.ts | 51 + .../backend/src/routes/order_items.ts | 51 + .../mealprep-v2/backend/src/routes/orders.ts | 51 + .../backend/src/routes/products.ts | 51 + cases/mealprep-v2/backend/src/routes/users.ts | 51 + .../backend/src/services/logistic.ts | 58 + .../mealprep-v2/backend/src/services/order.ts | 56 + .../backend/src/services/order_item.ts | 56 + .../backend/src/services/product.ts | 57 + .../mealprep-v2/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + cases/mealprep-v2/backend/src/types/index.ts | 165 + .../mealprep-v2/backend/src/types/sql.js.d.ts | 27 + cases/mealprep-v2/backend/tsconfig.json | 27 + cases/mealprep-v2/electron/README.md | 82 + cases/mealprep-v2/electron/assets/icon.png | Bin 0 -> 66 bytes .../mealprep-v2/electron/electron-builder.yml | 100 + cases/mealprep-v2/electron/electron/ipc.ts | 77 + cases/mealprep-v2/electron/electron/main.ts | 238 + .../mealprep-v2/electron/electron/preload.ts | 92 + cases/mealprep-v2/electron/electron/tray.ts | 61 + .../mealprep-v2/electron/electron/updater.ts | 87 + cases/mealprep-v2/electron/electron/utils.ts | 80 + .../electron/entitlements.mac.plist | 18 + cases/mealprep-v2/electron/package.json | 41 + cases/mealprep-v2/electron/tsconfig.json | 32 + cases/mealprep-v2/frontend/app/globals.css | 8 + cases/mealprep-v2/frontend/app/layout.tsx | 30 + cases/mealprep-v2/frontend/app/page.tsx | 50 + .../frontend/app/客户管理/page.tsx | 51 + .../frontend/app/模板功能/page.tsx | 29 + .../frontend/app/营养计算/page.tsx | 51 + .../frontend/app/购物清单/page.tsx | 29 + .../mealprep-v2/frontend/app/食物库/page.tsx | 51 + .../mealprep-v2/frontend/app/餐计划/page.tsx | 51 + .../components/ecommerce/ProductCard.tsx | 24 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/ui/Button.tsx | 31 + .../frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + cases/mealprep-v2/frontend/hooks/useAuth.ts | 35 + .../mealprep-v2/frontend/hooks/useDebounce.ts | 14 + cases/mealprep-v2/frontend/hooks/useForm.ts | 33 + cases/mealprep-v2/frontend/hooks/useItem.ts | 35 + cases/mealprep-v2/frontend/next.config.ts | 7 + cases/mealprep-v2/frontend/package.json | 25 + cases/mealprep-v2/frontend/postcss.config.js | 6 + cases/mealprep-v2/frontend/services/api.ts | 47 + cases/mealprep-v2/frontend/services/auth.ts | 10 + .../mealprep-v2/frontend/services/客户管理.ts | 10 + .../mealprep-v2/frontend/services/营养计算.ts | 10 + cases/mealprep-v2/frontend/services/食物库.ts | 10 + cases/mealprep-v2/frontend/services/餐计划.ts | 10 + cases/mealprep-v2/frontend/tailwind.config.ts | 10 + cases/mealprep-v2/frontend/tsconfig.json | 40 + cases/mealprep-v2/frontend/types/index.ts | 63 + cases/mealprep-v2/prd.json | 1 + cases/mealprep-v2/release/release/README.md | 33 + .../release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/mealprep-0.1.0.AppImage | 3 + .../release/linux/mealprep_0.1.0_amd64.deb | 3 + .../release/macos/mealprep-0.1.0-arm64.dmg | 3 + .../release/release/macos/mealprep-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 55 + .../mealprep-v2/release/release/version.json | 10 + .../windows/mealprep-0.1.0-portable.exe | 3 + .../release/windows/mealprep-setup-0.1.0.exe | 3 + cases/mealprep-v3/arch.json | 1 + cases/mealprep-v3/backend/.gitignore | 7 + cases/mealprep-v3/backend/README.md | 139 + cases/mealprep-v3/backend/package.json | 27 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/customers.test.ts | 122 + .../backend/src/__tests__/foods.test.ts | 122 + .../backend/src/__tests__/logistics.test.ts | 122 + .../backend/src/__tests__/meal_plans.test.ts | 122 + .../backend/src/__tests__/nutrition.test.ts | 122 + .../backend/src/__tests__/order_items.test.ts | 120 + .../backend/src/__tests__/orders.test.ts | 120 + .../backend/src/__tests__/products.test.ts | 122 + .../src/__tests__/shopping_lists.test.ts | 122 + .../backend/src/__tests__/templates.test.ts | 122 + .../backend/src/__tests__/users.test.ts | 118 + cases/mealprep-v3/backend/src/db/client.ts | 93 + cases/mealprep-v3/backend/src/db/schema.ts | 30 + cases/mealprep-v3/backend/src/index.ts | 81 + .../backend/src/middleware/auth.ts | 41 + cases/mealprep-v3/backend/src/routes/auth.ts | 121 + .../backend/src/routes/customers.ts | 51 + cases/mealprep-v3/backend/src/routes/foods.ts | 51 + .../backend/src/routes/logistics.ts | 51 + .../backend/src/routes/meal_plans.ts | 51 + .../backend/src/routes/nutrition.ts | 51 + .../backend/src/routes/order_items.ts | 51 + .../mealprep-v3/backend/src/routes/orders.ts | 51 + .../backend/src/routes/products.ts | 51 + .../backend/src/routes/shopping_lists.ts | 51 + .../backend/src/routes/templates.ts | 51 + cases/mealprep-v3/backend/src/routes/users.ts | 51 + .../backend/src/services/customer.ts | 58 + .../mealprep-v3/backend/src/services/food.ts | 58 + .../backend/src/services/logistic.ts | 58 + .../backend/src/services/meal_plan.ts | 58 + .../backend/src/services/nutrition.ts | 58 + .../mealprep-v3/backend/src/services/order.ts | 56 + .../backend/src/services/order_item.ts | 56 + .../backend/src/services/product.ts | 57 + .../backend/src/services/shopping_list.ts | 58 + .../backend/src/services/template.ts | 58 + .../mealprep-v3/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + cases/mealprep-v3/backend/src/types/index.ts | 327 + .../mealprep-v3/backend/src/types/sql.js.d.ts | 27 + cases/mealprep-v3/backend/tsconfig.json | 27 + cases/mealprep-v3/prd.json | 1 + cases/mealprep/prd.json | 1 + cases/teamflow-v2/arch.json | 1 + cases/teamflow-v2/backend/.gitignore | 7 + cases/teamflow-v2/backend/README.md | 90 + cases/teamflow-v2/backend/package.json | 27 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/note_tags.test.ts | 118 + .../backend/src/__tests__/notes.test.ts | 122 + .../backend/src/__tests__/tags.test.ts | 116 + .../backend/src/__tests__/users.test.ts | 118 + cases/teamflow-v2/backend/src/db/client.ts | 93 + cases/teamflow-v2/backend/src/db/schema.ts | 19 + cases/teamflow-v2/backend/src/index.ts | 67 + .../backend/src/middleware/auth.ts | 41 + cases/teamflow-v2/backend/src/routes/auth.ts | 121 + .../backend/src/routes/note_tags.ts | 51 + cases/teamflow-v2/backend/src/routes/notes.ts | 51 + cases/teamflow-v2/backend/src/routes/tags.ts | 51 + cases/teamflow-v2/backend/src/routes/users.ts | 51 + .../teamflow-v2/backend/src/services/note.ts | 58 + .../backend/src/services/note_tag.ts | 54 + cases/teamflow-v2/backend/src/services/tag.ts | 54 + .../teamflow-v2/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + cases/teamflow-v2/backend/src/types/index.ts | 126 + .../teamflow-v2/backend/src/types/sql.js.d.ts | 27 + cases/teamflow-v2/backend/tsconfig.json | 27 + cases/teamflow-v2/electron/README.md | 82 + cases/teamflow-v2/electron/assets/icon.png | Bin 0 -> 66 bytes .../teamflow-v2/electron/electron-builder.yml | 100 + cases/teamflow-v2/electron/electron/ipc.ts | 77 + cases/teamflow-v2/electron/electron/main.ts | 238 + .../teamflow-v2/electron/electron/preload.ts | 92 + cases/teamflow-v2/electron/electron/tray.ts | 61 + .../teamflow-v2/electron/electron/updater.ts | 87 + cases/teamflow-v2/electron/electron/utils.ts | 80 + .../electron/entitlements.mac.plist | 18 + cases/teamflow-v2/electron/package.json | 41 + cases/teamflow-v2/electron/tsconfig.json | 32 + cases/teamflow-v2/frontend/app/globals.css | 8 + cases/teamflow-v2/frontend/app/layout.tsx | 30 + cases/teamflow-v2/frontend/app/page.tsx | 50 + .../frontend/app/任务管理/page.tsx | 51 + .../frontend/app/团队成员/page.tsx | 51 + .../frontend/app/数据统计/page.tsx | 29 + .../frontend/app/活动日志/page.tsx | 29 + .../frontend/app/看板视图/page.tsx | 51 + .../frontend/app/筛选和搜索/page.tsx | 51 + .../frontend/components/layout/BottomNav.tsx | 38 + .../frontend/components/layout/Sidebar.tsx | 44 + .../frontend/components/note/NoteCard.tsx | 20 + .../frontend/components/ui/Button.tsx | 31 + .../frontend/components/ui/Card.tsx | 18 + .../frontend/components/ui/EmptyState.tsx | 19 + .../frontend/components/ui/Input.tsx | 22 + .../frontend/components/ui/Modal.tsx | 33 + cases/teamflow-v2/frontend/hooks/useAuth.ts | 35 + .../teamflow-v2/frontend/hooks/useDebounce.ts | 14 + cases/teamflow-v2/frontend/hooks/useForm.ts | 33 + cases/teamflow-v2/frontend/hooks/useItem.ts | 35 + cases/teamflow-v2/frontend/next.config.ts | 7 + cases/teamflow-v2/frontend/package.json | 25 + cases/teamflow-v2/frontend/postcss.config.js | 6 + cases/teamflow-v2/frontend/services/api.ts | 47 + cases/teamflow-v2/frontend/services/auth.ts | 10 + .../teamflow-v2/frontend/services/任务管理.ts | 10 + .../teamflow-v2/frontend/services/团队成员.ts | 10 + .../teamflow-v2/frontend/services/看板视图.ts | 10 + .../frontend/services/筛选和搜索.ts | 10 + cases/teamflow-v2/frontend/tailwind.config.ts | 10 + cases/teamflow-v2/frontend/tsconfig.json | 40 + cases/teamflow-v2/frontend/types/index.ts | 46 + cases/teamflow-v2/prd.json | 1 + cases/teamflow-v2/release/release/README.md | 33 + .../release/release/build-info.json | 16 + .../release/release/checksums/checksums.txt | 6 + .../release/linux/teamflow-0.1.0.AppImage | 3 + .../release/linux/teamflow_0.1.0_amd64.deb | 3 + .../release/macos/teamflow-0.1.0-arm64.dmg | 3 + .../release/release/macos/teamflow-0.1.0.dmg | 3 + .../release/release/manifests/manifest.json | 47 + .../release/release-notes/release-notes.md | 54 + .../teamflow-v2/release/release/version.json | 10 + .../windows/teamflow-0.1.0-portable.exe | 3 + .../release/windows/teamflow-setup-0.1.0.exe | 3 + cases/teamflow-v3/arch.json | 1 + cases/teamflow-v3/backend/.gitignore | 7 + cases/teamflow-v3/backend/README.md | 132 + cases/teamflow-v3/backend/package.json | 27 + .../src/__tests__/activity_logs.test.ts | 122 + .../backend/src/__tests__/auth.test.ts | 96 + .../backend/src/__tests__/boards.test.ts | 122 + .../backend/src/__tests__/items.test.ts | 122 + .../backend/src/__tests__/members.test.ts | 122 + .../backend/src/__tests__/note_tags.test.ts | 118 + .../backend/src/__tests__/notes.test.ts | 122 + .../backend/src/__tests__/stats.test.ts | 122 + .../backend/src/__tests__/tags.test.ts | 116 + .../backend/src/__tests__/tasks.test.ts | 122 + .../backend/src/__tests__/users.test.ts | 118 + cases/teamflow-v3/backend/src/db/client.ts | 93 + cases/teamflow-v3/backend/src/db/schema.ts | 31 + cases/teamflow-v3/backend/src/index.ts | 79 + .../backend/src/middleware/auth.ts | 41 + .../backend/src/routes/activity_logs.ts | 51 + cases/teamflow-v3/backend/src/routes/auth.ts | 121 + .../teamflow-v3/backend/src/routes/boards.ts | 51 + cases/teamflow-v3/backend/src/routes/items.ts | 51 + .../teamflow-v3/backend/src/routes/members.ts | 51 + .../backend/src/routes/note_tags.ts | 51 + cases/teamflow-v3/backend/src/routes/notes.ts | 51 + cases/teamflow-v3/backend/src/routes/stats.ts | 51 + cases/teamflow-v3/backend/src/routes/tags.ts | 51 + cases/teamflow-v3/backend/src/routes/tasks.ts | 51 + cases/teamflow-v3/backend/src/routes/users.ts | 51 + .../backend/src/services/activity_log.ts | 58 + .../teamflow-v3/backend/src/services/board.ts | 58 + .../teamflow-v3/backend/src/services/item.ts | 58 + .../backend/src/services/member.ts | 58 + .../teamflow-v3/backend/src/services/note.ts | 58 + .../backend/src/services/note_tag.ts | 54 + .../teamflow-v3/backend/src/services/stat.ts | 58 + cases/teamflow-v3/backend/src/services/tag.ts | 54 + .../teamflow-v3/backend/src/services/task.ts | 58 + .../teamflow-v3/backend/src/services/user.ts | 56 + .../backend/src/types/fastify.d.ts | 8 + cases/teamflow-v3/backend/src/types/index.ts | 288 + .../teamflow-v3/backend/src/types/sql.js.d.ts | 27 + cases/teamflow-v3/backend/tsconfig.json | 27 + cases/teamflow-v3/prd.json | 1 + cases/teamflow/prd.json | 1 + claw-os-v2 | 1 + docs/AGENTS-PROTOCOL-FULL.md | 458 + docs/README.md | 134 + docs/architecture-agent.md | 195 + docs/architecture-freeze-v2.md | 125 + docs/factories/sf-01-product-strategy.md | 218 + .../sf-02-requirement-engineering.md | 264 + docs/factory-registry.md | 218 + docs/frontend-builder-agent.md | 151 + docs/project-intake-agent.md | 209 + docs/reports/backend-semantic-fix-report.md | 64 + docs/reports/benchmark-report.md | 411 + docs/reports/benchmark-results.json | 1373 ++ docs/reports/benchmark-v2-report.md | 124 + docs/reports/capability-boundary.md | 66 + docs/reports/capability-registry.json | 34 + docs/reports/capability-report.md | 67 + docs/reports/certification-report.md | 106 + docs/reports/contract-consistency-report.md | 153 + docs/reports/desktop-benchmark-report.md | 146 + docs/reports/domain-certification.json | 154 + docs/reports/end-to-end-benchmark-v1.md | 67 + docs/reports/failure-catalog.json | 64 + .../real-requirement-benchmark-report.md | 114 + docs/reports/release-benchmark-report.md | 99 + docs/software-factory-core.md | 362 + docs/v2-factory-fusion-freeze.md | 232 + memory/.dream-cycle-last-run | 1 + memory/.dreams/events.jsonl | 22 + memory/.dreams/phase-signals.json | 685 + memory/.dreams/session-corpus/2026-06-01.txt | 12 + memory/.dreams/session-corpus/2026-06-03.txt | 27 + memory/.dreams/session-corpus/2026-06-04.txt | 179 + memory/.dreams/session-corpus/2026-06-05.txt | 262 + memory/.dreams/session-ingestion.json | 946 + memory/.dreams/short-term-recall.json | 15499 ++++++++++++++++ memory/.memory-auto.log | 3 + memory/.session-compact.log | 18 + memory/SESSION_START.md | 37 + memory/daily/2026-05-30.md | 26 + memory/daily/2026-05-31.md | 12 + memory/daily/2026-06-01.md | 93 + memory/daily/2026-06-02.md | 138 + memory/daily/2026-06-03.md | 47 + memory/daily/2026-06-04.md | 53 + memory/daily/2026-06-05.md | 942 + memory/daily/2026-06-06.md | 10 + memory/daily/sync-log-20260604.md | 52 + memory/dreaming/deep/2026-06-06.md | 5 + memory/dreaming/light/2026-06-06.md | 487 + memory/dreaming/rem/2026-06-06.md | 7 + memory/index.md | 255 + memory/projects/claw-agent-os-v2.md | 678 + memory/projects/github-top20-analysis.md | 141 + memory/projects/hermes-agent.md | 38 + .../agent-packages.json | 380 + .../dep-graph.json | 156 + .../execution-plan.md | 385 + .../roadmap.json | 33 + .../task-tree.json | 66 + memory/projects/priority-install-plan.md | 73 + memory/projects/software-generator-mvp-v1.md | 100 + memory/projects/supermemory-study.md | 178 + memory/registers/_index.md | 33 + memory/registers/companions.md | 36 + memory/registers/memory-routing.md | 83 + memory/registers/open-loops.md | 52 + memory/registers/people.md | 20 + memory/registers/preferences.md | 15 + memory/registers/tool-env.md | 77 + memory/vault.md | 70 + memory/verification-log.md | 826 + network-tool/.gitignore | 9 + network-tool/README.md | 140 + .../apps/client-electron/package.json | 45 + .../apps/client-electron/src/main/index.ts | 160 + .../src/main/network-client.ts | 173 + .../apps/client-electron/src/main/preload.ts | 50 + .../apps/client-electron/src/renderer/App.tsx | 100 + .../client-electron/src/renderer/env.d.ts | 39 + .../client-electron/src/renderer/index.html | 13 + .../client-electron/src/renderer/index.tsx | 10 + .../src/renderer/pages/ConnectionsPage.tsx | 228 + .../src/renderer/pages/DevicesPage.tsx | 140 + .../src/renderer/pages/LoginPage.tsx | 70 + .../src/renderer/pages/LogsPage.tsx | 64 + .../src/renderer/pages/NodesPage.tsx | 67 + .../src/renderer/pages/RegisterPage.tsx | 94 + .../src/renderer/pages/SettingsPage.tsx | 66 + .../src/renderer/styles/global.css | 494 + .../apps/client-electron/tsconfig.json | 14 + .../apps/client-electron/tsconfig.main.json | 13 + .../apps/client-electron/vite.config.ts | 22 + network-tool/apps/control-server/package.json | 25 + .../apps/control-server/src/db/database.ts | 196 + .../apps/control-server/src/db/init.ts | 10 + network-tool/apps/control-server/src/index.ts | 106 + .../control-server/src/middleware/auth.ts | 45 + .../control-server/src/routes/auth.routes.ts | 96 + .../src/routes/connection.routes.ts | 71 + .../src/routes/device.routes.ts | 87 + .../control-server/src/routes/node.routes.ts | 65 + .../src/services/auth.service.ts | 45 + .../src/services/connection.service.ts | 106 + .../src/services/device.service.ts | 50 + .../src/services/node.service.ts | 54 + .../apps/control-server/src/types/sql.js.d.ts | 33 + .../apps/control-server/tsconfig.json | 14 + network-tool/apps/relay-node/package.json | 22 + network-tool/apps/relay-node/src/index.ts | 304 + network-tool/apps/relay-node/tsconfig.json | 12 + network-tool/package.json | 18 + .../packages/shared-crypto/package.json | 21 + .../packages/shared-crypto/src/index.ts | 90 + .../packages/shared-crypto/tsconfig.json | 13 + .../packages/shared-protocol/package.json | 16 + .../packages/shared-protocol/src/index.ts | 66 + .../packages/shared-protocol/tsconfig.json | 13 + .../packages/shared-types/package.json | 13 + .../packages/shared-types/src/index.ts | 163 + .../packages/shared-types/tsconfig.json | 13 + network-tool/test-e2e.js | 213 + output/agent-architecture-audit.md | 547 + output/contract-mgmt/.gitignore | 8 + output/contract-mgmt/README.md | 118 + output/contract-mgmt/apps/api/.gitignore | 7 + output/contract-mgmt/apps/api/README.md | 111 + output/contract-mgmt/apps/api/package.json | 29 + .../apps/api/src/__tests__/approvals.test.ts | 121 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/clients.test.ts | 119 + .../apps/api/src/__tests__/contracts.test.ts | 124 + .../apps/api/src/__tests__/customers.test.ts | 122 + .../apps/api/src/__tests__/items.test.ts | 119 + .../apps/api/src/__tests__/reminders.test.ts | 121 + .../apps/api/src/__tests__/templates.test.ts | 123 + .../contract-mgmt/apps/api/src/db/client.ts | 93 + .../contract-mgmt/apps/api/src/db/schema.ts | 19 + output/contract-mgmt/apps/api/src/index.ts | 77 + .../apps/api/src/middleware/auth.ts | 41 + .../apps/api/src/routes/approvals.ts | 51 + .../contract-mgmt/apps/api/src/routes/auth.ts | 121 + .../apps/api/src/routes/clients.ts | 51 + .../apps/api/src/routes/contracts.ts | 51 + .../apps/api/src/routes/customers.ts | 51 + .../apps/api/src/routes/items.ts | 51 + .../apps/api/src/routes/reminders.ts | 51 + .../apps/api/src/routes/templates.ts | 51 + .../apps/api/src/services/approval.ts | 56 + .../apps/api/src/services/client.ts | 54 + .../apps/api/src/services/contract.ts | 59 + .../apps/api/src/services/customer.ts | 57 + .../apps/api/src/services/item.ts | 54 + .../apps/api/src/services/reminder.ts | 56 + .../apps/api/src/services/template.ts | 58 + .../apps/api/src/types/fastify.d.ts | 8 + .../contract-mgmt/apps/api/src/types/index.ts | 205 + .../apps/api/src/types/sql.js.d.ts | 27 + output/contract-mgmt/apps/api/tsconfig.json | 27 + .../apps/web/app/approvals/page.tsx | 35 + .../apps/web/app/clients/page.tsx | 29 + .../apps/web/app/contracts/page.tsx | 51 + output/contract-mgmt/apps/web/app/globals.css | 8 + .../contract-mgmt/apps/web/app/items/page.tsx | 51 + output/contract-mgmt/apps/web/app/layout.tsx | 30 + output/contract-mgmt/apps/web/app/page.tsx | 50 + .../apps/web/app/reminders/page.tsx | 29 + .../apps/web/app/templates/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../apps/web/hooks/useApprovals.ts | 35 + .../contract-mgmt/apps/web/hooks/useAuth.ts | 35 + .../apps/web/hooks/useContracts.ts | 35 + .../apps/web/hooks/useDebounce.ts | 14 + .../contract-mgmt/apps/web/hooks/useForm.ts | 33 + .../contract-mgmt/apps/web/hooks/useItems.ts | 35 + output/contract-mgmt/apps/web/next-env.d.ts | 6 + output/contract-mgmt/apps/web/next.config.ts | 7 + output/contract-mgmt/apps/web/package.json | 27 + .../contract-mgmt/apps/web/postcss.config.js | 6 + output/contract-mgmt/apps/web/services/api.ts | 49 + .../apps/web/services/approvals.ts | 10 + .../contract-mgmt/apps/web/services/auth.ts | 10 + .../apps/web/services/contracts.ts | 10 + .../contract-mgmt/apps/web/services/items.ts | 22 + .../contract-mgmt/apps/web/tailwind.config.ts | 10 + output/contract-mgmt/apps/web/tsconfig.json | 40 + output/contract-mgmt/apps/web/types/index.ts | 121 + output/contract-mgmt/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 123 + .../packages/shared-types/tsconfig.json | 15 + output/contract-mgmt/scripts/build.mjs | 26 + output/contract-mgmt/scripts/dev.mjs | 32 + output/equipment-inspection/.gitignore | 8 + output/equipment-inspection/README.md | 125 + .../equipment-inspection/apps/api/.gitignore | 7 + .../equipment-inspection/apps/api/README.md | 118 + .../apps/api/package.json | 29 + .../apps/api/src/__tests__/approvals.test.ts | 121 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/contracts.test.ts | 124 + .../apps/api/src/__tests__/customers.test.ts | 122 + .../apps/api/src/__tests__/equipment.test.ts | 121 + .../src/__tests__/inspection_plans.test.ts | 128 + .../apps/api/src/__tests__/items.test.ts | 119 + .../apps/api/src/__tests__/reminders.test.ts | 121 + .../apps/api/src/__tests__/tasks.test.ts | 121 + .../apps/api/src/db/client.ts | 93 + .../apps/api/src/db/schema.ts | 20 + .../apps/api/src/index.ts | 79 + .../apps/api/src/middleware/auth.ts | 41 + .../apps/api/src/routes/approvals.ts | 51 + .../apps/api/src/routes/auth.ts | 121 + .../apps/api/src/routes/contracts.ts | 51 + .../apps/api/src/routes/customers.ts | 51 + .../apps/api/src/routes/equipment.ts | 51 + .../apps/api/src/routes/inspection_plans.ts | 51 + .../apps/api/src/routes/items.ts | 51 + .../apps/api/src/routes/reminders.ts | 51 + .../apps/api/src/routes/tasks.ts | 51 + .../apps/api/src/services/approval.ts | 56 + .../apps/api/src/services/contract.ts | 59 + .../apps/api/src/services/customer.ts | 57 + .../apps/api/src/services/equipment.ts | 56 + .../apps/api/src/services/inspection_plan.ts | 55 + .../apps/api/src/services/item.ts | 54 + .../apps/api/src/services/reminder.ts | 56 + .../apps/api/src/services/task.ts | 56 + .../apps/api/src/types/fastify.d.ts | 8 + .../apps/api/src/types/index.ts | 223 + .../apps/api/src/types/sql.js.d.ts | 27 + .../apps/api/tsconfig.json | 27 + .../apps/web/app/equipment/page.tsx | 51 + .../apps/web/app/globals.css | 8 + .../apps/web/app/inspection_plans/page.tsx | 51 + .../apps/web/app/items/page.tsx | 51 + .../apps/web/app/layout.tsx | 30 + .../apps/web/app/page.tsx | 50 + .../apps/web/app/tasks/page.tsx | 51 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/ui/Button.tsx | 31 + .../apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../apps/web/hooks/useAuth.ts | 35 + .../apps/web/hooks/useDebounce.ts | 14 + .../apps/web/hooks/useEquipment.ts | 35 + .../apps/web/hooks/useForm.ts | 33 + .../apps/web/hooks/useInspection_plans.ts | 35 + .../apps/web/hooks/useItems.ts | 35 + .../apps/web/hooks/useTasks.ts | 35 + .../apps/web/next-env.d.ts | 6 + .../apps/web/next.config.ts | 7 + .../apps/web/package.json | 27 + .../apps/web/postcss.config.js | 6 + .../apps/web/services/api.ts | 49 + .../apps/web/services/auth.ts | 10 + .../apps/web/services/equipment.ts | 10 + .../apps/web/services/inspection_plans.ts | 10 + .../apps/web/services/items.ts | 10 + .../apps/web/services/tasks.ts | 10 + .../apps/web/tailwind.config.ts | 10 + .../apps/web/tsconfig.json | 40 + .../apps/web/types/index.ts | 134 + output/equipment-inspection/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 136 + .../packages/shared-types/tsconfig.json | 15 + output/equipment-inspection/scripts/build.mjs | 26 + output/equipment-inspection/scripts/dev.mjs | 32 + output/phase1-analysis.md | 63 + output/planning-refactor-report.md | 219 + output/planning-system-audit.md | 397 + output/real-world-qualification-report.md | 228 + output/warehouse-mgmt/.gitignore | 8 + output/warehouse-mgmt/README.md | 111 + output/warehouse-mgmt/apps/api/.gitignore | 7 + output/warehouse-mgmt/apps/api/README.md | 104 + output/warehouse-mgmt/apps/api/package.json | 29 + .../apps/api/src/__tests__/auth.test.ts | 96 + .../apps/api/src/__tests__/items.test.ts | 119 + .../api/src/__tests__/stock_alerts.test.ts | 119 + .../apps/api/src/__tests__/stock_in.test.ts | 120 + .../apps/api/src/__tests__/stock_out.test.ts | 121 + .../api/src/__tests__/stocktaking.test.ts | 121 + .../apps/api/src/__tests__/warehouse.test.ts | 120 + .../warehouse-mgmt/apps/api/src/db/client.ts | 93 + .../warehouse-mgmt/apps/api/src/db/schema.ts | 18 + output/warehouse-mgmt/apps/api/src/index.ts | 75 + .../apps/api/src/middleware/auth.ts | 41 + .../apps/api/src/routes/auth.ts | 121 + .../apps/api/src/routes/items.ts | 51 + .../apps/api/src/routes/stock_alerts.ts | 51 + .../apps/api/src/routes/stock_in.ts | 51 + .../apps/api/src/routes/stock_out.ts | 51 + .../apps/api/src/routes/stocktaking.ts | 51 + .../apps/api/src/routes/warehouse.ts | 51 + .../apps/api/src/services/item.ts | 54 + .../apps/api/src/services/stock_alert.ts | 54 + .../apps/api/src/services/stock_in.ts | 55 + .../apps/api/src/services/stock_out.ts | 56 + .../apps/api/src/services/stocktaking.ts | 56 + .../apps/api/src/services/warehouse.ts | 55 + .../apps/api/src/types/fastify.d.ts | 8 + .../apps/api/src/types/index.ts | 169 + .../apps/api/src/types/sql.js.d.ts | 27 + output/warehouse-mgmt/apps/api/tsconfig.json | 27 + .../warehouse-mgmt/apps/web/app/globals.css | 8 + .../apps/web/app/items/page.tsx | 51 + output/warehouse-mgmt/apps/web/app/layout.tsx | 30 + output/warehouse-mgmt/apps/web/app/page.tsx | 50 + .../apps/web/app/stock_alerts/page.tsx | 29 + .../apps/web/app/stock_in/page.tsx | 51 + .../apps/web/app/stock_out/page.tsx | 51 + .../apps/web/app/stocktaking/page.tsx | 51 + .../apps/web/app/warehouse/page.tsx | 29 + .../apps/web/components/layout/BottomNav.tsx | 38 + .../apps/web/components/layout/Sidebar.tsx | 44 + .../apps/web/components/note/NoteCard.tsx | 19 + .../apps/web/components/ui/Button.tsx | 31 + .../apps/web/components/ui/Card.tsx | 18 + .../apps/web/components/ui/EmptyState.tsx | 19 + .../apps/web/components/ui/Input.tsx | 22 + .../apps/web/components/ui/Modal.tsx | 33 + .../warehouse-mgmt/apps/web/hooks/useAuth.ts | 35 + .../apps/web/hooks/useDebounce.ts | 14 + .../warehouse-mgmt/apps/web/hooks/useForm.ts | 33 + .../warehouse-mgmt/apps/web/hooks/useItems.ts | 35 + .../apps/web/hooks/useStock_in.ts | 35 + .../apps/web/hooks/useStock_out.ts | 35 + .../apps/web/hooks/useStocktaking.ts | 35 + output/warehouse-mgmt/apps/web/next-env.d.ts | 6 + output/warehouse-mgmt/apps/web/next.config.ts | 7 + output/warehouse-mgmt/apps/web/package.json | 27 + .../warehouse-mgmt/apps/web/postcss.config.js | 6 + .../warehouse-mgmt/apps/web/services/api.ts | 49 + .../warehouse-mgmt/apps/web/services/auth.ts | 10 + .../warehouse-mgmt/apps/web/services/items.ts | 10 + .../apps/web/services/stock_in.ts | 10 + .../apps/web/services/stock_out.ts | 10 + .../apps/web/services/stocktaking.ts | 10 + .../apps/web/tailwind.config.ts | 10 + output/warehouse-mgmt/apps/web/tsconfig.json | 40 + output/warehouse-mgmt/apps/web/types/index.ts | 103 + output/warehouse-mgmt/package.json | 19 + .../packages/shared-config/package.json | 7 + .../packages/shared-config/src/index.ts | 16 + .../packages/shared-config/tsconfig.json | 15 + .../packages/shared-types/package.json | 7 + .../packages/shared-types/src/index.ts | 105 + .../packages/shared-types/tsconfig.json | 15 + output/warehouse-mgmt/scripts/build.mjs | 26 + output/warehouse-mgmt/scripts/dev.mjs | 32 + package.json | 31 + patterns/best-practices.md | 63 + patterns/design-patterns.md | 154 + patterns/memory-system-architecture.md | 94 + patterns/prompt-templates.md | 123 + playbooks/agent-evolution.md | 97 + playbooks/memory-audit.md | 101 + playbooks/tool-strategy.md | 111 + prd-example.json | 320 + progress.log | 15 + protocol-reference/metrics.md | 58 + protocol-reference/quality-gates.md | 54 + protocol-reference/templates.md | 147 + reflections/2026-06-03-agent-evolution.md | 109 + reflections/2026-06-04-memory-audit.md | 64 + reflections/2026-06-05-ddl-leakage.md | 31 + reflections/2026-06-05-falsy-coercion.md | 28 + reflections/2026-06-05-jsx-template.md | 32 + reflections/2026-06-05-nextjs-path.md | 31 + research/claude-code-analysis.md | 448 + research/claude-code-learnings.md | 153 + research/hermes-agent-study.md | 72 + research/hermes-internalization-guide.md | 92 + .../pr-37/docs/pr-37-delivery-planning.md | 241 + .../pr-37/docs/pr-37-domain-intelligence.md | 281 + .../pr-37-delivery-planning/batch-planner.mjs | 284 + .../capacity-planner.mjs | 277 + .../pr-37/pr-37-delivery-planning/index.mjs | 195 + .../release-planner.mjs | 385 + .../roadmap-aligner.mjs | 375 + .../sprint-planner.mjs | 473 + ...in-intelligence-delivery-planning.test.mjs | 830 + .../domain-classifier.mjs | 456 + .../domain-registry.mjs | 1278 ++ .../domain-validator.mjs | 290 + .../pr-37/pr-37-domain-intelligence/index.mjs | 179 + .../industry-epic-generator.mjs | 265 + .../industry-feature-generator.mjs | 263 + rules/error-handling.md | 44 + rules/git-convention.md | 33 + rules/task-workflow.md | 139 + rules/tool-definition.md | 38 + scripts/architecture-agent.mjs | 1220 ++ scripts/audit-agents.mjs | 249 + scripts/audit-all.mjs | 197 + scripts/audit-governance.mjs | 347 + scripts/audit-portfolio.mjs | 339 + scripts/auto-checkpoint.sh | 28 + scripts/backend-builder-agent.mjs | 1464 ++ scripts/check-deprecations.mjs | 472 + scripts/contract-consistency-test.mjs | 776 + scripts/contract-e2e.mjs | 206 + scripts/domain-benchmark.mjs | 873 + scripts/dream-cycle.sh | 349 + scripts/e2e-benchmark-v1.mjs | 385 + scripts/electron-builder-agent.mjs | 1200 ++ scripts/extract-skill.sh | 64 + scripts/factory-router.mjs | 85 + scripts/frontend-builder-agent.mjs | 2112 +++ scripts/fullstack-composer-agent.mjs | 764 + scripts/guard-all.sh | 103 + scripts/guard-dreaming-phase.mjs | 106 + scripts/guard-memory-backend.mjs | 120 + scripts/guard-memory-write.mjs | 141 + scripts/guard-runtime-core.mjs | 189 + scripts/guard-tool-path.mjs | 143 + scripts/guard-tool-trace.mjs | 132 + scripts/init-workspace.sh | 42 + scripts/lib/deprecation-warning.mjs | 74 + scripts/memory-auto.sh | 196 + scripts/memory-sync.sh | 210 + scripts/model-contract.mjs | 648 + scripts/orchestrator.mjs | 263 + scripts/production-check.sh | 372 + scripts/project-intake-agent.mjs | 1043 ++ scripts/regression-gate.mjs | 165 + scripts/release-benchmark.mjs | 394 + scripts/release-builder-agent.mjs | 542 + scripts/runtime.mjs | 86 + scripts/session-compact.sh | 269 + scripts/smoke-test.mjs | 144 + scripts/sync-agent-repos.sh | 76 + scripts/templates/blocker.md.template | 26 + scripts/templates/dashboard.md.template | 53 + scripts/templates/execution-audit.md.template | 57 + scripts/templates/portfolio-audit.md.template | 54 + scripts/templates/portfolio.md.template | 26 + scripts/templates/progress.log.template | 21 + scripts/templates/recovery-report.md.template | 55 + scripts/test-baseline.sh | 82 + scripts/upgrade-test.mjs | 382 + scripts/watchdog.sh | 85 + skills/code-explorer.md | 86 + skills/compression.md | 84 + skills/security-check.md | 56 + skills/self-evolution.md | 84 + src/active-memory/baseline-thresholds.mjs | 60 + .../compare-release-candidate-baseline.mjs | 122 + src/active-memory/evaluate-release-gate.mjs | 166 + .../generate-release-candidate-report.mjs | 268 + src/active-memory/precompute-cache.mjs | 162 + .../precompute-circuit-breaker.mjs | 292 + src/active-memory/precompute-flag.mjs | 174 + src/active-memory/precompute-gateway.mjs | 286 + .../precompute-readiness-gate.mjs | 381 + ...precompute-release-candidate-checklist.mjs | 293 + .../precompute-replacement-policy.mjs | 376 + .../precompute-rollout-controller.mjs | 144 + .../precompute-rollout-stage-policy.mjs | 414 + src/active-memory/precompute-runner.mjs | 218 + src/active-memory/precompute-shadow-diff.mjs | 319 + .../precompute-shadow-integration.mjs | 934 + src/agent-adapters/agent-adapter.mjs | 133 + src/agent-adapters/claw-adapter.mjs | 125 + src/agent-adapters/mock-adapter.mjs | 96 + src/agent-runtime/adapters.mjs | 434 + src/agent-runtime/artifact-registry.mjs | 153 + src/agent-runtime/domain.mjs | 447 + src/agent-runtime/index.mjs | 48 + src/agent-runtime/pipeline.mjs | 369 + src/agent-runtime/reporting.mjs | 405 + src/agent-runtime/state-machine.mjs | 288 + src/codegraph/index.js | 795 + src/codegraph/schema.sql | 97 + src/compression/byte-surgery.js | 323 + src/compression/ccr-backends.js | 358 + src/compression/ccr-store.js | 236 + src/compression/ctx-compress.js | 372 + src/compression/detector.js | 187 + src/compression/embedding-scorer.js | 322 + src/compression/smart-crusher.js | 624 + src/config/toml-config.js | 389 + src/config/toml-parser.js | 278 + src/governance/index.js | 434 + src/memory-core/memory-core-facade.mjs | 280 + src/memory-core/memory-core-types.mjs | 106 + .../memory-fts5-search-adapter.mjs | 209 + src/memory-core/memory-search-adapter.mjs | 178 + src/memory-core/memory-store-adapter.mjs | 276 + src/memory/context-graph.js | 457 + src/memory/sync-context-graph.js | 121 + src/orchestration/cross-session.js | 414 + src/orchestration/session-fsm.js | 386 + src/security/risk-scorer.js | 236 + src/sf-01-product-strategy/business-model.mjs | 364 + .../competitor-analysis.mjs | 203 + src/sf-01-product-strategy/domain-model.mjs | 568 + src/sf-01-product-strategy/gtm-roadmap.mjs | 315 + src/sf-01-product-strategy/idea-analysis.mjs | 480 + src/sf-01-product-strategy/index.mjs | 380 + .../market-analysis.mjs | 254 + src/sf-01-product-strategy/viability.mjs | 353 + .../dependency-engine.mjs | 617 + .../domain-model.mjs | 960 + src/sf-02-requirement-engineering/index.mjs | 678 + .../strategy-to-requirement.mjs | 1040 ++ src/software-factory-core/artifact-graph.mjs | 603 + .../factory-contract.mjs | 468 + .../factory-registry.mjs | 560 + src/software-factory-core/index.mjs | 39 + test/architecture-agent.test.mjs | 703 + .../architecture/deprecation-warning.test.mjs | 271 + test/architecture/freeze.test.mjs | 168 + test/architecture/guard.test.mjs | 76 + test/architecture/production-check.test.mjs | 132 + test/backend-builder-agent.test.mjs | 567 + test/baseline/smoke.test.mjs | 65 + .../baseline/test-01-gateway-startup.test.mjs | 91 + test/baseline/test-02-session-create.test.mjs | 83 + test/baseline/test-03-memory-search.test.mjs | 108 + test/baseline/test-04-memory-get.test.mjs | 71 + test/baseline/test-05-tool-exec.test.mjs | 94 + test/baseline/test-06-agent-reply.test.mjs | 85 + test/baseline/test-07-mcp-discovery.test.mjs | 101 + test/baseline/test-08-config-load.test.mjs | 109 + test/baseline/test-09-multi-session.test.mjs | 115 + .../test-10-session-recovery.test.mjs | 90 + test/baseline/test-11-compaction.test.mjs | 110 + test/baseline/test-12-active-memory.test.mjs | 149 + test/benchmark/domain-benchmark-suite.mjs | 451 + test/benchmark/smoke-one.mjs | 146 + test/e2e-regression.test.mjs | 373 + test/electron-builder-agent.test.mjs | 453 + .../mock-agent/archives/deprecated.md | 1 + .../mock-agent/archives/old-001.md | 1 + .../mock-agent/archives/old-002.md | 1 + .../mock-agent/contexts/ctx-main.md | 1 + .../mock-agent/contexts/ctx-projects.md | 1 + .../mock-agent/contexts/ctx-settings.md | 1 + .../mock-agent/threads/th-001.md | 1 + .../mock-agent/threads/th-002.md | 1 + .../mock-agent/threads/th-003.md | 1 + .../mock-agent/threads/th-004.md | 1 + .../mock-agent/threads/th-005.md | 1 + .../degrading-agent-history.json | 41 + .../healthy-agent-history.json | 41 + .../improving-agent-history.json | 57 + .../mixed-fleet-history.json | 103 + .../unstable-agent-history.json | 56 + .../agent-registry/deprecated-agent.json | 29 + .../agent-registry/duplicate-agent-id.json | 29 + .../agent-registry/empty-registry.json | 4 + .../missing-required-field.json | 11 + .../agent-registry/multi-agent-registry.json | 53 + .../agent-registry/unsupported-agent.json | 17 + .../agent-registry/valid-registry.json | 17 + .../architecture-agent/ecommerce-prd.json | 1 + .../architecture-agent/education-prd.json | 1 + .../architecture-agent/empty-prd.json | 5 + .../architecture-agent/enterprise-prd.json | 1 + .../architecture-agent/generic-prd.json | 24 + .../fixtures/architecture-agent/note-prd.json | 1 + .../certification-revoked-input.json | 24 + .../compatibility-failure-input.json | 24 + .../health-degradation-input.json | 24 + .../mixed-severity-input.json | 47 + .../release-failure-input.json | 24 + .../warning-only-input.json | 27 + .../baseline-evolution/evolution-report.json | 110 + .../threshold-stability-report.json | 259 + .../baseline-evolution/v1-baseline.json | 19 + .../baseline-evolution/v2-baseline.json | 23 + .../baseline-evolution/v3-baseline.json | 14 + .../baseline-rotation/current-baseline.json | 7 + .../baseline-rotation/rotation-policy.json | 8 + .../baseline-rotation/v1-baseline.json | 19 + .../baseline-rotation/v2-baseline.json | 23 + .../baseline-rotation/v3-baseline.json | 14 + .../claw-0.1.x-deprecated.json | 48 + .../claw-1.0.x-current-compatible.json | 35 + .../claw-1.0.x-missing-candidates.json | 49 + .../claw-1.1.x-minor-add-fields.json | 46 + .../claw-1.2.x-minor-rename-fields.json | 47 + .../claw-1.3.x-new-directory.json | 48 + .../claw-1.4.x-middleware-compat.json | 49 + .../claw-1.5.x-directory-change.json | 49 + .../claw-2.0.x-major-schema-change.json | 49 + .../critical-monitoring-history.json | 81 + .../degrading-monitoring-history.json | 67 + .../healthy-monitoring-history.json | 67 + .../missing-data-monitoring-history.json | 35 + .../mixed-monitoring-history.json | 221 + .../rollback-spike-history.json | 81 + .../all-healthy-fleet.json | 31 + .../conditionally-ready-fleet.json | 41 + .../high-risk-fleet.json | 27 + .../mixed-fleet.json | 36 + .../not-ready-fleet.json | 25 + .../incident-management/empty-input.json | 6 + .../lifecycle-test-input.json | 14 + .../mixed-fleet-input.json | 17 + .../multiple-critical-input.json | 12 + .../recovery-tracking-input.json | 12 + .../repeat-offender-input.json | 14 + .../healthy-dashboard-input.json | 41 + .../missing-data-dashboard-input.json | 38 + .../mixed-risk-dashboard-input.json | 67 + .../multi-agent-dashboard-output.json | 57 + .../unsupported-agent-dashboard-input.json | 26 + .../production-readiness/full-pass.json | 16 + .../missing-baseline.json | 16 + .../production-readiness/missing-compat.json | 16 + .../missing-dashboard.json | 16 + .../candidate-snapshot.json | 11 + .../stable-baseline.json | 19 + .../validation-report-output.md | 56 + .../validation-report.json | 168 + .../project-intake/ecommerce-miniapp.json | 4 + .../fixtures/project-intake/edu-platform.json | 4 + test/fixtures/project-intake/empty-input.json | 4 + .../project-intake/enterprise-oa.json | 4 + test/fixtures/project-intake/fitness-app.json | 4 + test/fixtures/project-intake/minimal.json | 4 + .../project-intake/pet-management.json | 4 + test/fixtures/release-history/empty.json | 1 + .../release-history/sample-events.json | 110 + .../release-history/single-release.json | 11 + .../release-pipeline/baseline-fail.json | 16 + .../release-pipeline/baseline-pass.json | 16 + .../release-pipeline/gate-closed.json | 4 + test/fixtures/release-pipeline/gate-open.json | 4 + .../release-pipeline/rc-check-fail.json | 7 + .../release-pipeline/rc-check-pass.json | 7 + test/fixtures/release-pipeline/report-fail.md | 89 + test/fixtures/release-pipeline/report-pass.md | 56 + .../reliability-center/empty-input.json | 6 + .../error-budget-input.json | 37 + .../healthy-fleet-input.json | 97 + .../low-reliability-input.json | 37 + .../reliability-center/mixed-fleet-input.json | 247 + .../slo-violation-input.json | 37 + .../reliability-center/slo-warning-input.json | 67 + test/frontend-builder-agent.test.mjs | 373 + test/fullstack-composer-agent.test.mjs | 464 + test/memory-core/facade-health.test.mjs | 167 + test/memory-core/facade.test.mjs | 243 + test/memory-core/fts5-adapter.test.mjs | 186 + test/memory-core/get-shadow.test.mjs | 222 + test/memory-core/search-shadow.test.mjs | 143 + test/orchestrator.test.mjs | 215 + test/project-intake-agent.test.mjs | 622 + test/release-builder-agent.test.mjs | 498 + test/sf-01-product-strategy.test.mjs | 796 + test/sf-02-requirement-engineering.test.mjs | 1427 ++ test/software-factory-core.test.mjs | 332 + tools/agent-decomposition.md | 171 + tools/workspace-commands.md | 58 + upgrade-playbook.md | 194 + version-contract.md | 102 + 3201 files changed, 231817 insertions(+) create mode 100644 .benchmark/appointment/arch.json create mode 100644 .benchmark/appointment/backend/.gitignore create mode 100644 .benchmark/appointment/backend/README.md create mode 100644 .benchmark/appointment/backend/package.json create mode 100644 .benchmark/appointment/backend/src/__tests__/appointments.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/approvals.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/clients.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/contracts.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/customers.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/items.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/reminders.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/services.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/stats.test.ts create mode 100644 .benchmark/appointment/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/appointment/backend/src/db/client.ts create mode 100644 .benchmark/appointment/backend/src/db/schema.ts create mode 100644 .benchmark/appointment/backend/src/index.ts create mode 100644 .benchmark/appointment/backend/src/middleware/auth.ts create mode 100644 .benchmark/appointment/backend/src/routes/appointments.ts create mode 100644 .benchmark/appointment/backend/src/routes/approvals.ts create mode 100644 .benchmark/appointment/backend/src/routes/auth.ts create mode 100644 .benchmark/appointment/backend/src/routes/clients.ts create mode 100644 .benchmark/appointment/backend/src/routes/contracts.ts create mode 100644 .benchmark/appointment/backend/src/routes/customers.ts create mode 100644 .benchmark/appointment/backend/src/routes/items.ts create mode 100644 .benchmark/appointment/backend/src/routes/reminders.ts create mode 100644 .benchmark/appointment/backend/src/routes/services.ts create mode 100644 .benchmark/appointment/backend/src/routes/stats.ts create mode 100644 .benchmark/appointment/backend/src/routes/users.ts create mode 100644 .benchmark/appointment/backend/src/services/appointment.ts create mode 100644 .benchmark/appointment/backend/src/services/approval.ts create mode 100644 .benchmark/appointment/backend/src/services/client.ts create mode 100644 .benchmark/appointment/backend/src/services/contract.ts create mode 100644 .benchmark/appointment/backend/src/services/customer.ts create mode 100644 .benchmark/appointment/backend/src/services/item.ts create mode 100644 .benchmark/appointment/backend/src/services/reminder.ts create mode 100644 .benchmark/appointment/backend/src/services/service.ts create mode 100644 .benchmark/appointment/backend/src/services/stat.ts create mode 100644 .benchmark/appointment/backend/src/services/user.ts create mode 100644 .benchmark/appointment/backend/src/types/fastify.d.ts create mode 100644 .benchmark/appointment/backend/src/types/index.ts create mode 100644 .benchmark/appointment/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/appointment/backend/tsconfig.json create mode 100644 .benchmark/appointment/electron/README.md create mode 100644 .benchmark/appointment/electron/assets/icon.png create mode 100644 .benchmark/appointment/electron/electron-builder.yml create mode 100644 .benchmark/appointment/electron/electron/ipc.ts create mode 100644 .benchmark/appointment/electron/electron/main.ts create mode 100644 .benchmark/appointment/electron/electron/preload.ts create mode 100644 .benchmark/appointment/electron/electron/tray.ts create mode 100644 .benchmark/appointment/electron/electron/updater.ts create mode 100644 .benchmark/appointment/electron/electron/utils.ts create mode 100644 .benchmark/appointment/electron/entitlements.mac.plist create mode 100644 .benchmark/appointment/electron/package.json create mode 100644 .benchmark/appointment/electron/tsconfig.json create mode 100644 .benchmark/appointment/frontend/app/appointments/page.tsx create mode 100644 .benchmark/appointment/frontend/app/clients/page.tsx create mode 100644 .benchmark/appointment/frontend/app/feature/page.tsx create mode 100644 .benchmark/appointment/frontend/app/globals.css create mode 100644 .benchmark/appointment/frontend/app/layout.tsx create mode 100644 .benchmark/appointment/frontend/app/page.tsx create mode 100644 .benchmark/appointment/frontend/app/profile/page.tsx create mode 100644 .benchmark/appointment/frontend/app/services/page.tsx create mode 100644 .benchmark/appointment/frontend/app/stats/page.tsx create mode 100644 .benchmark/appointment/frontend/app/客户通知/page.tsx create mode 100644 .benchmark/appointment/frontend/app/时间段预约/page.tsx create mode 100644 .benchmark/appointment/frontend/app/服务项目/page.tsx create mode 100644 .benchmark/appointment/frontend/app/预约统计/page.tsx create mode 100644 .benchmark/appointment/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/appointment/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/appointment/frontend/components/ui/Button.tsx create mode 100644 .benchmark/appointment/frontend/components/ui/Card.tsx create mode 100644 .benchmark/appointment/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/appointment/frontend/components/ui/Input.tsx create mode 100644 .benchmark/appointment/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/appointment/frontend/hooks/useAppointments.ts create mode 100644 .benchmark/appointment/frontend/hooks/useAuth.ts create mode 100644 .benchmark/appointment/frontend/hooks/useClients.ts create mode 100644 .benchmark/appointment/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/appointment/frontend/hooks/useForm.ts create mode 100644 .benchmark/appointment/frontend/hooks/useHealth.ts create mode 100644 .benchmark/appointment/frontend/hooks/useItem.ts create mode 100644 .benchmark/appointment/frontend/hooks/useServices.ts create mode 100644 .benchmark/appointment/frontend/hooks/useStats.ts create mode 100644 .benchmark/appointment/frontend/hooks/useUsers.ts create mode 100644 .benchmark/appointment/frontend/next-env.d.ts create mode 100644 .benchmark/appointment/frontend/next.config.ts create mode 100644 .benchmark/appointment/frontend/package.json create mode 100644 .benchmark/appointment/frontend/postcss.config.js create mode 100644 .benchmark/appointment/frontend/services/api.ts create mode 100644 .benchmark/appointment/frontend/services/appointments.ts create mode 100644 .benchmark/appointment/frontend/services/auth.ts create mode 100644 .benchmark/appointment/frontend/services/clients.ts create mode 100644 .benchmark/appointment/frontend/services/health.ts create mode 100644 .benchmark/appointment/frontend/services/services.ts create mode 100644 .benchmark/appointment/frontend/services/stats.ts create mode 100644 .benchmark/appointment/frontend/services/users.ts create mode 100644 .benchmark/appointment/frontend/services/客户通知.ts create mode 100644 .benchmark/appointment/frontend/services/时间段预约.ts create mode 100644 .benchmark/appointment/frontend/services/服务项目.ts create mode 100644 .benchmark/appointment/frontend/services/预约统计.ts create mode 100644 .benchmark/appointment/frontend/tailwind.config.ts create mode 100644 .benchmark/appointment/frontend/tsconfig.json create mode 100644 .benchmark/appointment/frontend/types/index.ts create mode 100644 .benchmark/appointment/fullstack/.gitignore create mode 100644 .benchmark/appointment/fullstack/README.md create mode 100644 .benchmark/appointment/fullstack/apps/api/.gitignore create mode 100644 .benchmark/appointment/fullstack/apps/api/README.md create mode 100644 .benchmark/appointment/fullstack/apps/api/package.json create mode 100644 .benchmark/appointment/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/__tests__/items.test.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/routes/items.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/services/item.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/appointment/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/appointment/fullstack/apps/web/app/feature/page.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/appointment/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/app/profile/page.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/appointment/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/hooks/useHealth.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/hooks/useUsers.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/package.json create mode 100644 .benchmark/appointment/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/appointment/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/services/health.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/services/users.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/appointment/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/appointment/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/appointment/fullstack/package.json create mode 100644 .benchmark/appointment/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/appointment/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/appointment/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/appointment/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/appointment/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/appointment/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/appointment/fullstack/scripts/build.mjs create mode 100644 .benchmark/appointment/fullstack/scripts/dev.mjs create mode 100644 .benchmark/appointment/prd.json create mode 100644 .benchmark/appointment/release/release/README.md create mode 100644 .benchmark/appointment/release/release/build-info.json create mode 100644 .benchmark/appointment/release/release/checksums/checksums.txt create mode 100644 .benchmark/appointment/release/release/linux/myproject-0.1.0.AppImage create mode 100644 .benchmark/appointment/release/release/linux/myproject_0.1.0_amd64.deb create mode 100644 .benchmark/appointment/release/release/macos/myproject-0.1.0-arm64.dmg create mode 100644 .benchmark/appointment/release/release/macos/myproject-0.1.0.dmg create mode 100644 .benchmark/appointment/release/release/manifests/manifest.json create mode 100644 .benchmark/appointment/release/release/release-notes/release-notes.md create mode 100644 .benchmark/appointment/release/release/version.json create mode 100644 .benchmark/appointment/release/release/windows/myproject-0.1.0-portable.exe create mode 100644 .benchmark/appointment/release/release/windows/myproject-setup-0.1.0.exe create mode 100644 .benchmark/asset/arch.json create mode 100644 .benchmark/asset/backend/.gitignore create mode 100644 .benchmark/asset/backend/README.md create mode 100644 .benchmark/asset/backend/package.json create mode 100644 .benchmark/asset/backend/src/__tests__/approvals.test.ts create mode 100644 .benchmark/asset/backend/src/__tests__/assets.test.ts create mode 100644 .benchmark/asset/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/asset/backend/src/__tests__/contracts.test.ts create mode 100644 .benchmark/asset/backend/src/__tests__/customers.test.ts create mode 100644 .benchmark/asset/backend/src/__tests__/items.test.ts create mode 100644 .benchmark/asset/backend/src/__tests__/reminders.test.ts create mode 100644 .benchmark/asset/backend/src/__tests__/stats.test.ts create mode 100644 .benchmark/asset/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/asset/backend/src/db/client.ts create mode 100644 .benchmark/asset/backend/src/db/schema.ts create mode 100644 .benchmark/asset/backend/src/index.ts create mode 100644 .benchmark/asset/backend/src/middleware/auth.ts create mode 100644 .benchmark/asset/backend/src/routes/approvals.ts create mode 100644 .benchmark/asset/backend/src/routes/assets.ts create mode 100644 .benchmark/asset/backend/src/routes/auth.ts create mode 100644 .benchmark/asset/backend/src/routes/contracts.ts create mode 100644 .benchmark/asset/backend/src/routes/customers.ts create mode 100644 .benchmark/asset/backend/src/routes/items.ts create mode 100644 .benchmark/asset/backend/src/routes/reminders.ts create mode 100644 .benchmark/asset/backend/src/routes/stats.ts create mode 100644 .benchmark/asset/backend/src/routes/users.ts create mode 100644 .benchmark/asset/backend/src/services/approval.ts create mode 100644 .benchmark/asset/backend/src/services/asset.ts create mode 100644 .benchmark/asset/backend/src/services/contract.ts create mode 100644 .benchmark/asset/backend/src/services/customer.ts create mode 100644 .benchmark/asset/backend/src/services/item.ts create mode 100644 .benchmark/asset/backend/src/services/reminder.ts create mode 100644 .benchmark/asset/backend/src/services/stat.ts create mode 100644 .benchmark/asset/backend/src/services/user.ts create mode 100644 .benchmark/asset/backend/src/types/fastify.d.ts create mode 100644 .benchmark/asset/backend/src/types/index.ts create mode 100644 .benchmark/asset/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/asset/backend/tsconfig.json create mode 100644 .benchmark/asset/electron/README.md create mode 100644 .benchmark/asset/electron/assets/icon.png create mode 100644 .benchmark/asset/electron/electron-builder.yml create mode 100644 .benchmark/asset/electron/electron/ipc.ts create mode 100644 .benchmark/asset/electron/electron/main.ts create mode 100644 .benchmark/asset/electron/electron/preload.ts create mode 100644 .benchmark/asset/electron/electron/tray.ts create mode 100644 .benchmark/asset/electron/electron/updater.ts create mode 100644 .benchmark/asset/electron/electron/utils.ts create mode 100644 .benchmark/asset/electron/entitlements.mac.plist create mode 100644 .benchmark/asset/electron/package.json create mode 100644 .benchmark/asset/electron/tsconfig.json create mode 100644 .benchmark/asset/frontend/app/assets/page.tsx create mode 100644 .benchmark/asset/frontend/app/feature/page.tsx create mode 100644 .benchmark/asset/frontend/app/globals.css create mode 100644 .benchmark/asset/frontend/app/items/page.tsx create mode 100644 .benchmark/asset/frontend/app/layout.tsx create mode 100644 .benchmark/asset/frontend/app/page.tsx create mode 100644 .benchmark/asset/frontend/app/profile/page.tsx create mode 100644 .benchmark/asset/frontend/app/stats/page.tsx create mode 100644 .benchmark/asset/frontend/app/折旧计算/page.tsx create mode 100644 .benchmark/asset/frontend/app/盘点统计/page.tsx create mode 100644 .benchmark/asset/frontend/app/资产登记/page.tsx create mode 100644 .benchmark/asset/frontend/app/领用归还/page.tsx create mode 100644 .benchmark/asset/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/asset/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/asset/frontend/components/ui/Button.tsx create mode 100644 .benchmark/asset/frontend/components/ui/Card.tsx create mode 100644 .benchmark/asset/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/asset/frontend/components/ui/Input.tsx create mode 100644 .benchmark/asset/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/asset/frontend/hooks/useAssets.ts create mode 100644 .benchmark/asset/frontend/hooks/useAuth.ts create mode 100644 .benchmark/asset/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/asset/frontend/hooks/useForm.ts create mode 100644 .benchmark/asset/frontend/hooks/useHealth.ts create mode 100644 .benchmark/asset/frontend/hooks/useItem.ts create mode 100644 .benchmark/asset/frontend/hooks/useItems.ts create mode 100644 .benchmark/asset/frontend/hooks/useStats.ts create mode 100644 .benchmark/asset/frontend/hooks/useUsers.ts create mode 100644 .benchmark/asset/frontend/next-env.d.ts create mode 100644 .benchmark/asset/frontend/next.config.ts create mode 100644 .benchmark/asset/frontend/package.json create mode 100644 .benchmark/asset/frontend/postcss.config.js create mode 100644 .benchmark/asset/frontend/services/api.ts create mode 100644 .benchmark/asset/frontend/services/assets.ts create mode 100644 .benchmark/asset/frontend/services/auth.ts create mode 100644 .benchmark/asset/frontend/services/health.ts create mode 100644 .benchmark/asset/frontend/services/items.ts create mode 100644 .benchmark/asset/frontend/services/stats.ts create mode 100644 .benchmark/asset/frontend/services/users.ts create mode 100644 .benchmark/asset/frontend/services/折旧计算.ts create mode 100644 .benchmark/asset/frontend/services/盘点统计.ts create mode 100644 .benchmark/asset/frontend/services/资产登记.ts create mode 100644 .benchmark/asset/frontend/services/领用归还.ts create mode 100644 .benchmark/asset/frontend/tailwind.config.ts create mode 100644 .benchmark/asset/frontend/tsconfig.json create mode 100644 .benchmark/asset/frontend/types/index.ts create mode 100644 .benchmark/asset/fullstack/.gitignore create mode 100644 .benchmark/asset/fullstack/README.md create mode 100644 .benchmark/asset/fullstack/apps/api/.gitignore create mode 100644 .benchmark/asset/fullstack/apps/api/README.md create mode 100644 .benchmark/asset/fullstack/apps/api/package.json create mode 100644 .benchmark/asset/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/__tests__/items.test.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/routes/items.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/services/item.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/asset/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/asset/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/asset/fullstack/apps/web/app/feature/page.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/asset/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/app/profile/page.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/asset/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/asset/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/asset/fullstack/apps/web/hooks/useHealth.ts create mode 100644 .benchmark/asset/fullstack/apps/web/hooks/useUsers.ts create mode 100644 .benchmark/asset/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/asset/fullstack/apps/web/package.json create mode 100644 .benchmark/asset/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/asset/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/asset/fullstack/apps/web/services/health.ts create mode 100644 .benchmark/asset/fullstack/apps/web/services/users.ts create mode 100644 .benchmark/asset/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/asset/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/asset/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/asset/fullstack/package.json create mode 100644 .benchmark/asset/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/asset/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/asset/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/asset/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/asset/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/asset/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/asset/fullstack/scripts/build.mjs create mode 100644 .benchmark/asset/fullstack/scripts/dev.mjs create mode 100644 .benchmark/asset/prd.json create mode 100644 .benchmark/asset/release/release/README.md create mode 100644 .benchmark/asset/release/release/build-info.json create mode 100644 .benchmark/asset/release/release/checksums/checksums.txt create mode 100644 .benchmark/asset/release/release/linux/myproject-0.1.0.AppImage create mode 100644 .benchmark/asset/release/release/linux/myproject_0.1.0_amd64.deb create mode 100644 .benchmark/asset/release/release/macos/myproject-0.1.0-arm64.dmg create mode 100644 .benchmark/asset/release/release/macos/myproject-0.1.0.dmg create mode 100644 .benchmark/asset/release/release/manifests/manifest.json create mode 100644 .benchmark/asset/release/release/release-notes/release-notes.md create mode 100644 .benchmark/asset/release/release/version.json create mode 100644 .benchmark/asset/release/release/windows/myproject-0.1.0-portable.exe create mode 100644 .benchmark/asset/release/release/windows/myproject-setup-0.1.0.exe create mode 100644 .benchmark/blog-cms/arch.json create mode 100644 .benchmark/blog-cms/backend/.gitignore create mode 100644 .benchmark/blog-cms/backend/README.md create mode 100644 .benchmark/blog-cms/backend/package.json create mode 100644 .benchmark/blog-cms/backend/src/__tests__/approvals.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/articles.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/comments.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/contracts.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/customers.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/items.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/media.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/reminders.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/tags.test.ts create mode 100644 .benchmark/blog-cms/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/blog-cms/backend/src/db/client.ts create mode 100644 .benchmark/blog-cms/backend/src/db/schema.ts create mode 100644 .benchmark/blog-cms/backend/src/index.ts create mode 100644 .benchmark/blog-cms/backend/src/middleware/auth.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/approvals.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/articles.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/auth.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/comments.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/contracts.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/customers.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/items.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/media.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/reminders.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/tags.ts create mode 100644 .benchmark/blog-cms/backend/src/routes/users.ts create mode 100644 .benchmark/blog-cms/backend/src/services/approval.ts create mode 100644 .benchmark/blog-cms/backend/src/services/article.ts create mode 100644 .benchmark/blog-cms/backend/src/services/comment.ts create mode 100644 .benchmark/blog-cms/backend/src/services/contract.ts create mode 100644 .benchmark/blog-cms/backend/src/services/customer.ts create mode 100644 .benchmark/blog-cms/backend/src/services/item.ts create mode 100644 .benchmark/blog-cms/backend/src/services/media.ts create mode 100644 .benchmark/blog-cms/backend/src/services/reminder.ts create mode 100644 .benchmark/blog-cms/backend/src/services/tag.ts create mode 100644 .benchmark/blog-cms/backend/src/services/user.ts create mode 100644 .benchmark/blog-cms/backend/src/types/fastify.d.ts create mode 100644 .benchmark/blog-cms/backend/src/types/index.ts create mode 100644 .benchmark/blog-cms/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/blog-cms/backend/tsconfig.json create mode 100644 .benchmark/blog-cms/electron/README.md create mode 100644 .benchmark/blog-cms/electron/assets/icon.png create mode 100644 .benchmark/blog-cms/electron/electron-builder.yml create mode 100644 .benchmark/blog-cms/electron/electron/ipc.ts create mode 100644 .benchmark/blog-cms/electron/electron/main.ts create mode 100644 .benchmark/blog-cms/electron/electron/preload.ts create mode 100644 .benchmark/blog-cms/electron/electron/tray.ts create mode 100644 .benchmark/blog-cms/electron/electron/updater.ts create mode 100644 .benchmark/blog-cms/electron/electron/utils.ts create mode 100644 .benchmark/blog-cms/electron/entitlements.mac.plist create mode 100644 .benchmark/blog-cms/electron/package.json create mode 100644 .benchmark/blog-cms/electron/tsconfig.json create mode 100644 .benchmark/blog-cms/frontend/app/articles/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/comments/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/globals.css create mode 100644 .benchmark/blog-cms/frontend/app/layout.tsx create mode 100644 .benchmark/blog-cms/frontend/app/media/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/messages/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/profile/[userId]/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/publish/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/tags/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/分类标签/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/媒体库/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/文章发布/page.tsx create mode 100644 .benchmark/blog-cms/frontend/app/评论管理/page.tsx create mode 100644 .benchmark/blog-cms/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/blog-cms/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/blog-cms/frontend/components/ui/Button.tsx create mode 100644 .benchmark/blog-cms/frontend/components/ui/Card.tsx create mode 100644 .benchmark/blog-cms/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/blog-cms/frontend/components/ui/Input.tsx create mode 100644 .benchmark/blog-cms/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/blog-cms/frontend/hooks/useArticles.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useAuth.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useComments.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useFeed.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useForm.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useItem.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useMedia.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/usePosts.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useTags.ts create mode 100644 .benchmark/blog-cms/frontend/hooks/useUsers.ts create mode 100644 .benchmark/blog-cms/frontend/next-env.d.ts create mode 100644 .benchmark/blog-cms/frontend/next.config.ts create mode 100644 .benchmark/blog-cms/frontend/package.json create mode 100644 .benchmark/blog-cms/frontend/postcss.config.js create mode 100644 .benchmark/blog-cms/frontend/services/api.ts create mode 100644 .benchmark/blog-cms/frontend/services/articles.ts create mode 100644 .benchmark/blog-cms/frontend/services/auth.ts create mode 100644 .benchmark/blog-cms/frontend/services/comments.ts create mode 100644 .benchmark/blog-cms/frontend/services/feed.ts create mode 100644 .benchmark/blog-cms/frontend/services/media.ts create mode 100644 .benchmark/blog-cms/frontend/services/posts.ts create mode 100644 .benchmark/blog-cms/frontend/services/tags.ts create mode 100644 .benchmark/blog-cms/frontend/services/users.ts create mode 100644 .benchmark/blog-cms/frontend/services/分类标签.ts create mode 100644 .benchmark/blog-cms/frontend/services/媒体库.ts create mode 100644 .benchmark/blog-cms/frontend/services/文章发布.ts create mode 100644 .benchmark/blog-cms/frontend/services/评论管理.ts create mode 100644 .benchmark/blog-cms/frontend/tailwind.config.ts create mode 100644 .benchmark/blog-cms/frontend/tsconfig.json create mode 100644 .benchmark/blog-cms/frontend/types/index.ts create mode 100644 .benchmark/blog-cms/fullstack/.gitignore create mode 100644 .benchmark/blog-cms/fullstack/README.md create mode 100644 .benchmark/blog-cms/fullstack/apps/api/.gitignore create mode 100644 .benchmark/blog-cms/fullstack/apps/api/README.md create mode 100644 .benchmark/blog-cms/fullstack/apps/api/package.json create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/__tests__/items.test.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/routes/items.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/services/item.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/blog-cms/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/blog-cms/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/app/messages/page.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/app/profile/[userId]/page.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/app/publish/page.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/blog-cms/fullstack/apps/web/hooks/useComments.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/hooks/useFeed.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/hooks/usePosts.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/hooks/useUsers.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/package.json create mode 100644 .benchmark/blog-cms/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/blog-cms/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/services/comments.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/services/feed.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/services/posts.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/services/users.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/blog-cms/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/blog-cms/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/blog-cms/fullstack/package.json create mode 100644 .benchmark/blog-cms/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/blog-cms/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/blog-cms/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/blog-cms/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/blog-cms/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/blog-cms/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/blog-cms/fullstack/scripts/build.mjs create mode 100644 .benchmark/blog-cms/fullstack/scripts/dev.mjs create mode 100644 .benchmark/blog-cms/prd.json create mode 100644 .benchmark/blog-cms/release/release/README.md create mode 100644 .benchmark/blog-cms/release/release/build-info.json create mode 100644 .benchmark/blog-cms/release/release/checksums/checksums.txt create mode 100644 .benchmark/blog-cms/release/release/linux/socialapp-0.1.0.AppImage create mode 100644 .benchmark/blog-cms/release/release/linux/socialapp_0.1.0_amd64.deb create mode 100644 .benchmark/blog-cms/release/release/macos/socialapp-0.1.0-arm64.dmg create mode 100644 .benchmark/blog-cms/release/release/macos/socialapp-0.1.0.dmg create mode 100644 .benchmark/blog-cms/release/release/manifests/manifest.json create mode 100644 .benchmark/blog-cms/release/release/release-notes/release-notes.md create mode 100644 .benchmark/blog-cms/release/release/version.json create mode 100644 .benchmark/blog-cms/release/release/windows/socialapp-0.1.0-portable.exe create mode 100644 .benchmark/blog-cms/release/release/windows/socialapp-setup-0.1.0.exe create mode 100644 .benchmark/contract-mgmt/arch.json create mode 100644 .benchmark/contract-mgmt/backend/.gitignore create mode 100644 .benchmark/contract-mgmt/backend/README.md create mode 100644 .benchmark/contract-mgmt/backend/package.json create mode 100644 .benchmark/contract-mgmt/backend/src/__tests__/approvals.test.ts create mode 100644 .benchmark/contract-mgmt/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/contract-mgmt/backend/src/__tests__/contracts.test.ts create mode 100644 .benchmark/contract-mgmt/backend/src/__tests__/customers.test.ts create mode 100644 .benchmark/contract-mgmt/backend/src/__tests__/operation_logs.test.ts create mode 100644 .benchmark/contract-mgmt/backend/src/__tests__/reminders.test.ts create mode 100644 .benchmark/contract-mgmt/backend/src/__tests__/stats.test.ts create mode 100644 .benchmark/contract-mgmt/backend/src/db/client.ts create mode 100644 .benchmark/contract-mgmt/backend/src/db/schema.ts create mode 100644 .benchmark/contract-mgmt/backend/src/index.ts create mode 100644 .benchmark/contract-mgmt/backend/src/middleware/auth.ts create mode 100644 .benchmark/contract-mgmt/backend/src/routes/approvals.ts create mode 100644 .benchmark/contract-mgmt/backend/src/routes/auth.ts create mode 100644 .benchmark/contract-mgmt/backend/src/routes/contracts.ts create mode 100644 .benchmark/contract-mgmt/backend/src/routes/customers.ts create mode 100644 .benchmark/contract-mgmt/backend/src/routes/operation_logs.ts create mode 100644 .benchmark/contract-mgmt/backend/src/routes/reminders.ts create mode 100644 .benchmark/contract-mgmt/backend/src/routes/stats.ts create mode 100644 .benchmark/contract-mgmt/backend/src/services/approval.ts create mode 100644 .benchmark/contract-mgmt/backend/src/services/contract.ts create mode 100644 .benchmark/contract-mgmt/backend/src/services/customer.ts create mode 100644 .benchmark/contract-mgmt/backend/src/services/operation_log.ts create mode 100644 .benchmark/contract-mgmt/backend/src/services/reminder.ts create mode 100644 .benchmark/contract-mgmt/backend/src/services/stat.ts create mode 100644 .benchmark/contract-mgmt/backend/src/types/fastify.d.ts create mode 100644 .benchmark/contract-mgmt/backend/src/types/index.ts create mode 100644 .benchmark/contract-mgmt/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/contract-mgmt/backend/tsconfig.json create mode 100644 .benchmark/contract-mgmt/electron/README.md create mode 100644 .benchmark/contract-mgmt/electron/assets/icon.png create mode 100644 .benchmark/contract-mgmt/electron/electron-builder.yml create mode 100644 .benchmark/contract-mgmt/electron/electron/ipc.ts create mode 100644 .benchmark/contract-mgmt/electron/electron/main.ts create mode 100644 .benchmark/contract-mgmt/electron/electron/preload.ts create mode 100644 .benchmark/contract-mgmt/electron/electron/tray.ts create mode 100644 .benchmark/contract-mgmt/electron/electron/updater.ts create mode 100644 .benchmark/contract-mgmt/electron/electron/utils.ts create mode 100644 .benchmark/contract-mgmt/electron/entitlements.mac.plist create mode 100644 .benchmark/contract-mgmt/electron/package.json create mode 100644 .benchmark/contract-mgmt/electron/tsconfig.json create mode 100644 .benchmark/contract-mgmt/frontend/app/approvals/page.tsx create mode 100644 .benchmark/contract-mgmt/frontend/app/contracts/page.tsx create mode 100644 .benchmark/contract-mgmt/frontend/app/customers/page.tsx create mode 100644 .benchmark/contract-mgmt/frontend/app/globals.css create mode 100644 .benchmark/contract-mgmt/frontend/app/layout.tsx create mode 100644 .benchmark/contract-mgmt/frontend/app/operation_logs/page.tsx create mode 100644 .benchmark/contract-mgmt/frontend/app/page.tsx create mode 100644 .benchmark/contract-mgmt/frontend/app/reminders/page.tsx create mode 100644 .benchmark/contract-mgmt/frontend/app/stats/page.tsx create mode 100644 .benchmark/contract-mgmt/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/contract-mgmt/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/contract-mgmt/frontend/components/ui/Button.tsx create mode 100644 .benchmark/contract-mgmt/frontend/components/ui/Card.tsx create mode 100644 .benchmark/contract-mgmt/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/contract-mgmt/frontend/components/ui/Input.tsx create mode 100644 .benchmark/contract-mgmt/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/contract-mgmt/frontend/hooks/useApprovals.ts create mode 100644 .benchmark/contract-mgmt/frontend/hooks/useAuth.ts create mode 100644 .benchmark/contract-mgmt/frontend/hooks/useContracts.ts create mode 100644 .benchmark/contract-mgmt/frontend/hooks/useCustomers.ts create mode 100644 .benchmark/contract-mgmt/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/contract-mgmt/frontend/hooks/useForm.ts create mode 100644 .benchmark/contract-mgmt/frontend/hooks/useReminders.ts create mode 100644 .benchmark/contract-mgmt/frontend/next.config.ts create mode 100644 .benchmark/contract-mgmt/frontend/package.json create mode 100644 .benchmark/contract-mgmt/frontend/postcss.config.js create mode 100644 .benchmark/contract-mgmt/frontend/services/api.ts create mode 100644 .benchmark/contract-mgmt/frontend/services/approvals.ts create mode 100644 .benchmark/contract-mgmt/frontend/services/auth.ts create mode 100644 .benchmark/contract-mgmt/frontend/services/contracts.ts create mode 100644 .benchmark/contract-mgmt/frontend/services/customers.ts create mode 100644 .benchmark/contract-mgmt/frontend/services/reminders.ts create mode 100644 .benchmark/contract-mgmt/frontend/tailwind.config.ts create mode 100644 .benchmark/contract-mgmt/frontend/tsconfig.json create mode 100644 .benchmark/contract-mgmt/frontend/types/index.ts create mode 100644 .benchmark/contract-mgmt/prd.json create mode 100644 .benchmark/contract-mgmt/release/release/README.md create mode 100644 .benchmark/contract-mgmt/release/release/build-info.json create mode 100644 .benchmark/contract-mgmt/release/release/checksums/checksums.txt create mode 100644 .benchmark/contract-mgmt/release/release/linux/oaflow-0.1.0.AppImage create mode 100644 .benchmark/contract-mgmt/release/release/linux/oaflow_0.1.0_amd64.deb create mode 100644 .benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0-arm64.dmg create mode 100644 .benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0.dmg create mode 100644 .benchmark/contract-mgmt/release/release/manifests/manifest.json create mode 100644 .benchmark/contract-mgmt/release/release/release-notes/release-notes.md create mode 100644 .benchmark/contract-mgmt/release/release/version.json create mode 100644 .benchmark/contract-mgmt/release/release/windows/oaflow-0.1.0-portable.exe create mode 100644 .benchmark/contract-mgmt/release/release/windows/oaflow-setup-0.1.0.exe create mode 100644 .benchmark/contract-mgmt/summary.json create mode 100644 .benchmark/course/arch.json create mode 100644 .benchmark/course/backend/.gitignore create mode 100644 .benchmark/course/backend/README.md create mode 100644 .benchmark/course/backend/package.json create mode 100644 .benchmark/course/backend/src/__tests__/assignments.test.ts create mode 100644 .benchmark/course/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/course/backend/src/__tests__/chapters.test.ts create mode 100644 .benchmark/course/backend/src/__tests__/courses.test.ts create mode 100644 .benchmark/course/backend/src/__tests__/exercises.test.ts create mode 100644 .benchmark/course/backend/src/__tests__/lessons.test.ts create mode 100644 .benchmark/course/backend/src/__tests__/students.test.ts create mode 100644 .benchmark/course/backend/src/__tests__/user_progress.test.ts create mode 100644 .benchmark/course/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/course/backend/src/db/client.ts create mode 100644 .benchmark/course/backend/src/db/schema.ts create mode 100644 .benchmark/course/backend/src/index.ts create mode 100644 .benchmark/course/backend/src/middleware/auth.ts create mode 100644 .benchmark/course/backend/src/routes/assignments.ts create mode 100644 .benchmark/course/backend/src/routes/auth.ts create mode 100644 .benchmark/course/backend/src/routes/chapters.ts create mode 100644 .benchmark/course/backend/src/routes/courses.ts create mode 100644 .benchmark/course/backend/src/routes/exercises.ts create mode 100644 .benchmark/course/backend/src/routes/lessons.ts create mode 100644 .benchmark/course/backend/src/routes/students.ts create mode 100644 .benchmark/course/backend/src/routes/user_progress.ts create mode 100644 .benchmark/course/backend/src/routes/users.ts create mode 100644 .benchmark/course/backend/src/services/assignment.ts create mode 100644 .benchmark/course/backend/src/services/chapter.ts create mode 100644 .benchmark/course/backend/src/services/cours.ts create mode 100644 .benchmark/course/backend/src/services/exercis.ts create mode 100644 .benchmark/course/backend/src/services/lesson.ts create mode 100644 .benchmark/course/backend/src/services/student.ts create mode 100644 .benchmark/course/backend/src/services/user.ts create mode 100644 .benchmark/course/backend/src/services/user_progress.ts create mode 100644 .benchmark/course/backend/src/types/fastify.d.ts create mode 100644 .benchmark/course/backend/src/types/index.ts create mode 100644 .benchmark/course/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/course/backend/tsconfig.json create mode 100644 .benchmark/course/electron/README.md create mode 100644 .benchmark/course/electron/assets/icon.png create mode 100644 .benchmark/course/electron/electron-builder.yml create mode 100644 .benchmark/course/electron/electron/ipc.ts create mode 100644 .benchmark/course/electron/electron/main.ts create mode 100644 .benchmark/course/electron/electron/preload.ts create mode 100644 .benchmark/course/electron/electron/tray.ts create mode 100644 .benchmark/course/electron/electron/updater.ts create mode 100644 .benchmark/course/electron/electron/utils.ts create mode 100644 .benchmark/course/electron/entitlements.mac.plist create mode 100644 .benchmark/course/electron/package.json create mode 100644 .benchmark/course/electron/tsconfig.json create mode 100644 .benchmark/course/frontend/app/assignments/page.tsx create mode 100644 .benchmark/course/frontend/app/chapters/page.tsx create mode 100644 .benchmark/course/frontend/app/course/[id]/page.tsx create mode 100644 .benchmark/course/frontend/app/courses/page.tsx create mode 100644 .benchmark/course/frontend/app/exercises/page.tsx create mode 100644 .benchmark/course/frontend/app/globals.css create mode 100644 .benchmark/course/frontend/app/layout.tsx create mode 100644 .benchmark/course/frontend/app/learn/[courseId]/[lessonId]/page.tsx create mode 100644 .benchmark/course/frontend/app/live/page.tsx create mode 100644 .benchmark/course/frontend/app/page.tsx create mode 100644 .benchmark/course/frontend/app/profile/page.tsx create mode 100644 .benchmark/course/frontend/app/students/page.tsx create mode 100644 .benchmark/course/frontend/app/作业批改/page.tsx create mode 100644 .benchmark/course/frontend/app/学员进度/page.tsx create mode 100644 .benchmark/course/frontend/app/章节管理/page.tsx create mode 100644 .benchmark/course/frontend/app/课程发布/page.tsx create mode 100644 .benchmark/course/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/course/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/course/frontend/components/ui/Button.tsx create mode 100644 .benchmark/course/frontend/components/ui/Card.tsx create mode 100644 .benchmark/course/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/course/frontend/components/ui/Input.tsx create mode 100644 .benchmark/course/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/course/frontend/hooks/useAssignments.ts create mode 100644 .benchmark/course/frontend/hooks/useAuth.ts create mode 100644 .benchmark/course/frontend/hooks/useChapters.ts create mode 100644 .benchmark/course/frontend/hooks/useCourses.ts create mode 100644 .benchmark/course/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/course/frontend/hooks/useExercises.ts create mode 100644 .benchmark/course/frontend/hooks/useForm.ts create mode 100644 .benchmark/course/frontend/hooks/useItem.ts create mode 100644 .benchmark/course/frontend/hooks/useLessons.ts create mode 100644 .benchmark/course/frontend/hooks/useProgress.ts create mode 100644 .benchmark/course/frontend/hooks/useStudents.ts create mode 100644 .benchmark/course/frontend/next-env.d.ts create mode 100644 .benchmark/course/frontend/next.config.ts create mode 100644 .benchmark/course/frontend/package.json create mode 100644 .benchmark/course/frontend/postcss.config.js create mode 100644 .benchmark/course/frontend/services/api.ts create mode 100644 .benchmark/course/frontend/services/assignments.ts create mode 100644 .benchmark/course/frontend/services/auth.ts create mode 100644 .benchmark/course/frontend/services/chapters.ts create mode 100644 .benchmark/course/frontend/services/courses.ts create mode 100644 .benchmark/course/frontend/services/exercises.ts create mode 100644 .benchmark/course/frontend/services/lessons.ts create mode 100644 .benchmark/course/frontend/services/progress.ts create mode 100644 .benchmark/course/frontend/services/students.ts create mode 100644 .benchmark/course/frontend/services/作业批改.ts create mode 100644 .benchmark/course/frontend/services/学员进度.ts create mode 100644 .benchmark/course/frontend/services/章节管理.ts create mode 100644 .benchmark/course/frontend/services/课程发布.ts create mode 100644 .benchmark/course/frontend/tailwind.config.ts create mode 100644 .benchmark/course/frontend/tsconfig.json create mode 100644 .benchmark/course/frontend/types/index.ts create mode 100644 .benchmark/course/fullstack/.gitignore create mode 100644 .benchmark/course/fullstack/README.md create mode 100644 .benchmark/course/fullstack/apps/api/.gitignore create mode 100644 .benchmark/course/fullstack/apps/api/README.md create mode 100644 .benchmark/course/fullstack/apps/api/package.json create mode 100644 .benchmark/course/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/__tests__/courses.test.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/__tests__/exercises.test.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/__tests__/lessons.test.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/__tests__/user_progress.test.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/routes/courses.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/routes/exercises.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/routes/lessons.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/routes/user_progress.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/services/cours.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/services/exercis.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/services/lesson.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/services/user_progress.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/course/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/course/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/course/fullstack/apps/web/app/course/[id]/page.tsx create mode 100644 .benchmark/course/fullstack/apps/web/app/exercises/page.tsx create mode 100644 .benchmark/course/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/course/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/course/fullstack/apps/web/app/learn/[courseId]/[lessonId]/page.tsx create mode 100644 .benchmark/course/fullstack/apps/web/app/live/page.tsx create mode 100644 .benchmark/course/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/course/fullstack/apps/web/app/profile/page.tsx create mode 100644 .benchmark/course/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/course/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/course/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/course/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/course/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/course/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/course/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/course/fullstack/apps/web/hooks/useCourses.ts create mode 100644 .benchmark/course/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/course/fullstack/apps/web/hooks/useExercises.ts create mode 100644 .benchmark/course/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/course/fullstack/apps/web/hooks/useLessons.ts create mode 100644 .benchmark/course/fullstack/apps/web/hooks/useProgress.ts create mode 100644 .benchmark/course/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/course/fullstack/apps/web/package.json create mode 100644 .benchmark/course/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/course/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/course/fullstack/apps/web/services/courses.ts create mode 100644 .benchmark/course/fullstack/apps/web/services/exercises.ts create mode 100644 .benchmark/course/fullstack/apps/web/services/lessons.ts create mode 100644 .benchmark/course/fullstack/apps/web/services/progress.ts create mode 100644 .benchmark/course/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/course/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/course/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/course/fullstack/package.json create mode 100644 .benchmark/course/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/course/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/course/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/course/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/course/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/course/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/course/fullstack/scripts/build.mjs create mode 100644 .benchmark/course/fullstack/scripts/dev.mjs create mode 100644 .benchmark/course/prd.json create mode 100644 .benchmark/course/release/release/README.md create mode 100644 .benchmark/course/release/release/build-info.json create mode 100644 .benchmark/course/release/release/checksums/checksums.txt create mode 100644 .benchmark/course/release/release/linux/eduplatform-0.1.0.AppImage create mode 100644 .benchmark/course/release/release/linux/eduplatform_0.1.0_amd64.deb create mode 100644 .benchmark/course/release/release/macos/eduplatform-0.1.0-arm64.dmg create mode 100644 .benchmark/course/release/release/macos/eduplatform-0.1.0.dmg create mode 100644 .benchmark/course/release/release/manifests/manifest.json create mode 100644 .benchmark/course/release/release/release-notes/release-notes.md create mode 100644 .benchmark/course/release/release/version.json create mode 100644 .benchmark/course/release/release/windows/eduplatform-0.1.0-portable.exe create mode 100644 .benchmark/course/release/release/windows/eduplatform-setup-0.1.0.exe create mode 100644 .benchmark/crm/arch.json create mode 100644 .benchmark/crm/backend/.gitignore create mode 100644 .benchmark/crm/backend/README.md create mode 100644 .benchmark/crm/backend/package.json create mode 100644 .benchmark/crm/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/crm/backend/src/__tests__/customers.test.ts create mode 100644 .benchmark/crm/backend/src/__tests__/dashboard.test.ts create mode 100644 .benchmark/crm/backend/src/__tests__/follow_ups.test.ts create mode 100644 .benchmark/crm/backend/src/__tests__/note_tags.test.ts create mode 100644 .benchmark/crm/backend/src/__tests__/notes.test.ts create mode 100644 .benchmark/crm/backend/src/__tests__/sales_pipeline.test.ts create mode 100644 .benchmark/crm/backend/src/__tests__/tags.test.ts create mode 100644 .benchmark/crm/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/crm/backend/src/db/client.ts create mode 100644 .benchmark/crm/backend/src/db/schema.ts create mode 100644 .benchmark/crm/backend/src/index.ts create mode 100644 .benchmark/crm/backend/src/middleware/auth.ts create mode 100644 .benchmark/crm/backend/src/routes/auth.ts create mode 100644 .benchmark/crm/backend/src/routes/customers.ts create mode 100644 .benchmark/crm/backend/src/routes/dashboard.ts create mode 100644 .benchmark/crm/backend/src/routes/follow_ups.ts create mode 100644 .benchmark/crm/backend/src/routes/note_tags.ts create mode 100644 .benchmark/crm/backend/src/routes/notes.ts create mode 100644 .benchmark/crm/backend/src/routes/sales_pipeline.ts create mode 100644 .benchmark/crm/backend/src/routes/tags.ts create mode 100644 .benchmark/crm/backend/src/routes/users.ts create mode 100644 .benchmark/crm/backend/src/services/customer.ts create mode 100644 .benchmark/crm/backend/src/services/dashboard.ts create mode 100644 .benchmark/crm/backend/src/services/follow_up.ts create mode 100644 .benchmark/crm/backend/src/services/note.ts create mode 100644 .benchmark/crm/backend/src/services/note_tag.ts create mode 100644 .benchmark/crm/backend/src/services/sales_pipeline.ts create mode 100644 .benchmark/crm/backend/src/services/tag.ts create mode 100644 .benchmark/crm/backend/src/services/user.ts create mode 100644 .benchmark/crm/backend/src/types/fastify.d.ts create mode 100644 .benchmark/crm/backend/src/types/index.ts create mode 100644 .benchmark/crm/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/crm/backend/tsconfig.json create mode 100644 .benchmark/crm/electron/README.md create mode 100644 .benchmark/crm/electron/assets/icon.png create mode 100644 .benchmark/crm/electron/electron-builder.yml create mode 100644 .benchmark/crm/electron/electron/ipc.ts create mode 100644 .benchmark/crm/electron/electron/main.ts create mode 100644 .benchmark/crm/electron/electron/preload.ts create mode 100644 .benchmark/crm/electron/electron/tray.ts create mode 100644 .benchmark/crm/electron/electron/updater.ts create mode 100644 .benchmark/crm/electron/electron/utils.ts create mode 100644 .benchmark/crm/electron/entitlements.mac.plist create mode 100644 .benchmark/crm/electron/package.json create mode 100644 .benchmark/crm/electron/tsconfig.json create mode 100644 .benchmark/crm/frontend/app/customers/page.tsx create mode 100644 .benchmark/crm/frontend/app/dashboard/page.tsx create mode 100644 .benchmark/crm/frontend/app/follow_ups/page.tsx create mode 100644 .benchmark/crm/frontend/app/globals.css create mode 100644 .benchmark/crm/frontend/app/layout.tsx create mode 100644 .benchmark/crm/frontend/app/note/[id]/page.tsx create mode 100644 .benchmark/crm/frontend/app/notes/page.tsx create mode 100644 .benchmark/crm/frontend/app/page.tsx create mode 100644 .benchmark/crm/frontend/app/sales_pipeline/page.tsx create mode 100644 .benchmark/crm/frontend/app/settings/page.tsx create mode 100644 .benchmark/crm/frontend/app/tags/page.tsx create mode 100644 .benchmark/crm/frontend/app/客户管理/page.tsx create mode 100644 .benchmark/crm/frontend/app/数据分析仪表盘/page.tsx create mode 100644 .benchmark/crm/frontend/app/跟进记录/page.tsx create mode 100644 .benchmark/crm/frontend/app/销售漏斗/page.tsx create mode 100644 .benchmark/crm/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/crm/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/crm/frontend/components/note/NoteCard.tsx create mode 100644 .benchmark/crm/frontend/components/ui/Button.tsx create mode 100644 .benchmark/crm/frontend/components/ui/Card.tsx create mode 100644 .benchmark/crm/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/crm/frontend/components/ui/Input.tsx create mode 100644 .benchmark/crm/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/crm/frontend/hooks/useAuth.ts create mode 100644 .benchmark/crm/frontend/hooks/useCustomers.ts create mode 100644 .benchmark/crm/frontend/hooks/useDashboard.ts create mode 100644 .benchmark/crm/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/crm/frontend/hooks/useFollow_ups.ts create mode 100644 .benchmark/crm/frontend/hooks/useForm.ts create mode 100644 .benchmark/crm/frontend/hooks/useItem.ts create mode 100644 .benchmark/crm/frontend/hooks/useNotes.ts create mode 100644 .benchmark/crm/frontend/hooks/useSales_pipeline.ts create mode 100644 .benchmark/crm/frontend/hooks/useTags.ts create mode 100644 .benchmark/crm/frontend/next-env.d.ts create mode 100644 .benchmark/crm/frontend/next.config.ts create mode 100644 .benchmark/crm/frontend/package.json create mode 100644 .benchmark/crm/frontend/postcss.config.js create mode 100644 .benchmark/crm/frontend/services/api.ts create mode 100644 .benchmark/crm/frontend/services/auth.ts create mode 100644 .benchmark/crm/frontend/services/customers.ts create mode 100644 .benchmark/crm/frontend/services/dashboard.ts create mode 100644 .benchmark/crm/frontend/services/follow_ups.ts create mode 100644 .benchmark/crm/frontend/services/notes.ts create mode 100644 .benchmark/crm/frontend/services/sales_pipeline.ts create mode 100644 .benchmark/crm/frontend/services/tags.ts create mode 100644 .benchmark/crm/frontend/services/客户管理.ts create mode 100644 .benchmark/crm/frontend/services/数据分析仪表盘.ts create mode 100644 .benchmark/crm/frontend/services/跟进记录.ts create mode 100644 .benchmark/crm/frontend/services/销售漏斗.ts create mode 100644 .benchmark/crm/frontend/tailwind.config.ts create mode 100644 .benchmark/crm/frontend/tsconfig.json create mode 100644 .benchmark/crm/frontend/types/index.ts create mode 100644 .benchmark/crm/fullstack/.gitignore create mode 100644 .benchmark/crm/fullstack/README.md create mode 100644 .benchmark/crm/fullstack/apps/api/.gitignore create mode 100644 .benchmark/crm/fullstack/apps/api/README.md create mode 100644 .benchmark/crm/fullstack/apps/api/package.json create mode 100644 .benchmark/crm/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/__tests__/note_tags.test.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/__tests__/notes.test.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/__tests__/tags.test.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/routes/note_tags.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/routes/notes.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/routes/tags.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/services/note.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/services/note_tag.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/services/tag.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/crm/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/crm/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/crm/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/crm/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/app/note/[id]/page.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/app/notes/page.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/app/settings/page.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/app/tags/page.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/components/note/NoteCard.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/crm/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/crm/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/crm/fullstack/apps/web/hooks/useNotes.ts create mode 100644 .benchmark/crm/fullstack/apps/web/hooks/useTags.ts create mode 100644 .benchmark/crm/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/crm/fullstack/apps/web/package.json create mode 100644 .benchmark/crm/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/crm/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/crm/fullstack/apps/web/services/notes.ts create mode 100644 .benchmark/crm/fullstack/apps/web/services/tags.ts create mode 100644 .benchmark/crm/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/crm/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/crm/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/crm/fullstack/package.json create mode 100644 .benchmark/crm/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/crm/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/crm/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/crm/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/crm/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/crm/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/crm/fullstack/scripts/build.mjs create mode 100644 .benchmark/crm/fullstack/scripts/dev.mjs create mode 100644 .benchmark/crm/prd.json create mode 100644 .benchmark/crm/release/release/README.md create mode 100644 .benchmark/crm/release/release/build-info.json create mode 100644 .benchmark/crm/release/release/checksums/checksums.txt create mode 100644 .benchmark/crm/release/release/linux/noteapp-0.1.0.AppImage create mode 100644 .benchmark/crm/release/release/linux/noteapp_0.1.0_amd64.deb create mode 100644 .benchmark/crm/release/release/macos/noteapp-0.1.0-arm64.dmg create mode 100644 .benchmark/crm/release/release/macos/noteapp-0.1.0.dmg create mode 100644 .benchmark/crm/release/release/manifests/manifest.json create mode 100644 .benchmark/crm/release/release/release-notes/release-notes.md create mode 100644 .benchmark/crm/release/release/version.json create mode 100644 .benchmark/crm/release/release/windows/noteapp-0.1.0-portable.exe create mode 100644 .benchmark/crm/release/release/windows/noteapp-setup-0.1.0.exe create mode 100644 .benchmark/hr/arch.json create mode 100644 .benchmark/hr/backend/.gitignore create mode 100644 .benchmark/hr/backend/README.md create mode 100644 .benchmark/hr/backend/package.json create mode 100644 .benchmark/hr/backend/src/__tests__/approval_steps.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/approvals.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/attendance.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/contracts.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/customers.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/departments.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/employees.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/items.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/performance.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/reminders.test.ts create mode 100644 .benchmark/hr/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/hr/backend/src/db/client.ts create mode 100644 .benchmark/hr/backend/src/db/schema.ts create mode 100644 .benchmark/hr/backend/src/index.ts create mode 100644 .benchmark/hr/backend/src/middleware/auth.ts create mode 100644 .benchmark/hr/backend/src/routes/approval_steps.ts create mode 100644 .benchmark/hr/backend/src/routes/approvals.ts create mode 100644 .benchmark/hr/backend/src/routes/attendance.ts create mode 100644 .benchmark/hr/backend/src/routes/auth.ts create mode 100644 .benchmark/hr/backend/src/routes/contracts.ts create mode 100644 .benchmark/hr/backend/src/routes/customers.ts create mode 100644 .benchmark/hr/backend/src/routes/departments.ts create mode 100644 .benchmark/hr/backend/src/routes/employees.ts create mode 100644 .benchmark/hr/backend/src/routes/items.ts create mode 100644 .benchmark/hr/backend/src/routes/performance.ts create mode 100644 .benchmark/hr/backend/src/routes/reminders.ts create mode 100644 .benchmark/hr/backend/src/routes/users.ts create mode 100644 .benchmark/hr/backend/src/services/approval.ts create mode 100644 .benchmark/hr/backend/src/services/approval_step.ts create mode 100644 .benchmark/hr/backend/src/services/attendance.ts create mode 100644 .benchmark/hr/backend/src/services/contract.ts create mode 100644 .benchmark/hr/backend/src/services/customer.ts create mode 100644 .benchmark/hr/backend/src/services/department.ts create mode 100644 .benchmark/hr/backend/src/services/employee.ts create mode 100644 .benchmark/hr/backend/src/services/item.ts create mode 100644 .benchmark/hr/backend/src/services/performance.ts create mode 100644 .benchmark/hr/backend/src/services/reminder.ts create mode 100644 .benchmark/hr/backend/src/services/user.ts create mode 100644 .benchmark/hr/backend/src/types/fastify.d.ts create mode 100644 .benchmark/hr/backend/src/types/index.ts create mode 100644 .benchmark/hr/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/hr/backend/tsconfig.json create mode 100644 .benchmark/hr/electron/README.md create mode 100644 .benchmark/hr/electron/assets/icon.png create mode 100644 .benchmark/hr/electron/electron-builder.yml create mode 100644 .benchmark/hr/electron/electron/ipc.ts create mode 100644 .benchmark/hr/electron/electron/main.ts create mode 100644 .benchmark/hr/electron/electron/preload.ts create mode 100644 .benchmark/hr/electron/electron/tray.ts create mode 100644 .benchmark/hr/electron/electron/updater.ts create mode 100644 .benchmark/hr/electron/electron/utils.ts create mode 100644 .benchmark/hr/electron/entitlements.mac.plist create mode 100644 .benchmark/hr/electron/package.json create mode 100644 .benchmark/hr/electron/tsconfig.json create mode 100644 .benchmark/hr/frontend/app/approvals/page.tsx create mode 100644 .benchmark/hr/frontend/app/attendance/page.tsx create mode 100644 .benchmark/hr/frontend/app/contacts/page.tsx create mode 100644 .benchmark/hr/frontend/app/employees/page.tsx create mode 100644 .benchmark/hr/frontend/app/globals.css create mode 100644 .benchmark/hr/frontend/app/items/page.tsx create mode 100644 .benchmark/hr/frontend/app/layout.tsx create mode 100644 .benchmark/hr/frontend/app/notices/page.tsx create mode 100644 .benchmark/hr/frontend/app/page.tsx create mode 100644 .benchmark/hr/frontend/app/profile/page.tsx create mode 100644 .benchmark/hr/frontend/app/员工档案/page.tsx create mode 100644 .benchmark/hr/frontend/app/招聘流程/page.tsx create mode 100644 .benchmark/hr/frontend/app/绩效评估/page.tsx create mode 100644 .benchmark/hr/frontend/app/考勤管理/page.tsx create mode 100644 .benchmark/hr/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/hr/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/hr/frontend/components/ui/Button.tsx create mode 100644 .benchmark/hr/frontend/components/ui/Card.tsx create mode 100644 .benchmark/hr/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/hr/frontend/components/ui/Input.tsx create mode 100644 .benchmark/hr/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/hr/frontend/hooks/useApprovals.ts create mode 100644 .benchmark/hr/frontend/hooks/useAttendance.ts create mode 100644 .benchmark/hr/frontend/hooks/useAuth.ts create mode 100644 .benchmark/hr/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/hr/frontend/hooks/useDepartments.ts create mode 100644 .benchmark/hr/frontend/hooks/useEmployees.ts create mode 100644 .benchmark/hr/frontend/hooks/useForm.ts create mode 100644 .benchmark/hr/frontend/hooks/useItem.ts create mode 100644 .benchmark/hr/frontend/hooks/useItems.ts create mode 100644 .benchmark/hr/frontend/hooks/useNotices.ts create mode 100644 .benchmark/hr/frontend/hooks/useUsers.ts create mode 100644 .benchmark/hr/frontend/next-env.d.ts create mode 100644 .benchmark/hr/frontend/next.config.ts create mode 100644 .benchmark/hr/frontend/package.json create mode 100644 .benchmark/hr/frontend/postcss.config.js create mode 100644 .benchmark/hr/frontend/services/api.ts create mode 100644 .benchmark/hr/frontend/services/approvals.ts create mode 100644 .benchmark/hr/frontend/services/attendance.ts create mode 100644 .benchmark/hr/frontend/services/auth.ts create mode 100644 .benchmark/hr/frontend/services/departments.ts create mode 100644 .benchmark/hr/frontend/services/employees.ts create mode 100644 .benchmark/hr/frontend/services/items.ts create mode 100644 .benchmark/hr/frontend/services/notices.ts create mode 100644 .benchmark/hr/frontend/services/users.ts create mode 100644 .benchmark/hr/frontend/services/员工档案.ts create mode 100644 .benchmark/hr/frontend/services/招聘流程.ts create mode 100644 .benchmark/hr/frontend/services/绩效评估.ts create mode 100644 .benchmark/hr/frontend/services/考勤管理.ts create mode 100644 .benchmark/hr/frontend/tailwind.config.ts create mode 100644 .benchmark/hr/frontend/tsconfig.json create mode 100644 .benchmark/hr/frontend/types/index.ts create mode 100644 .benchmark/hr/fullstack/.gitignore create mode 100644 .benchmark/hr/fullstack/README.md create mode 100644 .benchmark/hr/fullstack/apps/api/.gitignore create mode 100644 .benchmark/hr/fullstack/apps/api/README.md create mode 100644 .benchmark/hr/fullstack/apps/api/package.json create mode 100644 .benchmark/hr/fullstack/apps/api/src/__tests__/approval_steps.test.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/__tests__/approvals.test.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/__tests__/attendance.test.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/__tests__/departments.test.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/routes/approval_steps.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/routes/approvals.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/routes/attendance.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/routes/departments.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/services/approval.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/services/approval_step.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/services/attendance.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/services/department.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/hr/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/hr/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/hr/fullstack/apps/web/app/approvals/page.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/app/attendance/page.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/app/contacts/page.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/hr/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/app/notices/page.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/app/profile/page.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/hr/fullstack/apps/web/hooks/useApprovals.ts create mode 100644 .benchmark/hr/fullstack/apps/web/hooks/useAttendance.ts create mode 100644 .benchmark/hr/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/hr/fullstack/apps/web/hooks/useDepartments.ts create mode 100644 .benchmark/hr/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/hr/fullstack/apps/web/hooks/useNotices.ts create mode 100644 .benchmark/hr/fullstack/apps/web/hooks/useUsers.ts create mode 100644 .benchmark/hr/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/hr/fullstack/apps/web/package.json create mode 100644 .benchmark/hr/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/hr/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/hr/fullstack/apps/web/services/approvals.ts create mode 100644 .benchmark/hr/fullstack/apps/web/services/attendance.ts create mode 100644 .benchmark/hr/fullstack/apps/web/services/departments.ts create mode 100644 .benchmark/hr/fullstack/apps/web/services/notices.ts create mode 100644 .benchmark/hr/fullstack/apps/web/services/users.ts create mode 100644 .benchmark/hr/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/hr/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/hr/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/hr/fullstack/package.json create mode 100644 .benchmark/hr/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/hr/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/hr/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/hr/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/hr/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/hr/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/hr/fullstack/scripts/build.mjs create mode 100644 .benchmark/hr/fullstack/scripts/dev.mjs create mode 100644 .benchmark/hr/prd.json create mode 100644 .benchmark/hr/release/release/README.md create mode 100644 .benchmark/hr/release/release/build-info.json create mode 100644 .benchmark/hr/release/release/checksums/checksums.txt create mode 100644 .benchmark/hr/release/release/linux/oaflow-0.1.0.AppImage create mode 100644 .benchmark/hr/release/release/linux/oaflow_0.1.0_amd64.deb create mode 100644 .benchmark/hr/release/release/macos/oaflow-0.1.0-arm64.dmg create mode 100644 .benchmark/hr/release/release/macos/oaflow-0.1.0.dmg create mode 100644 .benchmark/hr/release/release/manifests/manifest.json create mode 100644 .benchmark/hr/release/release/release-notes/release-notes.md create mode 100644 .benchmark/hr/release/release/version.json create mode 100644 .benchmark/hr/release/release/windows/oaflow-0.1.0-portable.exe create mode 100644 .benchmark/hr/release/release/windows/oaflow-setup-0.1.0.exe create mode 100644 .benchmark/inventory/arch.json create mode 100644 .benchmark/inventory/backend/.gitignore create mode 100644 .benchmark/inventory/backend/README.md create mode 100644 .benchmark/inventory/backend/package.json create mode 100644 .benchmark/inventory/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/inventory.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/logistics.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/order_items.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/orders.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/products.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/stock_alerts.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/stocktaking.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/suppliers.test.ts create mode 100644 .benchmark/inventory/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/inventory/backend/src/db/client.ts create mode 100644 .benchmark/inventory/backend/src/db/schema.ts create mode 100644 .benchmark/inventory/backend/src/index.ts create mode 100644 .benchmark/inventory/backend/src/middleware/auth.ts create mode 100644 .benchmark/inventory/backend/src/routes/auth.ts create mode 100644 .benchmark/inventory/backend/src/routes/inventory.ts create mode 100644 .benchmark/inventory/backend/src/routes/logistics.ts create mode 100644 .benchmark/inventory/backend/src/routes/order_items.ts create mode 100644 .benchmark/inventory/backend/src/routes/orders.ts create mode 100644 .benchmark/inventory/backend/src/routes/products.ts create mode 100644 .benchmark/inventory/backend/src/routes/stock_alerts.ts create mode 100644 .benchmark/inventory/backend/src/routes/stocktaking.ts create mode 100644 .benchmark/inventory/backend/src/routes/suppliers.ts create mode 100644 .benchmark/inventory/backend/src/routes/users.ts create mode 100644 .benchmark/inventory/backend/src/services/inventory.ts create mode 100644 .benchmark/inventory/backend/src/services/logistic.ts create mode 100644 .benchmark/inventory/backend/src/services/order.ts create mode 100644 .benchmark/inventory/backend/src/services/order_item.ts create mode 100644 .benchmark/inventory/backend/src/services/product.ts create mode 100644 .benchmark/inventory/backend/src/services/stock_alert.ts create mode 100644 .benchmark/inventory/backend/src/services/stocktaking.ts create mode 100644 .benchmark/inventory/backend/src/services/supplier.ts create mode 100644 .benchmark/inventory/backend/src/services/user.ts create mode 100644 .benchmark/inventory/backend/src/types/fastify.d.ts create mode 100644 .benchmark/inventory/backend/src/types/index.ts create mode 100644 .benchmark/inventory/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/inventory/backend/tsconfig.json create mode 100644 .benchmark/inventory/electron/README.md create mode 100644 .benchmark/inventory/electron/assets/icon.png create mode 100644 .benchmark/inventory/electron/electron-builder.yml create mode 100644 .benchmark/inventory/electron/electron/ipc.ts create mode 100644 .benchmark/inventory/electron/electron/main.ts create mode 100644 .benchmark/inventory/electron/electron/preload.ts create mode 100644 .benchmark/inventory/electron/electron/tray.ts create mode 100644 .benchmark/inventory/electron/electron/updater.ts create mode 100644 .benchmark/inventory/electron/electron/utils.ts create mode 100644 .benchmark/inventory/electron/entitlements.mac.plist create mode 100644 .benchmark/inventory/electron/package.json create mode 100644 .benchmark/inventory/electron/tsconfig.json create mode 100644 .benchmark/inventory/frontend/app/cart/page.tsx create mode 100644 .benchmark/inventory/frontend/app/globals.css create mode 100644 .benchmark/inventory/frontend/app/inventory/page.tsx create mode 100644 .benchmark/inventory/frontend/app/layout.tsx create mode 100644 .benchmark/inventory/frontend/app/order/[id]/page.tsx create mode 100644 .benchmark/inventory/frontend/app/orders/page.tsx create mode 100644 .benchmark/inventory/frontend/app/page.tsx create mode 100644 .benchmark/inventory/frontend/app/product/[id]/page.tsx create mode 100644 .benchmark/inventory/frontend/app/products/page.tsx create mode 100644 .benchmark/inventory/frontend/app/profile/page.tsx create mode 100644 .benchmark/inventory/frontend/app/stock_alerts/page.tsx create mode 100644 .benchmark/inventory/frontend/app/stocktaking/page.tsx create mode 100644 .benchmark/inventory/frontend/app/suppliers/page.tsx create mode 100644 .benchmark/inventory/frontend/app/供应商管理/page.tsx create mode 100644 .benchmark/inventory/frontend/app/商品入库出库/page.tsx create mode 100644 .benchmark/inventory/frontend/app/库存盘点/page.tsx create mode 100644 .benchmark/inventory/frontend/app/库存预警/page.tsx create mode 100644 .benchmark/inventory/frontend/components/ecommerce/ProductCard.tsx create mode 100644 .benchmark/inventory/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/inventory/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/inventory/frontend/components/ui/Button.tsx create mode 100644 .benchmark/inventory/frontend/components/ui/Card.tsx create mode 100644 .benchmark/inventory/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/inventory/frontend/components/ui/Input.tsx create mode 100644 .benchmark/inventory/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/inventory/frontend/hooks/useAuth.ts create mode 100644 .benchmark/inventory/frontend/hooks/useCoupons.ts create mode 100644 .benchmark/inventory/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/inventory/frontend/hooks/useForm.ts create mode 100644 .benchmark/inventory/frontend/hooks/useInventory.ts create mode 100644 .benchmark/inventory/frontend/hooks/useItem.ts create mode 100644 .benchmark/inventory/frontend/hooks/useLogistics.ts create mode 100644 .benchmark/inventory/frontend/hooks/useOrders.ts create mode 100644 .benchmark/inventory/frontend/hooks/usePayments.ts create mode 100644 .benchmark/inventory/frontend/hooks/useProducts.ts create mode 100644 .benchmark/inventory/frontend/hooks/useStock_alerts.ts create mode 100644 .benchmark/inventory/frontend/hooks/useStocktaking.ts create mode 100644 .benchmark/inventory/frontend/hooks/useSuppliers.ts create mode 100644 .benchmark/inventory/frontend/next-env.d.ts create mode 100644 .benchmark/inventory/frontend/next.config.ts create mode 100644 .benchmark/inventory/frontend/package.json create mode 100644 .benchmark/inventory/frontend/postcss.config.js create mode 100644 .benchmark/inventory/frontend/services/api.ts create mode 100644 .benchmark/inventory/frontend/services/auth.ts create mode 100644 .benchmark/inventory/frontend/services/coupons.ts create mode 100644 .benchmark/inventory/frontend/services/inventory.ts create mode 100644 .benchmark/inventory/frontend/services/logistics.ts create mode 100644 .benchmark/inventory/frontend/services/orders.ts create mode 100644 .benchmark/inventory/frontend/services/payments.ts create mode 100644 .benchmark/inventory/frontend/services/products.ts create mode 100644 .benchmark/inventory/frontend/services/stock_alerts.ts create mode 100644 .benchmark/inventory/frontend/services/stocktaking.ts create mode 100644 .benchmark/inventory/frontend/services/suppliers.ts create mode 100644 .benchmark/inventory/frontend/services/供应商管理.ts create mode 100644 .benchmark/inventory/frontend/services/商品入库出库.ts create mode 100644 .benchmark/inventory/frontend/services/库存盘点.ts create mode 100644 .benchmark/inventory/frontend/services/库存预警.ts create mode 100644 .benchmark/inventory/frontend/tailwind.config.ts create mode 100644 .benchmark/inventory/frontend/tsconfig.json create mode 100644 .benchmark/inventory/frontend/types/index.ts create mode 100644 .benchmark/inventory/fullstack/.gitignore create mode 100644 .benchmark/inventory/fullstack/README.md create mode 100644 .benchmark/inventory/fullstack/apps/api/.gitignore create mode 100644 .benchmark/inventory/fullstack/apps/api/README.md create mode 100644 .benchmark/inventory/fullstack/apps/api/package.json create mode 100644 .benchmark/inventory/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/__tests__/logistics.test.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/__tests__/order_items.test.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/__tests__/orders.test.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/__tests__/products.test.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/routes/logistics.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/routes/order_items.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/routes/orders.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/routes/products.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/services/logistic.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/services/order.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/services/order_item.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/services/product.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/inventory/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/inventory/fullstack/apps/web/app/cart/page.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/inventory/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/app/order/[id]/page.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/app/orders/page.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/app/product/[id]/page.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/app/profile/page.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/components/ecommerce/ProductCard.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/inventory/fullstack/apps/web/hooks/useCoupons.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/hooks/useLogistics.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/hooks/useOrders.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/hooks/usePayments.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/hooks/useProducts.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/package.json create mode 100644 .benchmark/inventory/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/inventory/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/services/coupons.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/services/logistics.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/services/orders.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/services/payments.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/services/products.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/inventory/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/inventory/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/inventory/fullstack/package.json create mode 100644 .benchmark/inventory/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/inventory/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/inventory/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/inventory/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/inventory/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/inventory/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/inventory/fullstack/scripts/build.mjs create mode 100644 .benchmark/inventory/fullstack/scripts/dev.mjs create mode 100644 .benchmark/inventory/prd.json create mode 100644 .benchmark/inventory/release/release/README.md create mode 100644 .benchmark/inventory/release/release/build-info.json create mode 100644 .benchmark/inventory/release/release/checksums/checksums.txt create mode 100644 .benchmark/inventory/release/release/linux/shopapp-0.1.0.AppImage create mode 100644 .benchmark/inventory/release/release/linux/shopapp_0.1.0_amd64.deb create mode 100644 .benchmark/inventory/release/release/macos/shopapp-0.1.0-arm64.dmg create mode 100644 .benchmark/inventory/release/release/macos/shopapp-0.1.0.dmg create mode 100644 .benchmark/inventory/release/release/manifests/manifest.json create mode 100644 .benchmark/inventory/release/release/release-notes/release-notes.md create mode 100644 .benchmark/inventory/release/release/version.json create mode 100644 .benchmark/inventory/release/release/windows/shopapp-0.1.0-portable.exe create mode 100644 .benchmark/inventory/release/release/windows/shopapp-setup-0.1.0.exe create mode 100644 .benchmark/petcare-e2e/arch.json create mode 100644 .benchmark/petcare-e2e/backend/.gitignore create mode 100644 .benchmark/petcare-e2e/backend/README.md create mode 100644 .benchmark/petcare-e2e/backend/package.json create mode 100644 .benchmark/petcare-e2e/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/petcare-e2e/backend/src/__tests__/daily_logs.test.ts create mode 100644 .benchmark/petcare-e2e/backend/src/__tests__/pets.test.ts create mode 100644 .benchmark/petcare-e2e/backend/src/__tests__/schedules.test.ts create mode 100644 .benchmark/petcare-e2e/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/petcare-e2e/backend/src/db/client.ts create mode 100644 .benchmark/petcare-e2e/backend/src/db/schema.ts create mode 100644 .benchmark/petcare-e2e/backend/src/index.ts create mode 100644 .benchmark/petcare-e2e/backend/src/middleware/auth.ts create mode 100644 .benchmark/petcare-e2e/backend/src/routes/auth.ts create mode 100644 .benchmark/petcare-e2e/backend/src/routes/daily_logs.ts create mode 100644 .benchmark/petcare-e2e/backend/src/routes/pets.ts create mode 100644 .benchmark/petcare-e2e/backend/src/routes/schedules.ts create mode 100644 .benchmark/petcare-e2e/backend/src/routes/users.ts create mode 100644 .benchmark/petcare-e2e/backend/src/services/daily_log.ts create mode 100644 .benchmark/petcare-e2e/backend/src/services/pet.ts create mode 100644 .benchmark/petcare-e2e/backend/src/services/schedule.ts create mode 100644 .benchmark/petcare-e2e/backend/src/services/user.ts create mode 100644 .benchmark/petcare-e2e/backend/src/types/fastify.d.ts create mode 100644 .benchmark/petcare-e2e/backend/src/types/index.ts create mode 100644 .benchmark/petcare-e2e/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/petcare-e2e/backend/tsconfig.json create mode 100644 .benchmark/petcare-e2e/electron/README.md create mode 100644 .benchmark/petcare-e2e/electron/assets/icon.png create mode 100644 .benchmark/petcare-e2e/electron/electron-builder.yml create mode 100644 .benchmark/petcare-e2e/electron/electron/ipc.ts create mode 100644 .benchmark/petcare-e2e/electron/electron/main.ts create mode 100644 .benchmark/petcare-e2e/electron/electron/preload.ts create mode 100644 .benchmark/petcare-e2e/electron/electron/tray.ts create mode 100644 .benchmark/petcare-e2e/electron/electron/updater.ts create mode 100644 .benchmark/petcare-e2e/electron/electron/utils.ts create mode 100644 .benchmark/petcare-e2e/electron/entitlements.mac.plist create mode 100644 .benchmark/petcare-e2e/electron/package.json create mode 100644 .benchmark/petcare-e2e/electron/tsconfig.json create mode 100644 .benchmark/petcare-e2e/frontend/app/album/page.tsx create mode 100644 .benchmark/petcare-e2e/frontend/app/daily-log/page.tsx create mode 100644 .benchmark/petcare-e2e/frontend/app/globals.css create mode 100644 .benchmark/petcare-e2e/frontend/app/hospitals/page.tsx create mode 100644 .benchmark/petcare-e2e/frontend/app/layout.tsx create mode 100644 .benchmark/petcare-e2e/frontend/app/page.tsx create mode 100644 .benchmark/petcare-e2e/frontend/app/pet/[id]/page.tsx create mode 100644 .benchmark/petcare-e2e/frontend/app/profile/page.tsx create mode 100644 .benchmark/petcare-e2e/frontend/app/schedule/page.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/pet/PetCard.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/pet/ScheduleCard.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/ui/Button.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/ui/Card.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/ui/Input.tsx create mode 100644 .benchmark/petcare-e2e/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/petcare-e2e/frontend/hooks/useAlbums.ts create mode 100644 .benchmark/petcare-e2e/frontend/hooks/useDailylogs.ts create mode 100644 .benchmark/petcare-e2e/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/petcare-e2e/frontend/hooks/useForm.ts create mode 100644 .benchmark/petcare-e2e/frontend/hooks/useHospitals.ts create mode 100644 .benchmark/petcare-e2e/frontend/hooks/usePets.ts create mode 100644 .benchmark/petcare-e2e/frontend/hooks/useSchedules.ts create mode 100644 .benchmark/petcare-e2e/frontend/next.config.ts create mode 100644 .benchmark/petcare-e2e/frontend/package.json create mode 100644 .benchmark/petcare-e2e/frontend/postcss.config.js create mode 100644 .benchmark/petcare-e2e/frontend/services/albums.ts create mode 100644 .benchmark/petcare-e2e/frontend/services/api.ts create mode 100644 .benchmark/petcare-e2e/frontend/services/daily-logs.ts create mode 100644 .benchmark/petcare-e2e/frontend/services/hospitals.ts create mode 100644 .benchmark/petcare-e2e/frontend/services/pets.ts create mode 100644 .benchmark/petcare-e2e/frontend/services/schedules.ts create mode 100644 .benchmark/petcare-e2e/frontend/tailwind.config.ts create mode 100644 .benchmark/petcare-e2e/frontend/tsconfig.json create mode 100644 .benchmark/petcare-e2e/frontend/types/index.ts create mode 100644 .benchmark/petcare-e2e/prd.json create mode 100644 .benchmark/petcare-e2e/release/release/README.md create mode 100644 .benchmark/petcare-e2e/release/release/build-info.json create mode 100644 .benchmark/petcare-e2e/release/release/checksums/checksums.txt create mode 100644 .benchmark/petcare-e2e/release/release/linux/petcare-0.1.0.AppImage create mode 100644 .benchmark/petcare-e2e/release/release/linux/petcare_0.1.0_amd64.deb create mode 100644 .benchmark/petcare-e2e/release/release/macos/petcare-0.1.0-arm64.dmg create mode 100644 .benchmark/petcare-e2e/release/release/macos/petcare-0.1.0.dmg create mode 100644 .benchmark/petcare-e2e/release/release/manifests/manifest.json create mode 100644 .benchmark/petcare-e2e/release/release/release-notes/release-notes.md create mode 100644 .benchmark/petcare-e2e/release/release/version.json create mode 100644 .benchmark/petcare-e2e/release/release/windows/petcare-0.1.0-portable.exe create mode 100644 .benchmark/petcare-e2e/release/release/windows/petcare-setup-0.1.0.exe create mode 100644 .benchmark/petcare/arch.json create mode 100644 .benchmark/petcare/backend/.gitignore create mode 100644 .benchmark/petcare/backend/README.md create mode 100644 .benchmark/petcare/backend/package.json create mode 100644 .benchmark/petcare/backend/src/__tests__/albums.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/daily-logs.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/daily_logs.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/hospitals.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/items.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/pets.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/records.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/schedules.test.ts create mode 100644 .benchmark/petcare/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/petcare/backend/src/db/client.ts create mode 100644 .benchmark/petcare/backend/src/db/schema.ts create mode 100644 .benchmark/petcare/backend/src/index.ts create mode 100644 .benchmark/petcare/backend/src/middleware/auth.ts create mode 100644 .benchmark/petcare/backend/src/routes/albums.ts create mode 100644 .benchmark/petcare/backend/src/routes/auth.ts create mode 100644 .benchmark/petcare/backend/src/routes/daily-logs.ts create mode 100644 .benchmark/petcare/backend/src/routes/daily_logs.ts create mode 100644 .benchmark/petcare/backend/src/routes/hospitals.ts create mode 100644 .benchmark/petcare/backend/src/routes/items.ts create mode 100644 .benchmark/petcare/backend/src/routes/pets.ts create mode 100644 .benchmark/petcare/backend/src/routes/records.ts create mode 100644 .benchmark/petcare/backend/src/routes/schedules.ts create mode 100644 .benchmark/petcare/backend/src/routes/users.ts create mode 100644 .benchmark/petcare/backend/src/services/album.ts create mode 100644 .benchmark/petcare/backend/src/services/daily-log.ts create mode 100644 .benchmark/petcare/backend/src/services/daily_log.ts create mode 100644 .benchmark/petcare/backend/src/services/hospital.ts create mode 100644 .benchmark/petcare/backend/src/services/item.ts create mode 100644 .benchmark/petcare/backend/src/services/pet.ts create mode 100644 .benchmark/petcare/backend/src/services/record.ts create mode 100644 .benchmark/petcare/backend/src/services/schedule.ts create mode 100644 .benchmark/petcare/backend/src/services/user.ts create mode 100644 .benchmark/petcare/backend/src/types/fastify.d.ts create mode 100644 .benchmark/petcare/backend/src/types/index.ts create mode 100644 .benchmark/petcare/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/petcare/backend/tsconfig.json create mode 100644 .benchmark/petcare/electron/README.md create mode 100644 .benchmark/petcare/electron/assets/icon.png create mode 100644 .benchmark/petcare/electron/electron-builder.yml create mode 100644 .benchmark/petcare/electron/electron/ipc.ts create mode 100644 .benchmark/petcare/electron/electron/main.ts create mode 100644 .benchmark/petcare/electron/electron/preload.ts create mode 100644 .benchmark/petcare/electron/electron/tray.ts create mode 100644 .benchmark/petcare/electron/electron/updater.ts create mode 100644 .benchmark/petcare/electron/electron/utils.ts create mode 100644 .benchmark/petcare/electron/entitlements.mac.plist create mode 100644 .benchmark/petcare/electron/package.json create mode 100644 .benchmark/petcare/electron/tsconfig.json create mode 100644 .benchmark/petcare/frontend/app/album/page.tsx create mode 100644 .benchmark/petcare/frontend/app/albums/page.tsx create mode 100644 .benchmark/petcare/frontend/app/daily-log/page.tsx create mode 100644 .benchmark/petcare/frontend/app/globals.css create mode 100644 .benchmark/petcare/frontend/app/hospitals/page.tsx create mode 100644 .benchmark/petcare/frontend/app/items/page.tsx create mode 100644 .benchmark/petcare/frontend/app/layout.tsx create mode 100644 .benchmark/petcare/frontend/app/page.tsx create mode 100644 .benchmark/petcare/frontend/app/pet/[id]/page.tsx create mode 100644 .benchmark/petcare/frontend/app/pets/page.tsx create mode 100644 .benchmark/petcare/frontend/app/profile/page.tsx create mode 100644 .benchmark/petcare/frontend/app/records/page.tsx create mode 100644 .benchmark/petcare/frontend/app/schedule/page.tsx create mode 100644 .benchmark/petcare/frontend/app/schedules/page.tsx create mode 100644 .benchmark/petcare/frontend/app/健康日程/page.tsx create mode 100644 .benchmark/petcare/frontend/app/健康百科/page.tsx create mode 100644 .benchmark/petcare/frontend/app/宠物档案/page.tsx create mode 100644 .benchmark/petcare/frontend/app/成长相册/page.tsx create mode 100644 .benchmark/petcare/frontend/app/日常记录/page.tsx create mode 100644 .benchmark/petcare/frontend/app/社区分享/page.tsx create mode 100644 .benchmark/petcare/frontend/app/附近医院/page.tsx create mode 100644 .benchmark/petcare/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/petcare/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/petcare/frontend/components/pet/PetCard.tsx create mode 100644 .benchmark/petcare/frontend/components/pet/ScheduleCard.tsx create mode 100644 .benchmark/petcare/frontend/components/ui/Button.tsx create mode 100644 .benchmark/petcare/frontend/components/ui/Card.tsx create mode 100644 .benchmark/petcare/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/petcare/frontend/components/ui/Input.tsx create mode 100644 .benchmark/petcare/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/petcare/frontend/hooks/useAlbums.ts create mode 100644 .benchmark/petcare/frontend/hooks/useDailylogs.ts create mode 100644 .benchmark/petcare/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/petcare/frontend/hooks/useForm.ts create mode 100644 .benchmark/petcare/frontend/hooks/useHospitals.ts create mode 100644 .benchmark/petcare/frontend/hooks/usePets.ts create mode 100644 .benchmark/petcare/frontend/hooks/useSchedules.ts create mode 100644 .benchmark/petcare/frontend/next-env.d.ts create mode 100644 .benchmark/petcare/frontend/next.config.ts create mode 100644 .benchmark/petcare/frontend/package.json create mode 100644 .benchmark/petcare/frontend/postcss.config.js create mode 100644 .benchmark/petcare/frontend/services/albums.ts create mode 100644 .benchmark/petcare/frontend/services/api.ts create mode 100644 .benchmark/petcare/frontend/services/daily-logs.ts create mode 100644 .benchmark/petcare/frontend/services/hospitals.ts create mode 100644 .benchmark/petcare/frontend/services/pets.ts create mode 100644 .benchmark/petcare/frontend/services/schedules.ts create mode 100644 .benchmark/petcare/frontend/tailwind.config.ts create mode 100644 .benchmark/petcare/frontend/tsconfig.json create mode 100644 .benchmark/petcare/frontend/types/index.ts create mode 100644 .benchmark/petcare/fullstack/.gitignore create mode 100644 .benchmark/petcare/fullstack/README.md create mode 100644 .benchmark/petcare/fullstack/apps/api/.gitignore create mode 100644 .benchmark/petcare/fullstack/apps/api/README.md create mode 100644 .benchmark/petcare/fullstack/apps/api/package.json create mode 100644 .benchmark/petcare/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/__tests__/daily_logs.test.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/__tests__/pets.test.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/__tests__/schedules.test.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/routes/daily_logs.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/routes/pets.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/routes/schedules.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/services/daily_log.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/services/pet.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/services/schedule.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/petcare/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/petcare/fullstack/apps/web/app/album/page.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/app/daily-log/page.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/petcare/fullstack/apps/web/app/hospitals/page.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/app/pet/[id]/page.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/app/profile/page.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/app/schedule/page.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/pet/PetCard.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/pet/ScheduleCard.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/petcare/fullstack/apps/web/hooks/useAlbums.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/hooks/useDailylogs.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/hooks/useHospitals.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/hooks/usePets.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/hooks/useSchedules.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/package.json create mode 100644 .benchmark/petcare/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/petcare/fullstack/apps/web/services/albums.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/services/daily-logs.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/services/hospitals.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/services/pets.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/services/schedules.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/petcare/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/petcare/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/petcare/fullstack/package.json create mode 100644 .benchmark/petcare/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/petcare/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/petcare/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/petcare/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/petcare/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/petcare/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/petcare/fullstack/scripts/build.mjs create mode 100644 .benchmark/petcare/fullstack/scripts/dev.mjs create mode 100644 .benchmark/petcare/prd.json create mode 100644 .benchmark/petcare/release/release/README.md create mode 100644 .benchmark/petcare/release/release/build-info.json create mode 100644 .benchmark/petcare/release/release/checksums/checksums.txt create mode 100644 .benchmark/petcare/release/release/linux/petcare-0.1.0.AppImage create mode 100644 .benchmark/petcare/release/release/linux/petcare_0.1.0_amd64.deb create mode 100644 .benchmark/petcare/release/release/macos/petcare-0.1.0-arm64.dmg create mode 100644 .benchmark/petcare/release/release/macos/petcare-0.1.0.dmg create mode 100644 .benchmark/petcare/release/release/manifests/manifest.json create mode 100644 .benchmark/petcare/release/release/release-notes/release-notes.md create mode 100644 .benchmark/petcare/release/release/version.json create mode 100644 .benchmark/petcare/release/release/windows/petcare-0.1.0-portable.exe create mode 100644 .benchmark/petcare/release/release/windows/petcare-setup-0.1.0.exe create mode 100644 .benchmark/project-mgmt/arch.json create mode 100644 .benchmark/project-mgmt/backend/.gitignore create mode 100644 .benchmark/project-mgmt/backend/README.md create mode 100644 .benchmark/project-mgmt/backend/package.json create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/approvals.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/boards.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/contracts.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/customers.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/items.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/reminders.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/tasks.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/project-mgmt/backend/src/db/client.ts create mode 100644 .benchmark/project-mgmt/backend/src/db/schema.ts create mode 100644 .benchmark/project-mgmt/backend/src/index.ts create mode 100644 .benchmark/project-mgmt/backend/src/middleware/auth.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/approvals.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/auth.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/boards.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/contracts.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/customers.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/items.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/reminders.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/tasks.ts create mode 100644 .benchmark/project-mgmt/backend/src/routes/users.ts create mode 100644 .benchmark/project-mgmt/backend/src/services/approval.ts create mode 100644 .benchmark/project-mgmt/backend/src/services/board.ts create mode 100644 .benchmark/project-mgmt/backend/src/services/contract.ts create mode 100644 .benchmark/project-mgmt/backend/src/services/customer.ts create mode 100644 .benchmark/project-mgmt/backend/src/services/item.ts create mode 100644 .benchmark/project-mgmt/backend/src/services/reminder.ts create mode 100644 .benchmark/project-mgmt/backend/src/services/task.ts create mode 100644 .benchmark/project-mgmt/backend/src/services/user.ts create mode 100644 .benchmark/project-mgmt/backend/src/types/fastify.d.ts create mode 100644 .benchmark/project-mgmt/backend/src/types/index.ts create mode 100644 .benchmark/project-mgmt/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/project-mgmt/backend/tsconfig.json create mode 100644 .benchmark/project-mgmt/electron/README.md create mode 100644 .benchmark/project-mgmt/electron/assets/icon.png create mode 100644 .benchmark/project-mgmt/electron/electron-builder.yml create mode 100644 .benchmark/project-mgmt/electron/electron/ipc.ts create mode 100644 .benchmark/project-mgmt/electron/electron/main.ts create mode 100644 .benchmark/project-mgmt/electron/electron/preload.ts create mode 100644 .benchmark/project-mgmt/electron/electron/tray.ts create mode 100644 .benchmark/project-mgmt/electron/electron/updater.ts create mode 100644 .benchmark/project-mgmt/electron/electron/utils.ts create mode 100644 .benchmark/project-mgmt/electron/entitlements.mac.plist create mode 100644 .benchmark/project-mgmt/electron/package.json create mode 100644 .benchmark/project-mgmt/electron/tsconfig.json create mode 100644 .benchmark/project-mgmt/frontend/app/boards/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/feature/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/globals.css create mode 100644 .benchmark/project-mgmt/frontend/app/items/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/layout.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/profile/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/tasks/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/任务分配/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/团队协作/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/甘特图/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/app/项目看板/page.tsx create mode 100644 .benchmark/project-mgmt/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/project-mgmt/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/project-mgmt/frontend/components/ui/Button.tsx create mode 100644 .benchmark/project-mgmt/frontend/components/ui/Card.tsx create mode 100644 .benchmark/project-mgmt/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/project-mgmt/frontend/components/ui/Input.tsx create mode 100644 .benchmark/project-mgmt/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/project-mgmt/frontend/hooks/useAuth.ts create mode 100644 .benchmark/project-mgmt/frontend/hooks/useBoards.ts create mode 100644 .benchmark/project-mgmt/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/project-mgmt/frontend/hooks/useForm.ts create mode 100644 .benchmark/project-mgmt/frontend/hooks/useHealth.ts create mode 100644 .benchmark/project-mgmt/frontend/hooks/useItem.ts create mode 100644 .benchmark/project-mgmt/frontend/hooks/useItems.ts create mode 100644 .benchmark/project-mgmt/frontend/hooks/useTasks.ts create mode 100644 .benchmark/project-mgmt/frontend/hooks/useUsers.ts create mode 100644 .benchmark/project-mgmt/frontend/next-env.d.ts create mode 100644 .benchmark/project-mgmt/frontend/next.config.ts create mode 100644 .benchmark/project-mgmt/frontend/package.json create mode 100644 .benchmark/project-mgmt/frontend/postcss.config.js create mode 100644 .benchmark/project-mgmt/frontend/services/api.ts create mode 100644 .benchmark/project-mgmt/frontend/services/auth.ts create mode 100644 .benchmark/project-mgmt/frontend/services/boards.ts create mode 100644 .benchmark/project-mgmt/frontend/services/health.ts create mode 100644 .benchmark/project-mgmt/frontend/services/items.ts create mode 100644 .benchmark/project-mgmt/frontend/services/tasks.ts create mode 100644 .benchmark/project-mgmt/frontend/services/users.ts create mode 100644 .benchmark/project-mgmt/frontend/services/任务分配.ts create mode 100644 .benchmark/project-mgmt/frontend/services/团队协作.ts create mode 100644 .benchmark/project-mgmt/frontend/services/甘特图.ts create mode 100644 .benchmark/project-mgmt/frontend/services/项目看板.ts create mode 100644 .benchmark/project-mgmt/frontend/tailwind.config.ts create mode 100644 .benchmark/project-mgmt/frontend/tsconfig.json create mode 100644 .benchmark/project-mgmt/frontend/types/index.ts create mode 100644 .benchmark/project-mgmt/fullstack/.gitignore create mode 100644 .benchmark/project-mgmt/fullstack/README.md create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/.gitignore create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/README.md create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/package.json create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/__tests__/items.test.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/routes/items.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/services/item.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/app/feature/page.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/app/profile/page.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/hooks/useHealth.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/hooks/useUsers.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/package.json create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/services/health.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/services/users.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/project-mgmt/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/project-mgmt/fullstack/package.json create mode 100644 .benchmark/project-mgmt/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/project-mgmt/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/project-mgmt/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/project-mgmt/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/project-mgmt/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/project-mgmt/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/project-mgmt/fullstack/scripts/build.mjs create mode 100644 .benchmark/project-mgmt/fullstack/scripts/dev.mjs create mode 100644 .benchmark/project-mgmt/prd.json create mode 100644 .benchmark/project-mgmt/release/release/README.md create mode 100644 .benchmark/project-mgmt/release/release/build-info.json create mode 100644 .benchmark/project-mgmt/release/release/checksums/checksums.txt create mode 100644 .benchmark/project-mgmt/release/release/linux/myproject-0.1.0.AppImage create mode 100644 .benchmark/project-mgmt/release/release/linux/myproject_0.1.0_amd64.deb create mode 100644 .benchmark/project-mgmt/release/release/macos/myproject-0.1.0-arm64.dmg create mode 100644 .benchmark/project-mgmt/release/release/macos/myproject-0.1.0.dmg create mode 100644 .benchmark/project-mgmt/release/release/manifests/manifest.json create mode 100644 .benchmark/project-mgmt/release/release/release-notes/release-notes.md create mode 100644 .benchmark/project-mgmt/release/release/version.json create mode 100644 .benchmark/project-mgmt/release/release/windows/myproject-0.1.0-portable.exe create mode 100644 .benchmark/project-mgmt/release/release/windows/myproject-setup-0.1.0.exe create mode 100644 .benchmark/ticket/arch.json create mode 100644 .benchmark/ticket/backend/.gitignore create mode 100644 .benchmark/ticket/backend/README.md create mode 100644 .benchmark/ticket/backend/package.json create mode 100644 .benchmark/ticket/backend/src/__tests__/approval_steps.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/approvals.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/attendance.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/auth.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/contracts.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/customers.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/departments.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/items.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/reminders.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/tickets.test.ts create mode 100644 .benchmark/ticket/backend/src/__tests__/users.test.ts create mode 100644 .benchmark/ticket/backend/src/db/client.ts create mode 100644 .benchmark/ticket/backend/src/db/schema.ts create mode 100644 .benchmark/ticket/backend/src/index.ts create mode 100644 .benchmark/ticket/backend/src/middleware/auth.ts create mode 100644 .benchmark/ticket/backend/src/routes/approval_steps.ts create mode 100644 .benchmark/ticket/backend/src/routes/approvals.ts create mode 100644 .benchmark/ticket/backend/src/routes/attendance.ts create mode 100644 .benchmark/ticket/backend/src/routes/auth.ts create mode 100644 .benchmark/ticket/backend/src/routes/contracts.ts create mode 100644 .benchmark/ticket/backend/src/routes/customers.ts create mode 100644 .benchmark/ticket/backend/src/routes/departments.ts create mode 100644 .benchmark/ticket/backend/src/routes/items.ts create mode 100644 .benchmark/ticket/backend/src/routes/reminders.ts create mode 100644 .benchmark/ticket/backend/src/routes/tickets.ts create mode 100644 .benchmark/ticket/backend/src/routes/users.ts create mode 100644 .benchmark/ticket/backend/src/services/approval.ts create mode 100644 .benchmark/ticket/backend/src/services/approval_step.ts create mode 100644 .benchmark/ticket/backend/src/services/attendance.ts create mode 100644 .benchmark/ticket/backend/src/services/contract.ts create mode 100644 .benchmark/ticket/backend/src/services/customer.ts create mode 100644 .benchmark/ticket/backend/src/services/department.ts create mode 100644 .benchmark/ticket/backend/src/services/item.ts create mode 100644 .benchmark/ticket/backend/src/services/reminder.ts create mode 100644 .benchmark/ticket/backend/src/services/ticket.ts create mode 100644 .benchmark/ticket/backend/src/services/user.ts create mode 100644 .benchmark/ticket/backend/src/types/fastify.d.ts create mode 100644 .benchmark/ticket/backend/src/types/index.ts create mode 100644 .benchmark/ticket/backend/src/types/sql.js.d.ts create mode 100644 .benchmark/ticket/backend/tsconfig.json create mode 100644 .benchmark/ticket/electron/README.md create mode 100644 .benchmark/ticket/electron/assets/icon.png create mode 100644 .benchmark/ticket/electron/electron-builder.yml create mode 100644 .benchmark/ticket/electron/electron/ipc.ts create mode 100644 .benchmark/ticket/electron/electron/main.ts create mode 100644 .benchmark/ticket/electron/electron/preload.ts create mode 100644 .benchmark/ticket/electron/electron/tray.ts create mode 100644 .benchmark/ticket/electron/electron/updater.ts create mode 100644 .benchmark/ticket/electron/electron/utils.ts create mode 100644 .benchmark/ticket/electron/entitlements.mac.plist create mode 100644 .benchmark/ticket/electron/package.json create mode 100644 .benchmark/ticket/electron/tsconfig.json create mode 100644 .benchmark/ticket/frontend/app/approvals/page.tsx create mode 100644 .benchmark/ticket/frontend/app/attendance/page.tsx create mode 100644 .benchmark/ticket/frontend/app/contacts/page.tsx create mode 100644 .benchmark/ticket/frontend/app/globals.css create mode 100644 .benchmark/ticket/frontend/app/items/page.tsx create mode 100644 .benchmark/ticket/frontend/app/layout.tsx create mode 100644 .benchmark/ticket/frontend/app/notices/page.tsx create mode 100644 .benchmark/ticket/frontend/app/page.tsx create mode 100644 .benchmark/ticket/frontend/app/profile/page.tsx create mode 100644 .benchmark/ticket/frontend/app/tickets/page.tsx create mode 100644 .benchmark/ticket/frontend/app/优先级管理/page.tsx create mode 100644 .benchmark/ticket/frontend/app/分配/page.tsx create mode 100644 .benchmark/ticket/frontend/app/处理流程/page.tsx create mode 100644 .benchmark/ticket/frontend/app/工单创建/page.tsx create mode 100644 .benchmark/ticket/frontend/app/工单归档/page.tsx create mode 100644 .benchmark/ticket/frontend/components/layout/BottomNav.tsx create mode 100644 .benchmark/ticket/frontend/components/layout/Sidebar.tsx create mode 100644 .benchmark/ticket/frontend/components/ui/Button.tsx create mode 100644 .benchmark/ticket/frontend/components/ui/Card.tsx create mode 100644 .benchmark/ticket/frontend/components/ui/EmptyState.tsx create mode 100644 .benchmark/ticket/frontend/components/ui/Input.tsx create mode 100644 .benchmark/ticket/frontend/components/ui/Modal.tsx create mode 100644 .benchmark/ticket/frontend/hooks/useApprovals.ts create mode 100644 .benchmark/ticket/frontend/hooks/useAttendance.ts create mode 100644 .benchmark/ticket/frontend/hooks/useAuth.ts create mode 100644 .benchmark/ticket/frontend/hooks/useDebounce.ts create mode 100644 .benchmark/ticket/frontend/hooks/useDepartments.ts create mode 100644 .benchmark/ticket/frontend/hooks/useForm.ts create mode 100644 .benchmark/ticket/frontend/hooks/useItem.ts create mode 100644 .benchmark/ticket/frontend/hooks/useItems.ts create mode 100644 .benchmark/ticket/frontend/hooks/useNotices.ts create mode 100644 .benchmark/ticket/frontend/hooks/useTickets.ts create mode 100644 .benchmark/ticket/frontend/hooks/useUsers.ts create mode 100644 .benchmark/ticket/frontend/next-env.d.ts create mode 100644 .benchmark/ticket/frontend/next.config.ts create mode 100644 .benchmark/ticket/frontend/package.json create mode 100644 .benchmark/ticket/frontend/postcss.config.js create mode 100644 .benchmark/ticket/frontend/services/api.ts create mode 100644 .benchmark/ticket/frontend/services/approvals.ts create mode 100644 .benchmark/ticket/frontend/services/attendance.ts create mode 100644 .benchmark/ticket/frontend/services/auth.ts create mode 100644 .benchmark/ticket/frontend/services/departments.ts create mode 100644 .benchmark/ticket/frontend/services/items.ts create mode 100644 .benchmark/ticket/frontend/services/notices.ts create mode 100644 .benchmark/ticket/frontend/services/tickets.ts create mode 100644 .benchmark/ticket/frontend/services/users.ts create mode 100644 .benchmark/ticket/frontend/services/优先级管理.ts create mode 100644 .benchmark/ticket/frontend/services/分配.ts create mode 100644 .benchmark/ticket/frontend/services/处理流程.ts create mode 100644 .benchmark/ticket/frontend/services/工单创建.ts create mode 100644 .benchmark/ticket/frontend/tailwind.config.ts create mode 100644 .benchmark/ticket/frontend/tsconfig.json create mode 100644 .benchmark/ticket/frontend/types/index.ts create mode 100644 .benchmark/ticket/fullstack/.gitignore create mode 100644 .benchmark/ticket/fullstack/README.md create mode 100644 .benchmark/ticket/fullstack/apps/api/.gitignore create mode 100644 .benchmark/ticket/fullstack/apps/api/README.md create mode 100644 .benchmark/ticket/fullstack/apps/api/package.json create mode 100644 .benchmark/ticket/fullstack/apps/api/src/__tests__/approval_steps.test.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/__tests__/approvals.test.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/__tests__/attendance.test.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/__tests__/auth.test.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/__tests__/departments.test.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/__tests__/users.test.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/db/client.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/db/schema.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/index.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/middleware/auth.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/routes/approval_steps.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/routes/approvals.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/routes/attendance.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/routes/auth.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/routes/departments.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/routes/users.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/services/approval.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/services/approval_step.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/services/attendance.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/services/department.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/services/user.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/types/fastify.d.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/types/index.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/src/types/sql.js.d.ts create mode 100644 .benchmark/ticket/fullstack/apps/api/tsconfig.json create mode 100644 .benchmark/ticket/fullstack/apps/web/app/approvals/page.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/app/attendance/page.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/app/contacts/page.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/app/globals.css create mode 100644 .benchmark/ticket/fullstack/apps/web/app/layout.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/app/notices/page.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/app/page.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/app/profile/page.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/components/layout/BottomNav.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/components/layout/Sidebar.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/components/ui/Button.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/components/ui/Card.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/components/ui/EmptyState.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/components/ui/Input.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/components/ui/Modal.tsx create mode 100644 .benchmark/ticket/fullstack/apps/web/hooks/useApprovals.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/hooks/useAttendance.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/hooks/useDebounce.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/hooks/useDepartments.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/hooks/useForm.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/hooks/useNotices.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/hooks/useUsers.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/next.config.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/package.json create mode 100644 .benchmark/ticket/fullstack/apps/web/postcss.config.js create mode 100644 .benchmark/ticket/fullstack/apps/web/services/api.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/services/approvals.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/services/attendance.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/services/departments.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/services/notices.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/services/users.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/tailwind.config.ts create mode 100644 .benchmark/ticket/fullstack/apps/web/tsconfig.json create mode 100644 .benchmark/ticket/fullstack/apps/web/types/index.ts create mode 100644 .benchmark/ticket/fullstack/package.json create mode 100644 .benchmark/ticket/fullstack/packages/shared-config/package.json create mode 100644 .benchmark/ticket/fullstack/packages/shared-config/src/index.ts create mode 100644 .benchmark/ticket/fullstack/packages/shared-config/tsconfig.json create mode 100644 .benchmark/ticket/fullstack/packages/shared-types/package.json create mode 100644 .benchmark/ticket/fullstack/packages/shared-types/src/index.ts create mode 100644 .benchmark/ticket/fullstack/packages/shared-types/tsconfig.json create mode 100644 .benchmark/ticket/fullstack/scripts/build.mjs create mode 100644 .benchmark/ticket/fullstack/scripts/dev.mjs create mode 100644 .benchmark/ticket/prd.json create mode 100644 .benchmark/ticket/release/release/README.md create mode 100644 .benchmark/ticket/release/release/build-info.json create mode 100644 .benchmark/ticket/release/release/checksums/checksums.txt create mode 100644 .benchmark/ticket/release/release/linux/oaflow-0.1.0.AppImage create mode 100644 .benchmark/ticket/release/release/linux/oaflow_0.1.0_amd64.deb create mode 100644 .benchmark/ticket/release/release/macos/oaflow-0.1.0-arm64.dmg create mode 100644 .benchmark/ticket/release/release/macos/oaflow-0.1.0.dmg create mode 100644 .benchmark/ticket/release/release/manifests/manifest.json create mode 100644 .benchmark/ticket/release/release/release-notes/release-notes.md create mode 100644 .benchmark/ticket/release/release/version.json create mode 100644 .benchmark/ticket/release/release/windows/oaflow-0.1.0-portable.exe create mode 100644 .benchmark/ticket/release/release/windows/oaflow-setup-0.1.0.exe create mode 100644 .github/workflows/release-candidate.yml create mode 100644 .gitignore create mode 100644 .tmp-pr8-test/b01.jsonl create mode 100644 .tmp-pr8-test/b02.jsonl create mode 100644 .tmp-pr8-test/b03-thresholds.json create mode 100644 .tmp-pr8-test/b03.jsonl create mode 100644 .tmp-pr8-test/b04.jsonl create mode 100644 .tmp-pr8-test/b04b-thresholds.json create mode 100644 .tmp-pr8-test/b04b.jsonl create mode 100644 .tmp-pr8-test/b05.jsonl create mode 100644 .tmp-pr8-test/c01.json create mode 100644 .tmp-pr8-test/c01b.json create mode 100644 .tmp-pr8-test/c03.json create mode 100644 .tmp-pr8-test/c04.json create mode 100644 .tmp-pr8-test/c05-thresholds.json create mode 100644 .tmp-pr8-test/c05.json create mode 100644 .tmp-pr8-test/c05b.json create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 COMPATIBILITY-STRATEGY.md create mode 100644 DREAMS.md create mode 100644 GOVERNANCE.md create mode 100644 HEARTBEAT.md create mode 100644 IDENTITY.md create mode 100644 MEMORY.md create mode 100644 OpenClaw-Evolution-Plan.md create mode 100644 PORTFOLIO.md create mode 100644 PROJECT-COMPLETION-REPORT.md create mode 100644 README.md create mode 100644 RELEASE-NOTES-v2.md create mode 100644 SOUL.md create mode 100644 SYSTEM-AUDIT.md create mode 100644 TOOLS.md create mode 100644 USER.md create mode 100644 VERSION.md create mode 100644 architecture-example.json create mode 100644 audit-report.md create mode 100644 cases/README.md create mode 100644 cases/bookshelf-v2/arch.json create mode 100644 cases/bookshelf-v2/backend/.gitignore create mode 100644 cases/bookshelf-v2/backend/README.md create mode 100644 cases/bookshelf-v2/backend/package.json create mode 100644 cases/bookshelf-v2/backend/src/__tests__/auth.test.ts create mode 100644 cases/bookshelf-v2/backend/src/__tests__/note_tags.test.ts create mode 100644 cases/bookshelf-v2/backend/src/__tests__/notes.test.ts create mode 100644 cases/bookshelf-v2/backend/src/__tests__/tags.test.ts create mode 100644 cases/bookshelf-v2/backend/src/__tests__/users.test.ts create mode 100644 cases/bookshelf-v2/backend/src/db/client.ts create mode 100644 cases/bookshelf-v2/backend/src/db/schema.ts create mode 100644 cases/bookshelf-v2/backend/src/index.ts create mode 100644 cases/bookshelf-v2/backend/src/middleware/auth.ts create mode 100644 cases/bookshelf-v2/backend/src/routes/auth.ts create mode 100644 cases/bookshelf-v2/backend/src/routes/note_tags.ts create mode 100644 cases/bookshelf-v2/backend/src/routes/notes.ts create mode 100644 cases/bookshelf-v2/backend/src/routes/tags.ts create mode 100644 cases/bookshelf-v2/backend/src/routes/users.ts create mode 100644 cases/bookshelf-v2/backend/src/services/note.ts create mode 100644 cases/bookshelf-v2/backend/src/services/note_tag.ts create mode 100644 cases/bookshelf-v2/backend/src/services/tag.ts create mode 100644 cases/bookshelf-v2/backend/src/services/user.ts create mode 100644 cases/bookshelf-v2/backend/src/types/fastify.d.ts create mode 100644 cases/bookshelf-v2/backend/src/types/index.ts create mode 100644 cases/bookshelf-v2/backend/src/types/sql.js.d.ts create mode 100644 cases/bookshelf-v2/backend/tsconfig.json create mode 100644 cases/bookshelf-v2/electron/README.md create mode 100644 cases/bookshelf-v2/electron/assets/icon.png create mode 100644 cases/bookshelf-v2/electron/electron-builder.yml create mode 100644 cases/bookshelf-v2/electron/electron/ipc.ts create mode 100644 cases/bookshelf-v2/electron/electron/main.ts create mode 100644 cases/bookshelf-v2/electron/electron/preload.ts create mode 100644 cases/bookshelf-v2/electron/electron/tray.ts create mode 100644 cases/bookshelf-v2/electron/electron/updater.ts create mode 100644 cases/bookshelf-v2/electron/electron/utils.ts create mode 100644 cases/bookshelf-v2/electron/entitlements.mac.plist create mode 100644 cases/bookshelf-v2/electron/package.json create mode 100644 cases/bookshelf-v2/electron/tsconfig.json create mode 100644 cases/bookshelf-v2/frontend/app/globals.css create mode 100644 cases/bookshelf-v2/frontend/app/layout.tsx create mode 100644 cases/bookshelf-v2/frontend/app/page.tsx create mode 100644 cases/bookshelf-v2/frontend/app/书籍库/page.tsx create mode 100644 cases/bookshelf-v2/frontend/app/书评/page.tsx create mode 100644 cases/bookshelf-v2/frontend/app/搜索和筛选/page.tsx create mode 100644 cases/bookshelf-v2/frontend/app/笔记和标注/page.tsx create mode 100644 cases/bookshelf-v2/frontend/app/统计仪表盘/page.tsx create mode 100644 cases/bookshelf-v2/frontend/app/阅读状态/page.tsx create mode 100644 cases/bookshelf-v2/frontend/app/阅读进度/page.tsx create mode 100644 cases/bookshelf-v2/frontend/components/layout/BottomNav.tsx create mode 100644 cases/bookshelf-v2/frontend/components/layout/Sidebar.tsx create mode 100644 cases/bookshelf-v2/frontend/components/note/NoteCard.tsx create mode 100644 cases/bookshelf-v2/frontend/components/ui/Button.tsx create mode 100644 cases/bookshelf-v2/frontend/components/ui/Card.tsx create mode 100644 cases/bookshelf-v2/frontend/components/ui/EmptyState.tsx create mode 100644 cases/bookshelf-v2/frontend/components/ui/Input.tsx create mode 100644 cases/bookshelf-v2/frontend/components/ui/Modal.tsx create mode 100644 cases/bookshelf-v2/frontend/hooks/useAuth.ts create mode 100644 cases/bookshelf-v2/frontend/hooks/useDebounce.ts create mode 100644 cases/bookshelf-v2/frontend/hooks/useForm.ts create mode 100644 cases/bookshelf-v2/frontend/hooks/useItem.ts create mode 100644 cases/bookshelf-v2/frontend/next.config.ts create mode 100644 cases/bookshelf-v2/frontend/package.json create mode 100644 cases/bookshelf-v2/frontend/postcss.config.js create mode 100644 cases/bookshelf-v2/frontend/services/api.ts create mode 100644 cases/bookshelf-v2/frontend/services/auth.ts create mode 100644 cases/bookshelf-v2/frontend/services/书籍库.ts create mode 100644 cases/bookshelf-v2/frontend/services/笔记和标注.ts create mode 100644 cases/bookshelf-v2/frontend/services/阅读状态.ts create mode 100644 cases/bookshelf-v2/frontend/services/阅读进度.ts create mode 100644 cases/bookshelf-v2/frontend/tailwind.config.ts create mode 100644 cases/bookshelf-v2/frontend/tsconfig.json create mode 100644 cases/bookshelf-v2/frontend/types/index.ts create mode 100644 cases/bookshelf-v2/prd.json create mode 100644 cases/bookshelf-v2/release/release/README.md create mode 100644 cases/bookshelf-v2/release/release/build-info.json create mode 100644 cases/bookshelf-v2/release/release/checksums/checksums.txt create mode 100644 cases/bookshelf-v2/release/release/linux/bookshelf-0.1.0.AppImage create mode 100644 cases/bookshelf-v2/release/release/linux/bookshelf_0.1.0_amd64.deb create mode 100644 cases/bookshelf-v2/release/release/macos/bookshelf-0.1.0-arm64.dmg create mode 100644 cases/bookshelf-v2/release/release/macos/bookshelf-0.1.0.dmg create mode 100644 cases/bookshelf-v2/release/release/manifests/manifest.json create mode 100644 cases/bookshelf-v2/release/release/release-notes/release-notes.md create mode 100644 cases/bookshelf-v2/release/release/version.json create mode 100644 cases/bookshelf-v2/release/release/windows/bookshelf-0.1.0-portable.exe create mode 100644 cases/bookshelf-v2/release/release/windows/bookshelf-setup-0.1.0.exe create mode 100644 cases/bookshelf-v3/arch.json create mode 100644 cases/bookshelf-v3/backend/.gitignore create mode 100644 cases/bookshelf-v3/backend/README.md create mode 100644 cases/bookshelf-v3/backend/package.json create mode 100644 cases/bookshelf-v3/backend/src/__tests__/auth.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/books.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/items.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/note_tags.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/notes.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/reading_progress.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/reading_status.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/reviews.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/stats.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/tags.test.ts create mode 100644 cases/bookshelf-v3/backend/src/__tests__/users.test.ts create mode 100644 cases/bookshelf-v3/backend/src/db/client.ts create mode 100644 cases/bookshelf-v3/backend/src/db/schema.ts create mode 100644 cases/bookshelf-v3/backend/src/index.ts create mode 100644 cases/bookshelf-v3/backend/src/middleware/auth.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/auth.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/books.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/items.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/note_tags.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/notes.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/reading_progress.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/reading_status.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/reviews.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/stats.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/tags.ts create mode 100644 cases/bookshelf-v3/backend/src/routes/users.ts create mode 100644 cases/bookshelf-v3/backend/src/services/book.ts create mode 100644 cases/bookshelf-v3/backend/src/services/item.ts create mode 100644 cases/bookshelf-v3/backend/src/services/note.ts create mode 100644 cases/bookshelf-v3/backend/src/services/note_tag.ts create mode 100644 cases/bookshelf-v3/backend/src/services/reading_progress.ts create mode 100644 cases/bookshelf-v3/backend/src/services/reading_statu.ts create mode 100644 cases/bookshelf-v3/backend/src/services/review.ts create mode 100644 cases/bookshelf-v3/backend/src/services/stat.ts create mode 100644 cases/bookshelf-v3/backend/src/services/tag.ts create mode 100644 cases/bookshelf-v3/backend/src/services/user.ts create mode 100644 cases/bookshelf-v3/backend/src/types/fastify.d.ts create mode 100644 cases/bookshelf-v3/backend/src/types/index.ts create mode 100644 cases/bookshelf-v3/backend/src/types/sql.js.d.ts create mode 100644 cases/bookshelf-v3/backend/tsconfig.json create mode 100644 cases/bookshelf-v3/prd.json create mode 100644 cases/bookshelf/arch.json create mode 100644 cases/bookshelf/backend/.gitignore create mode 100644 cases/bookshelf/backend/README.md create mode 100644 cases/bookshelf/backend/package.json create mode 100644 cases/bookshelf/backend/src/__tests__/auth.test.ts create mode 100644 cases/bookshelf/backend/src/__tests__/note_tags.test.ts create mode 100644 cases/bookshelf/backend/src/__tests__/notes.test.ts create mode 100644 cases/bookshelf/backend/src/__tests__/tags.test.ts create mode 100644 cases/bookshelf/backend/src/__tests__/users.test.ts create mode 100644 cases/bookshelf/backend/src/db/client.ts create mode 100644 cases/bookshelf/backend/src/db/schema.ts create mode 100644 cases/bookshelf/backend/src/index.ts create mode 100644 cases/bookshelf/backend/src/middleware/auth.ts create mode 100644 cases/bookshelf/backend/src/routes/auth.ts create mode 100644 cases/bookshelf/backend/src/routes/note_tags.ts create mode 100644 cases/bookshelf/backend/src/routes/notes.ts create mode 100644 cases/bookshelf/backend/src/routes/tags.ts create mode 100644 cases/bookshelf/backend/src/routes/users.ts create mode 100644 cases/bookshelf/backend/src/services/note.ts create mode 100644 cases/bookshelf/backend/src/services/note_tag.ts create mode 100644 cases/bookshelf/backend/src/services/tag.ts create mode 100644 cases/bookshelf/backend/src/services/user.ts create mode 100644 cases/bookshelf/backend/src/types/fastify.d.ts create mode 100644 cases/bookshelf/backend/src/types/index.ts create mode 100644 cases/bookshelf/backend/src/types/sql.js.d.ts create mode 100644 cases/bookshelf/backend/tsconfig.json create mode 100644 cases/bookshelf/electron/README.md create mode 100644 cases/bookshelf/electron/assets/icon.png create mode 100644 cases/bookshelf/electron/electron-builder.yml create mode 100644 cases/bookshelf/electron/electron/ipc.ts create mode 100644 cases/bookshelf/electron/electron/main.ts create mode 100644 cases/bookshelf/electron/electron/preload.ts create mode 100644 cases/bookshelf/electron/electron/tray.ts create mode 100644 cases/bookshelf/electron/electron/updater.ts create mode 100644 cases/bookshelf/electron/electron/utils.ts create mode 100644 cases/bookshelf/electron/entitlements.mac.plist create mode 100644 cases/bookshelf/electron/package.json create mode 100644 cases/bookshelf/electron/tsconfig.json create mode 100644 cases/bookshelf/frontend/app/globals.css create mode 100644 cases/bookshelf/frontend/app/layout.tsx create mode 100644 cases/bookshelf/frontend/app/note/[id]/page.tsx create mode 100644 cases/bookshelf/frontend/app/notes/page.tsx create mode 100644 cases/bookshelf/frontend/app/page.tsx create mode 100644 cases/bookshelf/frontend/app/settings/page.tsx create mode 100644 cases/bookshelf/frontend/app/tags/page.tsx create mode 100644 cases/bookshelf/frontend/components/layout/BottomNav.tsx create mode 100644 cases/bookshelf/frontend/components/layout/Sidebar.tsx create mode 100644 cases/bookshelf/frontend/components/note/NoteCard.tsx create mode 100644 cases/bookshelf/frontend/components/ui/Button.tsx create mode 100644 cases/bookshelf/frontend/components/ui/Card.tsx create mode 100644 cases/bookshelf/frontend/components/ui/EmptyState.tsx create mode 100644 cases/bookshelf/frontend/components/ui/Input.tsx create mode 100644 cases/bookshelf/frontend/components/ui/Modal.tsx create mode 100644 cases/bookshelf/frontend/hooks/useDebounce.ts create mode 100644 cases/bookshelf/frontend/hooks/useForm.ts create mode 100644 cases/bookshelf/frontend/hooks/useNotes.ts create mode 100644 cases/bookshelf/frontend/hooks/useTags.ts create mode 100644 cases/bookshelf/frontend/next.config.ts create mode 100644 cases/bookshelf/frontend/package.json create mode 100644 cases/bookshelf/frontend/postcss.config.js create mode 100644 cases/bookshelf/frontend/services/api.ts create mode 100644 cases/bookshelf/frontend/services/notes.ts create mode 100644 cases/bookshelf/frontend/services/tags.ts create mode 100644 cases/bookshelf/frontend/tailwind.config.ts create mode 100644 cases/bookshelf/frontend/tsconfig.json create mode 100644 cases/bookshelf/frontend/types/index.ts create mode 100644 cases/bookshelf/prd.json create mode 100644 cases/bookshelf/release/release/README.md create mode 100644 cases/bookshelf/release/release/build-info.json create mode 100644 cases/bookshelf/release/release/checksums/checksums.txt create mode 100644 cases/bookshelf/release/release/linux/noteapp-0.1.0.AppImage create mode 100644 cases/bookshelf/release/release/linux/noteapp_0.1.0_amd64.deb create mode 100644 cases/bookshelf/release/release/macos/noteapp-0.1.0-arm64.dmg create mode 100644 cases/bookshelf/release/release/macos/noteapp-0.1.0.dmg create mode 100644 cases/bookshelf/release/release/manifests/manifest.json create mode 100644 cases/bookshelf/release/release/release-notes/release-notes.md create mode 100644 cases/bookshelf/release/release/version.json create mode 100644 cases/bookshelf/release/release/windows/noteapp-0.1.0-portable.exe create mode 100644 cases/bookshelf/release/release/windows/noteapp-setup-0.1.0.exe create mode 100644 cases/failure-report.md create mode 100644 cases/mealprep-v2/arch.json create mode 100644 cases/mealprep-v2/backend/.gitignore create mode 100644 cases/mealprep-v2/backend/README.md create mode 100644 cases/mealprep-v2/backend/package.json create mode 100644 cases/mealprep-v2/backend/src/__tests__/auth.test.ts create mode 100644 cases/mealprep-v2/backend/src/__tests__/logistics.test.ts create mode 100644 cases/mealprep-v2/backend/src/__tests__/order_items.test.ts create mode 100644 cases/mealprep-v2/backend/src/__tests__/orders.test.ts create mode 100644 cases/mealprep-v2/backend/src/__tests__/products.test.ts create mode 100644 cases/mealprep-v2/backend/src/__tests__/users.test.ts create mode 100644 cases/mealprep-v2/backend/src/db/client.ts create mode 100644 cases/mealprep-v2/backend/src/db/schema.ts create mode 100644 cases/mealprep-v2/backend/src/index.ts create mode 100644 cases/mealprep-v2/backend/src/middleware/auth.ts create mode 100644 cases/mealprep-v2/backend/src/routes/auth.ts create mode 100644 cases/mealprep-v2/backend/src/routes/logistics.ts create mode 100644 cases/mealprep-v2/backend/src/routes/order_items.ts create mode 100644 cases/mealprep-v2/backend/src/routes/orders.ts create mode 100644 cases/mealprep-v2/backend/src/routes/products.ts create mode 100644 cases/mealprep-v2/backend/src/routes/users.ts create mode 100644 cases/mealprep-v2/backend/src/services/logistic.ts create mode 100644 cases/mealprep-v2/backend/src/services/order.ts create mode 100644 cases/mealprep-v2/backend/src/services/order_item.ts create mode 100644 cases/mealprep-v2/backend/src/services/product.ts create mode 100644 cases/mealprep-v2/backend/src/services/user.ts create mode 100644 cases/mealprep-v2/backend/src/types/fastify.d.ts create mode 100644 cases/mealprep-v2/backend/src/types/index.ts create mode 100644 cases/mealprep-v2/backend/src/types/sql.js.d.ts create mode 100644 cases/mealprep-v2/backend/tsconfig.json create mode 100644 cases/mealprep-v2/electron/README.md create mode 100644 cases/mealprep-v2/electron/assets/icon.png create mode 100644 cases/mealprep-v2/electron/electron-builder.yml create mode 100644 cases/mealprep-v2/electron/electron/ipc.ts create mode 100644 cases/mealprep-v2/electron/electron/main.ts create mode 100644 cases/mealprep-v2/electron/electron/preload.ts create mode 100644 cases/mealprep-v2/electron/electron/tray.ts create mode 100644 cases/mealprep-v2/electron/electron/updater.ts create mode 100644 cases/mealprep-v2/electron/electron/utils.ts create mode 100644 cases/mealprep-v2/electron/entitlements.mac.plist create mode 100644 cases/mealprep-v2/electron/package.json create mode 100644 cases/mealprep-v2/electron/tsconfig.json create mode 100644 cases/mealprep-v2/frontend/app/globals.css create mode 100644 cases/mealprep-v2/frontend/app/layout.tsx create mode 100644 cases/mealprep-v2/frontend/app/page.tsx create mode 100644 cases/mealprep-v2/frontend/app/客户管理/page.tsx create mode 100644 cases/mealprep-v2/frontend/app/模板功能/page.tsx create mode 100644 cases/mealprep-v2/frontend/app/营养计算/page.tsx create mode 100644 cases/mealprep-v2/frontend/app/购物清单/page.tsx create mode 100644 cases/mealprep-v2/frontend/app/食物库/page.tsx create mode 100644 cases/mealprep-v2/frontend/app/餐计划/page.tsx create mode 100644 cases/mealprep-v2/frontend/components/ecommerce/ProductCard.tsx create mode 100644 cases/mealprep-v2/frontend/components/layout/BottomNav.tsx create mode 100644 cases/mealprep-v2/frontend/components/layout/Sidebar.tsx create mode 100644 cases/mealprep-v2/frontend/components/ui/Button.tsx create mode 100644 cases/mealprep-v2/frontend/components/ui/Card.tsx create mode 100644 cases/mealprep-v2/frontend/components/ui/EmptyState.tsx create mode 100644 cases/mealprep-v2/frontend/components/ui/Input.tsx create mode 100644 cases/mealprep-v2/frontend/components/ui/Modal.tsx create mode 100644 cases/mealprep-v2/frontend/hooks/useAuth.ts create mode 100644 cases/mealprep-v2/frontend/hooks/useDebounce.ts create mode 100644 cases/mealprep-v2/frontend/hooks/useForm.ts create mode 100644 cases/mealprep-v2/frontend/hooks/useItem.ts create mode 100644 cases/mealprep-v2/frontend/next.config.ts create mode 100644 cases/mealprep-v2/frontend/package.json create mode 100644 cases/mealprep-v2/frontend/postcss.config.js create mode 100644 cases/mealprep-v2/frontend/services/api.ts create mode 100644 cases/mealprep-v2/frontend/services/auth.ts create mode 100644 cases/mealprep-v2/frontend/services/客户管理.ts create mode 100644 cases/mealprep-v2/frontend/services/营养计算.ts create mode 100644 cases/mealprep-v2/frontend/services/食物库.ts create mode 100644 cases/mealprep-v2/frontend/services/餐计划.ts create mode 100644 cases/mealprep-v2/frontend/tailwind.config.ts create mode 100644 cases/mealprep-v2/frontend/tsconfig.json create mode 100644 cases/mealprep-v2/frontend/types/index.ts create mode 100644 cases/mealprep-v2/prd.json create mode 100644 cases/mealprep-v2/release/release/README.md create mode 100644 cases/mealprep-v2/release/release/build-info.json create mode 100644 cases/mealprep-v2/release/release/checksums/checksums.txt create mode 100644 cases/mealprep-v2/release/release/linux/mealprep-0.1.0.AppImage create mode 100644 cases/mealprep-v2/release/release/linux/mealprep_0.1.0_amd64.deb create mode 100644 cases/mealprep-v2/release/release/macos/mealprep-0.1.0-arm64.dmg create mode 100644 cases/mealprep-v2/release/release/macos/mealprep-0.1.0.dmg create mode 100644 cases/mealprep-v2/release/release/manifests/manifest.json create mode 100644 cases/mealprep-v2/release/release/release-notes/release-notes.md create mode 100644 cases/mealprep-v2/release/release/version.json create mode 100644 cases/mealprep-v2/release/release/windows/mealprep-0.1.0-portable.exe create mode 100644 cases/mealprep-v2/release/release/windows/mealprep-setup-0.1.0.exe create mode 100644 cases/mealprep-v3/arch.json create mode 100644 cases/mealprep-v3/backend/.gitignore create mode 100644 cases/mealprep-v3/backend/README.md create mode 100644 cases/mealprep-v3/backend/package.json create mode 100644 cases/mealprep-v3/backend/src/__tests__/auth.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/customers.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/foods.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/logistics.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/meal_plans.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/nutrition.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/order_items.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/orders.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/products.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/shopping_lists.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/templates.test.ts create mode 100644 cases/mealprep-v3/backend/src/__tests__/users.test.ts create mode 100644 cases/mealprep-v3/backend/src/db/client.ts create mode 100644 cases/mealprep-v3/backend/src/db/schema.ts create mode 100644 cases/mealprep-v3/backend/src/index.ts create mode 100644 cases/mealprep-v3/backend/src/middleware/auth.ts create mode 100644 cases/mealprep-v3/backend/src/routes/auth.ts create mode 100644 cases/mealprep-v3/backend/src/routes/customers.ts create mode 100644 cases/mealprep-v3/backend/src/routes/foods.ts create mode 100644 cases/mealprep-v3/backend/src/routes/logistics.ts create mode 100644 cases/mealprep-v3/backend/src/routes/meal_plans.ts create mode 100644 cases/mealprep-v3/backend/src/routes/nutrition.ts create mode 100644 cases/mealprep-v3/backend/src/routes/order_items.ts create mode 100644 cases/mealprep-v3/backend/src/routes/orders.ts create mode 100644 cases/mealprep-v3/backend/src/routes/products.ts create mode 100644 cases/mealprep-v3/backend/src/routes/shopping_lists.ts create mode 100644 cases/mealprep-v3/backend/src/routes/templates.ts create mode 100644 cases/mealprep-v3/backend/src/routes/users.ts create mode 100644 cases/mealprep-v3/backend/src/services/customer.ts create mode 100644 cases/mealprep-v3/backend/src/services/food.ts create mode 100644 cases/mealprep-v3/backend/src/services/logistic.ts create mode 100644 cases/mealprep-v3/backend/src/services/meal_plan.ts create mode 100644 cases/mealprep-v3/backend/src/services/nutrition.ts create mode 100644 cases/mealprep-v3/backend/src/services/order.ts create mode 100644 cases/mealprep-v3/backend/src/services/order_item.ts create mode 100644 cases/mealprep-v3/backend/src/services/product.ts create mode 100644 cases/mealprep-v3/backend/src/services/shopping_list.ts create mode 100644 cases/mealprep-v3/backend/src/services/template.ts create mode 100644 cases/mealprep-v3/backend/src/services/user.ts create mode 100644 cases/mealprep-v3/backend/src/types/fastify.d.ts create mode 100644 cases/mealprep-v3/backend/src/types/index.ts create mode 100644 cases/mealprep-v3/backend/src/types/sql.js.d.ts create mode 100644 cases/mealprep-v3/backend/tsconfig.json create mode 100644 cases/mealprep-v3/prd.json create mode 100644 cases/mealprep/prd.json create mode 100644 cases/teamflow-v2/arch.json create mode 100644 cases/teamflow-v2/backend/.gitignore create mode 100644 cases/teamflow-v2/backend/README.md create mode 100644 cases/teamflow-v2/backend/package.json create mode 100644 cases/teamflow-v2/backend/src/__tests__/auth.test.ts create mode 100644 cases/teamflow-v2/backend/src/__tests__/note_tags.test.ts create mode 100644 cases/teamflow-v2/backend/src/__tests__/notes.test.ts create mode 100644 cases/teamflow-v2/backend/src/__tests__/tags.test.ts create mode 100644 cases/teamflow-v2/backend/src/__tests__/users.test.ts create mode 100644 cases/teamflow-v2/backend/src/db/client.ts create mode 100644 cases/teamflow-v2/backend/src/db/schema.ts create mode 100644 cases/teamflow-v2/backend/src/index.ts create mode 100644 cases/teamflow-v2/backend/src/middleware/auth.ts create mode 100644 cases/teamflow-v2/backend/src/routes/auth.ts create mode 100644 cases/teamflow-v2/backend/src/routes/note_tags.ts create mode 100644 cases/teamflow-v2/backend/src/routes/notes.ts create mode 100644 cases/teamflow-v2/backend/src/routes/tags.ts create mode 100644 cases/teamflow-v2/backend/src/routes/users.ts create mode 100644 cases/teamflow-v2/backend/src/services/note.ts create mode 100644 cases/teamflow-v2/backend/src/services/note_tag.ts create mode 100644 cases/teamflow-v2/backend/src/services/tag.ts create mode 100644 cases/teamflow-v2/backend/src/services/user.ts create mode 100644 cases/teamflow-v2/backend/src/types/fastify.d.ts create mode 100644 cases/teamflow-v2/backend/src/types/index.ts create mode 100644 cases/teamflow-v2/backend/src/types/sql.js.d.ts create mode 100644 cases/teamflow-v2/backend/tsconfig.json create mode 100644 cases/teamflow-v2/electron/README.md create mode 100644 cases/teamflow-v2/electron/assets/icon.png create mode 100644 cases/teamflow-v2/electron/electron-builder.yml create mode 100644 cases/teamflow-v2/electron/electron/ipc.ts create mode 100644 cases/teamflow-v2/electron/electron/main.ts create mode 100644 cases/teamflow-v2/electron/electron/preload.ts create mode 100644 cases/teamflow-v2/electron/electron/tray.ts create mode 100644 cases/teamflow-v2/electron/electron/updater.ts create mode 100644 cases/teamflow-v2/electron/electron/utils.ts create mode 100644 cases/teamflow-v2/electron/entitlements.mac.plist create mode 100644 cases/teamflow-v2/electron/package.json create mode 100644 cases/teamflow-v2/electron/tsconfig.json create mode 100644 cases/teamflow-v2/frontend/app/globals.css create mode 100644 cases/teamflow-v2/frontend/app/layout.tsx create mode 100644 cases/teamflow-v2/frontend/app/page.tsx create mode 100644 cases/teamflow-v2/frontend/app/任务管理/page.tsx create mode 100644 cases/teamflow-v2/frontend/app/团队成员/page.tsx create mode 100644 cases/teamflow-v2/frontend/app/数据统计/page.tsx create mode 100644 cases/teamflow-v2/frontend/app/活动日志/page.tsx create mode 100644 cases/teamflow-v2/frontend/app/看板视图/page.tsx create mode 100644 cases/teamflow-v2/frontend/app/筛选和搜索/page.tsx create mode 100644 cases/teamflow-v2/frontend/components/layout/BottomNav.tsx create mode 100644 cases/teamflow-v2/frontend/components/layout/Sidebar.tsx create mode 100644 cases/teamflow-v2/frontend/components/note/NoteCard.tsx create mode 100644 cases/teamflow-v2/frontend/components/ui/Button.tsx create mode 100644 cases/teamflow-v2/frontend/components/ui/Card.tsx create mode 100644 cases/teamflow-v2/frontend/components/ui/EmptyState.tsx create mode 100644 cases/teamflow-v2/frontend/components/ui/Input.tsx create mode 100644 cases/teamflow-v2/frontend/components/ui/Modal.tsx create mode 100644 cases/teamflow-v2/frontend/hooks/useAuth.ts create mode 100644 cases/teamflow-v2/frontend/hooks/useDebounce.ts create mode 100644 cases/teamflow-v2/frontend/hooks/useForm.ts create mode 100644 cases/teamflow-v2/frontend/hooks/useItem.ts create mode 100644 cases/teamflow-v2/frontend/next.config.ts create mode 100644 cases/teamflow-v2/frontend/package.json create mode 100644 cases/teamflow-v2/frontend/postcss.config.js create mode 100644 cases/teamflow-v2/frontend/services/api.ts create mode 100644 cases/teamflow-v2/frontend/services/auth.ts create mode 100644 cases/teamflow-v2/frontend/services/任务管理.ts create mode 100644 cases/teamflow-v2/frontend/services/团队成员.ts create mode 100644 cases/teamflow-v2/frontend/services/看板视图.ts create mode 100644 cases/teamflow-v2/frontend/services/筛选和搜索.ts create mode 100644 cases/teamflow-v2/frontend/tailwind.config.ts create mode 100644 cases/teamflow-v2/frontend/tsconfig.json create mode 100644 cases/teamflow-v2/frontend/types/index.ts create mode 100644 cases/teamflow-v2/prd.json create mode 100644 cases/teamflow-v2/release/release/README.md create mode 100644 cases/teamflow-v2/release/release/build-info.json create mode 100644 cases/teamflow-v2/release/release/checksums/checksums.txt create mode 100644 cases/teamflow-v2/release/release/linux/teamflow-0.1.0.AppImage create mode 100644 cases/teamflow-v2/release/release/linux/teamflow_0.1.0_amd64.deb create mode 100644 cases/teamflow-v2/release/release/macos/teamflow-0.1.0-arm64.dmg create mode 100644 cases/teamflow-v2/release/release/macos/teamflow-0.1.0.dmg create mode 100644 cases/teamflow-v2/release/release/manifests/manifest.json create mode 100644 cases/teamflow-v2/release/release/release-notes/release-notes.md create mode 100644 cases/teamflow-v2/release/release/version.json create mode 100644 cases/teamflow-v2/release/release/windows/teamflow-0.1.0-portable.exe create mode 100644 cases/teamflow-v2/release/release/windows/teamflow-setup-0.1.0.exe create mode 100644 cases/teamflow-v3/arch.json create mode 100644 cases/teamflow-v3/backend/.gitignore create mode 100644 cases/teamflow-v3/backend/README.md create mode 100644 cases/teamflow-v3/backend/package.json create mode 100644 cases/teamflow-v3/backend/src/__tests__/activity_logs.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/auth.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/boards.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/items.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/members.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/note_tags.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/notes.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/stats.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/tags.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/tasks.test.ts create mode 100644 cases/teamflow-v3/backend/src/__tests__/users.test.ts create mode 100644 cases/teamflow-v3/backend/src/db/client.ts create mode 100644 cases/teamflow-v3/backend/src/db/schema.ts create mode 100644 cases/teamflow-v3/backend/src/index.ts create mode 100644 cases/teamflow-v3/backend/src/middleware/auth.ts create mode 100644 cases/teamflow-v3/backend/src/routes/activity_logs.ts create mode 100644 cases/teamflow-v3/backend/src/routes/auth.ts create mode 100644 cases/teamflow-v3/backend/src/routes/boards.ts create mode 100644 cases/teamflow-v3/backend/src/routes/items.ts create mode 100644 cases/teamflow-v3/backend/src/routes/members.ts create mode 100644 cases/teamflow-v3/backend/src/routes/note_tags.ts create mode 100644 cases/teamflow-v3/backend/src/routes/notes.ts create mode 100644 cases/teamflow-v3/backend/src/routes/stats.ts create mode 100644 cases/teamflow-v3/backend/src/routes/tags.ts create mode 100644 cases/teamflow-v3/backend/src/routes/tasks.ts create mode 100644 cases/teamflow-v3/backend/src/routes/users.ts create mode 100644 cases/teamflow-v3/backend/src/services/activity_log.ts create mode 100644 cases/teamflow-v3/backend/src/services/board.ts create mode 100644 cases/teamflow-v3/backend/src/services/item.ts create mode 100644 cases/teamflow-v3/backend/src/services/member.ts create mode 100644 cases/teamflow-v3/backend/src/services/note.ts create mode 100644 cases/teamflow-v3/backend/src/services/note_tag.ts create mode 100644 cases/teamflow-v3/backend/src/services/stat.ts create mode 100644 cases/teamflow-v3/backend/src/services/tag.ts create mode 100644 cases/teamflow-v3/backend/src/services/task.ts create mode 100644 cases/teamflow-v3/backend/src/services/user.ts create mode 100644 cases/teamflow-v3/backend/src/types/fastify.d.ts create mode 100644 cases/teamflow-v3/backend/src/types/index.ts create mode 100644 cases/teamflow-v3/backend/src/types/sql.js.d.ts create mode 100644 cases/teamflow-v3/backend/tsconfig.json create mode 100644 cases/teamflow-v3/prd.json create mode 100644 cases/teamflow/prd.json create mode 160000 claw-os-v2 create mode 100644 docs/AGENTS-PROTOCOL-FULL.md create mode 100644 docs/README.md create mode 100644 docs/architecture-agent.md create mode 100644 docs/architecture-freeze-v2.md create mode 100644 docs/factories/sf-01-product-strategy.md create mode 100644 docs/factories/sf-02-requirement-engineering.md create mode 100644 docs/factory-registry.md create mode 100644 docs/frontend-builder-agent.md create mode 100644 docs/project-intake-agent.md create mode 100644 docs/reports/backend-semantic-fix-report.md create mode 100644 docs/reports/benchmark-report.md create mode 100644 docs/reports/benchmark-results.json create mode 100644 docs/reports/benchmark-v2-report.md create mode 100644 docs/reports/capability-boundary.md create mode 100644 docs/reports/capability-registry.json create mode 100644 docs/reports/capability-report.md create mode 100644 docs/reports/certification-report.md create mode 100644 docs/reports/contract-consistency-report.md create mode 100644 docs/reports/desktop-benchmark-report.md create mode 100644 docs/reports/domain-certification.json create mode 100644 docs/reports/end-to-end-benchmark-v1.md create mode 100644 docs/reports/failure-catalog.json create mode 100644 docs/reports/real-requirement-benchmark-report.md create mode 100644 docs/reports/release-benchmark-report.md create mode 100644 docs/software-factory-core.md create mode 100644 docs/v2-factory-fusion-freeze.md create mode 100644 memory/.dream-cycle-last-run create mode 100644 memory/.dreams/events.jsonl create mode 100644 memory/.dreams/phase-signals.json create mode 100644 memory/.dreams/session-corpus/2026-06-01.txt create mode 100644 memory/.dreams/session-corpus/2026-06-03.txt create mode 100644 memory/.dreams/session-corpus/2026-06-04.txt create mode 100644 memory/.dreams/session-corpus/2026-06-05.txt create mode 100644 memory/.dreams/session-ingestion.json create mode 100644 memory/.dreams/short-term-recall.json create mode 100644 memory/.memory-auto.log create mode 100644 memory/.session-compact.log create mode 100644 memory/SESSION_START.md create mode 100644 memory/daily/2026-05-30.md create mode 100644 memory/daily/2026-05-31.md create mode 100644 memory/daily/2026-06-01.md create mode 100644 memory/daily/2026-06-02.md create mode 100644 memory/daily/2026-06-03.md create mode 100644 memory/daily/2026-06-04.md create mode 100644 memory/daily/2026-06-05.md create mode 100644 memory/daily/2026-06-06.md create mode 100644 memory/daily/sync-log-20260604.md create mode 100644 memory/dreaming/deep/2026-06-06.md create mode 100644 memory/dreaming/light/2026-06-06.md create mode 100644 memory/dreaming/rem/2026-06-06.md create mode 100644 memory/index.md create mode 100644 memory/projects/claw-agent-os-v2.md create mode 100644 memory/projects/github-top20-analysis.md create mode 100644 memory/projects/hermes-agent.md create mode 100644 memory/projects/orchestrated-execution-plan-for-persist-test-spec-level-level-1-tasks-4-agent-packages-4/agent-packages.json create mode 100644 memory/projects/orchestrated-execution-plan-for-persist-test-spec-level-level-1-tasks-4-agent-packages-4/dep-graph.json create mode 100644 memory/projects/orchestrated-execution-plan-for-persist-test-spec-level-level-1-tasks-4-agent-packages-4/execution-plan.md create mode 100644 memory/projects/orchestrated-execution-plan-for-persist-test-spec-level-level-1-tasks-4-agent-packages-4/roadmap.json create mode 100644 memory/projects/orchestrated-execution-plan-for-persist-test-spec-level-level-1-tasks-4-agent-packages-4/task-tree.json create mode 100644 memory/projects/priority-install-plan.md create mode 100644 memory/projects/software-generator-mvp-v1.md create mode 100644 memory/projects/supermemory-study.md create mode 100644 memory/registers/_index.md create mode 100644 memory/registers/companions.md create mode 100644 memory/registers/memory-routing.md create mode 100644 memory/registers/open-loops.md create mode 100644 memory/registers/people.md create mode 100644 memory/registers/preferences.md create mode 100644 memory/registers/tool-env.md create mode 100644 memory/vault.md create mode 100644 memory/verification-log.md create mode 100644 network-tool/.gitignore create mode 100644 network-tool/README.md create mode 100644 network-tool/apps/client-electron/package.json create mode 100644 network-tool/apps/client-electron/src/main/index.ts create mode 100644 network-tool/apps/client-electron/src/main/network-client.ts create mode 100644 network-tool/apps/client-electron/src/main/preload.ts create mode 100644 network-tool/apps/client-electron/src/renderer/App.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/env.d.ts create mode 100644 network-tool/apps/client-electron/src/renderer/index.html create mode 100644 network-tool/apps/client-electron/src/renderer/index.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/pages/ConnectionsPage.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/pages/DevicesPage.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/pages/LoginPage.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/pages/LogsPage.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/pages/NodesPage.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/pages/RegisterPage.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/pages/SettingsPage.tsx create mode 100644 network-tool/apps/client-electron/src/renderer/styles/global.css create mode 100644 network-tool/apps/client-electron/tsconfig.json create mode 100644 network-tool/apps/client-electron/tsconfig.main.json create mode 100644 network-tool/apps/client-electron/vite.config.ts create mode 100644 network-tool/apps/control-server/package.json create mode 100644 network-tool/apps/control-server/src/db/database.ts create mode 100644 network-tool/apps/control-server/src/db/init.ts create mode 100644 network-tool/apps/control-server/src/index.ts create mode 100644 network-tool/apps/control-server/src/middleware/auth.ts create mode 100644 network-tool/apps/control-server/src/routes/auth.routes.ts create mode 100644 network-tool/apps/control-server/src/routes/connection.routes.ts create mode 100644 network-tool/apps/control-server/src/routes/device.routes.ts create mode 100644 network-tool/apps/control-server/src/routes/node.routes.ts create mode 100644 network-tool/apps/control-server/src/services/auth.service.ts create mode 100644 network-tool/apps/control-server/src/services/connection.service.ts create mode 100644 network-tool/apps/control-server/src/services/device.service.ts create mode 100644 network-tool/apps/control-server/src/services/node.service.ts create mode 100644 network-tool/apps/control-server/src/types/sql.js.d.ts create mode 100644 network-tool/apps/control-server/tsconfig.json create mode 100644 network-tool/apps/relay-node/package.json create mode 100644 network-tool/apps/relay-node/src/index.ts create mode 100644 network-tool/apps/relay-node/tsconfig.json create mode 100644 network-tool/package.json create mode 100644 network-tool/packages/shared-crypto/package.json create mode 100644 network-tool/packages/shared-crypto/src/index.ts create mode 100644 network-tool/packages/shared-crypto/tsconfig.json create mode 100644 network-tool/packages/shared-protocol/package.json create mode 100644 network-tool/packages/shared-protocol/src/index.ts create mode 100644 network-tool/packages/shared-protocol/tsconfig.json create mode 100644 network-tool/packages/shared-types/package.json create mode 100644 network-tool/packages/shared-types/src/index.ts create mode 100644 network-tool/packages/shared-types/tsconfig.json create mode 100644 network-tool/test-e2e.js create mode 100644 output/agent-architecture-audit.md create mode 100644 output/contract-mgmt/.gitignore create mode 100644 output/contract-mgmt/README.md create mode 100644 output/contract-mgmt/apps/api/.gitignore create mode 100644 output/contract-mgmt/apps/api/README.md create mode 100644 output/contract-mgmt/apps/api/package.json create mode 100644 output/contract-mgmt/apps/api/src/__tests__/approvals.test.ts create mode 100644 output/contract-mgmt/apps/api/src/__tests__/auth.test.ts create mode 100644 output/contract-mgmt/apps/api/src/__tests__/clients.test.ts create mode 100644 output/contract-mgmt/apps/api/src/__tests__/contracts.test.ts create mode 100644 output/contract-mgmt/apps/api/src/__tests__/customers.test.ts create mode 100644 output/contract-mgmt/apps/api/src/__tests__/items.test.ts create mode 100644 output/contract-mgmt/apps/api/src/__tests__/reminders.test.ts create mode 100644 output/contract-mgmt/apps/api/src/__tests__/templates.test.ts create mode 100644 output/contract-mgmt/apps/api/src/db/client.ts create mode 100644 output/contract-mgmt/apps/api/src/db/schema.ts create mode 100644 output/contract-mgmt/apps/api/src/index.ts create mode 100644 output/contract-mgmt/apps/api/src/middleware/auth.ts create mode 100644 output/contract-mgmt/apps/api/src/routes/approvals.ts create mode 100644 output/contract-mgmt/apps/api/src/routes/auth.ts create mode 100644 output/contract-mgmt/apps/api/src/routes/clients.ts create mode 100644 output/contract-mgmt/apps/api/src/routes/contracts.ts create mode 100644 output/contract-mgmt/apps/api/src/routes/customers.ts create mode 100644 output/contract-mgmt/apps/api/src/routes/items.ts create mode 100644 output/contract-mgmt/apps/api/src/routes/reminders.ts create mode 100644 output/contract-mgmt/apps/api/src/routes/templates.ts create mode 100644 output/contract-mgmt/apps/api/src/services/approval.ts create mode 100644 output/contract-mgmt/apps/api/src/services/client.ts create mode 100644 output/contract-mgmt/apps/api/src/services/contract.ts create mode 100644 output/contract-mgmt/apps/api/src/services/customer.ts create mode 100644 output/contract-mgmt/apps/api/src/services/item.ts create mode 100644 output/contract-mgmt/apps/api/src/services/reminder.ts create mode 100644 output/contract-mgmt/apps/api/src/services/template.ts create mode 100644 output/contract-mgmt/apps/api/src/types/fastify.d.ts create mode 100644 output/contract-mgmt/apps/api/src/types/index.ts create mode 100644 output/contract-mgmt/apps/api/src/types/sql.js.d.ts create mode 100644 output/contract-mgmt/apps/api/tsconfig.json create mode 100644 output/contract-mgmt/apps/web/app/approvals/page.tsx create mode 100644 output/contract-mgmt/apps/web/app/clients/page.tsx create mode 100644 output/contract-mgmt/apps/web/app/contracts/page.tsx create mode 100644 output/contract-mgmt/apps/web/app/globals.css create mode 100644 output/contract-mgmt/apps/web/app/items/page.tsx create mode 100644 output/contract-mgmt/apps/web/app/layout.tsx create mode 100644 output/contract-mgmt/apps/web/app/page.tsx create mode 100644 output/contract-mgmt/apps/web/app/reminders/page.tsx create mode 100644 output/contract-mgmt/apps/web/app/templates/page.tsx create mode 100644 output/contract-mgmt/apps/web/components/layout/BottomNav.tsx create mode 100644 output/contract-mgmt/apps/web/components/layout/Sidebar.tsx create mode 100644 output/contract-mgmt/apps/web/components/ui/Button.tsx create mode 100644 output/contract-mgmt/apps/web/components/ui/Card.tsx create mode 100644 output/contract-mgmt/apps/web/components/ui/EmptyState.tsx create mode 100644 output/contract-mgmt/apps/web/components/ui/Input.tsx create mode 100644 output/contract-mgmt/apps/web/components/ui/Modal.tsx create mode 100644 output/contract-mgmt/apps/web/hooks/useApprovals.ts create mode 100644 output/contract-mgmt/apps/web/hooks/useAuth.ts create mode 100644 output/contract-mgmt/apps/web/hooks/useContracts.ts create mode 100644 output/contract-mgmt/apps/web/hooks/useDebounce.ts create mode 100644 output/contract-mgmt/apps/web/hooks/useForm.ts create mode 100644 output/contract-mgmt/apps/web/hooks/useItems.ts create mode 100644 output/contract-mgmt/apps/web/next-env.d.ts create mode 100644 output/contract-mgmt/apps/web/next.config.ts create mode 100644 output/contract-mgmt/apps/web/package.json create mode 100644 output/contract-mgmt/apps/web/postcss.config.js create mode 100644 output/contract-mgmt/apps/web/services/api.ts create mode 100644 output/contract-mgmt/apps/web/services/approvals.ts create mode 100644 output/contract-mgmt/apps/web/services/auth.ts create mode 100644 output/contract-mgmt/apps/web/services/contracts.ts create mode 100644 output/contract-mgmt/apps/web/services/items.ts create mode 100644 output/contract-mgmt/apps/web/tailwind.config.ts create mode 100644 output/contract-mgmt/apps/web/tsconfig.json create mode 100644 output/contract-mgmt/apps/web/types/index.ts create mode 100644 output/contract-mgmt/package.json create mode 100644 output/contract-mgmt/packages/shared-config/package.json create mode 100644 output/contract-mgmt/packages/shared-config/src/index.ts create mode 100644 output/contract-mgmt/packages/shared-config/tsconfig.json create mode 100644 output/contract-mgmt/packages/shared-types/package.json create mode 100644 output/contract-mgmt/packages/shared-types/src/index.ts create mode 100644 output/contract-mgmt/packages/shared-types/tsconfig.json create mode 100644 output/contract-mgmt/scripts/build.mjs create mode 100644 output/contract-mgmt/scripts/dev.mjs create mode 100644 output/equipment-inspection/.gitignore create mode 100644 output/equipment-inspection/README.md create mode 100644 output/equipment-inspection/apps/api/.gitignore create mode 100644 output/equipment-inspection/apps/api/README.md create mode 100644 output/equipment-inspection/apps/api/package.json create mode 100644 output/equipment-inspection/apps/api/src/__tests__/approvals.test.ts create mode 100644 output/equipment-inspection/apps/api/src/__tests__/auth.test.ts create mode 100644 output/equipment-inspection/apps/api/src/__tests__/contracts.test.ts create mode 100644 output/equipment-inspection/apps/api/src/__tests__/customers.test.ts create mode 100644 output/equipment-inspection/apps/api/src/__tests__/equipment.test.ts create mode 100644 output/equipment-inspection/apps/api/src/__tests__/inspection_plans.test.ts create mode 100644 output/equipment-inspection/apps/api/src/__tests__/items.test.ts create mode 100644 output/equipment-inspection/apps/api/src/__tests__/reminders.test.ts create mode 100644 output/equipment-inspection/apps/api/src/__tests__/tasks.test.ts create mode 100644 output/equipment-inspection/apps/api/src/db/client.ts create mode 100644 output/equipment-inspection/apps/api/src/db/schema.ts create mode 100644 output/equipment-inspection/apps/api/src/index.ts create mode 100644 output/equipment-inspection/apps/api/src/middleware/auth.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/approvals.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/auth.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/contracts.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/customers.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/equipment.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/inspection_plans.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/items.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/reminders.ts create mode 100644 output/equipment-inspection/apps/api/src/routes/tasks.ts create mode 100644 output/equipment-inspection/apps/api/src/services/approval.ts create mode 100644 output/equipment-inspection/apps/api/src/services/contract.ts create mode 100644 output/equipment-inspection/apps/api/src/services/customer.ts create mode 100644 output/equipment-inspection/apps/api/src/services/equipment.ts create mode 100644 output/equipment-inspection/apps/api/src/services/inspection_plan.ts create mode 100644 output/equipment-inspection/apps/api/src/services/item.ts create mode 100644 output/equipment-inspection/apps/api/src/services/reminder.ts create mode 100644 output/equipment-inspection/apps/api/src/services/task.ts create mode 100644 output/equipment-inspection/apps/api/src/types/fastify.d.ts create mode 100644 output/equipment-inspection/apps/api/src/types/index.ts create mode 100644 output/equipment-inspection/apps/api/src/types/sql.js.d.ts create mode 100644 output/equipment-inspection/apps/api/tsconfig.json create mode 100644 output/equipment-inspection/apps/web/app/equipment/page.tsx create mode 100644 output/equipment-inspection/apps/web/app/globals.css create mode 100644 output/equipment-inspection/apps/web/app/inspection_plans/page.tsx create mode 100644 output/equipment-inspection/apps/web/app/items/page.tsx create mode 100644 output/equipment-inspection/apps/web/app/layout.tsx create mode 100644 output/equipment-inspection/apps/web/app/page.tsx create mode 100644 output/equipment-inspection/apps/web/app/tasks/page.tsx create mode 100644 output/equipment-inspection/apps/web/components/layout/BottomNav.tsx create mode 100644 output/equipment-inspection/apps/web/components/layout/Sidebar.tsx create mode 100644 output/equipment-inspection/apps/web/components/ui/Button.tsx create mode 100644 output/equipment-inspection/apps/web/components/ui/Card.tsx create mode 100644 output/equipment-inspection/apps/web/components/ui/EmptyState.tsx create mode 100644 output/equipment-inspection/apps/web/components/ui/Input.tsx create mode 100644 output/equipment-inspection/apps/web/components/ui/Modal.tsx create mode 100644 output/equipment-inspection/apps/web/hooks/useAuth.ts create mode 100644 output/equipment-inspection/apps/web/hooks/useDebounce.ts create mode 100644 output/equipment-inspection/apps/web/hooks/useEquipment.ts create mode 100644 output/equipment-inspection/apps/web/hooks/useForm.ts create mode 100644 output/equipment-inspection/apps/web/hooks/useInspection_plans.ts create mode 100644 output/equipment-inspection/apps/web/hooks/useItems.ts create mode 100644 output/equipment-inspection/apps/web/hooks/useTasks.ts create mode 100644 output/equipment-inspection/apps/web/next-env.d.ts create mode 100644 output/equipment-inspection/apps/web/next.config.ts create mode 100644 output/equipment-inspection/apps/web/package.json create mode 100644 output/equipment-inspection/apps/web/postcss.config.js create mode 100644 output/equipment-inspection/apps/web/services/api.ts create mode 100644 output/equipment-inspection/apps/web/services/auth.ts create mode 100644 output/equipment-inspection/apps/web/services/equipment.ts create mode 100644 output/equipment-inspection/apps/web/services/inspection_plans.ts create mode 100644 output/equipment-inspection/apps/web/services/items.ts create mode 100644 output/equipment-inspection/apps/web/services/tasks.ts create mode 100644 output/equipment-inspection/apps/web/tailwind.config.ts create mode 100644 output/equipment-inspection/apps/web/tsconfig.json create mode 100644 output/equipment-inspection/apps/web/types/index.ts create mode 100644 output/equipment-inspection/package.json create mode 100644 output/equipment-inspection/packages/shared-config/package.json create mode 100644 output/equipment-inspection/packages/shared-config/src/index.ts create mode 100644 output/equipment-inspection/packages/shared-config/tsconfig.json create mode 100644 output/equipment-inspection/packages/shared-types/package.json create mode 100644 output/equipment-inspection/packages/shared-types/src/index.ts create mode 100644 output/equipment-inspection/packages/shared-types/tsconfig.json create mode 100644 output/equipment-inspection/scripts/build.mjs create mode 100644 output/equipment-inspection/scripts/dev.mjs create mode 100644 output/phase1-analysis.md create mode 100644 output/planning-refactor-report.md create mode 100644 output/planning-system-audit.md create mode 100644 output/real-world-qualification-report.md create mode 100644 output/warehouse-mgmt/.gitignore create mode 100644 output/warehouse-mgmt/README.md create mode 100644 output/warehouse-mgmt/apps/api/.gitignore create mode 100644 output/warehouse-mgmt/apps/api/README.md create mode 100644 output/warehouse-mgmt/apps/api/package.json create mode 100644 output/warehouse-mgmt/apps/api/src/__tests__/auth.test.ts create mode 100644 output/warehouse-mgmt/apps/api/src/__tests__/items.test.ts create mode 100644 output/warehouse-mgmt/apps/api/src/__tests__/stock_alerts.test.ts create mode 100644 output/warehouse-mgmt/apps/api/src/__tests__/stock_in.test.ts create mode 100644 output/warehouse-mgmt/apps/api/src/__tests__/stock_out.test.ts create mode 100644 output/warehouse-mgmt/apps/api/src/__tests__/stocktaking.test.ts create mode 100644 output/warehouse-mgmt/apps/api/src/__tests__/warehouse.test.ts create mode 100644 output/warehouse-mgmt/apps/api/src/db/client.ts create mode 100644 output/warehouse-mgmt/apps/api/src/db/schema.ts create mode 100644 output/warehouse-mgmt/apps/api/src/index.ts create mode 100644 output/warehouse-mgmt/apps/api/src/middleware/auth.ts create mode 100644 output/warehouse-mgmt/apps/api/src/routes/auth.ts create mode 100644 output/warehouse-mgmt/apps/api/src/routes/items.ts create mode 100644 output/warehouse-mgmt/apps/api/src/routes/stock_alerts.ts create mode 100644 output/warehouse-mgmt/apps/api/src/routes/stock_in.ts create mode 100644 output/warehouse-mgmt/apps/api/src/routes/stock_out.ts create mode 100644 output/warehouse-mgmt/apps/api/src/routes/stocktaking.ts create mode 100644 output/warehouse-mgmt/apps/api/src/routes/warehouse.ts create mode 100644 output/warehouse-mgmt/apps/api/src/services/item.ts create mode 100644 output/warehouse-mgmt/apps/api/src/services/stock_alert.ts create mode 100644 output/warehouse-mgmt/apps/api/src/services/stock_in.ts create mode 100644 output/warehouse-mgmt/apps/api/src/services/stock_out.ts create mode 100644 output/warehouse-mgmt/apps/api/src/services/stocktaking.ts create mode 100644 output/warehouse-mgmt/apps/api/src/services/warehouse.ts create mode 100644 output/warehouse-mgmt/apps/api/src/types/fastify.d.ts create mode 100644 output/warehouse-mgmt/apps/api/src/types/index.ts create mode 100644 output/warehouse-mgmt/apps/api/src/types/sql.js.d.ts create mode 100644 output/warehouse-mgmt/apps/api/tsconfig.json create mode 100644 output/warehouse-mgmt/apps/web/app/globals.css create mode 100644 output/warehouse-mgmt/apps/web/app/items/page.tsx create mode 100644 output/warehouse-mgmt/apps/web/app/layout.tsx create mode 100644 output/warehouse-mgmt/apps/web/app/page.tsx create mode 100644 output/warehouse-mgmt/apps/web/app/stock_alerts/page.tsx create mode 100644 output/warehouse-mgmt/apps/web/app/stock_in/page.tsx create mode 100644 output/warehouse-mgmt/apps/web/app/stock_out/page.tsx create mode 100644 output/warehouse-mgmt/apps/web/app/stocktaking/page.tsx create mode 100644 output/warehouse-mgmt/apps/web/app/warehouse/page.tsx create mode 100644 output/warehouse-mgmt/apps/web/components/layout/BottomNav.tsx create mode 100644 output/warehouse-mgmt/apps/web/components/layout/Sidebar.tsx create mode 100644 output/warehouse-mgmt/apps/web/components/note/NoteCard.tsx create mode 100644 output/warehouse-mgmt/apps/web/components/ui/Button.tsx create mode 100644 output/warehouse-mgmt/apps/web/components/ui/Card.tsx create mode 100644 output/warehouse-mgmt/apps/web/components/ui/EmptyState.tsx create mode 100644 output/warehouse-mgmt/apps/web/components/ui/Input.tsx create mode 100644 output/warehouse-mgmt/apps/web/components/ui/Modal.tsx create mode 100644 output/warehouse-mgmt/apps/web/hooks/useAuth.ts create mode 100644 output/warehouse-mgmt/apps/web/hooks/useDebounce.ts create mode 100644 output/warehouse-mgmt/apps/web/hooks/useForm.ts create mode 100644 output/warehouse-mgmt/apps/web/hooks/useItems.ts create mode 100644 output/warehouse-mgmt/apps/web/hooks/useStock_in.ts create mode 100644 output/warehouse-mgmt/apps/web/hooks/useStock_out.ts create mode 100644 output/warehouse-mgmt/apps/web/hooks/useStocktaking.ts create mode 100644 output/warehouse-mgmt/apps/web/next-env.d.ts create mode 100644 output/warehouse-mgmt/apps/web/next.config.ts create mode 100644 output/warehouse-mgmt/apps/web/package.json create mode 100644 output/warehouse-mgmt/apps/web/postcss.config.js create mode 100644 output/warehouse-mgmt/apps/web/services/api.ts create mode 100644 output/warehouse-mgmt/apps/web/services/auth.ts create mode 100644 output/warehouse-mgmt/apps/web/services/items.ts create mode 100644 output/warehouse-mgmt/apps/web/services/stock_in.ts create mode 100644 output/warehouse-mgmt/apps/web/services/stock_out.ts create mode 100644 output/warehouse-mgmt/apps/web/services/stocktaking.ts create mode 100644 output/warehouse-mgmt/apps/web/tailwind.config.ts create mode 100644 output/warehouse-mgmt/apps/web/tsconfig.json create mode 100644 output/warehouse-mgmt/apps/web/types/index.ts create mode 100644 output/warehouse-mgmt/package.json create mode 100644 output/warehouse-mgmt/packages/shared-config/package.json create mode 100644 output/warehouse-mgmt/packages/shared-config/src/index.ts create mode 100644 output/warehouse-mgmt/packages/shared-config/tsconfig.json create mode 100644 output/warehouse-mgmt/packages/shared-types/package.json create mode 100644 output/warehouse-mgmt/packages/shared-types/src/index.ts create mode 100644 output/warehouse-mgmt/packages/shared-types/tsconfig.json create mode 100644 output/warehouse-mgmt/scripts/build.mjs create mode 100644 output/warehouse-mgmt/scripts/dev.mjs create mode 100644 package.json create mode 100644 patterns/best-practices.md create mode 100644 patterns/design-patterns.md create mode 100644 patterns/memory-system-architecture.md create mode 100644 patterns/prompt-templates.md create mode 100644 playbooks/agent-evolution.md create mode 100644 playbooks/memory-audit.md create mode 100644 playbooks/tool-strategy.md create mode 100644 prd-example.json create mode 100644 progress.log create mode 100644 protocol-reference/metrics.md create mode 100644 protocol-reference/quality-gates.md create mode 100644 protocol-reference/templates.md create mode 100644 reflections/2026-06-03-agent-evolution.md create mode 100644 reflections/2026-06-04-memory-audit.md create mode 100644 reflections/2026-06-05-ddl-leakage.md create mode 100644 reflections/2026-06-05-falsy-coercion.md create mode 100644 reflections/2026-06-05-jsx-template.md create mode 100644 reflections/2026-06-05-nextjs-path.md create mode 100644 research/claude-code-analysis.md create mode 100644 research/claude-code-learnings.md create mode 100644 research/hermes-agent-study.md create mode 100644 research/hermes-internalization-guide.md create mode 100644 research/pr-37/docs/pr-37-delivery-planning.md create mode 100644 research/pr-37/docs/pr-37-domain-intelligence.md create mode 100644 research/pr-37/pr-37-delivery-planning/batch-planner.mjs create mode 100644 research/pr-37/pr-37-delivery-planning/capacity-planner.mjs create mode 100644 research/pr-37/pr-37-delivery-planning/index.mjs create mode 100644 research/pr-37/pr-37-delivery-planning/release-planner.mjs create mode 100644 research/pr-37/pr-37-delivery-planning/roadmap-aligner.mjs create mode 100644 research/pr-37/pr-37-delivery-planning/sprint-planner.mjs create mode 100644 research/pr-37/pr-37-domain-intelligence-delivery-planning.test.mjs create mode 100644 research/pr-37/pr-37-domain-intelligence/domain-classifier.mjs create mode 100644 research/pr-37/pr-37-domain-intelligence/domain-registry.mjs create mode 100644 research/pr-37/pr-37-domain-intelligence/domain-validator.mjs create mode 100644 research/pr-37/pr-37-domain-intelligence/index.mjs create mode 100644 research/pr-37/pr-37-domain-intelligence/industry-epic-generator.mjs create mode 100644 research/pr-37/pr-37-domain-intelligence/industry-feature-generator.mjs create mode 100644 rules/error-handling.md create mode 100644 rules/git-convention.md create mode 100644 rules/task-workflow.md create mode 100644 rules/tool-definition.md create mode 100644 scripts/architecture-agent.mjs create mode 100644 scripts/audit-agents.mjs create mode 100644 scripts/audit-all.mjs create mode 100644 scripts/audit-governance.mjs create mode 100644 scripts/audit-portfolio.mjs create mode 100755 scripts/auto-checkpoint.sh create mode 100644 scripts/backend-builder-agent.mjs create mode 100644 scripts/check-deprecations.mjs create mode 100644 scripts/contract-consistency-test.mjs create mode 100644 scripts/contract-e2e.mjs create mode 100644 scripts/domain-benchmark.mjs create mode 100755 scripts/dream-cycle.sh create mode 100644 scripts/e2e-benchmark-v1.mjs create mode 100644 scripts/electron-builder-agent.mjs create mode 100755 scripts/extract-skill.sh create mode 100644 scripts/factory-router.mjs create mode 100644 scripts/frontend-builder-agent.mjs create mode 100644 scripts/fullstack-composer-agent.mjs create mode 100644 scripts/guard-all.sh create mode 100644 scripts/guard-dreaming-phase.mjs create mode 100644 scripts/guard-memory-backend.mjs create mode 100644 scripts/guard-memory-write.mjs create mode 100644 scripts/guard-runtime-core.mjs create mode 100644 scripts/guard-tool-path.mjs create mode 100644 scripts/guard-tool-trace.mjs create mode 100755 scripts/init-workspace.sh create mode 100644 scripts/lib/deprecation-warning.mjs create mode 100755 scripts/memory-auto.sh create mode 100755 scripts/memory-sync.sh create mode 100644 scripts/model-contract.mjs create mode 100644 scripts/orchestrator.mjs create mode 100755 scripts/production-check.sh create mode 100644 scripts/project-intake-agent.mjs create mode 100644 scripts/regression-gate.mjs create mode 100644 scripts/release-benchmark.mjs create mode 100644 scripts/release-builder-agent.mjs create mode 100644 scripts/runtime.mjs create mode 100755 scripts/session-compact.sh create mode 100644 scripts/smoke-test.mjs create mode 100755 scripts/sync-agent-repos.sh create mode 100644 scripts/templates/blocker.md.template create mode 100644 scripts/templates/dashboard.md.template create mode 100644 scripts/templates/execution-audit.md.template create mode 100644 scripts/templates/portfolio-audit.md.template create mode 100644 scripts/templates/portfolio.md.template create mode 100644 scripts/templates/progress.log.template create mode 100644 scripts/templates/recovery-report.md.template create mode 100644 scripts/test-baseline.sh create mode 100644 scripts/upgrade-test.mjs create mode 100755 scripts/watchdog.sh create mode 100644 skills/code-explorer.md create mode 100644 skills/compression.md create mode 100644 skills/security-check.md create mode 100644 skills/self-evolution.md create mode 100644 src/active-memory/baseline-thresholds.mjs create mode 100644 src/active-memory/compare-release-candidate-baseline.mjs create mode 100644 src/active-memory/evaluate-release-gate.mjs create mode 100644 src/active-memory/generate-release-candidate-report.mjs create mode 100644 src/active-memory/precompute-cache.mjs create mode 100644 src/active-memory/precompute-circuit-breaker.mjs create mode 100644 src/active-memory/precompute-flag.mjs create mode 100644 src/active-memory/precompute-gateway.mjs create mode 100644 src/active-memory/precompute-readiness-gate.mjs create mode 100644 src/active-memory/precompute-release-candidate-checklist.mjs create mode 100644 src/active-memory/precompute-replacement-policy.mjs create mode 100644 src/active-memory/precompute-rollout-controller.mjs create mode 100644 src/active-memory/precompute-rollout-stage-policy.mjs create mode 100644 src/active-memory/precompute-runner.mjs create mode 100644 src/active-memory/precompute-shadow-diff.mjs create mode 100644 src/active-memory/precompute-shadow-integration.mjs create mode 100644 src/agent-adapters/agent-adapter.mjs create mode 100644 src/agent-adapters/claw-adapter.mjs create mode 100644 src/agent-adapters/mock-adapter.mjs create mode 100644 src/agent-runtime/adapters.mjs create mode 100644 src/agent-runtime/artifact-registry.mjs create mode 100644 src/agent-runtime/domain.mjs create mode 100644 src/agent-runtime/index.mjs create mode 100644 src/agent-runtime/pipeline.mjs create mode 100644 src/agent-runtime/reporting.mjs create mode 100644 src/agent-runtime/state-machine.mjs create mode 100644 src/codegraph/index.js create mode 100644 src/codegraph/schema.sql create mode 100644 src/compression/byte-surgery.js create mode 100644 src/compression/ccr-backends.js create mode 100644 src/compression/ccr-store.js create mode 100644 src/compression/ctx-compress.js create mode 100644 src/compression/detector.js create mode 100644 src/compression/embedding-scorer.js create mode 100644 src/compression/smart-crusher.js create mode 100644 src/config/toml-config.js create mode 100644 src/config/toml-parser.js create mode 100644 src/governance/index.js create mode 100644 src/memory-core/memory-core-facade.mjs create mode 100644 src/memory-core/memory-core-types.mjs create mode 100644 src/memory-core/memory-fts5-search-adapter.mjs create mode 100644 src/memory-core/memory-search-adapter.mjs create mode 100644 src/memory-core/memory-store-adapter.mjs create mode 100644 src/memory/context-graph.js create mode 100644 src/memory/sync-context-graph.js create mode 100644 src/orchestration/cross-session.js create mode 100644 src/orchestration/session-fsm.js create mode 100644 src/security/risk-scorer.js create mode 100644 src/sf-01-product-strategy/business-model.mjs create mode 100644 src/sf-01-product-strategy/competitor-analysis.mjs create mode 100644 src/sf-01-product-strategy/domain-model.mjs create mode 100644 src/sf-01-product-strategy/gtm-roadmap.mjs create mode 100644 src/sf-01-product-strategy/idea-analysis.mjs create mode 100644 src/sf-01-product-strategy/index.mjs create mode 100644 src/sf-01-product-strategy/market-analysis.mjs create mode 100644 src/sf-01-product-strategy/viability.mjs create mode 100644 src/sf-02-requirement-engineering/dependency-engine.mjs create mode 100644 src/sf-02-requirement-engineering/domain-model.mjs create mode 100644 src/sf-02-requirement-engineering/index.mjs create mode 100644 src/sf-02-requirement-engineering/strategy-to-requirement.mjs create mode 100644 src/software-factory-core/artifact-graph.mjs create mode 100644 src/software-factory-core/factory-contract.mjs create mode 100644 src/software-factory-core/factory-registry.mjs create mode 100644 src/software-factory-core/index.mjs create mode 100644 test/architecture-agent.test.mjs create mode 100644 test/architecture/deprecation-warning.test.mjs create mode 100644 test/architecture/freeze.test.mjs create mode 100644 test/architecture/guard.test.mjs create mode 100644 test/architecture/production-check.test.mjs create mode 100644 test/backend-builder-agent.test.mjs create mode 100644 test/baseline/smoke.test.mjs create mode 100644 test/baseline/test-01-gateway-startup.test.mjs create mode 100644 test/baseline/test-02-session-create.test.mjs create mode 100644 test/baseline/test-03-memory-search.test.mjs create mode 100644 test/baseline/test-04-memory-get.test.mjs create mode 100644 test/baseline/test-05-tool-exec.test.mjs create mode 100644 test/baseline/test-06-agent-reply.test.mjs create mode 100644 test/baseline/test-07-mcp-discovery.test.mjs create mode 100644 test/baseline/test-08-config-load.test.mjs create mode 100644 test/baseline/test-09-multi-session.test.mjs create mode 100644 test/baseline/test-10-session-recovery.test.mjs create mode 100644 test/baseline/test-11-compaction.test.mjs create mode 100644 test/baseline/test-12-active-memory.test.mjs create mode 100644 test/benchmark/domain-benchmark-suite.mjs create mode 100644 test/benchmark/smoke-one.mjs create mode 100644 test/e2e-regression.test.mjs create mode 100644 test/electron-builder-agent.test.mjs create mode 100644 test/fixtures/agent-adapters/mock-agent/archives/deprecated.md create mode 100644 test/fixtures/agent-adapters/mock-agent/archives/old-001.md create mode 100644 test/fixtures/agent-adapters/mock-agent/archives/old-002.md create mode 100644 test/fixtures/agent-adapters/mock-agent/contexts/ctx-main.md create mode 100644 test/fixtures/agent-adapters/mock-agent/contexts/ctx-projects.md create mode 100644 test/fixtures/agent-adapters/mock-agent/contexts/ctx-settings.md create mode 100644 test/fixtures/agent-adapters/mock-agent/threads/th-001.md create mode 100644 test/fixtures/agent-adapters/mock-agent/threads/th-002.md create mode 100644 test/fixtures/agent-adapters/mock-agent/threads/th-003.md create mode 100644 test/fixtures/agent-adapters/mock-agent/threads/th-004.md create mode 100644 test/fixtures/agent-adapters/mock-agent/threads/th-005.md create mode 100644 test/fixtures/agent-observability/degrading-agent-history.json create mode 100644 test/fixtures/agent-observability/healthy-agent-history.json create mode 100644 test/fixtures/agent-observability/improving-agent-history.json create mode 100644 test/fixtures/agent-observability/mixed-fleet-history.json create mode 100644 test/fixtures/agent-observability/unstable-agent-history.json create mode 100644 test/fixtures/agent-registry/deprecated-agent.json create mode 100644 test/fixtures/agent-registry/duplicate-agent-id.json create mode 100644 test/fixtures/agent-registry/empty-registry.json create mode 100644 test/fixtures/agent-registry/missing-required-field.json create mode 100644 test/fixtures/agent-registry/multi-agent-registry.json create mode 100644 test/fixtures/agent-registry/unsupported-agent.json create mode 100644 test/fixtures/agent-registry/valid-registry.json create mode 100644 test/fixtures/architecture-agent/ecommerce-prd.json create mode 100644 test/fixtures/architecture-agent/education-prd.json create mode 100644 test/fixtures/architecture-agent/empty-prd.json create mode 100644 test/fixtures/architecture-agent/enterprise-prd.json create mode 100644 test/fixtures/architecture-agent/generic-prd.json create mode 100644 test/fixtures/architecture-agent/note-prd.json create mode 100644 test/fixtures/autonomous-response/certification-revoked-input.json create mode 100644 test/fixtures/autonomous-response/compatibility-failure-input.json create mode 100644 test/fixtures/autonomous-response/health-degradation-input.json create mode 100644 test/fixtures/autonomous-response/mixed-severity-input.json create mode 100644 test/fixtures/autonomous-response/release-failure-input.json create mode 100644 test/fixtures/autonomous-response/warning-only-input.json create mode 100644 test/fixtures/baseline-evolution/evolution-report.json create mode 100644 test/fixtures/baseline-evolution/threshold-stability-report.json create mode 100644 test/fixtures/baseline-evolution/v1-baseline.json create mode 100644 test/fixtures/baseline-evolution/v2-baseline.json create mode 100644 test/fixtures/baseline-evolution/v3-baseline.json create mode 100644 test/fixtures/baseline-rotation/current-baseline.json create mode 100644 test/fixtures/baseline-rotation/rotation-policy.json create mode 100644 test/fixtures/baseline-rotation/v1-baseline.json create mode 100644 test/fixtures/baseline-rotation/v2-baseline.json create mode 100644 test/fixtures/baseline-rotation/v3-baseline.json create mode 100644 test/fixtures/compatibility-matrix/claw-0.1.x-deprecated.json create mode 100644 test/fixtures/compatibility-matrix/claw-1.0.x-current-compatible.json create mode 100644 test/fixtures/compatibility-matrix/claw-1.0.x-missing-candidates.json create mode 100644 test/fixtures/compatibility-matrix/claw-1.1.x-minor-add-fields.json create mode 100644 test/fixtures/compatibility-matrix/claw-1.2.x-minor-rename-fields.json create mode 100644 test/fixtures/compatibility-matrix/claw-1.3.x-new-directory.json create mode 100644 test/fixtures/compatibility-matrix/claw-1.4.x-middleware-compat.json create mode 100644 test/fixtures/compatibility-matrix/claw-1.5.x-directory-change.json create mode 100644 test/fixtures/compatibility-matrix/claw-2.0.x-major-schema-change.json create mode 100644 test/fixtures/continuous-monitoring/critical-monitoring-history.json create mode 100644 test/fixtures/continuous-monitoring/degrading-monitoring-history.json create mode 100644 test/fixtures/continuous-monitoring/healthy-monitoring-history.json create mode 100644 test/fixtures/continuous-monitoring/missing-data-monitoring-history.json create mode 100644 test/fixtures/continuous-monitoring/mixed-monitoring-history.json create mode 100644 test/fixtures/continuous-monitoring/rollback-spike-history.json create mode 100644 test/fixtures/cross-agent-certification/all-healthy-fleet.json create mode 100644 test/fixtures/cross-agent-certification/conditionally-ready-fleet.json create mode 100644 test/fixtures/cross-agent-certification/high-risk-fleet.json create mode 100644 test/fixtures/cross-agent-certification/mixed-fleet.json create mode 100644 test/fixtures/cross-agent-certification/not-ready-fleet.json create mode 100644 test/fixtures/incident-management/empty-input.json create mode 100644 test/fixtures/incident-management/lifecycle-test-input.json create mode 100644 test/fixtures/incident-management/mixed-fleet-input.json create mode 100644 test/fixtures/incident-management/multiple-critical-input.json create mode 100644 test/fixtures/incident-management/recovery-tracking-input.json create mode 100644 test/fixtures/incident-management/repeat-offender-input.json create mode 100644 test/fixtures/multi-agent-dashboard/healthy-dashboard-input.json create mode 100644 test/fixtures/multi-agent-dashboard/missing-data-dashboard-input.json create mode 100644 test/fixtures/multi-agent-dashboard/mixed-risk-dashboard-input.json create mode 100644 test/fixtures/multi-agent-dashboard/multi-agent-dashboard-output.json create mode 100644 test/fixtures/multi-agent-dashboard/unsupported-agent-dashboard-input.json create mode 100644 test/fixtures/production-readiness/full-pass.json create mode 100644 test/fixtures/production-readiness/missing-baseline.json create mode 100644 test/fixtures/production-readiness/missing-compat.json create mode 100644 test/fixtures/production-readiness/missing-dashboard.json create mode 100644 test/fixtures/production-validation/candidate-snapshot.json create mode 100644 test/fixtures/production-validation/stable-baseline.json create mode 100644 test/fixtures/production-validation/validation-report-output.md create mode 100644 test/fixtures/production-validation/validation-report.json create mode 100644 test/fixtures/project-intake/ecommerce-miniapp.json create mode 100644 test/fixtures/project-intake/edu-platform.json create mode 100644 test/fixtures/project-intake/empty-input.json create mode 100644 test/fixtures/project-intake/enterprise-oa.json create mode 100644 test/fixtures/project-intake/fitness-app.json create mode 100644 test/fixtures/project-intake/minimal.json create mode 100644 test/fixtures/project-intake/pet-management.json create mode 100644 test/fixtures/release-history/empty.json create mode 100644 test/fixtures/release-history/sample-events.json create mode 100644 test/fixtures/release-history/single-release.json create mode 100644 test/fixtures/release-pipeline/baseline-fail.json create mode 100644 test/fixtures/release-pipeline/baseline-pass.json create mode 100644 test/fixtures/release-pipeline/gate-closed.json create mode 100644 test/fixtures/release-pipeline/gate-open.json create mode 100644 test/fixtures/release-pipeline/rc-check-fail.json create mode 100644 test/fixtures/release-pipeline/rc-check-pass.json create mode 100644 test/fixtures/release-pipeline/report-fail.md create mode 100644 test/fixtures/release-pipeline/report-pass.md create mode 100644 test/fixtures/reliability-center/empty-input.json create mode 100644 test/fixtures/reliability-center/error-budget-input.json create mode 100644 test/fixtures/reliability-center/healthy-fleet-input.json create mode 100644 test/fixtures/reliability-center/low-reliability-input.json create mode 100644 test/fixtures/reliability-center/mixed-fleet-input.json create mode 100644 test/fixtures/reliability-center/slo-violation-input.json create mode 100644 test/fixtures/reliability-center/slo-warning-input.json create mode 100644 test/frontend-builder-agent.test.mjs create mode 100644 test/fullstack-composer-agent.test.mjs create mode 100644 test/memory-core/facade-health.test.mjs create mode 100644 test/memory-core/facade.test.mjs create mode 100644 test/memory-core/fts5-adapter.test.mjs create mode 100644 test/memory-core/get-shadow.test.mjs create mode 100644 test/memory-core/search-shadow.test.mjs create mode 100644 test/orchestrator.test.mjs create mode 100644 test/project-intake-agent.test.mjs create mode 100644 test/release-builder-agent.test.mjs create mode 100644 test/sf-01-product-strategy.test.mjs create mode 100644 test/sf-02-requirement-engineering.test.mjs create mode 100644 test/software-factory-core.test.mjs create mode 100644 tools/agent-decomposition.md create mode 100644 tools/workspace-commands.md create mode 100644 upgrade-playbook.md create mode 100644 version-contract.md diff --git a/.benchmark/appointment/arch.json b/.benchmark/appointment/arch.json new file mode 100644 index 0000000..38cbd6c --- /dev/null +++ b/.benchmark/appointment/arch.json @@ -0,0 +1 @@ +{"projectName":"MyProject","domain":"generic","contract":{"projectName":"MyProject","domain":"generic","entities":[{"name":"User","table":"users","description":"系统用户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"hasMany","entity":"contracts","via":"user_id"},{"type":"hasMany","entity":"customers","via":"user_id"}]},{"name":"Contract","table":"contracts","description":"合同","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","defaultValue":"draft","enum":["draft","pending","active","expired","terminated"]},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Approval","table":"approvals","description":"审批流程","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"applicant_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"status","type":"VARCHAR(32)","defaultValue":"pending","enum":["pending","approved","rejected"]},{"name":"form_data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"users","via":"applicant_id","as":"applicant"}]},{"name":"Customer","table":"customers","description":"客户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","defaultValue":"[]"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Reminder","table":"reminders","description":"到期提醒","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"remind_at","type":"TIMESTAMP","required":true},{"name":"message","type":"TEXT"},{"name":"sent","type":"BOOLEAN","defaultValue":"false"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Services","table":"services","description":"服务项目表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"duration_min","type":"INTEGER","required":false,"isPrimary":false,"isAuto":false},{"name":"price","type":"DECIMAL(10,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Appointments","table":"appointments","description":"时间段预约表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"client_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"clients","fkColumn":"id"},{"name":"service_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"services","fkColumn":"id"},{"name":"start_time","type":"TIMESTAMPTZ","required":true,"isPrimary":false,"isAuto":false},{"name":"end_time","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"notes","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"clients","via":"client_id"},{"type":"belongsTo","entity":"services","via":"service_id"}]},{"name":"Clients","table":"clients","description":"客户通知表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"data","type":"JSONB","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Stats","table":"stats","description":"预约统计表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"client_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"clients","fkColumn":"id"},{"name":"service_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"services","fkColumn":"id"},{"name":"start_time","type":"TIMESTAMPTZ","required":true,"isPrimary":false,"isAuto":false},{"name":"end_time","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"notes","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"clients","via":"client_id"},{"type":"belongsTo","entity":"services","via":"service_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","remind_at":"remindAt","duration_min":"durationMin","client_id":"clientId","service_id":"serviceId","start_time":"startTime","end_time":"endTime","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","remindAt":"remind_at","durationMin":"duration_min","clientId":"client_id","serviceId":"service_id","startTime":"start_time","endTime":"end_time","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Contract":{"user_id":{"required":true},"title":{"required":true},"status":{"enum":["draft","pending","active","expired","terminated"],"defaultValue":"draft"}},"Approval":{"user_id":{"required":true},"entity_type":{"required":true},"applicant_id":{"required":true},"status":{"enum":["pending","approved","rejected"],"defaultValue":"pending"}},"Customer":{"user_id":{"required":true},"name":{"required":true},"tags":{"defaultValue":"[]"}},"Reminder":{"user_id":{"required":true},"entity_type":{"required":true},"remind_at":{"required":true},"sent":{"defaultValue":"false"}},"Services":{"id":{"required":true},"name":{"required":true}},"Appointments":{"id":{"required":true},"start_time":{"required":true}},"Clients":{"id":{"required":true},"title":{"required":true}},"Stats":{"id":{"required":true},"start_time":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO(文件存储)","deployment":"Docker Compose + Nginx / K8s(规模化)","considerations":[]},"architectureDiagram":"┌─────────────────────────────────────────────────────────┐\n│ MyProject — System Architecture │\n├─────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────────────┐ ┌──────────────────────┐ │\n│ │ Client Layer │ │ Admin / Web │ │\n│ │ React Native 0.76 + Expo / Flutter 3.x │ │ React + Vite (Web) │ │\n│ └──────────┬───────────┘ └──────────┬───────────┘ │\n│ │ │ │\n│ └──────────┬────────────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ API Gateway │ │\n│ │ Nginx / Kong │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ Backend Services │ │\n│ │ NestJS + Prisma │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌───────────────┼───────────────┐ │\n│ │ │ │ │\n│ ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ │\n│ │ PostgreSQL 15│ │ Redis Cache │ │ MinIO │ │\n│ │ Primary │ │ Session │ │ Files │ │\n│ └───────────┘ └─────────────┘ └──────────┘ │\n│ │\n├─────────────────────────────────────────────────────────┤\n│ Modules: │\n│ 1 服务项目 │\n│ 2 Schedule │\n│ 3 Notification │\n│ 4 Analytics │\n│ │\n└─────────────────────────────────────────────────────────┘","dataFlows":[{"page":"服务项目","route":"/services","description":"服务项目","dataFlow":["GET /api/services → 获取服务项目列表","POST /api/services → 创建服务项目"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"时间段预约","route":"/appointments","description":"时间段预约","dataFlow":["GET /api/appointments → 获取时间段预约列表","POST /api/appointments → 创建时间段预约"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"客户通知","route":"/clients","description":"客户通知","dataFlow":["GET /api/clients → 获取客户通知列表","POST /api/clients → 创建客户通知"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"预约统计","route":"/stats","description":"预约统计","dataFlow":["GET /api/stats → 获取预约统计列表","POST /api/stats → 创建预约统计"],"direction":"Client → API Gateway → Backend → Database → Response → Client"}],"modules":[{"name":"服务项目","label":"服务项目","features":["服务项目"],"responsibilities":["提供 服务项目 相关功能"]},{"name":"schedule","label":"Schedule","features":["时间段预约"],"responsibilities":["提供 时间段预约 相关功能"]},{"name":"notification","label":"Notification","features":["客户通知"],"responsibilities":["提供 客户通知 相关功能"]},{"name":"analytics","label":"Analytics","features":["预约统计"],"responsibilities":["提供 预约统计 相关功能"]}],"databaseSchema":[{"table":"users","description":"用户表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"services","description":"服务项目表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"duration_min","type":"INTEGER"},{"name":"price","type":"DECIMAL(10,2)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_services_user ON services(user_id)"]},{"table":"appointments","description":"时间段预约表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"client_id","type":"UUID","constraints":"FK → clients.id"},{"name":"service_id","type":"UUID","constraints":"FK → services.id"},{"name":"start_time","type":"TIMESTAMPTZ","constraints":"NOT NULL"},{"name":"end_time","type":"TIMESTAMPTZ"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'pending'"},{"name":"notes","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_appointments_user ON appointments(user_id)"]},{"table":"clients","description":"客户通知表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'active'"},{"name":"data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_clients_user ON clients(user_id)"]},{"table":"stats","description":"预约统计表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"client_id","type":"UUID","constraints":"FK → clients.id"},{"name":"service_id","type":"UUID","constraints":"FK → services.id"},{"name":"start_time","type":"TIMESTAMPTZ","constraints":"NOT NULL"},{"name":"end_time","type":"TIMESTAMPTZ"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'pending'"},{"name":"notes","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_stats_user ON stats(user_id)"]}],"apiDesign":[{"resource":"appointments","basePath":"/api/appointments","endpoints":[{"method":"GET","path":"/api/appointments","description":"获取时间段预约列表"},{"method":"POST","path":"/api/appointments","description":"创建时间段预约"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}]},{"resource":"clients","basePath":"/api/clients","endpoints":[{"method":"GET","path":"/api/clients","description":"获取客户通知列表"},{"method":"POST","path":"/api/clients","description":"创建客户通知"}]},{"resource":"services","basePath":"/api/services","endpoints":[{"method":"GET","path":"/api/services","description":"获取服务项目列表"},{"method":"POST","path":"/api/services","description":"创建服务项目"}]},{"resource":"stats","basePath":"/api/stats","endpoints":[{"method":"GET","path":"/api/stats","description":"获取预约统计列表"},{"method":"POST","path":"/api/stats","description":"创建预约统计"}]}],"directoryStructure":["myproject/","├── apps/","│ ├── mobile/ # React Native / Flutter 移动端","│ │ ├── src/","│ │ │ ├── screens/ # 页面组件","│ │ │ │ ├── 服务项目/","│ │ │ │ ├── schedule/","│ │ │ │ ├── notification/","│ │ │ │ ├── analytics/","│ │ │ ├── components/ # 通用组件","│ │ │ ├── hooks/ # 自定义 Hooks","│ │ │ ├── services/ # API 调用层","│ │ │ ├── store/ # 状态管理","│ │ │ └── utils/ # 工具函数","│ │ ├── app.json","│ │ └── package.json","│ └── web/ # Web 管理后台","│ ├── src/","│ │ ├── pages/","│ │ ├── components/","│ │ └── layouts/","│ └── package.json","├── packages/","│ └── shared/ # 共享类型/常量","├── server/ # NestJS 后端服务","│ ├── src/","│ │ ├── modules/ # 业务模块","│ │ │ ├── 服务项目/","│ │ │ │ ├── 服务项目.controller.ts","│ │ │ │ ├── 服务项目.service.ts","│ │ │ │ ├── 服务项目.module.ts","│ │ │ │ └── dto/","│ │ │ ├── schedule/","│ │ │ │ ├── schedule.controller.ts","│ │ │ │ ├── schedule.service.ts","│ │ │ │ ├── schedule.module.ts","│ │ │ │ └── dto/","│ │ │ ├── notification/","│ │ │ │ ├── notification.controller.ts","│ │ │ │ ├── notification.service.ts","│ │ │ │ ├── notification.module.ts","│ │ │ │ └── dto/","│ │ │ ├── analytics/","│ │ │ │ ├── analytics.controller.ts","│ │ │ │ ├── analytics.service.ts","│ │ │ │ ├── analytics.module.ts","│ │ │ │ └── dto/","│ │ ├── common/ # 通用(guards, filters, interceptors)","│ │ ├── prisma/ # Prisma ORM","│ │ │ └── schema.prisma","│ │ ├── config/ # 环境配置","│ │ └── main.ts","│ ├── test/","│ ├── Dockerfile","│ └── package.json","├── docker-compose.yml","├── .github/workflows/ # CI/CD","│ └── ci.yml","├── .env.example","├── README.md","└── turbo.json # Monorepo 配置"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s(规模化)","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions → Build → Test → Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:37.851Z","sourceDomain":"generic","moduleCount":4,"tableCount":5,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/appointment/backend/.gitignore b/.benchmark/appointment/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/appointment/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/appointment/backend/README.md b/.benchmark/appointment/backend/README.md new file mode 100644 index 0000000..0c5cff6 --- /dev/null +++ b/.benchmark/appointment/backend/README.md @@ -0,0 +1,118 @@ +# MyProject — Backend API + +> 一款根据用户需求定制的应用。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` — List all +- `GET /api/contracts/:id` — Get by ID +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List all +- `GET /api/approvals/:id` — Get by ID +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List all +- `GET /api/customers/:id` — Get by ID +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List all +- `GET /api/reminders/:id` — Get by ID +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### services +- `GET /api/services` — List all +- `GET /api/services/:id` — Get by ID +- `POST /api/services` — Create +- `PUT /api/services/:id` — Update +- `DELETE /api/services/:id` — Delete + +#### appointments +- `GET /api/appointments` — List all +- `GET /api/appointments/:id` — Get by ID +- `POST /api/appointments` — Create +- `PUT /api/appointments/:id` — Update +- `DELETE /api/appointments/:id` — Delete + +#### clients +- `GET /api/clients` — List all +- `GET /api/clients/:id` — Get by ID +- `POST /api/clients` — Create +- `PUT /api/clients/:id` — Update +- `DELETE /api/clients/:id` — Delete + +#### stats +- `GET /api/stats` — List all +- `GET /api/stats/:id` — Get by ID +- `POST /api/stats` — Create +- `PUT /api/stats/:id` — Update +- `DELETE /api/stats/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/appointment/backend/package.json b/.benchmark/appointment/backend/package.json new file mode 100644 index 0000000..e3106a0 --- /dev/null +++ b/.benchmark/appointment/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/backend/src/__tests__/appointments.test.ts b/.benchmark/appointment/backend/src/__tests__/appointments.test.ts new file mode 100644 index 0000000..5aa044f --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/appointments.test.ts @@ -0,0 +1,121 @@ +// Appointments CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "clientId": "sample-clientid", + "serviceId": "sample-serviceid", + "startTime": "sample-starttime", + "endTime": "sample-endtime", + "notes": "sample-notes" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/appointments", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/appointments", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/appointments", () => { + it("creates a appointment", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/appointments", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/appointments/:id", () => { + it("returns the created appointment", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/appointments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/appointments/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/appointments/:id", () => { + it("updates the appointment", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/appointments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/appointments/:id", () => { + it("deletes the appointment", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/appointments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/appointments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/approvals.test.ts b/.benchmark/appointment/backend/src/__tests__/approvals.test.ts new file mode 100644 index 0000000..392316d --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/approvals.test.ts @@ -0,0 +1,121 @@ +// Approval CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "applicantId": regRes.json().user.id, + "status": "sample-status", + "formData": "sample-formdata" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/approvals", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/approvals", () => { + it("creates a approval", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/approvals/:id", () => { + it("returns the created approval", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/approvals/:id", () => { + it("updates the approval", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/approvals/:id", () => { + it("deletes the approval", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/auth.test.ts b/.benchmark/appointment/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/clients.test.ts b/.benchmark/appointment/backend/src/__tests__/clients.test.ts new file mode 100644 index 0000000..35e6627 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/clients.test.ts @@ -0,0 +1,119 @@ +// Clients CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "data": "sample-data" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/clients", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/clients", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/clients", () => { + it("creates a client", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/clients", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/clients/:id", () => { + it("returns the created client", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/clients/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/clients/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/clients/:id", () => { + it("updates the client", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/clients/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/clients/:id", () => { + it("deletes the client", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/clients/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/clients/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/contracts.test.ts b/.benchmark/appointment/backend/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..c2e9f16 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/contracts.test.ts @@ -0,0 +1,124 @@ +// Contract CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "partyA": "sample-partya", + "partyB": "sample-partyb", + "amount": 1, + "signedAt": "sample-signedat", + "expiresAt": "sample-expiresat", + "status": "sample-status", + "fileUrl": "sample-fileurl" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/contracts", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/contracts", () => { + it("creates a contract", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/contracts/:id", () => { + it("returns the created contract", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/contracts/:id", () => { + it("updates the contract", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/contracts/:id", () => { + it("deletes the contract", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/customers.test.ts b/.benchmark/appointment/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..f3112fa --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/customers.test.ts @@ -0,0 +1,122 @@ +// Customer CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source", + "tags": "sample-tags" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/items.test.ts b/.benchmark/appointment/backend/src/__tests__/items.test.ts new file mode 100644 index 0000000..7136843 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/items.test.ts @@ -0,0 +1,118 @@ +// Item CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/reminders.test.ts b/.benchmark/appointment/backend/src/__tests__/reminders.test.ts new file mode 100644 index 0000000..c573030 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/reminders.test.ts @@ -0,0 +1,121 @@ +// Reminder CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "remindAt": "sample-remindat", + "message": "sample-message", + "sent": true + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/reminders", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/reminders", () => { + it("creates a reminder", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/reminders/:id", () => { + it("returns the created reminder", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/reminders/:id", () => { + it("updates the reminder", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/reminders/:id", () => { + it("deletes the reminder", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/services.test.ts b/.benchmark/appointment/backend/src/__tests__/services.test.ts new file mode 100644 index 0000000..cc8293c --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/services.test.ts @@ -0,0 +1,120 @@ +// Services CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "description": "sample-description", + "durationMin": 1, + "price": 1 + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/services", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/services", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/services", () => { + it("creates a service", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/services", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/services/:id", () => { + it("returns the created service", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/services/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/services/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/services/:id", () => { + it("updates the service", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/services/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/services/:id", () => { + it("deletes the service", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/services/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/services/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/stats.test.ts b/.benchmark/appointment/backend/src/__tests__/stats.test.ts new file mode 100644 index 0000000..de14005 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/stats.test.ts @@ -0,0 +1,121 @@ +// Stats CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "clientId": "sample-clientid", + "serviceId": "sample-serviceid", + "startTime": "sample-starttime", + "endTime": "sample-endtime", + "notes": "sample-notes" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/stats", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/stats", () => { + it("creates a stat", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/stats/:id", () => { + it("returns the created stat", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/stats/:id", () => { + it("updates the stat", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/stats/:id", () => { + it("deletes the stat", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/users.test.ts b/.benchmark/appointment/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/db/client.ts b/.benchmark/appointment/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/appointment/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/appointment/backend/src/db/schema.ts b/.benchmark/appointment/backend/src/db/schema.ts new file mode 100644 index 0000000..ef6555c --- /dev/null +++ b/.benchmark/appointment/backend/src/db/schema.ts @@ -0,0 +1,20 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS services (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n description TEXT,\n duration_min INTEGER,\n price REAL,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS appointments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n client_id TEXT REFERENCES clients(id),\n service_id TEXT REFERENCES services(id),\n start_time TEXT NOT NULL,\n end_time TEXT,\n status TEXT,\n notes TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS clients (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n client_id TEXT REFERENCES clients(id),\n service_id TEXT REFERENCES services(id),\n start_time TEXT NOT NULL,\n end_time TEXT,\n status TEXT,\n notes TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/appointment/backend/src/index.ts b/.benchmark/appointment/backend/src/index.ts new file mode 100644 index 0000000..7d4b2cb --- /dev/null +++ b/.benchmark/appointment/backend/src/index.ts @@ -0,0 +1,79 @@ +// MyProject — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { servicesRoutes } from "./routes/services.js"; +import { appointmentsRoutes } from "./routes/appointments.js"; +import { clientsRoutes } from "./routes/clients.js"; +import { statsRoutes } from "./routes/stats.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-2d3e1114"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(servicesRoutes, { prefix: "/api/services" }); + await app.register(appointmentsRoutes, { prefix: "/api/appointments" }); + await app.register(clientsRoutes, { prefix: "/api/clients" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/appointment/backend/src/middleware/auth.ts b/.benchmark/appointment/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/appointment/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/appointment/backend/src/routes/appointments.ts b/.benchmark/appointment/backend/src/routes/appointments.ts new file mode 100644 index 0000000..b4c9882 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/appointments.ts @@ -0,0 +1,51 @@ +// Auto-generated Appointments routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { AppointmentsService } from "../services/appointment.js"; +import type { CreateAppointmentsInput, UpdateAppointmentsInput } from "../types/index.js"; + +const service = new AppointmentsService(); + +export async function appointmentsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/appointments — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/appointments/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Appointments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/appointments — create + app.post<{ Body: CreateAppointmentsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/appointments/:id — update + app.put<{ Params: { id: string }; Body: UpdateAppointmentsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Appointments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/appointments/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Appointments not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/approvals.ts b/.benchmark/appointment/backend/src/routes/approvals.ts new file mode 100644 index 0000000..3a974d2 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/approvals.ts @@ -0,0 +1,51 @@ +// Auto-generated Approval routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ApprovalService } from "../services/approval.js"; +import type { CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +const service = new ApprovalService(); + +export async function approvalsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/approvals — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/approvals/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/approvals — create + app.post<{ Body: CreateApprovalInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/approvals/:id — update + app.put<{ Params: { id: string }; Body: UpdateApprovalInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/approvals/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/auth.ts b/.benchmark/appointment/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/appointment/backend/src/routes/clients.ts b/.benchmark/appointment/backend/src/routes/clients.ts new file mode 100644 index 0000000..0b10f84 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/clients.ts @@ -0,0 +1,51 @@ +// Auto-generated Clients routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ClientsService } from "../services/client.js"; +import type { CreateClientsInput, UpdateClientsInput } from "../types/index.js"; + +const service = new ClientsService(); + +export async function clientsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/clients — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/clients/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Clients not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/clients — create + app.post<{ Body: CreateClientsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/clients/:id — update + app.put<{ Params: { id: string }; Body: UpdateClientsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Clients not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/clients/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Clients not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/contracts.ts b/.benchmark/appointment/backend/src/routes/contracts.ts new file mode 100644 index 0000000..ee83abf --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/contracts.ts @@ -0,0 +1,51 @@ +// Auto-generated Contract routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ContractService } from "../services/contract.js"; +import type { CreateContractInput, UpdateContractInput } from "../types/index.js"; + +const service = new ContractService(); + +export async function contractsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/contracts — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/contracts/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/contracts — create + app.post<{ Body: CreateContractInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/contracts/:id — update + app.put<{ Params: { id: string }; Body: UpdateContractInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/contracts/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/customers.ts b/.benchmark/appointment/backend/src/routes/customers.ts new file mode 100644 index 0000000..9a6f981 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customer routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomerService } from "../services/customer.js"; +import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +const service = new CustomerService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers — create + app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id — update + app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/items.ts b/.benchmark/appointment/backend/src/routes/items.ts new file mode 100644 index 0000000..2ad945e --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Item routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemService } from "../services/item.js"; +import type { CreateItemInput, UpdateItemInput } from "../types/index.js"; + +const service = new ItemService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items — create + app.post<{ Body: CreateItemInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id — update + app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/reminders.ts b/.benchmark/appointment/backend/src/routes/reminders.ts new file mode 100644 index 0000000..ee00754 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/reminders.ts @@ -0,0 +1,51 @@ +// Auto-generated Reminder routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ReminderService } from "../services/reminder.js"; +import type { CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +const service = new ReminderService(); + +export async function remindersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/reminders — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/reminders/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/reminders — create + app.post<{ Body: CreateReminderInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/reminders/:id — update + app.put<{ Params: { id: string }; Body: UpdateReminderInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/reminders/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/services.ts b/.benchmark/appointment/backend/src/routes/services.ts new file mode 100644 index 0000000..f280933 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/services.ts @@ -0,0 +1,51 @@ +// Auto-generated Services routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ServicesService } from "../services/service.js"; +import type { CreateServicesInput, UpdateServicesInput } from "../types/index.js"; + +const service = new ServicesService(); + +export async function servicesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/services — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/services/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Services not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/services — create + app.post<{ Body: CreateServicesInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/services/:id — update + app.put<{ Params: { id: string }; Body: UpdateServicesInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Services not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/services/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Services not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/stats.ts b/.benchmark/appointment/backend/src/routes/stats.ts new file mode 100644 index 0000000..92f64ea --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/stats.ts @@ -0,0 +1,51 @@ +// Auto-generated Stats routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { StatsService } from "../services/stat.js"; +import type { CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +const service = new StatsService(); + +export async function statsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/stats — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/stats/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/stats — create + app.post<{ Body: CreateStatsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/stats/:id — update + app.put<{ Params: { id: string }; Body: UpdateStatsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/stats/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/users.ts b/.benchmark/appointment/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/services/appointment.ts b/.benchmark/appointment/backend/src/services/appointment.ts new file mode 100644 index 0000000..337572e --- /dev/null +++ b/.benchmark/appointment/backend/src/services/appointment.ts @@ -0,0 +1,56 @@ +// Auto-generated Appointments service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Appointments, CreateAppointmentsInput, UpdateAppointmentsInput } from "../types/index.js"; + +export class AppointmentsService { + /** List all appointments */ + list(): Appointments[] { + return queryAll("SELECT * FROM appointments ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Appointments | undefined { + return queryOne("SELECT * FROM appointments WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateAppointmentsInput): Appointments { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "client_id", "service_id", "start_time", "end_time", "notes", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.clientId ?? null, input.serviceId ?? null, input.startTime ?? null, input.endTime ?? null, input.notes ?? null, now, now]; + + execute(`INSERT INTO appointments (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateAppointmentsInput): Appointments | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.clientId !== undefined) { sets.push("client_id = ?"); values.push(input.clientId); } + if (input.serviceId !== undefined) { sets.push("service_id = ?"); values.push(input.serviceId); } + if (input.startTime !== undefined) { sets.push("start_time = ?"); values.push(input.startTime); } + if (input.endTime !== undefined) { sets.push("end_time = ?"); values.push(input.endTime); } + if (input.notes !== undefined) { sets.push("notes = ?"); values.push(input.notes); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE appointments SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM appointments WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/approval.ts b/.benchmark/appointment/backend/src/services/approval.ts new file mode 100644 index 0000000..f40b2a2 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/approval.ts @@ -0,0 +1,56 @@ +// Auto-generated Approval service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Approval, CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +export class ApprovalService { + /** List all approvals */ + list(): Approval[] { + return queryAll("SELECT * FROM approvals ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Approval | undefined { + return queryOne("SELECT * FROM approvals WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateApprovalInput): Approval { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "applicant_id", "status", "form_data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.applicantId ?? null, input.status ?? null, input.formData ?? null, now, now]; + + execute(`INSERT INTO approvals (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateApprovalInput): Approval | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.applicantId !== undefined) { sets.push("applicant_id = ?"); values.push(input.applicantId); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.formData !== undefined) { sets.push("form_data = ?"); values.push(input.formData); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE approvals SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM approvals WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/client.ts b/.benchmark/appointment/backend/src/services/client.ts new file mode 100644 index 0000000..b0b1747 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/client.ts @@ -0,0 +1,54 @@ +// Auto-generated Clients service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Clients, CreateClientsInput, UpdateClientsInput } from "../types/index.js"; + +export class ClientsService { + /** List all clients */ + list(): Clients[] { + return queryAll("SELECT * FROM clients ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Clients | undefined { + return queryOne("SELECT * FROM clients WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateClientsInput): Clients { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now]; + + execute(`INSERT INTO clients (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateClientsInput): Clients | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE clients SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM clients WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/contract.ts b/.benchmark/appointment/backend/src/services/contract.ts new file mode 100644 index 0000000..7980109 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/contract.ts @@ -0,0 +1,59 @@ +// Auto-generated Contract service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Contract, CreateContractInput, UpdateContractInput } from "../types/index.js"; + +export class ContractService { + /** List all contracts */ + list(): Contract[] { + return queryAll("SELECT * FROM contracts ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Contract | undefined { + return queryOne("SELECT * FROM contracts WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateContractInput): Contract { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "party_a", "party_b", "amount", "signed_at", "expires_at", "status", "file_url", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.partyA ?? null, input.partyB ?? null, input.amount ?? null, input.signedAt ?? null, input.expiresAt ?? null, input.status ?? null, input.fileUrl ?? null, now, now]; + + execute(`INSERT INTO contracts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateContractInput): Contract | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.partyA !== undefined) { sets.push("party_a = ?"); values.push(input.partyA); } + if (input.partyB !== undefined) { sets.push("party_b = ?"); values.push(input.partyB); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.signedAt !== undefined) { sets.push("signed_at = ?"); values.push(input.signedAt); } + if (input.expiresAt !== undefined) { sets.push("expires_at = ?"); values.push(input.expiresAt); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.fileUrl !== undefined) { sets.push("file_url = ?"); values.push(input.fileUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM contracts WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/customer.ts b/.benchmark/appointment/backend/src/services/customer.ts new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/customer.ts @@ -0,0 +1,57 @@ +// Auto-generated Customer service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +export class CustomerService { + /** List all customers */ + list(): Customer[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customer | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomerInput): Customer { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "tags", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, input.tags ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomerInput): Customer | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + if (input.tags !== undefined) { sets.push("tags = ?"); values.push(input.tags); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/item.ts b/.benchmark/appointment/backend/src/services/item.ts new file mode 100644 index 0000000..24ec745 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/item.ts @@ -0,0 +1,55 @@ +// Auto-generated Item service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js"; + +export class ItemService { + /** List all items */ + list(): Item[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Item | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemInput): Item { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "title", "data", "created_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.data ?? null, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemInput): Item | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/reminder.ts b/.benchmark/appointment/backend/src/services/reminder.ts new file mode 100644 index 0000000..8c19dd5 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/reminder.ts @@ -0,0 +1,56 @@ +// Auto-generated Reminder service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Reminder, CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +export class ReminderService { + /** List all reminders */ + list(): Reminder[] { + return queryAll("SELECT * FROM reminders ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Reminder | undefined { + return queryOne("SELECT * FROM reminders WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateReminderInput): Reminder { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "remind_at", "message", "sent", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.remindAt ?? null, input.message ?? null, input.sent ?? null, now, now]; + + execute(`INSERT INTO reminders (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateReminderInput): Reminder | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.remindAt !== undefined) { sets.push("remind_at = ?"); values.push(input.remindAt); } + if (input.message !== undefined) { sets.push("message = ?"); values.push(input.message); } + if (input.sent !== undefined) { sets.push("sent = ?"); values.push(input.sent); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE reminders SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM reminders WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/service.ts b/.benchmark/appointment/backend/src/services/service.ts new file mode 100644 index 0000000..0a4b4c0 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/service.ts @@ -0,0 +1,55 @@ +// Auto-generated Services service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Services, CreateServicesInput, UpdateServicesInput } from "../types/index.js"; + +export class ServicesService { + /** List all services */ + list(): Services[] { + return queryAll("SELECT * FROM services ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Services | undefined { + return queryOne("SELECT * FROM services WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateServicesInput): Services { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "description", "duration_min", "price", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.description ?? null, input.durationMin ?? null, input.price ?? null, now, now]; + + execute(`INSERT INTO services (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateServicesInput): Services | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.durationMin !== undefined) { sets.push("duration_min = ?"); values.push(input.durationMin); } + if (input.price !== undefined) { sets.push("price = ?"); values.push(input.price); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE services SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM services WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/stat.ts b/.benchmark/appointment/backend/src/services/stat.ts new file mode 100644 index 0000000..ffdf1ec --- /dev/null +++ b/.benchmark/appointment/backend/src/services/stat.ts @@ -0,0 +1,56 @@ +// Auto-generated Stats service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Stats, CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +export class StatsService { + /** List all stats */ + list(): Stats[] { + return queryAll("SELECT * FROM stats ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Stats | undefined { + return queryOne("SELECT * FROM stats WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateStatsInput): Stats { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "client_id", "service_id", "start_time", "end_time", "notes", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.clientId ?? null, input.serviceId ?? null, input.startTime ?? null, input.endTime ?? null, input.notes ?? null, now, now]; + + execute(`INSERT INTO stats (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateStatsInput): Stats | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.clientId !== undefined) { sets.push("client_id = ?"); values.push(input.clientId); } + if (input.serviceId !== undefined) { sets.push("service_id = ?"); values.push(input.serviceId); } + if (input.startTime !== undefined) { sets.push("start_time = ?"); values.push(input.startTime); } + if (input.endTime !== undefined) { sets.push("end_time = ?"); values.push(input.endTime); } + if (input.notes !== undefined) { sets.push("notes = ?"); values.push(input.notes); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE stats SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM stats WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/user.ts b/.benchmark/appointment/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/appointment/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/types/fastify.d.ts b/.benchmark/appointment/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/appointment/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/appointment/backend/src/types/index.ts b/.benchmark/appointment/backend/src/types/index.ts new file mode 100644 index 0000000..d6ab46d --- /dev/null +++ b/.benchmark/appointment/backend/src/types/index.ts @@ -0,0 +1,322 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Services { + id: string; + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Appointments { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Clients { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateServicesInput { + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; +} + +export interface CreateAppointmentsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + notes?: string; +} + +export interface CreateClientsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface CreateStatsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + notes?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateServicesInput { + userId?: string; + name?: string; + description?: string; + durationMin?: number; + price?: number; +} + +export interface UpdateAppointmentsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime?: string; + endTime?: string; + notes?: string; +} + +export interface UpdateClientsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +export interface UpdateStatsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime?: string; + endTime?: string; + notes?: string; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/appointment/backend/src/types/sql.js.d.ts b/.benchmark/appointment/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/appointment/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/appointment/backend/tsconfig.json b/.benchmark/appointment/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/appointment/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/electron/README.md b/.benchmark/appointment/electron/README.md new file mode 100644 index 0000000..cb32bc8 --- /dev/null +++ b/.benchmark/appointment/electron/README.md @@ -0,0 +1,82 @@ +# myproject — Desktop + +> Electron desktop application wrapping the myproject fullstack web project. + +## Architecture + +``` +Web Fullstack Project + × + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** — Window management, menu, tray, IPC, auto-update +- **Preload** — Secure bridge between main and renderer +- **Renderer** — Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +├── main.ts — Main process (BrowserWindow, Menu, lifecycle) +├── preload.ts — Secure IPC bridge (contextBridge) +├── ipc.ts — IPC handlers (dialogs, store, notifications) +├── tray.ts — System tray +├── updater.ts — Auto-update (electron-updater) +└── utils.ts — Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/appointment/electron/assets/icon.png b/.benchmark/appointment/electron/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..554e8a31d1f0205fbf5ef853aba9a4fa2fc490dd GIT binary patch literal 66 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv4;KqeCd<5Tr}e}F6o MPgg&ebxsLQ09s=V>;M1& literal 0 HcmV?d00001 diff --git a/.benchmark/appointment/electron/electron-builder.yml b/.benchmark/appointment/electron/electron-builder.yml new file mode 100644 index 0000000..1ab271c --- /dev/null +++ b/.benchmark/appointment/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml — Auto-generated by SF-06 + +appId: com.myproject.desktop +productName: myproject +copyright: "Copyright © 2026 myproject" + +# ─── Directories ───────────────────────────────── +directories: + output: release + buildResources: assets + +# ─── Files ─────────────────────────────────────── +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# ─── Extra Resources ───────────────────────────── +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# ─── macOS ─────────────────────────────────────── +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# ─── Windows ───────────────────────────────────── +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: myproject + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: myproject + +# ─── Linux ─────────────────────────────────────── +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: myproject + Comment: myproject Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# ─── Auto Update ───────────────────────────────── +publish: + provider: generic + url: https://releases.example.com/myproject/ + channel: latest diff --git a/.benchmark/appointment/electron/electron/ipc.ts b/.benchmark/appointment/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/appointment/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // ── App Info ── + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // ── Window Controls ── + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // ── File Dialogs ── + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // ── Notifications ── + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // ── Shell ── + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // ── Persistent Store ── + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/appointment/electron/electron/main.ts b/.benchmark/appointment/electron/electron/main.ts new file mode 100644 index 0000000..eaab114 --- /dev/null +++ b/.benchmark/appointment/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// ─── Constants ─────────────────────────────────── +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "myproject"; + +// ─── Single Instance Lock ──────────────────────── +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// ─── State ─────────────────────────────────────── +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// ─── Create Window ─────────────────────────────── +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // ── Load content ── + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // ── Show when ready ── + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // ── External links → default browser ── + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // ── Prevent navigation away ── + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// ─── Icon Path ─────────────────────────────────── +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// ─── API Server (Production) ───────────────────── +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// ─── Menu ──────────────────────────────────────── +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// ─── App Lifecycle ─────────────────────────────── +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed — continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/appointment/electron/electron/preload.ts b/.benchmark/appointment/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/appointment/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// ─── Expose safe API to renderer ───────────────── +contextBridge.exposeInMainWorld("electronAPI", { + // ── App Info ── + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // ── Window Controls ── + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // ── File Dialogs ── + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // ── Notifications ── + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // ── Shell ── + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // ── Store ── + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // ── Updates ── + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // ── Update Events ── + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // ── Remove listener ── + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// ─── Type declarations for renderer ────────────── +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/appointment/electron/electron/tray.ts b/.benchmark/appointment/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/appointment/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/appointment/electron/electron/updater.ts b/.benchmark/appointment/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/appointment/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/appointment/electron/electron/utils.ts b/.benchmark/appointment/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/appointment/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// ─── Single Instance Lock ──────────────────────── +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// ─── Dev mode detection ────────────────────────── +export function isDev(): boolean { + return !app.isPackaged; +} + +// ─── Web URL ───────────────────────────────────── +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// ─── API Path ──────────────────────────────────── +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// ─── Get API port from env ─────────────────────── +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/appointment/electron/entitlements.mac.plist b/.benchmark/appointment/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/appointment/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/appointment/electron/package.json b/.benchmark/appointment/electron/package.json new file mode 100644 index 0000000..44bc859 --- /dev/null +++ b/.benchmark/appointment/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "myproject-desktop", + "version": "1.0.0", + "private": true, + "description": "myproject — Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "myproject", + "appId": "com.myproject.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/appointment/electron/tsconfig.json b/.benchmark/appointment/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/appointment/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/frontend/app/appointments/page.tsx b/.benchmark/appointment/frontend/app/appointments/page.tsx new file mode 100644 index 0000000..0bf4ee6 --- /dev/null +++ b/.benchmark/appointment/frontend/app/appointments/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useAppointments } from "@/hooks/useAppointments"; + +export default function PagePage() { + const { data, loading, error, refetch } = useAppointments(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

时间段预约

+

时间段预约

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/clients/page.tsx b/.benchmark/appointment/frontend/app/clients/page.tsx new file mode 100644 index 0000000..c4c750c --- /dev/null +++ b/.benchmark/appointment/frontend/app/clients/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useClients } from "@/hooks/useClients"; + +export default function PagePage() { + const { data, loading, error, refetch } = useClients(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

客户通知

+

客户通知

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/feature/page.tsx b/.benchmark/appointment/frontend/app/feature/page.tsx new file mode 100644 index 0000000..729c2d9 --- /dev/null +++ b/.benchmark/appointment/frontend/app/feature/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

功能页

+

核心功能页面

+
+ +
+ + { +

功能页 页面内容

+

路由: /feature

+
} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/globals.css b/.benchmark/appointment/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/appointment/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/appointment/frontend/app/layout.tsx b/.benchmark/appointment/frontend/app/layout.tsx new file mode 100644 index 0000000..84b9b86 --- /dev/null +++ b/.benchmark/appointment/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "MyProject", + description: "一款根据用户需求定制的应用。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/appointment/frontend/app/page.tsx b/.benchmark/appointment/frontend/app/page.tsx new file mode 100644 index 0000000..e7a78d1 --- /dev/null +++ b/.benchmark/appointment/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

MyProject

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 服务项目 + + + 📅 + 时间段预约 + + + 📝 + 客户通知 + + + 📸 + 预约统计 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 MyProject

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/appointment/frontend/app/profile/page.tsx b/.benchmark/appointment/frontend/app/profile/page.tsx new file mode 100644 index 0000000..61ddaaa --- /dev/null +++ b/.benchmark/appointment/frontend/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

我的

+

用户中心

+
+ +
+ + { +

我的 页面内容

+

路由: /profile

+
} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/services/page.tsx b/.benchmark/appointment/frontend/app/services/page.tsx new file mode 100644 index 0000000..4b3e8b6 --- /dev/null +++ b/.benchmark/appointment/frontend/app/services/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useServices } from "@/hooks/useServices"; + +export default function PagePage() { + const { data, loading, error, refetch } = useServices(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

服务项目

+

服务项目

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/stats/page.tsx b/.benchmark/appointment/frontend/app/stats/page.tsx new file mode 100644 index 0000000..3b8d151 --- /dev/null +++ b/.benchmark/appointment/frontend/app/stats/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useStats } from "@/hooks/useStats"; + +export default function PagePage() { + const { data, loading, error, refetch } = useStats(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

预约统计

+

预约统计

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/客户通知/page.tsx b/.benchmark/appointment/frontend/app/客户通知/page.tsx new file mode 100644 index 0000000..3213ba2 --- /dev/null +++ b/.benchmark/appointment/frontend/app/客户通知/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use客户通知 } from "@/hooks/use客户通知"; + +export default function PagePage() { + const { data, loading, error, refetch } = use客户通知(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

客户通知

+

客户通知

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/时间段预约/page.tsx b/.benchmark/appointment/frontend/app/时间段预约/page.tsx new file mode 100644 index 0000000..b4c9b36 --- /dev/null +++ b/.benchmark/appointment/frontend/app/时间段预约/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use时间段预约 } from "@/hooks/use时间段预约"; + +export default function PagePage() { + const { data, loading, error, refetch } = use时间段预约(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

时间段预约

+

时间段预约

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/服务项目/page.tsx b/.benchmark/appointment/frontend/app/服务项目/page.tsx new file mode 100644 index 0000000..5832ccc --- /dev/null +++ b/.benchmark/appointment/frontend/app/服务项目/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use服务项目 } from "@/hooks/use服务项目"; + +export default function PagePage() { + const { data, loading, error, refetch } = use服务项目(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

服务项目

+

服务项目

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/预约统计/page.tsx b/.benchmark/appointment/frontend/app/预约统计/page.tsx new file mode 100644 index 0000000..35fa74c --- /dev/null +++ b/.benchmark/appointment/frontend/app/预约统计/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use预约统计 } from "@/hooks/use预约统计"; + +export default function PagePage() { + const { data, loading, error, refetch } = use预约统计(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

预约统计

+

预约统计

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/components/layout/BottomNav.tsx b/.benchmark/appointment/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/appointment/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/appointment/frontend/components/layout/Sidebar.tsx b/.benchmark/appointment/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/appointment/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/appointment/frontend/components/ui/Button.tsx b/.benchmark/appointment/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/appointment/frontend/components/ui/Card.tsx b/.benchmark/appointment/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/appointment/frontend/components/ui/EmptyState.tsx b/.benchmark/appointment/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/appointment/frontend/components/ui/Input.tsx b/.benchmark/appointment/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/appointment/frontend/components/ui/Modal.tsx b/.benchmark/appointment/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/appointment/frontend/hooks/useAppointments.ts b/.benchmark/appointment/frontend/hooks/useAppointments.ts new file mode 100644 index 0000000..930eac5 --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useAppointments.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for appointments +type Appointment = Record; + +/** + * Fetch and manage appointments data. + */ +export function useAppointments() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/appointments`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useAuth.ts b/.benchmark/appointment/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useClients.ts b/.benchmark/appointment/frontend/hooks/useClients.ts new file mode 100644 index 0000000..86d98db --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useClients.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for clients +type Client = Record; + +/** + * Fetch and manage clients data. + */ +export function useClients() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/clients`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useDebounce.ts b/.benchmark/appointment/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/appointment/frontend/hooks/useForm.ts b/.benchmark/appointment/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/appointment/frontend/hooks/useHealth.ts b/.benchmark/appointment/frontend/hooks/useHealth.ts new file mode 100644 index 0000000..9aa622a --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useHealth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for health +type Health = Record; + +/** + * Fetch and manage health data. + */ +export function useHealth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/health`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useItem.ts b/.benchmark/appointment/frontend/hooks/useItem.ts new file mode 100644 index 0000000..83b19eb --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useItem.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for 预约统计 +type Item = Record; + +/** + * Fetch and manage 预约统计 data. + */ +export function useItem() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/预约统计`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useServices.ts b/.benchmark/appointment/frontend/hooks/useServices.ts new file mode 100644 index 0000000..c0bf950 --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useServices.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for services +type Service = Record; + +/** + * Fetch and manage services data. + */ +export function useServices() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/services`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useStats.ts b/.benchmark/appointment/frontend/hooks/useStats.ts new file mode 100644 index 0000000..f9f34cf --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useStats.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for stats +type Stat = Record; + +/** + * Fetch and manage stats data. + */ +export function useStats() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/stats`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useUsers.ts b/.benchmark/appointment/frontend/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/next-env.d.ts b/.benchmark/appointment/frontend/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/.benchmark/appointment/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/.benchmark/appointment/frontend/next.config.ts b/.benchmark/appointment/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/appointment/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/appointment/frontend/package.json b/.benchmark/appointment/frontend/package.json new file mode 100644 index 0000000..caa9eab --- /dev/null +++ b/.benchmark/appointment/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/frontend/postcss.config.js b/.benchmark/appointment/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/appointment/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/appointment/frontend/services/api.ts b/.benchmark/appointment/frontend/services/api.ts new file mode 100644 index 0000000..13abf7f --- /dev/null +++ b/.benchmark/appointment/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for MyProject + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/appointment/frontend/services/appointments.ts b/.benchmark/appointment/frontend/services/appointments.ts new file mode 100644 index 0000000..efbf64e --- /dev/null +++ b/.benchmark/appointment/frontend/services/appointments.ts @@ -0,0 +1,10 @@ +// Auto-generated appointments service +import { api } from "./api"; + +export function get() { + return api.get("/api/appointments"); +} + +export function post(data: Record) { + return api.post("/api/appointments", data); +} diff --git a/.benchmark/appointment/frontend/services/auth.ts b/.benchmark/appointment/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/appointment/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/appointment/frontend/services/clients.ts b/.benchmark/appointment/frontend/services/clients.ts new file mode 100644 index 0000000..43638ef --- /dev/null +++ b/.benchmark/appointment/frontend/services/clients.ts @@ -0,0 +1,10 @@ +// Auto-generated clients service +import { api } from "./api"; + +export function get() { + return api.get("/api/clients"); +} + +export function post(data: Record) { + return api.post("/api/clients", data); +} diff --git a/.benchmark/appointment/frontend/services/health.ts b/.benchmark/appointment/frontend/services/health.ts new file mode 100644 index 0000000..4fdf89e --- /dev/null +++ b/.benchmark/appointment/frontend/services/health.ts @@ -0,0 +1,6 @@ +// Auto-generated health service +import { api } from "./api"; + +export function get() { + return api.get("/api/health"); +} diff --git a/.benchmark/appointment/frontend/services/services.ts b/.benchmark/appointment/frontend/services/services.ts new file mode 100644 index 0000000..cdbe56a --- /dev/null +++ b/.benchmark/appointment/frontend/services/services.ts @@ -0,0 +1,10 @@ +// Auto-generated services service +import { api } from "./api"; + +export function get() { + return api.get("/api/services"); +} + +export function post(data: Record) { + return api.post("/api/services", data); +} diff --git a/.benchmark/appointment/frontend/services/stats.ts b/.benchmark/appointment/frontend/services/stats.ts new file mode 100644 index 0000000..e48d921 --- /dev/null +++ b/.benchmark/appointment/frontend/services/stats.ts @@ -0,0 +1,10 @@ +// Auto-generated stats service +import { api } from "./api"; + +export function get() { + return api.get("/api/stats"); +} + +export function post(data: Record) { + return api.post("/api/stats", data); +} diff --git a/.benchmark/appointment/frontend/services/users.ts b/.benchmark/appointment/frontend/services/users.ts new file mode 100644 index 0000000..03fad78 --- /dev/null +++ b/.benchmark/appointment/frontend/services/users.ts @@ -0,0 +1,10 @@ +// Auto-generated users service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/users", data); +} + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/appointment/frontend/services/客户通知.ts b/.benchmark/appointment/frontend/services/客户通知.ts new file mode 100644 index 0000000..32daa89 --- /dev/null +++ b/.benchmark/appointment/frontend/services/客户通知.ts @@ -0,0 +1,10 @@ +// Auto-generated 客户通知 service +import { api } from "./api"; + +export function get() { + return api.get("/api/客户通知"); +} + +export function post(data: Record) { + return api.post("/api/客户通知", data); +} diff --git a/.benchmark/appointment/frontend/services/时间段预约.ts b/.benchmark/appointment/frontend/services/时间段预约.ts new file mode 100644 index 0000000..277426d --- /dev/null +++ b/.benchmark/appointment/frontend/services/时间段预约.ts @@ -0,0 +1,10 @@ +// Auto-generated 时间段预约 service +import { api } from "./api"; + +export function get() { + return api.get("/api/时间段预约"); +} + +export function post(data: Record) { + return api.post("/api/时间段预约", data); +} diff --git a/.benchmark/appointment/frontend/services/服务项目.ts b/.benchmark/appointment/frontend/services/服务项目.ts new file mode 100644 index 0000000..10cd0fb --- /dev/null +++ b/.benchmark/appointment/frontend/services/服务项目.ts @@ -0,0 +1,10 @@ +// Auto-generated 服务项目 service +import { api } from "./api"; + +export function get() { + return api.get("/api/服务项目"); +} + +export function post(data: Record) { + return api.post("/api/服务项目", data); +} diff --git a/.benchmark/appointment/frontend/services/预约统计.ts b/.benchmark/appointment/frontend/services/预约统计.ts new file mode 100644 index 0000000..0206bb0 --- /dev/null +++ b/.benchmark/appointment/frontend/services/预约统计.ts @@ -0,0 +1,10 @@ +// Auto-generated 预约统计 service +import { api } from "./api"; + +export function get() { + return api.get("/api/预约统计"); +} + +export function post(data: Record) { + return api.post("/api/预约统计", data); +} diff --git a/.benchmark/appointment/frontend/tailwind.config.ts b/.benchmark/appointment/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/appointment/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/appointment/frontend/tsconfig.json b/.benchmark/appointment/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/appointment/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/frontend/types/index.ts b/.benchmark/appointment/frontend/types/index.ts new file mode 100644 index 0000000..eeef0b8 --- /dev/null +++ b/.benchmark/appointment/frontend/types/index.ts @@ -0,0 +1,132 @@ +// Auto-generated types for MyProject + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Services { + id: string; + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Appointments { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Clients { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/.gitignore b/.benchmark/appointment/fullstack/.gitignore new file mode 100644 index 0000000..7488f6e --- /dev/null +++ b/.benchmark/appointment/fullstack/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.next/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/appointment/fullstack/README.md b/.benchmark/appointment/fullstack/README.md new file mode 100644 index 0000000..18dd4a3 --- /dev/null +++ b/.benchmark/appointment/fullstack/README.md @@ -0,0 +1,125 @@ +# MyProject — Fullstack Project + +> 一款根据用户需求定制的应用。 + +## Architecture + +``` +apps/ +├── web/ # Next.js 15 + TypeScript + Tailwind CSS +└── api/ # Fastify 5 + TypeScript + SQLite (sql.js) + +packages/ +├── shared-types/ # @shared/types — shared TypeScript interfaces +└── shared-config/ # @shared/config — shared configuration +``` + +## Quick Start + +```bash +# Install all dependencies (root + workspaces) +npm install + +# Start both frontend and backend in dev mode +npm run dev + +# Or start individually +npm run dev:web # http://localhost:3000 +npm run dev:api # http://localhost:3001 +``` + +## Build + +```bash +# Build everything +npm run build + +# Or individually +npm run build:web +npm run build:api +``` + +## Testing + +```bash +# Run API tests +npm test +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/auth/me` + +### Resources +#### users +- `GET /api/users` — List +- `GET /api/users/:id` — Get +- `POST /api/users` — Create +- `PUT /api/users/:id` — Update +- `DELETE /api/users/:id` — Delete + +#### contracts +- `GET /api/contracts` — List +- `GET /api/contracts/:id` — Get +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List +- `GET /api/approvals/:id` — Get +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List +- `GET /api/customers/:id` — Get +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List +- `GET /api/reminders/:id` — Get +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### services +- `GET /api/services` — List +- `GET /api/services/:id` — Get +- `POST /api/services` — Create +- `PUT /api/services/:id` — Update +- `DELETE /api/services/:id` — Delete + +#### appointments +- `GET /api/appointments` — List +- `GET /api/appointments/:id` — Get +- `POST /api/appointments` — Create +- `PUT /api/appointments/:id` — Update +- `DELETE /api/appointments/:id` — Delete + +#### clients +- `GET /api/clients` — List +- `GET /api/clients/:id` — Get +- `POST /api/clients` — Create +- `PUT /api/clients/:id` — Update +- `DELETE /api/clients/:id` — Delete + +#### stats +- `GET /api/stats` — List +- `GET /api/stats/:id` — Get +- `POST /api/stats` — Create +- `PUT /api/stats/:id` — Update +- `DELETE /api/stats/:id` — Delete + +### System +- `GET /api/health` — Health check + +## Environment Variables + +See `.env.example` for all available configuration. diff --git a/.benchmark/appointment/fullstack/apps/api/.gitignore b/.benchmark/appointment/fullstack/apps/api/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/appointment/fullstack/apps/api/README.md b/.benchmark/appointment/fullstack/apps/api/README.md new file mode 100644 index 0000000..0c5cff6 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/README.md @@ -0,0 +1,118 @@ +# MyProject — Backend API + +> 一款根据用户需求定制的应用。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` — List all +- `GET /api/contracts/:id` — Get by ID +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List all +- `GET /api/approvals/:id` — Get by ID +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List all +- `GET /api/customers/:id` — Get by ID +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List all +- `GET /api/reminders/:id` — Get by ID +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### services +- `GET /api/services` — List all +- `GET /api/services/:id` — Get by ID +- `POST /api/services` — Create +- `PUT /api/services/:id` — Update +- `DELETE /api/services/:id` — Delete + +#### appointments +- `GET /api/appointments` — List all +- `GET /api/appointments/:id` — Get by ID +- `POST /api/appointments` — Create +- `PUT /api/appointments/:id` — Update +- `DELETE /api/appointments/:id` — Delete + +#### clients +- `GET /api/clients` — List all +- `GET /api/clients/:id` — Get by ID +- `POST /api/clients` — Create +- `PUT /api/clients/:id` — Update +- `DELETE /api/clients/:id` — Delete + +#### stats +- `GET /api/stats` — List all +- `GET /api/stats/:id` — Get by ID +- `POST /api/stats` — Create +- `PUT /api/stats/:id` — Update +- `DELETE /api/stats/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/appointment/fullstack/apps/api/package.json b/.benchmark/appointment/fullstack/apps/api/package.json new file mode 100644 index 0000000..2831924 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "myproject-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/apps/api/src/__tests__/auth.test.ts b/.benchmark/appointment/fullstack/apps/api/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/appointment/fullstack/apps/api/src/__tests__/items.test.ts b/.benchmark/appointment/fullstack/apps/api/src/__tests__/items.test.ts new file mode 100644 index 0000000..7136843 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/__tests__/items.test.ts @@ -0,0 +1,118 @@ +// Item CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/fullstack/apps/api/src/__tests__/users.test.ts b/.benchmark/appointment/fullstack/apps/api/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/fullstack/apps/api/src/db/client.ts b/.benchmark/appointment/fullstack/apps/api/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/db/schema.ts b/.benchmark/appointment/fullstack/apps/api/src/db/schema.ts new file mode 100644 index 0000000..ef6555c --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/db/schema.ts @@ -0,0 +1,20 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS services (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n description TEXT,\n duration_min INTEGER,\n price REAL,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS appointments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n client_id TEXT REFERENCES clients(id),\n service_id TEXT REFERENCES services(id),\n start_time TEXT NOT NULL,\n end_time TEXT,\n status TEXT,\n notes TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS clients (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n client_id TEXT REFERENCES clients(id),\n service_id TEXT REFERENCES services(id),\n start_time TEXT NOT NULL,\n end_time TEXT,\n status TEXT,\n notes TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/index.ts b/.benchmark/appointment/fullstack/apps/api/src/index.ts new file mode 100644 index 0000000..03855d5 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/index.ts @@ -0,0 +1,79 @@ +// MyProject — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { servicesRoutes } from "./routes/services.js"; +import { appointmentsRoutes } from "./routes/appointments.js"; +import { clientsRoutes } from "./routes/clients.js"; +import { statsRoutes } from "./routes/stats.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-c69a41fa"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(servicesRoutes, { prefix: "/api/services" }); + await app.register(appointmentsRoutes, { prefix: "/api/appointments" }); + await app.register(clientsRoutes, { prefix: "/api/clients" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/middleware/auth.ts b/.benchmark/appointment/fullstack/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/routes/auth.ts b/.benchmark/appointment/fullstack/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/routes/items.ts b/.benchmark/appointment/fullstack/apps/api/src/routes/items.ts new file mode 100644 index 0000000..2ad945e --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Item routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemService } from "../services/item.js"; +import type { CreateItemInput, UpdateItemInput } from "../types/index.js"; + +const service = new ItemService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items — create + app.post<{ Body: CreateItemInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id — update + app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/routes/users.ts b/.benchmark/appointment/fullstack/apps/api/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/services/item.ts b/.benchmark/appointment/fullstack/apps/api/src/services/item.ts new file mode 100644 index 0000000..24ec745 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/services/item.ts @@ -0,0 +1,55 @@ +// Auto-generated Item service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js"; + +export class ItemService { + /** List all items */ + list(): Item[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Item | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemInput): Item { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "title", "data", "created_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.data ?? null, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemInput): Item | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/services/user.ts b/.benchmark/appointment/fullstack/apps/api/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/types/fastify.d.ts b/.benchmark/appointment/fullstack/apps/api/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/types/index.ts b/.benchmark/appointment/fullstack/apps/api/src/types/index.ts new file mode 100644 index 0000000..1677bee --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/types/index.ts @@ -0,0 +1,223 @@ +// Import and re-export shared entity types +import type { + User, + Contract, + Approval, + Customer, + Reminder, + Services, + Appointments, + Clients, + Stats, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from "@shared/types"; + +export type { + User, + Contract, + Approval, + Customer, + Reminder, + Services, + Appointments, + Clients, + Stats, + ApiResponse, + PaginatedResponse, + ErrorResponse, +}; + +// Auto-generated types (from Model Contract) + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateServicesInput { + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; +} + +export interface CreateAppointmentsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + notes?: string; +} + +export interface CreateClientsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface CreateStatsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + notes?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateServicesInput { + userId?: string; + name?: string; + description?: string; + durationMin?: number; + price?: number; +} + +export interface UpdateAppointmentsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime?: string; + endTime?: string; + notes?: string; +} + +export interface UpdateClientsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +export interface UpdateStatsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime?: string; + endTime?: string; + notes?: string; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/types/sql.js.d.ts b/.benchmark/appointment/fullstack/apps/api/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/appointment/fullstack/apps/api/tsconfig.json b/.benchmark/appointment/fullstack/apps/api/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/apps/web/app/feature/page.tsx b/.benchmark/appointment/fullstack/apps/web/app/feature/page.tsx new file mode 100644 index 0000000..729c2d9 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/feature/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

功能页

+

核心功能页面

+
+ +
+ + { +

功能页 页面内容

+

路由: /feature

+
} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/app/globals.css b/.benchmark/appointment/fullstack/apps/web/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/appointment/fullstack/apps/web/app/layout.tsx b/.benchmark/appointment/fullstack/apps/web/app/layout.tsx new file mode 100644 index 0000000..84b9b86 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "MyProject", + description: "一款根据用户需求定制的应用。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/app/page.tsx b/.benchmark/appointment/fullstack/apps/web/app/page.tsx new file mode 100644 index 0000000..e7a78d1 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

MyProject

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 服务项目 + + + 📅 + 时间段预约 + + + 📝 + 客户通知 + + + 📸 + 预约统计 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 MyProject

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/app/profile/page.tsx b/.benchmark/appointment/fullstack/apps/web/app/profile/page.tsx new file mode 100644 index 0000000..61ddaaa --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

我的

+

用户中心

+
+ +
+ + { +

我的 页面内容

+

路由: /profile

+
} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/layout/BottomNav.tsx b/.benchmark/appointment/fullstack/apps/web/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/layout/Sidebar.tsx b/.benchmark/appointment/fullstack/apps/web/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/Button.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/Card.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/EmptyState.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/Input.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/Modal.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/hooks/useDebounce.ts b/.benchmark/appointment/fullstack/apps/web/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/appointment/fullstack/apps/web/hooks/useForm.ts b/.benchmark/appointment/fullstack/apps/web/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/appointment/fullstack/apps/web/hooks/useHealth.ts b/.benchmark/appointment/fullstack/apps/web/hooks/useHealth.ts new file mode 100644 index 0000000..9aa622a --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/hooks/useHealth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for health +type Health = Record; + +/** + * Fetch and manage health data. + */ +export function useHealth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/health`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/fullstack/apps/web/hooks/useUsers.ts b/.benchmark/appointment/fullstack/apps/web/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/fullstack/apps/web/next.config.ts b/.benchmark/appointment/fullstack/apps/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/appointment/fullstack/apps/web/package.json b/.benchmark/appointment/fullstack/apps/web/package.json new file mode 100644 index 0000000..0e5a224 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "myproject-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/apps/web/postcss.config.js b/.benchmark/appointment/fullstack/apps/web/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/appointment/fullstack/apps/web/services/api.ts b/.benchmark/appointment/fullstack/apps/web/services/api.ts new file mode 100644 index 0000000..b2e1f85 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/services/api.ts @@ -0,0 +1,49 @@ +// Auto-generated API client for MyProject + +import { API_BASE_URL, API_PREFIX } from "@shared/config"; + +const API_BASE = `${API_BASE_URL}${API_PREFIX}`; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/appointment/fullstack/apps/web/services/health.ts b/.benchmark/appointment/fullstack/apps/web/services/health.ts new file mode 100644 index 0000000..4fdf89e --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/services/health.ts @@ -0,0 +1,6 @@ +// Auto-generated health service +import { api } from "./api"; + +export function get() { + return api.get("/api/health"); +} diff --git a/.benchmark/appointment/fullstack/apps/web/services/users.ts b/.benchmark/appointment/fullstack/apps/web/services/users.ts new file mode 100644 index 0000000..03fad78 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/services/users.ts @@ -0,0 +1,10 @@ +// Auto-generated users service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/users", data); +} + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/appointment/fullstack/apps/web/tailwind.config.ts b/.benchmark/appointment/fullstack/apps/web/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/appointment/fullstack/apps/web/tsconfig.json b/.benchmark/appointment/fullstack/apps/web/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/apps/web/types/index.ts b/.benchmark/appointment/fullstack/apps/web/types/index.ts new file mode 100644 index 0000000..eeef0b8 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/types/index.ts @@ -0,0 +1,132 @@ +// Auto-generated types for MyProject + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Services { + id: string; + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Appointments { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Clients { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/package.json b/.benchmark/appointment/fullstack/package.json new file mode 100644 index 0000000..a32255b --- /dev/null +++ b/.benchmark/appointment/fullstack/package.json @@ -0,0 +1,19 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "node scripts/build.mjs", + "dev:web": "npm run dev -w apps/web", + "dev:api": "npm run dev -w apps/api", + "build:web": "npm run build -w apps/web", + "build:api": "npm run build -w apps/api", + "test": "npm run test -w apps/api", + "lint": "npm run lint -w apps/web" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/packages/shared-config/package.json b/.benchmark/appointment/fullstack/packages/shared-config/package.json new file mode 100644 index 0000000..c2f52a9 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/config", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/packages/shared-config/src/index.ts b/.benchmark/appointment/fullstack/packages/shared-config/src/index.ts new file mode 100644 index 0000000..abf84f0 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-config/src/index.ts @@ -0,0 +1,16 @@ +// Shared configuration for fullstack project + +/** API base URL — reads from env or defaults */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** API prefix for all endpoints */ +export const API_PREFIX = "/api"; + +/** Full API URL */ +export const API_URL = `${API_BASE_URL}${API_PREFIX}`; + +/** Auth token storage key */ +export const AUTH_TOKEN_KEY = "auth_token"; + +/** Default page size for paginated endpoints */ +export const DEFAULT_PAGE_SIZE = 20; diff --git a/.benchmark/appointment/fullstack/packages/shared-config/tsconfig.json b/.benchmark/appointment/fullstack/packages/shared-config/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/packages/shared-types/package.json b/.benchmark/appointment/fullstack/packages/shared-types/package.json new file mode 100644 index 0000000..d7fb84c --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/types", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/packages/shared-types/src/index.ts b/.benchmark/appointment/fullstack/packages/shared-types/src/index.ts new file mode 100644 index 0000000..5b2cca5 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-types/src/index.ts @@ -0,0 +1,134 @@ +// Shared types for fullstack project +// Auto-generated — used by both apps/web and apps/api + +// ─── Entities ──────────────────────────────────────── +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Services { + id: string; + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Appointments { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Clients { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +// ─── API Response Wrappers ─────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} diff --git a/.benchmark/appointment/fullstack/packages/shared-types/tsconfig.json b/.benchmark/appointment/fullstack/packages/shared-types/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/scripts/build.mjs b/.benchmark/appointment/fullstack/scripts/build.mjs new file mode 100644 index 0000000..84fa093 --- /dev/null +++ b/.benchmark/appointment/fullstack/scripts/build.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Build script — builds both web and api. + * Usage: node scripts/build.mjs + */ + +import { execSync } from "node:child_process"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function run(cmd, cwd) { + console.log(`\n🔨 ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("🏗️ Building fullstack project...\n"); + +try { + run("npm run build", `${ROOT}/apps/api`); + run("npm run build", `${ROOT}/apps/web`); + console.log("\n✅ Build complete!"); +} catch (e) { + console.error("\n❌ Build failed:", e.message); + process.exit(1); +} diff --git a/.benchmark/appointment/fullstack/scripts/dev.mjs b/.benchmark/appointment/fullstack/scripts/dev.mjs new file mode 100644 index 0000000..dcb8118 --- /dev/null +++ b/.benchmark/appointment/fullstack/scripts/dev.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +/** + * Dev script — starts both web and api in parallel. + * Usage: node scripts/dev.mjs + */ + +import { spawn } from "node:child_process"; + +function start(name, command, args, cwd) { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + env: { ...process.env, FORCE_COLOR: "1" }, + }); + child.on("error", (err) => console.error(`[${name}] Failed: ${err.message}`)); + child.on("exit", (code) => { + if (code !== 0 && code !== null) console.error(`[${name}] Exited with code ${code}`); + }); + return child; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +console.log("🚀 Starting fullstack dev servers...\n"); + +const api = start("api", "npm", ["run", "dev"], `${ROOT}/apps/api`); +const web = start("web", "npm", ["run", "dev"], `${ROOT}/apps/web`); + +process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); }); +process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); }); diff --git a/.benchmark/appointment/prd.json b/.benchmark/appointment/prd.json new file mode 100644 index 0000000..21997ce --- /dev/null +++ b/.benchmark/appointment/prd.json @@ -0,0 +1 @@ +{"projectName":"MyProject","chineseName":"通用应用","domain":"generic","matchConfidence":"low","summary":"一款根据用户需求定制的应用。","personas":[{"name":"用户","description":"目标用户群体","painPoints":["待明确"]}],"userStories":[{"id":"US-001","as":"用户","want":"服务项目","soThat":"解决痛点:待明确","priority":"P0"},{"id":"US-002","as":"用户","want":"时间段预约","soThat":"解决痛点:待明确","priority":"P0"},{"id":"US-003","as":"用户","want":"客户通知","soThat":"解决痛点:待明确","priority":"P0"}],"mvpScope":{"description":"MVP 聚焦 服务项目、时间段预约、客户通知、预约统计,包含页面:首页、服务项目、时间段预约","features":["服务项目","时间段预约","客户通知","预约统计"],"pages":["首页","服务项目","时间段预约"],"estimatedWeeks":3},"features":[{"name":"服务项目","description":"服务项目","priority":"P0"},{"name":"时间段预约","description":"时间段预约","priority":"P0"},{"name":"客户通知","description":"客户通知","priority":"P0"},{"name":"预约统计","description":"预约统计","priority":"P0"}],"pages":[{"name":"首页","route":"/home","description":"应用首页"},{"name":"服务项目","route":"/services","description":"服务项目"},{"name":"时间段预约","route":"/appointments","description":"时间段预约"},{"name":"客户通知","route":"/clients","description":"客户通知"},{"name":"预约统计","route":"/stats","description":"预约统计"}],"apiRequirements":[{"method":"GET","path":"/api/services","description":"获取服务项目列表"},{"method":"POST","path":"/api/services","description":"创建服务项目"},{"method":"GET","path":"/api/appointments","description":"获取时间段预约列表"},{"method":"POST","path":"/api/appointments","description":"创建时间段预约"},{"method":"GET","path":"/api/clients","description":"获取客户通知列表"},{"method":"POST","path":"/api/clients","description":"创建客户通知"},{"method":"GET","path":"/api/stats","description":"获取预约统计列表"},{"method":"POST","path":"/api/stats","description":"创建预约统计"},{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter(跨平台)","considerations":["建议跨平台框架减少开发成本","需对接推送服务(APNs / FCM / 个推)"]},"extraFeatures":["push-notification"],"devTasks":[{"id":"T-001","title":"项目脚手架搭建","description":"初始化 MyProject 项目结构,配置构建工具和 CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"数据库设计与初始化","description":"设计数据模型,创建数据库迁移脚本","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"页面开发:首页","description":"开发 首页 页面(/home):应用首页","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"页面开发:服务项目","description":"开发 服务项目 页面(/services):服务项目","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"页面开发:时间段预约","description":"开发 时间段预约 页面(/appointments):时间段预约","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"页面开发:客户通知","description":"开发 客户通知 页面(/clients):客户通知","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API 开发:GET /api/services","description":"获取服务项目列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API 开发:POST /api/services","description":"创建服务项目","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API 开发:GET /api/appointments","description":"获取时间段预约列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API 开发:POST /api/appointments","description":"创建时间段预约","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"前后端联调","description":"前端页面与后端 API 集成联调","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"测试与修复","description":"功能测试、Bug 修复、性能优化","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T22:08:37.795Z","inputLength":32,"domainMatchCount":0,"extractedFeatureCount":4,"extractedPageCount":5,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/appointment/release/release/README.md b/.benchmark/appointment/release/release/README.md new file mode 100644 index 0000000..e41b6d8 --- /dev/null +++ b/.benchmark/appointment/release/release/README.md @@ -0,0 +1,33 @@ +# myproject — Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T22:08:38.148Z + +## Directory Structure + +``` +release/ +├── version.json — Version metadata +├── build-info.json — Build environment info +├── README.md — This file +├── manifests/ +│ └── manifest.json — Release manifest with file hashes +├── checksums/ +│ └── checksums.txt — SHA256 checksums for verification +├── release-notes/ +│ └── release-notes.md — Human-readable release notes +├── windows/ — Windows installers +├── macos/ — macOS disk images +└── linux/ — Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/appointment/release/release/build-info.json b/.benchmark/appointment/release/release/build-info.json new file mode 100644 index 0000000..d15a6b8 --- /dev/null +++ b/.benchmark/appointment/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T22:08:38.148Z", + "domain": "unknown", + "entityCount": 11, + "routeCount": 1, + "screenCount": 10, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "myproject", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/appointment/release/release/checksums/checksums.txt b/.benchmark/appointment/release/release/checksums/checksums.txt new file mode 100644 index 0000000..4829db1 --- /dev/null +++ b/.benchmark/appointment/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +2655f13daf610abdd377aaecf56a7e5998b154d3fc7796c5c7c79ccec520fb81 windows/myproject-setup-0.1.0.exe +f67e8b1cf8423a05fa9e50cc34301b92599c85b5c3db83c5203b6afad6ab219b windows/myproject-0.1.0-portable.exe +1a44d99066ab124ad4662f5b1b1097633670eb2546effdaad54cc2b56517c7c4 macos/myproject-0.1.0.dmg +6fa1c4cca0c3760eeebe9824e66eeee02847430194a8daf68797279c2d11ab81 macos/myproject-0.1.0-arm64.dmg +fbc3f5117a66cb12131b35a5c8cf60c0615cc31cbc4a5a46c58a3e244b77369f linux/myproject-0.1.0.AppImage +8412b2ffc545a1036a7a60f0207e4345293ed3a7c2c5a81a5cd94798e0302f9e linux/myproject_0.1.0_amd64.deb diff --git a/.benchmark/appointment/release/release/linux/myproject-0.1.0.AppImage b/.benchmark/appointment/release/release/linux/myproject-0.1.0.AppImage new file mode 100644 index 0000000..17ff5b8 --- /dev/null +++ b/.benchmark/appointment/release/release/linux/myproject-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/linux/myproject_0.1.0_amd64.deb b/.benchmark/appointment/release/release/linux/myproject_0.1.0_amd64.deb new file mode 100644 index 0000000..82f71b2 --- /dev/null +++ b/.benchmark/appointment/release/release/linux/myproject_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/macos/myproject-0.1.0-arm64.dmg b/.benchmark/appointment/release/release/macos/myproject-0.1.0-arm64.dmg new file mode 100644 index 0000000..b3f27bc --- /dev/null +++ b/.benchmark/appointment/release/release/macos/myproject-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/macos/myproject-0.1.0.dmg b/.benchmark/appointment/release/release/macos/myproject-0.1.0.dmg new file mode 100644 index 0000000..5c61779 --- /dev/null +++ b/.benchmark/appointment/release/release/macos/myproject-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/manifests/manifest.json b/.benchmark/appointment/release/release/manifests/manifest.json new file mode 100644 index 0000000..77aabfc --- /dev/null +++ b/.benchmark/appointment/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "myproject", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T22:08:38.148Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/myproject-setup-0.1.0.exe", + "size": 144, + "sha256": "2655f13daf610abdd377aaecf56a7e5998b154d3fc7796c5c7c79ccec520fb81" + }, + { + "path": "windows/myproject-0.1.0-portable.exe", + "size": 148, + "sha256": "f67e8b1cf8423a05fa9e50cc34301b92599c85b5c3db83c5203b6afad6ab219b" + }, + { + "path": "macos/myproject-0.1.0.dmg", + "size": 140, + "sha256": "1a44d99066ab124ad4662f5b1b1097633670eb2546effdaad54cc2b56517c7c4" + }, + { + "path": "macos/myproject-0.1.0-arm64.dmg", + "size": 142, + "sha256": "6fa1c4cca0c3760eeebe9824e66eeee02847430194a8daf68797279c2d11ab81" + }, + { + "path": "linux/myproject-0.1.0.AppImage", + "size": 133, + "sha256": "fbc3f5117a66cb12131b35a5c8cf60c0615cc31cbc4a5a46c58a3e244b77369f" + }, + { + "path": "linux/myproject_0.1.0_amd64.deb", + "size": 143, + "sha256": "8412b2ffc545a1036a7a60f0207e4345293ed3a7c2c5a81a5cd94798e0302f9e" + } + ], + "totalFiles": 6, + "totalSize": 850 +} \ No newline at end of file diff --git a/.benchmark/appointment/release/release/release-notes/release-notes.md b/.benchmark/appointment/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..10715a8 --- /dev/null +++ b/.benchmark/appointment/release/release/release-notes/release-notes.md @@ -0,0 +1,60 @@ +# myproject v0.1.0 + +> Release Builder Agent — SF-07 +> Generated: 2026-06-05 22:08:38 UTC + +## Version + +- **Name:** myproject +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- appointments +- approvals +- auth +- clients +- contracts +- customers +- items +- reminders +- services +- stats +- users + +## Build Info + +- **Build Time:** 2026-06-05T22:08:38.148Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- ✅ windows +- ✅ macos +- ✅ linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/appointment/release/release/version.json b/.benchmark/appointment/release/release/version.json new file mode 100644 index 0000000..3a7e2b5 --- /dev/null +++ b/.benchmark/appointment/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "myproject", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/release/release/windows/myproject-0.1.0-portable.exe b/.benchmark/appointment/release/release/windows/myproject-0.1.0-portable.exe new file mode 100644 index 0000000..077132d --- /dev/null +++ b/.benchmark/appointment/release/release/windows/myproject-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/windows/myproject-setup-0.1.0.exe b/.benchmark/appointment/release/release/windows/myproject-setup-0.1.0.exe new file mode 100644 index 0000000..4bbe350 --- /dev/null +++ b/.benchmark/appointment/release/release/windows/myproject-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/asset/arch.json b/.benchmark/asset/arch.json new file mode 100644 index 0000000..9e2b44f --- /dev/null +++ b/.benchmark/asset/arch.json @@ -0,0 +1 @@ +{"projectName":"MyProject","domain":"generic","contract":{"projectName":"MyProject","domain":"generic","entities":[{"name":"User","table":"users","description":"系统用户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"hasMany","entity":"contracts","via":"user_id"},{"type":"hasMany","entity":"customers","via":"user_id"}]},{"name":"Contract","table":"contracts","description":"合同","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","defaultValue":"draft","enum":["draft","pending","active","expired","terminated"]},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Approval","table":"approvals","description":"审批流程","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"applicant_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"status","type":"VARCHAR(32)","defaultValue":"pending","enum":["pending","approved","rejected"]},{"name":"form_data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"users","via":"applicant_id","as":"applicant"}]},{"name":"Customer","table":"customers","description":"客户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","defaultValue":"[]"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Reminder","table":"reminders","description":"到期提醒","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"remind_at","type":"TIMESTAMP","required":true},{"name":"message","type":"TEXT"},{"name":"sent","type":"BOOLEAN","defaultValue":"false"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Assets","table":"assets","description":"资产登记表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true,"isPrimary":false,"isAuto":false},{"name":"category","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"purchase_date","type":"DATE","required":false,"isPrimary":false,"isAuto":false},{"name":"purchase_price","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"current_value","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"location","type":"VARCHAR(128)","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Items","table":"items","description":"领用归还表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"data","type":"JSONB","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Stats","table":"stats","description":"盘点统计表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"metric","type":"VARCHAR(64)","required":true,"isPrimary":false,"isAuto":false},{"name":"value","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"period","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":false},{"name":"recorded_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","remind_at":"remindAt","purchase_date":"purchaseDate","purchase_price":"purchasePrice","current_value":"currentValue","recorded_at":"recordedAt","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","remindAt":"remind_at","purchaseDate":"purchase_date","purchasePrice":"purchase_price","currentValue":"current_value","recordedAt":"recorded_at","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Contract":{"user_id":{"required":true},"title":{"required":true},"status":{"enum":["draft","pending","active","expired","terminated"],"defaultValue":"draft"}},"Approval":{"user_id":{"required":true},"entity_type":{"required":true},"applicant_id":{"required":true},"status":{"enum":["pending","approved","rejected"],"defaultValue":"pending"}},"Customer":{"user_id":{"required":true},"name":{"required":true},"tags":{"defaultValue":"[]"}},"Reminder":{"user_id":{"required":true},"entity_type":{"required":true},"remind_at":{"required":true},"sent":{"defaultValue":"false"}},"Assets":{"id":{"required":true},"name":{"required":true}},"Items":{"id":{"required":true},"title":{"required":true}},"Stats":{"id":{"required":true},"metric":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO(文件存储)","deployment":"Docker Compose + Nginx / K8s(规模化)","considerations":[]},"architectureDiagram":"┌─────────────────────────────────────────────────────────┐\n│ MyProject — System Architecture │\n├─────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────────────┐ ┌──────────────────────┐ │\n│ │ Client Layer │ │ Admin / Web │ │\n│ │ React Native 0.76 + Expo / Flutter 3.x │ │ React + Vite (Web) │ │\n│ └──────────┬───────────┘ └──────────┬───────────┘ │\n│ │ │ │\n│ └──────────┬────────────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ API Gateway │ │\n│ │ Nginx / Kong │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ Backend Services │ │\n│ │ NestJS + Prisma │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌───────────────┼───────────────┐ │\n│ │ │ │ │\n│ ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ │\n│ │ PostgreSQL 15│ │ Redis Cache │ │ MinIO │ │\n│ │ Primary │ │ Session │ │ Files │ │\n│ └───────────┘ └─────────────┘ └──────────┘ │\n│ │\n├─────────────────────────────────────────────────────────┤\n│ Modules: │\n│ 1 资产登记 │\n│ 2 领用归还 │\n│ 3 折旧计算 │\n│ 4 Analytics │\n│ │\n└─────────────────────────────────────────────────────────┘","dataFlows":[{"page":"资产登记","route":"/assets","description":"资产登记","dataFlow":["GET /api/assets → 获取资产登记列表","POST /api/assets → 创建资产登记"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"领用归还","route":"/items","description":"领用归还","dataFlow":["GET /api/items → 获取领用归还列表","POST /api/items → 创建领用归还"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"折旧计算","route":"/items","description":"折旧计算","dataFlow":["GET /api/items → 获取领用归还列表","POST /api/items → 创建领用归还"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"盘点统计","route":"/stats","description":"盘点统计","dataFlow":["GET /api/stats → 获取盘点统计列表","POST /api/stats → 创建盘点统计"],"direction":"Client → API Gateway → Backend → Database → Response → Client"}],"modules":[{"name":"资产登记","label":"资产登记","features":["资产登记"],"responsibilities":["提供 资产登记 相关功能"]},{"name":"领用归还","label":"领用归还","features":["领用归还"],"responsibilities":["提供 领用归还 相关功能"]},{"name":"折旧计算","label":"折旧计算","features":["折旧计算"],"responsibilities":["提供 折旧计算 相关功能"]},{"name":"analytics","label":"Analytics","features":["盘点统计"],"responsibilities":["提供 盘点统计 相关功能"]}],"databaseSchema":[{"table":"users","description":"用户表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"assets","description":"资产登记表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"category","type":"VARCHAR(64)"},{"name":"purchase_date","type":"DATE"},{"name":"purchase_price","type":"DECIMAL(12,2)"},{"name":"current_value","type":"DECIMAL(12,2)"},{"name":"location","type":"VARCHAR(128)"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'available'"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_assets_user ON assets(user_id)"]},{"table":"items","description":"领用归还表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'active'"},{"name":"data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_items_user ON items(user_id)"]},{"table":"stats","description":"盘点统计表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"metric","type":"VARCHAR(64)","constraints":"NOT NULL"},{"name":"value","type":"DECIMAL(12,2)"},{"name":"period","type":"VARCHAR(32)"},{"name":"recorded_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_stats_user ON stats(user_id)"]}],"apiDesign":[{"resource":"assets","basePath":"/api/assets","endpoints":[{"method":"GET","path":"/api/assets","description":"获取资产登记列表"},{"method":"POST","path":"/api/assets","description":"创建资产登记"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}]},{"resource":"items","basePath":"/api/items","endpoints":[{"method":"GET","path":"/api/items","description":"获取领用归还列表"},{"method":"POST","path":"/api/items","description":"创建领用归还"},{"method":"GET","path":"/api/items","description":"获取折旧计算列表"},{"method":"POST","path":"/api/items","description":"创建折旧计算"}]},{"resource":"stats","basePath":"/api/stats","endpoints":[{"method":"GET","path":"/api/stats","description":"获取盘点统计列表"},{"method":"POST","path":"/api/stats","description":"创建盘点统计"}]}],"directoryStructure":["myproject/","├── apps/","│ ├── mobile/ # React Native / Flutter 移动端","│ │ ├── src/","│ │ │ ├── screens/ # 页面组件","│ │ │ │ ├── 资产登记/","│ │ │ │ ├── 领用归还/","│ │ │ │ ├── 折旧计算/","│ │ │ │ ├── analytics/","│ │ │ ├── components/ # 通用组件","│ │ │ ├── hooks/ # 自定义 Hooks","│ │ │ ├── services/ # API 调用层","│ │ │ ├── store/ # 状态管理","│ │ │ └── utils/ # 工具函数","│ │ ├── app.json","│ │ └── package.json","│ └── web/ # Web 管理后台","│ ├── src/","│ │ ├── pages/","│ │ ├── components/","│ │ └── layouts/","│ └── package.json","├── packages/","│ └── shared/ # 共享类型/常量","├── server/ # NestJS 后端服务","│ ├── src/","│ │ ├── modules/ # 业务模块","│ │ │ ├── 资产登记/","│ │ │ │ ├── 资产登记.controller.ts","│ │ │ │ ├── 资产登记.service.ts","│ │ │ │ ├── 资产登记.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 领用归还/","│ │ │ │ ├── 领用归还.controller.ts","│ │ │ │ ├── 领用归还.service.ts","│ │ │ │ ├── 领用归还.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 折旧计算/","│ │ │ │ ├── 折旧计算.controller.ts","│ │ │ │ ├── 折旧计算.service.ts","│ │ │ │ ├── 折旧计算.module.ts","│ │ │ │ └── dto/","│ │ │ ├── analytics/","│ │ │ │ ├── analytics.controller.ts","│ │ │ │ ├── analytics.service.ts","│ │ │ │ ├── analytics.module.ts","│ │ │ │ └── dto/","│ │ ├── common/ # 通用(guards, filters, interceptors)","│ │ ├── prisma/ # Prisma ORM","│ │ │ └── schema.prisma","│ │ ├── config/ # 环境配置","│ │ └── main.ts","│ ├── test/","│ ├── Dockerfile","│ └── package.json","├── docker-compose.yml","├── .github/workflows/ # CI/CD","│ └── ci.yml","├── .env.example","├── README.md","└── turbo.json # Monorepo 配置"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s(规模化)","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions → Build → Test → Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:37.042Z","sourceDomain":"generic","moduleCount":4,"tableCount":4,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/asset/backend/.gitignore b/.benchmark/asset/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/asset/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/asset/backend/README.md b/.benchmark/asset/backend/README.md new file mode 100644 index 0000000..8983604 --- /dev/null +++ b/.benchmark/asset/backend/README.md @@ -0,0 +1,111 @@ +# MyProject — Backend API + +> 一款根据用户需求定制的应用。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` — List all +- `GET /api/contracts/:id` — Get by ID +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List all +- `GET /api/approvals/:id` — Get by ID +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List all +- `GET /api/customers/:id` — Get by ID +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List all +- `GET /api/reminders/:id` — Get by ID +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### assets +- `GET /api/assets` — List all +- `GET /api/assets/:id` — Get by ID +- `POST /api/assets` — Create +- `PUT /api/assets/:id` — Update +- `DELETE /api/assets/:id` — Delete + +#### items +- `GET /api/items` — List all +- `GET /api/items/:id` — Get by ID +- `POST /api/items` — Create +- `PUT /api/items/:id` — Update +- `DELETE /api/items/:id` — Delete + +#### stats +- `GET /api/stats` — List all +- `GET /api/stats/:id` — Get by ID +- `POST /api/stats` — Create +- `PUT /api/stats/:id` — Update +- `DELETE /api/stats/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/asset/backend/package.json b/.benchmark/asset/backend/package.json new file mode 100644 index 0000000..e3106a0 --- /dev/null +++ b/.benchmark/asset/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/asset/backend/src/__tests__/approvals.test.ts b/.benchmark/asset/backend/src/__tests__/approvals.test.ts new file mode 100644 index 0000000..392316d --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/approvals.test.ts @@ -0,0 +1,121 @@ +// Approval CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "applicantId": regRes.json().user.id, + "status": "sample-status", + "formData": "sample-formdata" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/approvals", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/approvals", () => { + it("creates a approval", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/approvals/:id", () => { + it("returns the created approval", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/approvals/:id", () => { + it("updates the approval", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/approvals/:id", () => { + it("deletes the approval", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/assets.test.ts b/.benchmark/asset/backend/src/__tests__/assets.test.ts new file mode 100644 index 0000000..5e3205f --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/assets.test.ts @@ -0,0 +1,122 @@ +// Assets CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "category": "sample-category", + "purchaseDate": "sample-purchasedate", + "purchasePrice": 1, + "currentValue": 1, + "location": "sample-location" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/assets", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/assets", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/assets", () => { + it("creates a asset", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/assets", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/assets/:id", () => { + it("returns the created asset", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assets/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assets/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/assets/:id", () => { + it("updates the asset", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/assets/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/assets/:id", () => { + it("deletes the asset", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/assets/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assets/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/auth.test.ts b/.benchmark/asset/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/contracts.test.ts b/.benchmark/asset/backend/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..c2e9f16 --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/contracts.test.ts @@ -0,0 +1,124 @@ +// Contract CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "partyA": "sample-partya", + "partyB": "sample-partyb", + "amount": 1, + "signedAt": "sample-signedat", + "expiresAt": "sample-expiresat", + "status": "sample-status", + "fileUrl": "sample-fileurl" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/contracts", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/contracts", () => { + it("creates a contract", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/contracts/:id", () => { + it("returns the created contract", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/contracts/:id", () => { + it("updates the contract", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/contracts/:id", () => { + it("deletes the contract", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/customers.test.ts b/.benchmark/asset/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..f3112fa --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/customers.test.ts @@ -0,0 +1,122 @@ +// Customer CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source", + "tags": "sample-tags" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/items.test.ts b/.benchmark/asset/backend/src/__tests__/items.test.ts new file mode 100644 index 0000000..ff42fbb --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/items.test.ts @@ -0,0 +1,119 @@ +// Items CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "data": "sample-data" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/reminders.test.ts b/.benchmark/asset/backend/src/__tests__/reminders.test.ts new file mode 100644 index 0000000..c573030 --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/reminders.test.ts @@ -0,0 +1,121 @@ +// Reminder CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "remindAt": "sample-remindat", + "message": "sample-message", + "sent": true + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/reminders", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/reminders", () => { + it("creates a reminder", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/reminders/:id", () => { + it("returns the created reminder", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/reminders/:id", () => { + it("updates the reminder", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/reminders/:id", () => { + it("deletes the reminder", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/stats.test.ts b/.benchmark/asset/backend/src/__tests__/stats.test.ts new file mode 100644 index 0000000..1b64b6d --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/stats.test.ts @@ -0,0 +1,119 @@ +// Stats CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "metric": "sample-metric", + "value": 1, + "period": "sample-period" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/stats", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/stats", () => { + it("creates a stat", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/stats/:id", () => { + it("returns the created stat", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/stats/:id", () => { + it("updates the stat", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/stats/:id", () => { + it("deletes the stat", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/users.test.ts b/.benchmark/asset/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/db/client.ts b/.benchmark/asset/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/asset/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/asset/backend/src/db/schema.ts b/.benchmark/asset/backend/src/db/schema.ts new file mode 100644 index 0000000..425ac3a --- /dev/null +++ b/.benchmark/asset/backend/src/db/schema.ts @@ -0,0 +1,19 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS assets (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n category TEXT,\n purchase_date TEXT,\n purchase_price REAL,\n current_value REAL,\n location TEXT,\n status TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS items (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n metric TEXT NOT NULL,\n value REAL,\n period TEXT,\n recorded_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/asset/backend/src/index.ts b/.benchmark/asset/backend/src/index.ts new file mode 100644 index 0000000..a72a7fc --- /dev/null +++ b/.benchmark/asset/backend/src/index.ts @@ -0,0 +1,77 @@ +// MyProject — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { assetsRoutes } from "./routes/assets.js"; +import { itemsRoutes } from "./routes/items.js"; +import { statsRoutes } from "./routes/stats.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-a91d83e4"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(assetsRoutes, { prefix: "/api/assets" }); + await app.register(itemsRoutes, { prefix: "/api/items" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/asset/backend/src/middleware/auth.ts b/.benchmark/asset/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/asset/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/asset/backend/src/routes/approvals.ts b/.benchmark/asset/backend/src/routes/approvals.ts new file mode 100644 index 0000000..3a974d2 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/approvals.ts @@ -0,0 +1,51 @@ +// Auto-generated Approval routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ApprovalService } from "../services/approval.js"; +import type { CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +const service = new ApprovalService(); + +export async function approvalsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/approvals — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/approvals/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/approvals — create + app.post<{ Body: CreateApprovalInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/approvals/:id — update + app.put<{ Params: { id: string }; Body: UpdateApprovalInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/approvals/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/assets.ts b/.benchmark/asset/backend/src/routes/assets.ts new file mode 100644 index 0000000..7c19598 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/assets.ts @@ -0,0 +1,51 @@ +// Auto-generated Assets routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { AssetsService } from "../services/asset.js"; +import type { CreateAssetsInput, UpdateAssetsInput } from "../types/index.js"; + +const service = new AssetsService(); + +export async function assetsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/assets — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/assets/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Assets not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/assets — create + app.post<{ Body: CreateAssetsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/assets/:id — update + app.put<{ Params: { id: string }; Body: UpdateAssetsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Assets not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/assets/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Assets not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/auth.ts b/.benchmark/asset/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/asset/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/asset/backend/src/routes/contracts.ts b/.benchmark/asset/backend/src/routes/contracts.ts new file mode 100644 index 0000000..ee83abf --- /dev/null +++ b/.benchmark/asset/backend/src/routes/contracts.ts @@ -0,0 +1,51 @@ +// Auto-generated Contract routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ContractService } from "../services/contract.js"; +import type { CreateContractInput, UpdateContractInput } from "../types/index.js"; + +const service = new ContractService(); + +export async function contractsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/contracts — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/contracts/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/contracts — create + app.post<{ Body: CreateContractInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/contracts/:id — update + app.put<{ Params: { id: string }; Body: UpdateContractInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/contracts/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/customers.ts b/.benchmark/asset/backend/src/routes/customers.ts new file mode 100644 index 0000000..9a6f981 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customer routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomerService } from "../services/customer.js"; +import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +const service = new CustomerService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers — create + app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id — update + app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/items.ts b/.benchmark/asset/backend/src/routes/items.ts new file mode 100644 index 0000000..a649fff --- /dev/null +++ b/.benchmark/asset/backend/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Items routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemsService } from "../services/item.js"; +import type { CreateItemsInput, UpdateItemsInput } from "../types/index.js"; + +const service = new ItemsService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items — create + app.post<{ Body: CreateItemsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id — update + app.put<{ Params: { id: string }; Body: UpdateItemsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/reminders.ts b/.benchmark/asset/backend/src/routes/reminders.ts new file mode 100644 index 0000000..ee00754 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/reminders.ts @@ -0,0 +1,51 @@ +// Auto-generated Reminder routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ReminderService } from "../services/reminder.js"; +import type { CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +const service = new ReminderService(); + +export async function remindersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/reminders — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/reminders/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/reminders — create + app.post<{ Body: CreateReminderInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/reminders/:id — update + app.put<{ Params: { id: string }; Body: UpdateReminderInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/reminders/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/stats.ts b/.benchmark/asset/backend/src/routes/stats.ts new file mode 100644 index 0000000..92f64ea --- /dev/null +++ b/.benchmark/asset/backend/src/routes/stats.ts @@ -0,0 +1,51 @@ +// Auto-generated Stats routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { StatsService } from "../services/stat.js"; +import type { CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +const service = new StatsService(); + +export async function statsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/stats — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/stats/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/stats — create + app.post<{ Body: CreateStatsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/stats/:id — update + app.put<{ Params: { id: string }; Body: UpdateStatsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/stats/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/users.ts b/.benchmark/asset/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/services/approval.ts b/.benchmark/asset/backend/src/services/approval.ts new file mode 100644 index 0000000..f40b2a2 --- /dev/null +++ b/.benchmark/asset/backend/src/services/approval.ts @@ -0,0 +1,56 @@ +// Auto-generated Approval service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Approval, CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +export class ApprovalService { + /** List all approvals */ + list(): Approval[] { + return queryAll("SELECT * FROM approvals ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Approval | undefined { + return queryOne("SELECT * FROM approvals WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateApprovalInput): Approval { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "applicant_id", "status", "form_data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.applicantId ?? null, input.status ?? null, input.formData ?? null, now, now]; + + execute(`INSERT INTO approvals (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateApprovalInput): Approval | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.applicantId !== undefined) { sets.push("applicant_id = ?"); values.push(input.applicantId); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.formData !== undefined) { sets.push("form_data = ?"); values.push(input.formData); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE approvals SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM approvals WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/asset.ts b/.benchmark/asset/backend/src/services/asset.ts new file mode 100644 index 0000000..4727aea --- /dev/null +++ b/.benchmark/asset/backend/src/services/asset.ts @@ -0,0 +1,57 @@ +// Auto-generated Assets service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Assets, CreateAssetsInput, UpdateAssetsInput } from "../types/index.js"; + +export class AssetsService { + /** List all assets */ + list(): Assets[] { + return queryAll("SELECT * FROM assets ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Assets | undefined { + return queryOne("SELECT * FROM assets WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateAssetsInput): Assets { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "category", "purchase_date", "purchase_price", "current_value", "location", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.category ?? null, input.purchaseDate ?? null, input.purchasePrice ?? null, input.currentValue ?? null, input.location ?? null, now, now]; + + execute(`INSERT INTO assets (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateAssetsInput): Assets | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.category !== undefined) { sets.push("category = ?"); values.push(input.category); } + if (input.purchaseDate !== undefined) { sets.push("purchase_date = ?"); values.push(input.purchaseDate); } + if (input.purchasePrice !== undefined) { sets.push("purchase_price = ?"); values.push(input.purchasePrice); } + if (input.currentValue !== undefined) { sets.push("current_value = ?"); values.push(input.currentValue); } + if (input.location !== undefined) { sets.push("location = ?"); values.push(input.location); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE assets SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM assets WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/contract.ts b/.benchmark/asset/backend/src/services/contract.ts new file mode 100644 index 0000000..7980109 --- /dev/null +++ b/.benchmark/asset/backend/src/services/contract.ts @@ -0,0 +1,59 @@ +// Auto-generated Contract service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Contract, CreateContractInput, UpdateContractInput } from "../types/index.js"; + +export class ContractService { + /** List all contracts */ + list(): Contract[] { + return queryAll("SELECT * FROM contracts ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Contract | undefined { + return queryOne("SELECT * FROM contracts WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateContractInput): Contract { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "party_a", "party_b", "amount", "signed_at", "expires_at", "status", "file_url", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.partyA ?? null, input.partyB ?? null, input.amount ?? null, input.signedAt ?? null, input.expiresAt ?? null, input.status ?? null, input.fileUrl ?? null, now, now]; + + execute(`INSERT INTO contracts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateContractInput): Contract | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.partyA !== undefined) { sets.push("party_a = ?"); values.push(input.partyA); } + if (input.partyB !== undefined) { sets.push("party_b = ?"); values.push(input.partyB); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.signedAt !== undefined) { sets.push("signed_at = ?"); values.push(input.signedAt); } + if (input.expiresAt !== undefined) { sets.push("expires_at = ?"); values.push(input.expiresAt); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.fileUrl !== undefined) { sets.push("file_url = ?"); values.push(input.fileUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM contracts WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/customer.ts b/.benchmark/asset/backend/src/services/customer.ts new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/.benchmark/asset/backend/src/services/customer.ts @@ -0,0 +1,57 @@ +// Auto-generated Customer service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +export class CustomerService { + /** List all customers */ + list(): Customer[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customer | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomerInput): Customer { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "tags", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, input.tags ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomerInput): Customer | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + if (input.tags !== undefined) { sets.push("tags = ?"); values.push(input.tags); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/item.ts b/.benchmark/asset/backend/src/services/item.ts new file mode 100644 index 0000000..6ec5f12 --- /dev/null +++ b/.benchmark/asset/backend/src/services/item.ts @@ -0,0 +1,54 @@ +// Auto-generated Items service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Items, CreateItemsInput, UpdateItemsInput } from "../types/index.js"; + +export class ItemsService { + /** List all items */ + list(): Items[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Items | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemsInput): Items { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemsInput): Items | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/reminder.ts b/.benchmark/asset/backend/src/services/reminder.ts new file mode 100644 index 0000000..8c19dd5 --- /dev/null +++ b/.benchmark/asset/backend/src/services/reminder.ts @@ -0,0 +1,56 @@ +// Auto-generated Reminder service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Reminder, CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +export class ReminderService { + /** List all reminders */ + list(): Reminder[] { + return queryAll("SELECT * FROM reminders ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Reminder | undefined { + return queryOne("SELECT * FROM reminders WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateReminderInput): Reminder { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "remind_at", "message", "sent", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.remindAt ?? null, input.message ?? null, input.sent ?? null, now, now]; + + execute(`INSERT INTO reminders (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateReminderInput): Reminder | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.remindAt !== undefined) { sets.push("remind_at = ?"); values.push(input.remindAt); } + if (input.message !== undefined) { sets.push("message = ?"); values.push(input.message); } + if (input.sent !== undefined) { sets.push("sent = ?"); values.push(input.sent); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE reminders SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM reminders WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/stat.ts b/.benchmark/asset/backend/src/services/stat.ts new file mode 100644 index 0000000..1d4080d --- /dev/null +++ b/.benchmark/asset/backend/src/services/stat.ts @@ -0,0 +1,54 @@ +// Auto-generated Stats service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Stats, CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +export class StatsService { + /** List all stats */ + list(): Stats[] { + return queryAll("SELECT * FROM stats ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Stats | undefined { + return queryOne("SELECT * FROM stats WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateStatsInput): Stats { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "metric", "value", "period", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.metric ?? null, input.value ?? null, input.period ?? null, now, now]; + + execute(`INSERT INTO stats (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateStatsInput): Stats | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.metric !== undefined) { sets.push("metric = ?"); values.push(input.metric); } + if (input.value !== undefined) { sets.push("value = ?"); values.push(input.value); } + if (input.period !== undefined) { sets.push("period = ?"); values.push(input.period); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE stats SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM stats WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/user.ts b/.benchmark/asset/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/asset/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/types/fastify.d.ts b/.benchmark/asset/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/asset/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/asset/backend/src/types/index.ts b/.benchmark/asset/backend/src/types/index.ts new file mode 100644 index 0000000..e5650b7 --- /dev/null +++ b/.benchmark/asset/backend/src/types/index.ts @@ -0,0 +1,292 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Assets { + id: string; + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; + status?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Items { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateAssetsInput { + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; +} + +export interface CreateItemsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface CreateStatsInput { + userId?: string; + metric: string; + value?: number; + period?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateAssetsInput { + userId?: string; + name?: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; +} + +export interface UpdateItemsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +export interface UpdateStatsInput { + userId?: string; + metric?: string; + value?: number; + period?: string; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/asset/backend/src/types/sql.js.d.ts b/.benchmark/asset/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/asset/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/asset/backend/tsconfig.json b/.benchmark/asset/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/asset/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/electron/README.md b/.benchmark/asset/electron/README.md new file mode 100644 index 0000000..cb32bc8 --- /dev/null +++ b/.benchmark/asset/electron/README.md @@ -0,0 +1,82 @@ +# myproject — Desktop + +> Electron desktop application wrapping the myproject fullstack web project. + +## Architecture + +``` +Web Fullstack Project + × + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** — Window management, menu, tray, IPC, auto-update +- **Preload** — Secure bridge between main and renderer +- **Renderer** — Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +├── main.ts — Main process (BrowserWindow, Menu, lifecycle) +├── preload.ts — Secure IPC bridge (contextBridge) +├── ipc.ts — IPC handlers (dialogs, store, notifications) +├── tray.ts — System tray +├── updater.ts — Auto-update (electron-updater) +└── utils.ts — Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/asset/electron/assets/icon.png b/.benchmark/asset/electron/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..554e8a31d1f0205fbf5ef853aba9a4fa2fc490dd GIT binary patch literal 66 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv4;KqeCd<5Tr}e}F6o MPgg&ebxsLQ09s=V>;M1& literal 0 HcmV?d00001 diff --git a/.benchmark/asset/electron/electron-builder.yml b/.benchmark/asset/electron/electron-builder.yml new file mode 100644 index 0000000..1ab271c --- /dev/null +++ b/.benchmark/asset/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml — Auto-generated by SF-06 + +appId: com.myproject.desktop +productName: myproject +copyright: "Copyright © 2026 myproject" + +# ─── Directories ───────────────────────────────── +directories: + output: release + buildResources: assets + +# ─── Files ─────────────────────────────────────── +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# ─── Extra Resources ───────────────────────────── +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# ─── macOS ─────────────────────────────────────── +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# ─── Windows ───────────────────────────────────── +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: myproject + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: myproject + +# ─── Linux ─────────────────────────────────────── +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: myproject + Comment: myproject Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# ─── Auto Update ───────────────────────────────── +publish: + provider: generic + url: https://releases.example.com/myproject/ + channel: latest diff --git a/.benchmark/asset/electron/electron/ipc.ts b/.benchmark/asset/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/asset/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // ── App Info ── + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // ── Window Controls ── + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // ── File Dialogs ── + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // ── Notifications ── + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // ── Shell ── + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // ── Persistent Store ── + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/asset/electron/electron/main.ts b/.benchmark/asset/electron/electron/main.ts new file mode 100644 index 0000000..eaab114 --- /dev/null +++ b/.benchmark/asset/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// ─── Constants ─────────────────────────────────── +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "myproject"; + +// ─── Single Instance Lock ──────────────────────── +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// ─── State ─────────────────────────────────────── +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// ─── Create Window ─────────────────────────────── +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // ── Load content ── + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // ── Show when ready ── + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // ── External links → default browser ── + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // ── Prevent navigation away ── + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// ─── Icon Path ─────────────────────────────────── +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// ─── API Server (Production) ───────────────────── +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// ─── Menu ──────────────────────────────────────── +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// ─── App Lifecycle ─────────────────────────────── +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed — continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/asset/electron/electron/preload.ts b/.benchmark/asset/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/asset/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// ─── Expose safe API to renderer ───────────────── +contextBridge.exposeInMainWorld("electronAPI", { + // ── App Info ── + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // ── Window Controls ── + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // ── File Dialogs ── + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // ── Notifications ── + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // ── Shell ── + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // ── Store ── + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // ── Updates ── + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // ── Update Events ── + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // ── Remove listener ── + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// ─── Type declarations for renderer ────────────── +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/asset/electron/electron/tray.ts b/.benchmark/asset/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/asset/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/asset/electron/electron/updater.ts b/.benchmark/asset/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/asset/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/asset/electron/electron/utils.ts b/.benchmark/asset/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/asset/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// ─── Single Instance Lock ──────────────────────── +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// ─── Dev mode detection ────────────────────────── +export function isDev(): boolean { + return !app.isPackaged; +} + +// ─── Web URL ───────────────────────────────────── +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// ─── API Path ──────────────────────────────────── +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// ─── Get API port from env ─────────────────────── +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/asset/electron/entitlements.mac.plist b/.benchmark/asset/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/asset/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/asset/electron/package.json b/.benchmark/asset/electron/package.json new file mode 100644 index 0000000..44bc859 --- /dev/null +++ b/.benchmark/asset/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "myproject-desktop", + "version": "1.0.0", + "private": true, + "description": "myproject — Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "myproject", + "appId": "com.myproject.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/asset/electron/tsconfig.json b/.benchmark/asset/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/asset/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/frontend/app/assets/page.tsx b/.benchmark/asset/frontend/app/assets/page.tsx new file mode 100644 index 0000000..6d60dc2 --- /dev/null +++ b/.benchmark/asset/frontend/app/assets/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useAssets } from "@/hooks/useAssets"; + +export default function PagePage() { + const { data, loading, error, refetch } = useAssets(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

资产登记

+

资产登记

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/feature/page.tsx b/.benchmark/asset/frontend/app/feature/page.tsx new file mode 100644 index 0000000..729c2d9 --- /dev/null +++ b/.benchmark/asset/frontend/app/feature/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

功能页

+

核心功能页面

+
+ +
+ + { +

功能页 页面内容

+

路由: /feature

+
} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/globals.css b/.benchmark/asset/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/asset/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/asset/frontend/app/items/page.tsx b/.benchmark/asset/frontend/app/items/page.tsx new file mode 100644 index 0000000..808e453 --- /dev/null +++ b/.benchmark/asset/frontend/app/items/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useItems } from "@/hooks/useItems"; + +export default function PagePage() { + const { data, loading, error, refetch } = useItems(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

折旧计算

+

折旧计算

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/layout.tsx b/.benchmark/asset/frontend/app/layout.tsx new file mode 100644 index 0000000..43f1cb9 --- /dev/null +++ b/.benchmark/asset/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "MyProject", + description: "一款根据用户需求定制的应用。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/asset/frontend/app/page.tsx b/.benchmark/asset/frontend/app/page.tsx new file mode 100644 index 0000000..203f24f --- /dev/null +++ b/.benchmark/asset/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

MyProject

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 资产登记 + + + 📅 + 领用归还 + + + 📝 + 折旧计算 + + + 📸 + 盘点统计 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 MyProject

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/asset/frontend/app/profile/page.tsx b/.benchmark/asset/frontend/app/profile/page.tsx new file mode 100644 index 0000000..61ddaaa --- /dev/null +++ b/.benchmark/asset/frontend/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

我的

+

用户中心

+
+ +
+ + { +

我的 页面内容

+

路由: /profile

+
} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/stats/page.tsx b/.benchmark/asset/frontend/app/stats/page.tsx new file mode 100644 index 0000000..a11cc1e --- /dev/null +++ b/.benchmark/asset/frontend/app/stats/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useStats } from "@/hooks/useStats"; + +export default function PagePage() { + const { data, loading, error, refetch } = useStats(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

盘点统计

+

盘点统计

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/折旧计算/page.tsx b/.benchmark/asset/frontend/app/折旧计算/page.tsx new file mode 100644 index 0000000..1e72994 --- /dev/null +++ b/.benchmark/asset/frontend/app/折旧计算/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use折旧计算 } from "@/hooks/use折旧计算"; + +export default function PagePage() { + const { data, loading, error, refetch } = use折旧计算(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

折旧计算

+

折旧计算

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/盘点统计/page.tsx b/.benchmark/asset/frontend/app/盘点统计/page.tsx new file mode 100644 index 0000000..7298592 --- /dev/null +++ b/.benchmark/asset/frontend/app/盘点统计/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use盘点统计 } from "@/hooks/use盘点统计"; + +export default function PagePage() { + const { data, loading, error, refetch } = use盘点统计(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

盘点统计

+

盘点统计

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/资产登记/page.tsx b/.benchmark/asset/frontend/app/资产登记/page.tsx new file mode 100644 index 0000000..05fae34 --- /dev/null +++ b/.benchmark/asset/frontend/app/资产登记/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use资产登记 } from "@/hooks/use资产登记"; + +export default function PagePage() { + const { data, loading, error, refetch } = use资产登记(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

资产登记

+

资产登记

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/领用归还/page.tsx b/.benchmark/asset/frontend/app/领用归还/page.tsx new file mode 100644 index 0000000..c2190c9 --- /dev/null +++ b/.benchmark/asset/frontend/app/领用归还/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use领用归还 } from "@/hooks/use领用归还"; + +export default function PagePage() { + const { data, loading, error, refetch } = use领用归还(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

领用归还

+

领用归还

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/components/layout/BottomNav.tsx b/.benchmark/asset/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/asset/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/asset/frontend/components/layout/Sidebar.tsx b/.benchmark/asset/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/asset/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/asset/frontend/components/ui/Button.tsx b/.benchmark/asset/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/asset/frontend/components/ui/Card.tsx b/.benchmark/asset/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/asset/frontend/components/ui/EmptyState.tsx b/.benchmark/asset/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/asset/frontend/components/ui/Input.tsx b/.benchmark/asset/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/asset/frontend/components/ui/Modal.tsx b/.benchmark/asset/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/asset/frontend/hooks/useAssets.ts b/.benchmark/asset/frontend/hooks/useAssets.ts new file mode 100644 index 0000000..2645554 --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useAssets.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for assets +type Asset = Record; + +/** + * Fetch and manage assets data. + */ +export function useAssets() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/assets`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useAuth.ts b/.benchmark/asset/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useDebounce.ts b/.benchmark/asset/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/asset/frontend/hooks/useForm.ts b/.benchmark/asset/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/asset/frontend/hooks/useHealth.ts b/.benchmark/asset/frontend/hooks/useHealth.ts new file mode 100644 index 0000000..9aa622a --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useHealth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for health +type Health = Record; + +/** + * Fetch and manage health data. + */ +export function useHealth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/health`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useItem.ts b/.benchmark/asset/frontend/hooks/useItem.ts new file mode 100644 index 0000000..65342ec --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useItem.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for 领用归还 +type Item = Record; + +/** + * Fetch and manage 领用归还 data. + */ +export function useItem() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/领用归还`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useItems.ts b/.benchmark/asset/frontend/hooks/useItems.ts new file mode 100644 index 0000000..ef60559 --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useItems.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for items +type Item = Record; + +/** + * Fetch and manage items data. + */ +export function useItems() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/items`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useStats.ts b/.benchmark/asset/frontend/hooks/useStats.ts new file mode 100644 index 0000000..f9f34cf --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useStats.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for stats +type Stat = Record; + +/** + * Fetch and manage stats data. + */ +export function useStats() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/stats`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useUsers.ts b/.benchmark/asset/frontend/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/next-env.d.ts b/.benchmark/asset/frontend/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/.benchmark/asset/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/.benchmark/asset/frontend/next.config.ts b/.benchmark/asset/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/asset/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/asset/frontend/package.json b/.benchmark/asset/frontend/package.json new file mode 100644 index 0000000..caa9eab --- /dev/null +++ b/.benchmark/asset/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/asset/frontend/postcss.config.js b/.benchmark/asset/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/asset/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/asset/frontend/services/api.ts b/.benchmark/asset/frontend/services/api.ts new file mode 100644 index 0000000..13abf7f --- /dev/null +++ b/.benchmark/asset/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for MyProject + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/asset/frontend/services/assets.ts b/.benchmark/asset/frontend/services/assets.ts new file mode 100644 index 0000000..976b03b --- /dev/null +++ b/.benchmark/asset/frontend/services/assets.ts @@ -0,0 +1,10 @@ +// Auto-generated assets service +import { api } from "./api"; + +export function get() { + return api.get("/api/assets"); +} + +export function post(data: Record) { + return api.post("/api/assets", data); +} diff --git a/.benchmark/asset/frontend/services/auth.ts b/.benchmark/asset/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/asset/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/asset/frontend/services/health.ts b/.benchmark/asset/frontend/services/health.ts new file mode 100644 index 0000000..4fdf89e --- /dev/null +++ b/.benchmark/asset/frontend/services/health.ts @@ -0,0 +1,6 @@ +// Auto-generated health service +import { api } from "./api"; + +export function get() { + return api.get("/api/health"); +} diff --git a/.benchmark/asset/frontend/services/items.ts b/.benchmark/asset/frontend/services/items.ts new file mode 100644 index 0000000..f9825fa --- /dev/null +++ b/.benchmark/asset/frontend/services/items.ts @@ -0,0 +1,18 @@ +// Auto-generated items service +import { api } from "./api"; + +export function get() { + return api.get("/api/items"); +} + +export function post(data: Record) { + return api.post("/api/items", data); +} + +export function get() { + return api.get("/api/items"); +} + +export function post(data: Record) { + return api.post("/api/items", data); +} diff --git a/.benchmark/asset/frontend/services/stats.ts b/.benchmark/asset/frontend/services/stats.ts new file mode 100644 index 0000000..e48d921 --- /dev/null +++ b/.benchmark/asset/frontend/services/stats.ts @@ -0,0 +1,10 @@ +// Auto-generated stats service +import { api } from "./api"; + +export function get() { + return api.get("/api/stats"); +} + +export function post(data: Record) { + return api.post("/api/stats", data); +} diff --git a/.benchmark/asset/frontend/services/users.ts b/.benchmark/asset/frontend/services/users.ts new file mode 100644 index 0000000..03fad78 --- /dev/null +++ b/.benchmark/asset/frontend/services/users.ts @@ -0,0 +1,10 @@ +// Auto-generated users service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/users", data); +} + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/asset/frontend/services/折旧计算.ts b/.benchmark/asset/frontend/services/折旧计算.ts new file mode 100644 index 0000000..0db59b0 --- /dev/null +++ b/.benchmark/asset/frontend/services/折旧计算.ts @@ -0,0 +1,10 @@ +// Auto-generated 折旧计算 service +import { api } from "./api"; + +export function get() { + return api.get("/api/折旧计算"); +} + +export function post(data: Record) { + return api.post("/api/折旧计算", data); +} diff --git a/.benchmark/asset/frontend/services/盘点统计.ts b/.benchmark/asset/frontend/services/盘点统计.ts new file mode 100644 index 0000000..b677698 --- /dev/null +++ b/.benchmark/asset/frontend/services/盘点统计.ts @@ -0,0 +1,10 @@ +// Auto-generated 盘点统计 service +import { api } from "./api"; + +export function get() { + return api.get("/api/盘点统计"); +} + +export function post(data: Record) { + return api.post("/api/盘点统计", data); +} diff --git a/.benchmark/asset/frontend/services/资产登记.ts b/.benchmark/asset/frontend/services/资产登记.ts new file mode 100644 index 0000000..9b13293 --- /dev/null +++ b/.benchmark/asset/frontend/services/资产登记.ts @@ -0,0 +1,10 @@ +// Auto-generated 资产登记 service +import { api } from "./api"; + +export function get() { + return api.get("/api/资产登记"); +} + +export function post(data: Record) { + return api.post("/api/资产登记", data); +} diff --git a/.benchmark/asset/frontend/services/领用归还.ts b/.benchmark/asset/frontend/services/领用归还.ts new file mode 100644 index 0000000..c2ec9cc --- /dev/null +++ b/.benchmark/asset/frontend/services/领用归还.ts @@ -0,0 +1,10 @@ +// Auto-generated 领用归还 service +import { api } from "./api"; + +export function get() { + return api.get("/api/领用归还"); +} + +export function post(data: Record) { + return api.post("/api/领用归还", data); +} diff --git a/.benchmark/asset/frontend/tailwind.config.ts b/.benchmark/asset/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/asset/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/asset/frontend/tsconfig.json b/.benchmark/asset/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/asset/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/frontend/types/index.ts b/.benchmark/asset/frontend/types/index.ts new file mode 100644 index 0000000..e1b2628 --- /dev/null +++ b/.benchmark/asset/frontend/types/index.ts @@ -0,0 +1,120 @@ +// Auto-generated types for MyProject + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Assets { + id: string; + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; + status?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Items { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/.gitignore b/.benchmark/asset/fullstack/.gitignore new file mode 100644 index 0000000..7488f6e --- /dev/null +++ b/.benchmark/asset/fullstack/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.next/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/asset/fullstack/README.md b/.benchmark/asset/fullstack/README.md new file mode 100644 index 0000000..e33895f --- /dev/null +++ b/.benchmark/asset/fullstack/README.md @@ -0,0 +1,118 @@ +# MyProject — Fullstack Project + +> 一款根据用户需求定制的应用。 + +## Architecture + +``` +apps/ +├── web/ # Next.js 15 + TypeScript + Tailwind CSS +└── api/ # Fastify 5 + TypeScript + SQLite (sql.js) + +packages/ +├── shared-types/ # @shared/types — shared TypeScript interfaces +└── shared-config/ # @shared/config — shared configuration +``` + +## Quick Start + +```bash +# Install all dependencies (root + workspaces) +npm install + +# Start both frontend and backend in dev mode +npm run dev + +# Or start individually +npm run dev:web # http://localhost:3000 +npm run dev:api # http://localhost:3001 +``` + +## Build + +```bash +# Build everything +npm run build + +# Or individually +npm run build:web +npm run build:api +``` + +## Testing + +```bash +# Run API tests +npm test +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/auth/me` + +### Resources +#### users +- `GET /api/users` — List +- `GET /api/users/:id` — Get +- `POST /api/users` — Create +- `PUT /api/users/:id` — Update +- `DELETE /api/users/:id` — Delete + +#### contracts +- `GET /api/contracts` — List +- `GET /api/contracts/:id` — Get +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List +- `GET /api/approvals/:id` — Get +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List +- `GET /api/customers/:id` — Get +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List +- `GET /api/reminders/:id` — Get +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### assets +- `GET /api/assets` — List +- `GET /api/assets/:id` — Get +- `POST /api/assets` — Create +- `PUT /api/assets/:id` — Update +- `DELETE /api/assets/:id` — Delete + +#### items +- `GET /api/items` — List +- `GET /api/items/:id` — Get +- `POST /api/items` — Create +- `PUT /api/items/:id` — Update +- `DELETE /api/items/:id` — Delete + +#### stats +- `GET /api/stats` — List +- `GET /api/stats/:id` — Get +- `POST /api/stats` — Create +- `PUT /api/stats/:id` — Update +- `DELETE /api/stats/:id` — Delete + +### System +- `GET /api/health` — Health check + +## Environment Variables + +See `.env.example` for all available configuration. diff --git a/.benchmark/asset/fullstack/apps/api/.gitignore b/.benchmark/asset/fullstack/apps/api/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/asset/fullstack/apps/api/README.md b/.benchmark/asset/fullstack/apps/api/README.md new file mode 100644 index 0000000..8983604 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/README.md @@ -0,0 +1,111 @@ +# MyProject — Backend API + +> 一款根据用户需求定制的应用。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` — List all +- `GET /api/contracts/:id` — Get by ID +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List all +- `GET /api/approvals/:id` — Get by ID +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List all +- `GET /api/customers/:id` — Get by ID +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List all +- `GET /api/reminders/:id` — Get by ID +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### assets +- `GET /api/assets` — List all +- `GET /api/assets/:id` — Get by ID +- `POST /api/assets` — Create +- `PUT /api/assets/:id` — Update +- `DELETE /api/assets/:id` — Delete + +#### items +- `GET /api/items` — List all +- `GET /api/items/:id` — Get by ID +- `POST /api/items` — Create +- `PUT /api/items/:id` — Update +- `DELETE /api/items/:id` — Delete + +#### stats +- `GET /api/stats` — List all +- `GET /api/stats/:id` — Get by ID +- `POST /api/stats` — Create +- `PUT /api/stats/:id` — Update +- `DELETE /api/stats/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/asset/fullstack/apps/api/package.json b/.benchmark/asset/fullstack/apps/api/package.json new file mode 100644 index 0000000..2831924 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "myproject-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/apps/api/src/__tests__/auth.test.ts b/.benchmark/asset/fullstack/apps/api/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/asset/fullstack/apps/api/src/__tests__/items.test.ts b/.benchmark/asset/fullstack/apps/api/src/__tests__/items.test.ts new file mode 100644 index 0000000..ff42fbb --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/__tests__/items.test.ts @@ -0,0 +1,119 @@ +// Items CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "data": "sample-data" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/fullstack/apps/api/src/__tests__/users.test.ts b/.benchmark/asset/fullstack/apps/api/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/fullstack/apps/api/src/db/client.ts b/.benchmark/asset/fullstack/apps/api/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/asset/fullstack/apps/api/src/db/schema.ts b/.benchmark/asset/fullstack/apps/api/src/db/schema.ts new file mode 100644 index 0000000..425ac3a --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/db/schema.ts @@ -0,0 +1,19 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS assets (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n category TEXT,\n purchase_date TEXT,\n purchase_price REAL,\n current_value REAL,\n location TEXT,\n status TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS items (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n metric TEXT NOT NULL,\n value REAL,\n period TEXT,\n recorded_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/index.ts b/.benchmark/asset/fullstack/apps/api/src/index.ts new file mode 100644 index 0000000..345d1ec --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/index.ts @@ -0,0 +1,77 @@ +// MyProject — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { assetsRoutes } from "./routes/assets.js"; +import { itemsRoutes } from "./routes/items.js"; +import { statsRoutes } from "./routes/stats.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-8dd37ee5"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(assetsRoutes, { prefix: "/api/assets" }); + await app.register(itemsRoutes, { prefix: "/api/items" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/asset/fullstack/apps/api/src/middleware/auth.ts b/.benchmark/asset/fullstack/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/routes/auth.ts b/.benchmark/asset/fullstack/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/asset/fullstack/apps/api/src/routes/items.ts b/.benchmark/asset/fullstack/apps/api/src/routes/items.ts new file mode 100644 index 0000000..a649fff --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Items routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemsService } from "../services/item.js"; +import type { CreateItemsInput, UpdateItemsInput } from "../types/index.js"; + +const service = new ItemsService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items — create + app.post<{ Body: CreateItemsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id — update + app.put<{ Params: { id: string }; Body: UpdateItemsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/fullstack/apps/api/src/routes/users.ts b/.benchmark/asset/fullstack/apps/api/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/fullstack/apps/api/src/services/item.ts b/.benchmark/asset/fullstack/apps/api/src/services/item.ts new file mode 100644 index 0000000..6ec5f12 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/services/item.ts @@ -0,0 +1,54 @@ +// Auto-generated Items service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Items, CreateItemsInput, UpdateItemsInput } from "../types/index.js"; + +export class ItemsService { + /** List all items */ + list(): Items[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Items | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemsInput): Items { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemsInput): Items | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/services/user.ts b/.benchmark/asset/fullstack/apps/api/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/types/fastify.d.ts b/.benchmark/asset/fullstack/apps/api/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/types/index.ts b/.benchmark/asset/fullstack/apps/api/src/types/index.ts new file mode 100644 index 0000000..74d1918 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/types/index.ts @@ -0,0 +1,203 @@ +// Import and re-export shared entity types +import type { + User, + Contract, + Approval, + Customer, + Reminder, + Assets, + Items, + Stats, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from "@shared/types"; + +export type { + User, + Contract, + Approval, + Customer, + Reminder, + Assets, + Items, + Stats, + ApiResponse, + PaginatedResponse, + ErrorResponse, +}; + +// Auto-generated types (from Model Contract) + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateAssetsInput { + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; +} + +export interface CreateItemsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface CreateStatsInput { + userId?: string; + metric: string; + value?: number; + period?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateAssetsInput { + userId?: string; + name?: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; +} + +export interface UpdateItemsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +export interface UpdateStatsInput { + userId?: string; + metric?: string; + value?: number; + period?: string; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/asset/fullstack/apps/api/src/types/sql.js.d.ts b/.benchmark/asset/fullstack/apps/api/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/asset/fullstack/apps/api/tsconfig.json b/.benchmark/asset/fullstack/apps/api/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/apps/web/app/feature/page.tsx b/.benchmark/asset/fullstack/apps/web/app/feature/page.tsx new file mode 100644 index 0000000..729c2d9 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/feature/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

功能页

+

核心功能页面

+
+ +
+ + { +

功能页 页面内容

+

路由: /feature

+
} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/app/globals.css b/.benchmark/asset/fullstack/apps/web/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/asset/fullstack/apps/web/app/layout.tsx b/.benchmark/asset/fullstack/apps/web/app/layout.tsx new file mode 100644 index 0000000..43f1cb9 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "MyProject", + description: "一款根据用户需求定制的应用。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/asset/fullstack/apps/web/app/page.tsx b/.benchmark/asset/fullstack/apps/web/app/page.tsx new file mode 100644 index 0000000..203f24f --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

MyProject

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 资产登记 + + + 📅 + 领用归还 + + + 📝 + 折旧计算 + + + 📸 + 盘点统计 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 MyProject

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/app/profile/page.tsx b/.benchmark/asset/fullstack/apps/web/app/profile/page.tsx new file mode 100644 index 0000000..61ddaaa --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

我的

+

用户中心

+
+ +
+ + { +

我的 页面内容

+

路由: /profile

+
} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/layout/BottomNav.tsx b/.benchmark/asset/fullstack/apps/web/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/layout/Sidebar.tsx b/.benchmark/asset/fullstack/apps/web/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/Button.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/Card.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/EmptyState.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/Input.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/Modal.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/hooks/useDebounce.ts b/.benchmark/asset/fullstack/apps/web/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/asset/fullstack/apps/web/hooks/useForm.ts b/.benchmark/asset/fullstack/apps/web/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/asset/fullstack/apps/web/hooks/useHealth.ts b/.benchmark/asset/fullstack/apps/web/hooks/useHealth.ts new file mode 100644 index 0000000..9aa622a --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/hooks/useHealth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for health +type Health = Record; + +/** + * Fetch and manage health data. + */ +export function useHealth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/health`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/fullstack/apps/web/hooks/useUsers.ts b/.benchmark/asset/fullstack/apps/web/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/fullstack/apps/web/next.config.ts b/.benchmark/asset/fullstack/apps/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/asset/fullstack/apps/web/package.json b/.benchmark/asset/fullstack/apps/web/package.json new file mode 100644 index 0000000..0e5a224 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "myproject-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/apps/web/postcss.config.js b/.benchmark/asset/fullstack/apps/web/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/asset/fullstack/apps/web/services/api.ts b/.benchmark/asset/fullstack/apps/web/services/api.ts new file mode 100644 index 0000000..b2e1f85 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/services/api.ts @@ -0,0 +1,49 @@ +// Auto-generated API client for MyProject + +import { API_BASE_URL, API_PREFIX } from "@shared/config"; + +const API_BASE = `${API_BASE_URL}${API_PREFIX}`; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/asset/fullstack/apps/web/services/health.ts b/.benchmark/asset/fullstack/apps/web/services/health.ts new file mode 100644 index 0000000..4fdf89e --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/services/health.ts @@ -0,0 +1,6 @@ +// Auto-generated health service +import { api } from "./api"; + +export function get() { + return api.get("/api/health"); +} diff --git a/.benchmark/asset/fullstack/apps/web/services/users.ts b/.benchmark/asset/fullstack/apps/web/services/users.ts new file mode 100644 index 0000000..03fad78 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/services/users.ts @@ -0,0 +1,10 @@ +// Auto-generated users service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/users", data); +} + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/asset/fullstack/apps/web/tailwind.config.ts b/.benchmark/asset/fullstack/apps/web/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/asset/fullstack/apps/web/tsconfig.json b/.benchmark/asset/fullstack/apps/web/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/apps/web/types/index.ts b/.benchmark/asset/fullstack/apps/web/types/index.ts new file mode 100644 index 0000000..e1b2628 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/types/index.ts @@ -0,0 +1,120 @@ +// Auto-generated types for MyProject + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Assets { + id: string; + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; + status?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Items { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/package.json b/.benchmark/asset/fullstack/package.json new file mode 100644 index 0000000..a32255b --- /dev/null +++ b/.benchmark/asset/fullstack/package.json @@ -0,0 +1,19 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "node scripts/build.mjs", + "dev:web": "npm run dev -w apps/web", + "dev:api": "npm run dev -w apps/api", + "build:web": "npm run build -w apps/web", + "build:api": "npm run build -w apps/api", + "test": "npm run test -w apps/api", + "lint": "npm run lint -w apps/web" + } +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/packages/shared-config/package.json b/.benchmark/asset/fullstack/packages/shared-config/package.json new file mode 100644 index 0000000..c2f52a9 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/config", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/packages/shared-config/src/index.ts b/.benchmark/asset/fullstack/packages/shared-config/src/index.ts new file mode 100644 index 0000000..abf84f0 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-config/src/index.ts @@ -0,0 +1,16 @@ +// Shared configuration for fullstack project + +/** API base URL — reads from env or defaults */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** API prefix for all endpoints */ +export const API_PREFIX = "/api"; + +/** Full API URL */ +export const API_URL = `${API_BASE_URL}${API_PREFIX}`; + +/** Auth token storage key */ +export const AUTH_TOKEN_KEY = "auth_token"; + +/** Default page size for paginated endpoints */ +export const DEFAULT_PAGE_SIZE = 20; diff --git a/.benchmark/asset/fullstack/packages/shared-config/tsconfig.json b/.benchmark/asset/fullstack/packages/shared-config/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/packages/shared-types/package.json b/.benchmark/asset/fullstack/packages/shared-types/package.json new file mode 100644 index 0000000..d7fb84c --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/types", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/packages/shared-types/src/index.ts b/.benchmark/asset/fullstack/packages/shared-types/src/index.ts new file mode 100644 index 0000000..cc57d56 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-types/src/index.ts @@ -0,0 +1,122 @@ +// Shared types for fullstack project +// Auto-generated — used by both apps/web and apps/api + +// ─── Entities ──────────────────────────────────────── +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Assets { + id: string; + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; + status?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Items { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +// ─── API Response Wrappers ─────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} diff --git a/.benchmark/asset/fullstack/packages/shared-types/tsconfig.json b/.benchmark/asset/fullstack/packages/shared-types/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/scripts/build.mjs b/.benchmark/asset/fullstack/scripts/build.mjs new file mode 100644 index 0000000..84fa093 --- /dev/null +++ b/.benchmark/asset/fullstack/scripts/build.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Build script — builds both web and api. + * Usage: node scripts/build.mjs + */ + +import { execSync } from "node:child_process"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function run(cmd, cwd) { + console.log(`\n🔨 ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("🏗️ Building fullstack project...\n"); + +try { + run("npm run build", `${ROOT}/apps/api`); + run("npm run build", `${ROOT}/apps/web`); + console.log("\n✅ Build complete!"); +} catch (e) { + console.error("\n❌ Build failed:", e.message); + process.exit(1); +} diff --git a/.benchmark/asset/fullstack/scripts/dev.mjs b/.benchmark/asset/fullstack/scripts/dev.mjs new file mode 100644 index 0000000..dcb8118 --- /dev/null +++ b/.benchmark/asset/fullstack/scripts/dev.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +/** + * Dev script — starts both web and api in parallel. + * Usage: node scripts/dev.mjs + */ + +import { spawn } from "node:child_process"; + +function start(name, command, args, cwd) { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + env: { ...process.env, FORCE_COLOR: "1" }, + }); + child.on("error", (err) => console.error(`[${name}] Failed: ${err.message}`)); + child.on("exit", (code) => { + if (code !== 0 && code !== null) console.error(`[${name}] Exited with code ${code}`); + }); + return child; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +console.log("🚀 Starting fullstack dev servers...\n"); + +const api = start("api", "npm", ["run", "dev"], `${ROOT}/apps/api`); +const web = start("web", "npm", ["run", "dev"], `${ROOT}/apps/web`); + +process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); }); +process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); }); diff --git a/.benchmark/asset/prd.json b/.benchmark/asset/prd.json new file mode 100644 index 0000000..a8768b5 --- /dev/null +++ b/.benchmark/asset/prd.json @@ -0,0 +1 @@ +{"projectName":"MyProject","chineseName":"通用应用","domain":"generic","matchConfidence":"low","summary":"一款根据用户需求定制的应用。","personas":[{"name":"用户","description":"目标用户群体","painPoints":["待明确"]}],"userStories":[{"id":"US-001","as":"用户","want":"资产登记","soThat":"解决痛点:待明确","priority":"P0"},{"id":"US-002","as":"用户","want":"领用归还","soThat":"解决痛点:待明确","priority":"P0"},{"id":"US-003","as":"用户","want":"折旧计算","soThat":"解决痛点:待明确","priority":"P0"}],"mvpScope":{"description":"MVP 聚焦 资产登记、领用归还、折旧计算、盘点统计,包含页面:首页、资产登记、领用归还","features":["资产登记","领用归还","折旧计算","盘点统计"],"pages":["首页","资产登记","领用归还"],"estimatedWeeks":3},"features":[{"name":"资产登记","description":"资产登记","priority":"P0"},{"name":"领用归还","description":"领用归还","priority":"P0"},{"name":"折旧计算","description":"折旧计算","priority":"P0"},{"name":"盘点统计","description":"盘点统计","priority":"P0"}],"pages":[{"name":"首页","route":"/home","description":"应用首页"},{"name":"资产登记","route":"/assets","description":"资产登记"},{"name":"领用归还","route":"/items","description":"领用归还"},{"name":"折旧计算","route":"/items","description":"折旧计算"},{"name":"盘点统计","route":"/stats","description":"盘点统计"}],"apiRequirements":[{"method":"GET","path":"/api/assets","description":"获取资产登记列表"},{"method":"POST","path":"/api/assets","description":"创建资产登记"},{"method":"GET","path":"/api/items","description":"获取领用归还列表"},{"method":"POST","path":"/api/items","description":"创建领用归还"},{"method":"GET","path":"/api/items","description":"获取折旧计算列表"},{"method":"POST","path":"/api/items","description":"创建折旧计算"},{"method":"GET","path":"/api/stats","description":"获取盘点统计列表"},{"method":"POST","path":"/api/stats","description":"创建盘点统计"},{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter(跨平台)","considerations":["建议跨平台框架减少开发成本"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"项目脚手架搭建","description":"初始化 MyProject 项目结构,配置构建工具和 CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"数据库设计与初始化","description":"设计数据模型,创建数据库迁移脚本","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"页面开发:首页","description":"开发 首页 页面(/home):应用首页","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"页面开发:资产登记","description":"开发 资产登记 页面(/assets):资产登记","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"页面开发:领用归还","description":"开发 领用归还 页面(/items):领用归还","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"页面开发:折旧计算","description":"开发 折旧计算 页面(/items):折旧计算","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API 开发:GET /api/assets","description":"获取资产登记列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API 开发:POST /api/assets","description":"创建资产登记","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API 开发:GET /api/items","description":"获取领用归还列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API 开发:POST /api/items","description":"创建领用归还","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"前后端联调","description":"前端页面与后端 API 集成联调","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"测试与修复","description":"功能测试、Bug 修复、性能优化","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T22:08:36.980Z","inputLength":33,"domainMatchCount":0,"extractedFeatureCount":4,"extractedPageCount":5,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/asset/release/release/README.md b/.benchmark/asset/release/release/README.md new file mode 100644 index 0000000..fae49a5 --- /dev/null +++ b/.benchmark/asset/release/release/README.md @@ -0,0 +1,33 @@ +# myproject — Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T22:08:37.343Z + +## Directory Structure + +``` +release/ +├── version.json — Version metadata +├── build-info.json — Build environment info +├── README.md — This file +├── manifests/ +│ └── manifest.json — Release manifest with file hashes +├── checksums/ +│ └── checksums.txt — SHA256 checksums for verification +├── release-notes/ +│ └── release-notes.md — Human-readable release notes +├── windows/ — Windows installers +├── macos/ — macOS disk images +└── linux/ — Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/asset/release/release/build-info.json b/.benchmark/asset/release/release/build-info.json new file mode 100644 index 0000000..bde0e23 --- /dev/null +++ b/.benchmark/asset/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T22:08:37.343Z", + "domain": "unknown", + "entityCount": 9, + "routeCount": 1, + "screenCount": 9, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "myproject", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/asset/release/release/checksums/checksums.txt b/.benchmark/asset/release/release/checksums/checksums.txt new file mode 100644 index 0000000..ce01b57 --- /dev/null +++ b/.benchmark/asset/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +be58304f2685656f24eea61b6f669909fcb4bba6033498c00ca929ddb9494b8e windows/myproject-setup-0.1.0.exe +cf95a39a77b8a45d00f09928cd9d14e7e0c9529065813470fac7e49b8db78925 windows/myproject-0.1.0-portable.exe +f072a9bc11e8123b5b4df0725cd0ffe8b550b0ab56f5b5441a1eec6ed34489eb macos/myproject-0.1.0.dmg +293084b0e5e79005ac0f864d0c773abfc06b143aff2bbfa05b36d66a8aceb2c2 macos/myproject-0.1.0-arm64.dmg +6796cb2b246f940d02372542e3005cbb70df962ca7377a753691f37572fff99b linux/myproject-0.1.0.AppImage +1ff107aa051cbe6724f19fba5e5411850f392ccd23af95fad7514bb490afd91f linux/myproject_0.1.0_amd64.deb diff --git a/.benchmark/asset/release/release/linux/myproject-0.1.0.AppImage b/.benchmark/asset/release/release/linux/myproject-0.1.0.AppImage new file mode 100644 index 0000000..91107d8 --- /dev/null +++ b/.benchmark/asset/release/release/linux/myproject-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/linux/myproject_0.1.0_amd64.deb b/.benchmark/asset/release/release/linux/myproject_0.1.0_amd64.deb new file mode 100644 index 0000000..128633b --- /dev/null +++ b/.benchmark/asset/release/release/linux/myproject_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/macos/myproject-0.1.0-arm64.dmg b/.benchmark/asset/release/release/macos/myproject-0.1.0-arm64.dmg new file mode 100644 index 0000000..5227ab3 --- /dev/null +++ b/.benchmark/asset/release/release/macos/myproject-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/macos/myproject-0.1.0.dmg b/.benchmark/asset/release/release/macos/myproject-0.1.0.dmg new file mode 100644 index 0000000..4ad7e6c --- /dev/null +++ b/.benchmark/asset/release/release/macos/myproject-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/manifests/manifest.json b/.benchmark/asset/release/release/manifests/manifest.json new file mode 100644 index 0000000..9d88664 --- /dev/null +++ b/.benchmark/asset/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "myproject", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T22:08:37.343Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/myproject-setup-0.1.0.exe", + "size": 144, + "sha256": "be58304f2685656f24eea61b6f669909fcb4bba6033498c00ca929ddb9494b8e" + }, + { + "path": "windows/myproject-0.1.0-portable.exe", + "size": 148, + "sha256": "cf95a39a77b8a45d00f09928cd9d14e7e0c9529065813470fac7e49b8db78925" + }, + { + "path": "macos/myproject-0.1.0.dmg", + "size": 140, + "sha256": "f072a9bc11e8123b5b4df0725cd0ffe8b550b0ab56f5b5441a1eec6ed34489eb" + }, + { + "path": "macos/myproject-0.1.0-arm64.dmg", + "size": 142, + "sha256": "293084b0e5e79005ac0f864d0c773abfc06b143aff2bbfa05b36d66a8aceb2c2" + }, + { + "path": "linux/myproject-0.1.0.AppImage", + "size": 133, + "sha256": "6796cb2b246f940d02372542e3005cbb70df962ca7377a753691f37572fff99b" + }, + { + "path": "linux/myproject_0.1.0_amd64.deb", + "size": 143, + "sha256": "1ff107aa051cbe6724f19fba5e5411850f392ccd23af95fad7514bb490afd91f" + } + ], + "totalFiles": 6, + "totalSize": 850 +} \ No newline at end of file diff --git a/.benchmark/asset/release/release/release-notes/release-notes.md b/.benchmark/asset/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..4e40d22 --- /dev/null +++ b/.benchmark/asset/release/release/release-notes/release-notes.md @@ -0,0 +1,58 @@ +# myproject v0.1.0 + +> Release Builder Agent — SF-07 +> Generated: 2026-06-05 22:08:37 UTC + +## Version + +- **Name:** myproject +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- approvals +- assets +- auth +- contracts +- customers +- items +- reminders +- stats +- users + +## Build Info + +- **Build Time:** 2026-06-05T22:08:37.343Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- ✅ windows +- ✅ macos +- ✅ linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/asset/release/release/version.json b/.benchmark/asset/release/release/version.json new file mode 100644 index 0000000..3a7e2b5 --- /dev/null +++ b/.benchmark/asset/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "myproject", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/release/release/windows/myproject-0.1.0-portable.exe b/.benchmark/asset/release/release/windows/myproject-0.1.0-portable.exe new file mode 100644 index 0000000..fa4b1a1 --- /dev/null +++ b/.benchmark/asset/release/release/windows/myproject-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/windows/myproject-setup-0.1.0.exe b/.benchmark/asset/release/release/windows/myproject-setup-0.1.0.exe new file mode 100644 index 0000000..b7adc7d --- /dev/null +++ b/.benchmark/asset/release/release/windows/myproject-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/blog-cms/arch.json b/.benchmark/blog-cms/arch.json new file mode 100644 index 0000000..b314723 --- /dev/null +++ b/.benchmark/blog-cms/arch.json @@ -0,0 +1 @@ +{"projectName":"SocialApp","domain":"social","contract":{"projectName":"SocialApp","domain":"social","entities":[{"name":"User","table":"users","description":"系统用户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"hasMany","entity":"contracts","via":"user_id"},{"type":"hasMany","entity":"customers","via":"user_id"}]},{"name":"Contract","table":"contracts","description":"合同","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","defaultValue":"draft","enum":["draft","pending","active","expired","terminated"]},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Approval","table":"approvals","description":"审批流程","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"applicant_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"status","type":"VARCHAR(32)","defaultValue":"pending","enum":["pending","approved","rejected"]},{"name":"form_data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"users","via":"applicant_id","as":"applicant"}]},{"name":"Customer","table":"customers","description":"客户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","defaultValue":"[]"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Reminder","table":"reminders","description":"到期提醒","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"remind_at","type":"TIMESTAMP","required":true},{"name":"message","type":"TEXT"},{"name":"sent","type":"BOOLEAN","defaultValue":"false"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Articles","table":"articles","description":"文章发布表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"content","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"excerpt","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"cover_url","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"author_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"authors","fkColumn":"id"},{"name":"category","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"published_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"authors","via":"author_id"}]},{"name":"Tags","table":"tags","description":"分类标签表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(64)","required":true,"isPrimary":false,"isAuto":false,"unique":true},{"name":"color","type":"VARCHAR(7)","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Comments","table":"comments","description":"评论管理表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true,"isPrimary":false,"isAuto":false},{"name":"entity_id","type":"UUID","required":true,"isPrimary":false,"isAuto":false},{"name":"content","type":"TEXT","required":true,"isPrimary":false,"isAuto":false},{"name":"parent_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"parents","fkColumn":"id"},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"parents","via":"parent_id"}]},{"name":"Media","table":"media","description":"媒体库表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"filename","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"url","type":"TEXT","required":true,"isPrimary":false,"isAuto":false},{"name":"mime_type","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"size_bytes","type":"INTEGER","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","remind_at":"remindAt","cover_url":"coverUrl","author_id":"authorId","published_at":"publishedAt","parent_id":"parentId","mime_type":"mimeType","size_bytes":"sizeBytes","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","remindAt":"remind_at","coverUrl":"cover_url","authorId":"author_id","publishedAt":"published_at","parentId":"parent_id","mimeType":"mime_type","sizeBytes":"size_bytes","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Contract":{"user_id":{"required":true},"title":{"required":true},"status":{"enum":["draft","pending","active","expired","terminated"],"defaultValue":"draft"}},"Approval":{"user_id":{"required":true},"entity_type":{"required":true},"applicant_id":{"required":true},"status":{"enum":["pending","approved","rejected"],"defaultValue":"pending"}},"Customer":{"user_id":{"required":true},"name":{"required":true},"tags":{"defaultValue":"[]"}},"Reminder":{"user_id":{"required":true},"entity_type":{"required":true},"remind_at":{"required":true},"sent":{"defaultValue":"false"}},"Articles":{"id":{"required":true},"title":{"required":true}},"Tags":{"id":{"required":true},"name":{"required":true,"unique":true}},"Comments":{"id":{"required":true},"entity_type":{"required":true},"entity_id":{"required":true},"content":{"required":true}},"Media":{"id":{"required":true},"filename":{"required":true},"url":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO(文件存储)","deployment":"Docker Compose + Nginx / K8s(规模化)","considerations":[]},"architectureDiagram":"┌─────────────────────────────────────────────────────────┐\n│ SocialApp — System Architecture │\n├─────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────────────┐ ┌──────────────────────┐ │\n│ │ Client Layer │ │ Admin / Web │ │\n│ │ React Native 0.76 + Expo / Flutter 3.x │ │ React + Vite (Web) │ │\n│ └──────────┬───────────┘ └──────────┬───────────┘ │\n│ │ │ │\n│ └──────────┬────────────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ API Gateway │ │\n│ │ Nginx / Kong │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ Backend Services │ │\n│ │ NestJS + Prisma │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌───────────────┼───────────────┐ │\n│ │ │ │ │\n│ ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ │\n│ │ PostgreSQL 15│ │ Redis Cache │ │ MinIO │ │\n│ │ Primary │ │ Session │ │ Files │ │\n│ └───────────┘ └─────────────┘ └──────────┘ │\n│ │\n├─────────────────────────────────────────────────────────┤\n│ Modules: │\n│ 1 文章发布 │\n│ 2 分类标签 │\n│ 3 Content │\n│ 4 媒体库 │\n│ │\n└─────────────────────────────────────────────────────────┘","dataFlows":[{"page":"文章发布","route":"/articles","description":"文章发布","dataFlow":["GET /api/articles → 获取文章发布列表","POST /api/articles → 创建文章发布"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"分类标签","route":"/tags","description":"分类标签","dataFlow":["GET /api/tags → 获取分类标签列表","POST /api/tags → 创建分类标签"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"评论管理","route":"/comments","description":"评论管理","dataFlow":["GET /api/comments → 获取评论管理列表","POST /api/comments → 创建评论管理"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"媒体库","route":"/media","description":"媒体库","dataFlow":["GET /api/media → 获取媒体库列表","POST /api/media → 创建媒体库"],"direction":"Client → API Gateway → Backend → Database → Response → Client"}],"modules":[{"name":"文章发布","label":"文章发布","features":["文章发布"],"responsibilities":["提供 文章发布 相关功能"]},{"name":"分类标签","label":"分类标签","features":["分类标签"],"responsibilities":["提供 分类标签 相关功能"]},{"name":"content","label":"Content","features":["评论管理"],"responsibilities":["提供 评论管理 相关功能"]},{"name":"媒体库","label":"媒体库","features":["媒体库"],"responsibilities":["提供 媒体库 相关功能"]}],"databaseSchema":[{"table":"users","description":"用户表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"articles","description":"文章发布表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"content","type":"TEXT"},{"name":"excerpt","type":"TEXT"},{"name":"cover_url","type":"TEXT"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'draft'"},{"name":"author_id","type":"UUID","constraints":"FK → users.id"},{"name":"category","type":"VARCHAR(64)"},{"name":"published_at","type":"TIMESTAMPTZ"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_articles_user ON articles(user_id)"]},{"table":"tags","description":"分类标签表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"name","type":"VARCHAR(64)","constraints":"NOT NULL, UNIQUE"},{"name":"color","type":"VARCHAR(7)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_tags_user ON tags(user_id)"]},{"table":"comments","description":"评论管理表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"entity_type","type":"VARCHAR(32)","constraints":"NOT NULL"},{"name":"entity_id","type":"UUID","constraints":"NOT NULL"},{"name":"content","type":"TEXT","constraints":"NOT NULL"},{"name":"parent_id","type":"UUID","constraints":"FK → comments.id (自引用)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_comments_user ON comments(user_id)"]},{"table":"media","description":"媒体库表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"filename","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"url","type":"TEXT","constraints":"NOT NULL"},{"name":"mime_type","type":"VARCHAR(64)"},{"name":"size_bytes","type":"INTEGER"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_media_user ON media(user_id)"]}],"apiDesign":[{"resource":"articles","basePath":"/api/articles","endpoints":[{"method":"GET","path":"/api/articles","description":"获取文章发布列表"},{"method":"POST","path":"/api/articles","description":"创建文章发布"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}]},{"resource":"comments","basePath":"/api/comments","endpoints":[{"method":"GET","path":"/api/comments","description":"获取评论管理列表"},{"method":"POST","path":"/api/comments","description":"创建评论管理"}]},{"resource":"media","basePath":"/api/media","endpoints":[{"method":"GET","path":"/api/media","description":"获取媒体库列表"},{"method":"POST","path":"/api/media","description":"创建媒体库"}]},{"resource":"tags","basePath":"/api/tags","endpoints":[{"method":"GET","path":"/api/tags","description":"获取分类标签列表"},{"method":"POST","path":"/api/tags","description":"创建分类标签"}]}],"directoryStructure":["socialapp/","├── apps/","│ ├── mobile/ # React Native / Flutter 移动端","│ │ ├── src/","│ │ │ ├── screens/ # 页面组件","│ │ │ │ ├── 文章发布/","│ │ │ │ ├── 分类标签/","│ │ │ │ ├── content/","│ │ │ │ ├── 媒体库/","│ │ │ ├── components/ # 通用组件","│ │ │ ├── hooks/ # 自定义 Hooks","│ │ │ ├── services/ # API 调用层","│ │ │ ├── store/ # 状态管理","│ │ │ └── utils/ # 工具函数","│ │ ├── app.json","│ │ └── package.json","│ └── web/ # Web 管理后台","│ ├── src/","│ │ ├── pages/","│ │ ├── components/","│ │ └── layouts/","│ └── package.json","├── packages/","│ └── shared/ # 共享类型/常量","├── server/ # NestJS 后端服务","│ ├── src/","│ │ ├── modules/ # 业务模块","│ │ │ ├── 文章发布/","│ │ │ │ ├── 文章发布.controller.ts","│ │ │ │ ├── 文章发布.service.ts","│ │ │ │ ├── 文章发布.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 分类标签/","│ │ │ │ ├── 分类标签.controller.ts","│ │ │ │ ├── 分类标签.service.ts","│ │ │ │ ├── 分类标签.module.ts","│ │ │ │ └── dto/","│ │ │ ├── content/","│ │ │ │ ├── content.controller.ts","│ │ │ │ ├── content.service.ts","│ │ │ │ ├── content.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 媒体库/","│ │ │ │ ├── 媒体库.controller.ts","│ │ │ │ ├── 媒体库.service.ts","│ │ │ │ ├── 媒体库.module.ts","│ │ │ │ └── dto/","│ │ ├── common/ # 通用(guards, filters, interceptors)","│ │ ├── prisma/ # Prisma ORM","│ │ │ └── schema.prisma","│ │ ├── config/ # 环境配置","│ │ └── main.ts","│ ├── test/","│ ├── Dockerfile","│ └── package.json","├── docker-compose.yml","├── .github/workflows/ # CI/CD","│ └── ci.yml","├── .env.example","├── README.md","└── turbo.json # Monorepo 配置"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s(规模化)","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions → Build → Test → Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:35.601Z","sourceDomain":"social","moduleCount":4,"tableCount":5,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/blog-cms/backend/.gitignore b/.benchmark/blog-cms/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/blog-cms/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/blog-cms/backend/README.md b/.benchmark/blog-cms/backend/README.md new file mode 100644 index 0000000..945cc2f --- /dev/null +++ b/.benchmark/blog-cms/backend/README.md @@ -0,0 +1,118 @@ +# SocialApp — Backend API + +> 一款以内容分享和用户互动为核心的社交应用。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` — List all +- `GET /api/contracts/:id` — Get by ID +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List all +- `GET /api/approvals/:id` — Get by ID +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List all +- `GET /api/customers/:id` — Get by ID +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List all +- `GET /api/reminders/:id` — Get by ID +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### articles +- `GET /api/articles` — List all +- `GET /api/articles/:id` — Get by ID +- `POST /api/articles` — Create +- `PUT /api/articles/:id` — Update +- `DELETE /api/articles/:id` — Delete + +#### tags +- `GET /api/tags` — List all +- `GET /api/tags/:id` — Get by ID +- `POST /api/tags` — Create +- `PUT /api/tags/:id` — Update +- `DELETE /api/tags/:id` — Delete + +#### comments +- `GET /api/comments` — List all +- `GET /api/comments/:id` — Get by ID +- `POST /api/comments` — Create +- `PUT /api/comments/:id` — Update +- `DELETE /api/comments/:id` — Delete + +#### media +- `GET /api/media` — List all +- `GET /api/media/:id` — Get by ID +- `POST /api/media` — Create +- `PUT /api/media/:id` — Update +- `DELETE /api/media/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/blog-cms/backend/package.json b/.benchmark/blog-cms/backend/package.json new file mode 100644 index 0000000..61e5ea4 --- /dev/null +++ b/.benchmark/blog-cms/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "socialapp", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/backend/src/__tests__/approvals.test.ts b/.benchmark/blog-cms/backend/src/__tests__/approvals.test.ts new file mode 100644 index 0000000..392316d --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/approvals.test.ts @@ -0,0 +1,121 @@ +// Approval CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "applicantId": regRes.json().user.id, + "status": "sample-status", + "formData": "sample-formdata" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/approvals", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/approvals", () => { + it("creates a approval", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/approvals/:id", () => { + it("returns the created approval", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/approvals/:id", () => { + it("updates the approval", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/approvals/:id", () => { + it("deletes the approval", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/articles.test.ts b/.benchmark/blog-cms/backend/src/__tests__/articles.test.ts new file mode 100644 index 0000000..efd81e7 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/articles.test.ts @@ -0,0 +1,123 @@ +// Articles CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "content": "sample-content", + "excerpt": "sample-excerpt", + "coverUrl": "sample-coverurl", + "authorId": "sample-authorid", + "category": "sample-category", + "publishedAt": "sample-publishedat" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/articles", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/articles", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/articles", () => { + it("creates a article", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/articles", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/articles/:id", () => { + it("returns the created article", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/articles/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/articles/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/articles/:id", () => { + it("updates the article", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/articles/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/articles/:id", () => { + it("deletes the article", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/articles/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/articles/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/auth.test.ts b/.benchmark/blog-cms/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/comments.test.ts b/.benchmark/blog-cms/backend/src/__tests__/comments.test.ts new file mode 100644 index 0000000..9e3145d --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/comments.test.ts @@ -0,0 +1,120 @@ +// Comments CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "content": "sample-content", + "parentId": "sample-parentid" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/comments", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/comments", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/comments", () => { + it("creates a comment", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/comments", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/comments/:id", () => { + it("returns the created comment", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/comments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/comments/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/comments/:id", () => { + it("updates the comment", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/comments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/comments/:id", () => { + it("deletes the comment", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/comments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/comments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/contracts.test.ts b/.benchmark/blog-cms/backend/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..c2e9f16 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/contracts.test.ts @@ -0,0 +1,124 @@ +// Contract CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "partyA": "sample-partya", + "partyB": "sample-partyb", + "amount": 1, + "signedAt": "sample-signedat", + "expiresAt": "sample-expiresat", + "status": "sample-status", + "fileUrl": "sample-fileurl" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/contracts", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/contracts", () => { + it("creates a contract", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/contracts/:id", () => { + it("returns the created contract", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/contracts/:id", () => { + it("updates the contract", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/contracts/:id", () => { + it("deletes the contract", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/customers.test.ts b/.benchmark/blog-cms/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..f3112fa --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/customers.test.ts @@ -0,0 +1,122 @@ +// Customer CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source", + "tags": "sample-tags" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/items.test.ts b/.benchmark/blog-cms/backend/src/__tests__/items.test.ts new file mode 100644 index 0000000..7136843 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/items.test.ts @@ -0,0 +1,118 @@ +// Item CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/media.test.ts b/.benchmark/blog-cms/backend/src/__tests__/media.test.ts new file mode 100644 index 0000000..9c5188b --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/media.test.ts @@ -0,0 +1,120 @@ +// Media CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "filename": "sample-filename", + "url": "sample-url", + "mimeType": "sample-mimetype", + "sizeBytes": 1 + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/media", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/media", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/media", () => { + it("creates a media", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/media", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/media/:id", () => { + it("returns the created media", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/media/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/media/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/media/:id", () => { + it("updates the media", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/media/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/media/:id", () => { + it("deletes the media", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/media/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/media/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/reminders.test.ts b/.benchmark/blog-cms/backend/src/__tests__/reminders.test.ts new file mode 100644 index 0000000..c573030 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/reminders.test.ts @@ -0,0 +1,121 @@ +// Reminder CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "remindAt": "sample-remindat", + "message": "sample-message", + "sent": true + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/reminders", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/reminders", () => { + it("creates a reminder", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/reminders/:id", () => { + it("returns the created reminder", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/reminders/:id", () => { + it("updates the reminder", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/reminders/:id", () => { + it("deletes the reminder", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/tags.test.ts b/.benchmark/blog-cms/backend/src/__tests__/tags.test.ts new file mode 100644 index 0000000..dc00d1f --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/tags.test.ts @@ -0,0 +1,118 @@ +// Tags CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "color": "sample-color" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/tags", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/tags", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/tags", () => { + it("creates a tag", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/tags", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/tags/:id", () => { + it("returns the created tag", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/tags/:id", () => { + it("updates the tag", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/tags/:id", () => { + it("deletes the tag", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/users.test.ts b/.benchmark/blog-cms/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/db/client.ts b/.benchmark/blog-cms/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/blog-cms/backend/src/db/schema.ts b/.benchmark/blog-cms/backend/src/db/schema.ts new file mode 100644 index 0000000..c1e9b27 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/db/schema.ts @@ -0,0 +1,20 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS articles (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n content TEXT,\n excerpt TEXT,\n cover_url TEXT,\n status TEXT,\n author_id TEXT REFERENCES authors(id),\n category TEXT,\n published_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS tags (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL UNIQUE,\n color TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n content TEXT NOT NULL,\n parent_id TEXT REFERENCES parents(id),\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS media (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n filename TEXT NOT NULL,\n url TEXT NOT NULL,\n mime_type TEXT,\n size_bytes INTEGER,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/blog-cms/backend/src/index.ts b/.benchmark/blog-cms/backend/src/index.ts new file mode 100644 index 0000000..b663dcf --- /dev/null +++ b/.benchmark/blog-cms/backend/src/index.ts @@ -0,0 +1,79 @@ +// SocialApp — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { articlesRoutes } from "./routes/articles.js"; +import { tagsRoutes } from "./routes/tags.js"; +import { commentsRoutes } from "./routes/comments.js"; +import { mediaRoutes } from "./routes/media.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-5f616668"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(articlesRoutes, { prefix: "/api/articles" }); + await app.register(tagsRoutes, { prefix: "/api/tags" }); + await app.register(commentsRoutes, { prefix: "/api/comments" }); + await app.register(mediaRoutes, { prefix: "/api/media" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/blog-cms/backend/src/middleware/auth.ts b/.benchmark/blog-cms/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/blog-cms/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/blog-cms/backend/src/routes/approvals.ts b/.benchmark/blog-cms/backend/src/routes/approvals.ts new file mode 100644 index 0000000..3a974d2 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/approvals.ts @@ -0,0 +1,51 @@ +// Auto-generated Approval routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ApprovalService } from "../services/approval.js"; +import type { CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +const service = new ApprovalService(); + +export async function approvalsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/approvals — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/approvals/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/approvals — create + app.post<{ Body: CreateApprovalInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/approvals/:id — update + app.put<{ Params: { id: string }; Body: UpdateApprovalInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/approvals/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/articles.ts b/.benchmark/blog-cms/backend/src/routes/articles.ts new file mode 100644 index 0000000..5a2ee89 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/articles.ts @@ -0,0 +1,51 @@ +// Auto-generated Articles routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ArticlesService } from "../services/article.js"; +import type { CreateArticlesInput, UpdateArticlesInput } from "../types/index.js"; + +const service = new ArticlesService(); + +export async function articlesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/articles — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/articles/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Articles not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/articles — create + app.post<{ Body: CreateArticlesInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/articles/:id — update + app.put<{ Params: { id: string }; Body: UpdateArticlesInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Articles not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/articles/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Articles not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/auth.ts b/.benchmark/blog-cms/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/comments.ts b/.benchmark/blog-cms/backend/src/routes/comments.ts new file mode 100644 index 0000000..fe2824f --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/comments.ts @@ -0,0 +1,51 @@ +// Auto-generated Comments routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CommentsService } from "../services/comment.js"; +import type { CreateCommentsInput, UpdateCommentsInput } from "../types/index.js"; + +const service = new CommentsService(); + +export async function commentsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/comments — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/comments/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Comments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/comments — create + app.post<{ Body: CreateCommentsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/comments/:id — update + app.put<{ Params: { id: string }; Body: UpdateCommentsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Comments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/comments/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Comments not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/contracts.ts b/.benchmark/blog-cms/backend/src/routes/contracts.ts new file mode 100644 index 0000000..ee83abf --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/contracts.ts @@ -0,0 +1,51 @@ +// Auto-generated Contract routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ContractService } from "../services/contract.js"; +import type { CreateContractInput, UpdateContractInput } from "../types/index.js"; + +const service = new ContractService(); + +export async function contractsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/contracts — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/contracts/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/contracts — create + app.post<{ Body: CreateContractInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/contracts/:id — update + app.put<{ Params: { id: string }; Body: UpdateContractInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/contracts/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/customers.ts b/.benchmark/blog-cms/backend/src/routes/customers.ts new file mode 100644 index 0000000..9a6f981 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customer routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomerService } from "../services/customer.js"; +import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +const service = new CustomerService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers — create + app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id — update + app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/items.ts b/.benchmark/blog-cms/backend/src/routes/items.ts new file mode 100644 index 0000000..2ad945e --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Item routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemService } from "../services/item.js"; +import type { CreateItemInput, UpdateItemInput } from "../types/index.js"; + +const service = new ItemService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items — create + app.post<{ Body: CreateItemInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id — update + app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/media.ts b/.benchmark/blog-cms/backend/src/routes/media.ts new file mode 100644 index 0000000..b3ec363 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/media.ts @@ -0,0 +1,51 @@ +// Auto-generated Media routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { MediaService } from "../services/media.js"; +import type { CreateMediaInput, UpdateMediaInput } from "../types/index.js"; + +const service = new MediaService(); + +export async function mediaRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/media — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/media/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Media not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/media — create + app.post<{ Body: CreateMediaInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/media/:id — update + app.put<{ Params: { id: string }; Body: UpdateMediaInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Media not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/media/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Media not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/reminders.ts b/.benchmark/blog-cms/backend/src/routes/reminders.ts new file mode 100644 index 0000000..ee00754 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/reminders.ts @@ -0,0 +1,51 @@ +// Auto-generated Reminder routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ReminderService } from "../services/reminder.js"; +import type { CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +const service = new ReminderService(); + +export async function remindersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/reminders — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/reminders/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/reminders — create + app.post<{ Body: CreateReminderInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/reminders/:id — update + app.put<{ Params: { id: string }; Body: UpdateReminderInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/reminders/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/tags.ts b/.benchmark/blog-cms/backend/src/routes/tags.ts new file mode 100644 index 0000000..511a348 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/tags.ts @@ -0,0 +1,51 @@ +// Auto-generated Tags routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { TagsService } from "../services/tag.js"; +import type { CreateTagsInput, UpdateTagsInput } from "../types/index.js"; + +const service = new TagsService(); + +export async function tagsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/tags — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/tags/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Tags not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/tags — create + app.post<{ Body: CreateTagsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/tags/:id — update + app.put<{ Params: { id: string }; Body: UpdateTagsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Tags not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/tags/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Tags not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/users.ts b/.benchmark/blog-cms/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/services/approval.ts b/.benchmark/blog-cms/backend/src/services/approval.ts new file mode 100644 index 0000000..f40b2a2 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/approval.ts @@ -0,0 +1,56 @@ +// Auto-generated Approval service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Approval, CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +export class ApprovalService { + /** List all approvals */ + list(): Approval[] { + return queryAll("SELECT * FROM approvals ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Approval | undefined { + return queryOne("SELECT * FROM approvals WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateApprovalInput): Approval { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "applicant_id", "status", "form_data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.applicantId ?? null, input.status ?? null, input.formData ?? null, now, now]; + + execute(`INSERT INTO approvals (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateApprovalInput): Approval | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.applicantId !== undefined) { sets.push("applicant_id = ?"); values.push(input.applicantId); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.formData !== undefined) { sets.push("form_data = ?"); values.push(input.formData); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE approvals SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM approvals WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/article.ts b/.benchmark/blog-cms/backend/src/services/article.ts new file mode 100644 index 0000000..500ae9e --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/article.ts @@ -0,0 +1,58 @@ +// Auto-generated Articles service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Articles, CreateArticlesInput, UpdateArticlesInput } from "../types/index.js"; + +export class ArticlesService { + /** List all articles */ + list(): Articles[] { + return queryAll("SELECT * FROM articles ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Articles | undefined { + return queryOne("SELECT * FROM articles WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateArticlesInput): Articles { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "content", "excerpt", "cover_url", "author_id", "category", "published_at", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.content ?? null, input.excerpt ?? null, input.coverUrl ?? null, input.authorId ?? null, input.category ?? null, input.publishedAt ?? null, now, now]; + + execute(`INSERT INTO articles (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateArticlesInput): Articles | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); } + if (input.excerpt !== undefined) { sets.push("excerpt = ?"); values.push(input.excerpt); } + if (input.coverUrl !== undefined) { sets.push("cover_url = ?"); values.push(input.coverUrl); } + if (input.authorId !== undefined) { sets.push("author_id = ?"); values.push(input.authorId); } + if (input.category !== undefined) { sets.push("category = ?"); values.push(input.category); } + if (input.publishedAt !== undefined) { sets.push("published_at = ?"); values.push(input.publishedAt); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE articles SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM articles WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/comment.ts b/.benchmark/blog-cms/backend/src/services/comment.ts new file mode 100644 index 0000000..fee428e --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/comment.ts @@ -0,0 +1,55 @@ +// Auto-generated Comments service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Comments, CreateCommentsInput, UpdateCommentsInput } from "../types/index.js"; + +export class CommentsService { + /** List all comments */ + list(): Comments[] { + return queryAll("SELECT * FROM comments ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Comments | undefined { + return queryOne("SELECT * FROM comments WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCommentsInput): Comments { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "content", "parent_id", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.content ?? null, input.parentId ?? null, now, now]; + + execute(`INSERT INTO comments (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCommentsInput): Comments | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); } + if (input.parentId !== undefined) { sets.push("parent_id = ?"); values.push(input.parentId); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE comments SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM comments WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/contract.ts b/.benchmark/blog-cms/backend/src/services/contract.ts new file mode 100644 index 0000000..7980109 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/contract.ts @@ -0,0 +1,59 @@ +// Auto-generated Contract service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Contract, CreateContractInput, UpdateContractInput } from "../types/index.js"; + +export class ContractService { + /** List all contracts */ + list(): Contract[] { + return queryAll("SELECT * FROM contracts ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Contract | undefined { + return queryOne("SELECT * FROM contracts WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateContractInput): Contract { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "party_a", "party_b", "amount", "signed_at", "expires_at", "status", "file_url", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.partyA ?? null, input.partyB ?? null, input.amount ?? null, input.signedAt ?? null, input.expiresAt ?? null, input.status ?? null, input.fileUrl ?? null, now, now]; + + execute(`INSERT INTO contracts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateContractInput): Contract | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.partyA !== undefined) { sets.push("party_a = ?"); values.push(input.partyA); } + if (input.partyB !== undefined) { sets.push("party_b = ?"); values.push(input.partyB); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.signedAt !== undefined) { sets.push("signed_at = ?"); values.push(input.signedAt); } + if (input.expiresAt !== undefined) { sets.push("expires_at = ?"); values.push(input.expiresAt); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.fileUrl !== undefined) { sets.push("file_url = ?"); values.push(input.fileUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM contracts WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/customer.ts b/.benchmark/blog-cms/backend/src/services/customer.ts new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/customer.ts @@ -0,0 +1,57 @@ +// Auto-generated Customer service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +export class CustomerService { + /** List all customers */ + list(): Customer[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customer | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomerInput): Customer { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "tags", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, input.tags ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomerInput): Customer | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + if (input.tags !== undefined) { sets.push("tags = ?"); values.push(input.tags); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/item.ts b/.benchmark/blog-cms/backend/src/services/item.ts new file mode 100644 index 0000000..24ec745 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/item.ts @@ -0,0 +1,55 @@ +// Auto-generated Item service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js"; + +export class ItemService { + /** List all items */ + list(): Item[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Item | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemInput): Item { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "title", "data", "created_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.data ?? null, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemInput): Item | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/media.ts b/.benchmark/blog-cms/backend/src/services/media.ts new file mode 100644 index 0000000..6a43a00 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/media.ts @@ -0,0 +1,55 @@ +// Auto-generated Media service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Media, CreateMediaInput, UpdateMediaInput } from "../types/index.js"; + +export class MediaService { + /** List all media */ + list(): Media[] { + return queryAll("SELECT * FROM media ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Media | undefined { + return queryOne("SELECT * FROM media WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateMediaInput): Media { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "filename", "url", "mime_type", "size_bytes", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.filename ?? null, input.url ?? null, input.mimeType ?? null, input.sizeBytes ?? null, now, now]; + + execute(`INSERT INTO media (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateMediaInput): Media | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.filename !== undefined) { sets.push("filename = ?"); values.push(input.filename); } + if (input.url !== undefined) { sets.push("url = ?"); values.push(input.url); } + if (input.mimeType !== undefined) { sets.push("mime_type = ?"); values.push(input.mimeType); } + if (input.sizeBytes !== undefined) { sets.push("size_bytes = ?"); values.push(input.sizeBytes); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE media SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM media WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/reminder.ts b/.benchmark/blog-cms/backend/src/services/reminder.ts new file mode 100644 index 0000000..8c19dd5 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/reminder.ts @@ -0,0 +1,56 @@ +// Auto-generated Reminder service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Reminder, CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +export class ReminderService { + /** List all reminders */ + list(): Reminder[] { + return queryAll("SELECT * FROM reminders ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Reminder | undefined { + return queryOne("SELECT * FROM reminders WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateReminderInput): Reminder { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "remind_at", "message", "sent", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.remindAt ?? null, input.message ?? null, input.sent ?? null, now, now]; + + execute(`INSERT INTO reminders (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateReminderInput): Reminder | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.remindAt !== undefined) { sets.push("remind_at = ?"); values.push(input.remindAt); } + if (input.message !== undefined) { sets.push("message = ?"); values.push(input.message); } + if (input.sent !== undefined) { sets.push("sent = ?"); values.push(input.sent); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE reminders SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM reminders WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/tag.ts b/.benchmark/blog-cms/backend/src/services/tag.ts new file mode 100644 index 0000000..073aa39 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/tag.ts @@ -0,0 +1,53 @@ +// Auto-generated Tags service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Tags, CreateTagsInput, UpdateTagsInput } from "../types/index.js"; + +export class TagsService { + /** List all tags */ + list(): Tags[] { + return queryAll("SELECT * FROM tags ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Tags | undefined { + return queryOne("SELECT * FROM tags WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateTagsInput): Tags { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "color", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.color ?? null, now, now]; + + execute(`INSERT INTO tags (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateTagsInput): Tags | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.color !== undefined) { sets.push("color = ?"); values.push(input.color); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE tags SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM tags WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/user.ts b/.benchmark/blog-cms/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/types/fastify.d.ts b/.benchmark/blog-cms/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/blog-cms/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/blog-cms/backend/src/types/index.ts b/.benchmark/blog-cms/backend/src/types/index.ts new file mode 100644 index 0000000..22919a9 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/types/index.ts @@ -0,0 +1,320 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Articles { + id: string; + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + status?: string; + authorId?: string; + category?: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Tags { + id: string; + userId?: string; + name: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Comments { + id: string; + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Media { + id: string; + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateArticlesInput { + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + authorId?: string; + category?: string; + publishedAt?: string; +} + +export interface CreateTagsInput { + userId?: string; + name: string; + color?: string; +} + +export interface CreateCommentsInput { + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; +} + +export interface CreateMediaInput { + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateArticlesInput { + userId?: string; + title?: string; + content?: string; + excerpt?: string; + coverUrl?: string; + authorId?: string; + category?: string; + publishedAt?: string; +} + +export interface UpdateTagsInput { + userId?: string; + name?: string; + color?: string; +} + +export interface UpdateCommentsInput { + userId?: string; + entityType?: string; + entityId?: string; + content?: string; + parentId?: string; +} + +export interface UpdateMediaInput { + userId?: string; + filename?: string; + url?: string; + mimeType?: string; + sizeBytes?: number; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/blog-cms/backend/src/types/sql.js.d.ts b/.benchmark/blog-cms/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/blog-cms/backend/tsconfig.json b/.benchmark/blog-cms/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/blog-cms/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/electron/README.md b/.benchmark/blog-cms/electron/README.md new file mode 100644 index 0000000..c65d8f8 --- /dev/null +++ b/.benchmark/blog-cms/electron/README.md @@ -0,0 +1,82 @@ +# socialapp — Desktop + +> Electron desktop application wrapping the socialapp fullstack web project. + +## Architecture + +``` +Web Fullstack Project + × + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** — Window management, menu, tray, IPC, auto-update +- **Preload** — Secure bridge between main and renderer +- **Renderer** — Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +├── main.ts — Main process (BrowserWindow, Menu, lifecycle) +├── preload.ts — Secure IPC bridge (contextBridge) +├── ipc.ts — IPC handlers (dialogs, store, notifications) +├── tray.ts — System tray +├── updater.ts — Auto-update (electron-updater) +└── utils.ts — Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/blog-cms/electron/assets/icon.png b/.benchmark/blog-cms/electron/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..554e8a31d1f0205fbf5ef853aba9a4fa2fc490dd GIT binary patch literal 66 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv4;KqeCd<5Tr}e}F6o MPgg&ebxsLQ09s=V>;M1& literal 0 HcmV?d00001 diff --git a/.benchmark/blog-cms/electron/electron-builder.yml b/.benchmark/blog-cms/electron/electron-builder.yml new file mode 100644 index 0000000..bb40cff --- /dev/null +++ b/.benchmark/blog-cms/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml — Auto-generated by SF-06 + +appId: com.socialapp.desktop +productName: socialapp +copyright: "Copyright © 2026 socialapp" + +# ─── Directories ───────────────────────────────── +directories: + output: release + buildResources: assets + +# ─── Files ─────────────────────────────────────── +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# ─── Extra Resources ───────────────────────────── +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# ─── macOS ─────────────────────────────────────── +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# ─── Windows ───────────────────────────────────── +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: socialapp + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: socialapp + +# ─── Linux ─────────────────────────────────────── +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: socialapp + Comment: socialapp Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# ─── Auto Update ───────────────────────────────── +publish: + provider: generic + url: https://releases.example.com/socialapp/ + channel: latest diff --git a/.benchmark/blog-cms/electron/electron/ipc.ts b/.benchmark/blog-cms/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // ── App Info ── + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // ── Window Controls ── + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // ── File Dialogs ── + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // ── Notifications ── + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // ── Shell ── + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // ── Persistent Store ── + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/blog-cms/electron/electron/main.ts b/.benchmark/blog-cms/electron/electron/main.ts new file mode 100644 index 0000000..9333acb --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// ─── Constants ─────────────────────────────────── +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "socialapp"; + +// ─── Single Instance Lock ──────────────────────── +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// ─── State ─────────────────────────────────────── +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// ─── Create Window ─────────────────────────────── +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // ── Load content ── + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // ── Show when ready ── + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // ── External links → default browser ── + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // ── Prevent navigation away ── + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// ─── Icon Path ─────────────────────────────────── +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// ─── API Server (Production) ───────────────────── +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// ─── Menu ──────────────────────────────────────── +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// ─── App Lifecycle ─────────────────────────────── +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed — continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/blog-cms/electron/electron/preload.ts b/.benchmark/blog-cms/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// ─── Expose safe API to renderer ───────────────── +contextBridge.exposeInMainWorld("electronAPI", { + // ── App Info ── + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // ── Window Controls ── + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // ── File Dialogs ── + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // ── Notifications ── + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // ── Shell ── + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // ── Store ── + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // ── Updates ── + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // ── Update Events ── + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // ── Remove listener ── + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// ─── Type declarations for renderer ────────────── +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/blog-cms/electron/electron/tray.ts b/.benchmark/blog-cms/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/blog-cms/electron/electron/updater.ts b/.benchmark/blog-cms/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/blog-cms/electron/electron/utils.ts b/.benchmark/blog-cms/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// ─── Single Instance Lock ──────────────────────── +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// ─── Dev mode detection ────────────────────────── +export function isDev(): boolean { + return !app.isPackaged; +} + +// ─── Web URL ───────────────────────────────────── +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// ─── API Path ──────────────────────────────────── +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// ─── Get API port from env ─────────────────────── +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/blog-cms/electron/entitlements.mac.plist b/.benchmark/blog-cms/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/blog-cms/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/blog-cms/electron/package.json b/.benchmark/blog-cms/electron/package.json new file mode 100644 index 0000000..47866c3 --- /dev/null +++ b/.benchmark/blog-cms/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "socialapp-desktop", + "version": "1.0.0", + "private": true, + "description": "socialapp — Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "socialapp", + "appId": "com.socialapp.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/electron/tsconfig.json b/.benchmark/blog-cms/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/blog-cms/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/frontend/app/articles/page.tsx b/.benchmark/blog-cms/frontend/app/articles/page.tsx new file mode 100644 index 0000000..b3111b3 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/articles/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useArticles } from "@/hooks/useArticles"; + +export default function PagePage() { + const { data, loading, error, refetch } = useArticles(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

文章发布

+

文章发布

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/comments/page.tsx b/.benchmark/blog-cms/frontend/app/comments/page.tsx new file mode 100644 index 0000000..0ed16e3 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/comments/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useComments } from "@/hooks/useComments"; + +export default function PagePage() { + const { data, loading, error, refetch } = useComments(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

评论管理

+

评论管理

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/globals.css b/.benchmark/blog-cms/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/blog-cms/frontend/app/layout.tsx b/.benchmark/blog-cms/frontend/app/layout.tsx new file mode 100644 index 0000000..3613994 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "SocialApp", + description: "一款以内容分享和用户互动为核心的社交应用。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/blog-cms/frontend/app/media/page.tsx b/.benchmark/blog-cms/frontend/app/media/page.tsx new file mode 100644 index 0000000..8297264 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/media/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useMedia } from "@/hooks/useMedia"; + +export default function PagePage() { + const { data, loading, error, refetch } = useMedia(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

媒体库

+

媒体库

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/messages/page.tsx b/.benchmark/blog-cms/frontend/app/messages/page.tsx new file mode 100644 index 0000000..bd9a20e --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/messages/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

消息

+

私信列表、聊天

+
+ +
+ + { +

消息 页面内容

+

路由: /messages

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/page.tsx b/.benchmark/blog-cms/frontend/app/page.tsx new file mode 100644 index 0000000..eb4a2cc --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

SocialApp

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 文章发布 + + + 📅 + 分类标签 + + + 📝 + 评论管理 + + + 📸 + 媒体库 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 SocialApp

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/profile/[userId]/page.tsx b/.benchmark/blog-cms/frontend/app/profile/[userId]/page.tsx new file mode 100644 index 0000000..3933192 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/profile/[userId]/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

个人

+

个人主页

+
+ +
+ + { +

个人 页面内容

+

路由: /profile/:userId

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/publish/page.tsx b/.benchmark/blog-cms/frontend/app/publish/page.tsx new file mode 100644 index 0000000..e09151b --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/publish/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

发布

+

编辑器、相册选择

+
+ +
+ + { +

发布 页面内容

+

路由: /publish

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/tags/page.tsx b/.benchmark/blog-cms/frontend/app/tags/page.tsx new file mode 100644 index 0000000..c9d5031 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/tags/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useTags } from "@/hooks/useTags"; + +export default function PagePage() { + const { data, loading, error, refetch } = useTags(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

分类标签

+

分类标签

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/分类标签/page.tsx b/.benchmark/blog-cms/frontend/app/分类标签/page.tsx new file mode 100644 index 0000000..64f5c2a --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/分类标签/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use分类标签 } from "@/hooks/use分类标签"; + +export default function PagePage() { + const { data, loading, error, refetch } = use分类标签(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

分类标签

+

分类标签

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/媒体库/page.tsx b/.benchmark/blog-cms/frontend/app/媒体库/page.tsx new file mode 100644 index 0000000..dd0093f --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/媒体库/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use媒体库 } from "@/hooks/use媒体库"; + +export default function PagePage() { + const { data, loading, error, refetch } = use媒体库(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

媒体库

+

媒体库

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/文章发布/page.tsx b/.benchmark/blog-cms/frontend/app/文章发布/page.tsx new file mode 100644 index 0000000..577a7ca --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/文章发布/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use文章发布 } from "@/hooks/use文章发布"; + +export default function PagePage() { + const { data, loading, error, refetch } = use文章发布(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

文章发布

+

文章发布

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/评论管理/page.tsx b/.benchmark/blog-cms/frontend/app/评论管理/page.tsx new file mode 100644 index 0000000..eca5fb4 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/评论管理/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use评论管理 } from "@/hooks/use评论管理"; + +export default function PagePage() { + const { data, loading, error, refetch } = use评论管理(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

评论管理

+

评论管理

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/components/layout/BottomNav.tsx b/.benchmark/blog-cms/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/blog-cms/frontend/components/layout/Sidebar.tsx b/.benchmark/blog-cms/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/Button.tsx b/.benchmark/blog-cms/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/Card.tsx b/.benchmark/blog-cms/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/EmptyState.tsx b/.benchmark/blog-cms/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/Input.tsx b/.benchmark/blog-cms/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/Modal.tsx b/.benchmark/blog-cms/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/blog-cms/frontend/hooks/useArticles.ts b/.benchmark/blog-cms/frontend/hooks/useArticles.ts new file mode 100644 index 0000000..17a123b --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useArticles.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for articles +type Article = Record; + +/** + * Fetch and manage articles data. + */ +export function useArticles() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/articles`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useAuth.ts b/.benchmark/blog-cms/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useComments.ts b/.benchmark/blog-cms/frontend/hooks/useComments.ts new file mode 100644 index 0000000..b6bfd57 --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useComments.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for comments +type Comment = Record; + +/** + * Fetch and manage comments data. + */ +export function useComments() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/comments`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useDebounce.ts b/.benchmark/blog-cms/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useFeed.ts b/.benchmark/blog-cms/frontend/hooks/useFeed.ts new file mode 100644 index 0000000..18364d8 --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useFeed.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for feed +type Feed = Record; + +/** + * Fetch and manage feed data. + */ +export function useFeed() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/feed`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useForm.ts b/.benchmark/blog-cms/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useItem.ts b/.benchmark/blog-cms/frontend/hooks/useItem.ts new file mode 100644 index 0000000..afd3121 --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useItem.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for 评论管理 +type Item = Record; + +/** + * Fetch and manage 评论管理 data. + */ +export function useItem() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/评论管理`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useMedia.ts b/.benchmark/blog-cms/frontend/hooks/useMedia.ts new file mode 100644 index 0000000..9f8e38f --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useMedia.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for media +type Media = Record; + +/** + * Fetch and manage media data. + */ +export function useMedia() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/media`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/usePosts.ts b/.benchmark/blog-cms/frontend/hooks/usePosts.ts new file mode 100644 index 0000000..6fa9aae --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/usePosts.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for posts +type Post = Record; + +/** + * Fetch and manage posts data. + */ +export function usePosts() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/posts`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useTags.ts b/.benchmark/blog-cms/frontend/hooks/useTags.ts new file mode 100644 index 0000000..ff0d19b --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useTags.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for tags +type Tag = Record; + +/** + * Fetch and manage tags data. + */ +export function useTags() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/tags`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useUsers.ts b/.benchmark/blog-cms/frontend/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/next-env.d.ts b/.benchmark/blog-cms/frontend/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/.benchmark/blog-cms/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/.benchmark/blog-cms/frontend/next.config.ts b/.benchmark/blog-cms/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/blog-cms/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/blog-cms/frontend/package.json b/.benchmark/blog-cms/frontend/package.json new file mode 100644 index 0000000..68e4bcf --- /dev/null +++ b/.benchmark/blog-cms/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "socialapp", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/frontend/postcss.config.js b/.benchmark/blog-cms/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/blog-cms/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/blog-cms/frontend/services/api.ts b/.benchmark/blog-cms/frontend/services/api.ts new file mode 100644 index 0000000..959cc20 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for SocialApp + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/blog-cms/frontend/services/articles.ts b/.benchmark/blog-cms/frontend/services/articles.ts new file mode 100644 index 0000000..ca7cee4 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/articles.ts @@ -0,0 +1,10 @@ +// Auto-generated articles service +import { api } from "./api"; + +export function get() { + return api.get("/api/articles"); +} + +export function post(data: Record) { + return api.post("/api/articles", data); +} diff --git a/.benchmark/blog-cms/frontend/services/auth.ts b/.benchmark/blog-cms/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/blog-cms/frontend/services/comments.ts b/.benchmark/blog-cms/frontend/services/comments.ts new file mode 100644 index 0000000..7df69f4 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/comments.ts @@ -0,0 +1,10 @@ +// Auto-generated comments service +import { api } from "./api"; + +export function get() { + return api.get("/api/comments"); +} + +export function post(data: Record) { + return api.post("/api/comments", data); +} diff --git a/.benchmark/blog-cms/frontend/services/feed.ts b/.benchmark/blog-cms/frontend/services/feed.ts new file mode 100644 index 0000000..85b119b --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/feed.ts @@ -0,0 +1,6 @@ +// Auto-generated feed service +import { api } from "./api"; + +export function get() { + return api.get("/api/feed"); +} diff --git a/.benchmark/blog-cms/frontend/services/media.ts b/.benchmark/blog-cms/frontend/services/media.ts new file mode 100644 index 0000000..5d7297c --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/media.ts @@ -0,0 +1,10 @@ +// Auto-generated media service +import { api } from "./api"; + +export function get() { + return api.get("/api/media"); +} + +export function post(data: Record) { + return api.post("/api/media", data); +} diff --git a/.benchmark/blog-cms/frontend/services/posts.ts b/.benchmark/blog-cms/frontend/services/posts.ts new file mode 100644 index 0000000..6aeb9b7 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/posts.ts @@ -0,0 +1,10 @@ +// Auto-generated posts service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/posts", data); +} + +export function post_id_like(data: Record) { + return api.post("/api/posts", data); +} diff --git a/.benchmark/blog-cms/frontend/services/tags.ts b/.benchmark/blog-cms/frontend/services/tags.ts new file mode 100644 index 0000000..7d52e35 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/tags.ts @@ -0,0 +1,10 @@ +// Auto-generated tags service +import { api } from "./api"; + +export function get() { + return api.get("/api/tags"); +} + +export function post(data: Record) { + return api.post("/api/tags", data); +} diff --git a/.benchmark/blog-cms/frontend/services/users.ts b/.benchmark/blog-cms/frontend/services/users.ts new file mode 100644 index 0000000..4b573b4 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/users.ts @@ -0,0 +1,6 @@ +// Auto-generated users service +import { api } from "./api"; + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/blog-cms/frontend/services/分类标签.ts b/.benchmark/blog-cms/frontend/services/分类标签.ts new file mode 100644 index 0000000..91394a3 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/分类标签.ts @@ -0,0 +1,10 @@ +// Auto-generated 分类标签 service +import { api } from "./api"; + +export function get() { + return api.get("/api/分类标签"); +} + +export function post(data: Record) { + return api.post("/api/分类标签", data); +} diff --git a/.benchmark/blog-cms/frontend/services/媒体库.ts b/.benchmark/blog-cms/frontend/services/媒体库.ts new file mode 100644 index 0000000..c880489 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/媒体库.ts @@ -0,0 +1,10 @@ +// Auto-generated 媒体库 service +import { api } from "./api"; + +export function get() { + return api.get("/api/媒体库"); +} + +export function post(data: Record) { + return api.post("/api/媒体库", data); +} diff --git a/.benchmark/blog-cms/frontend/services/文章发布.ts b/.benchmark/blog-cms/frontend/services/文章发布.ts new file mode 100644 index 0000000..903f3d5 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/文章发布.ts @@ -0,0 +1,10 @@ +// Auto-generated 文章发布 service +import { api } from "./api"; + +export function get() { + return api.get("/api/文章发布"); +} + +export function post(data: Record) { + return api.post("/api/文章发布", data); +} diff --git a/.benchmark/blog-cms/frontend/services/评论管理.ts b/.benchmark/blog-cms/frontend/services/评论管理.ts new file mode 100644 index 0000000..f96682d --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/评论管理.ts @@ -0,0 +1,10 @@ +// Auto-generated 评论管理 service +import { api } from "./api"; + +export function get() { + return api.get("/api/评论管理"); +} + +export function post(data: Record) { + return api.post("/api/评论管理", data); +} diff --git a/.benchmark/blog-cms/frontend/tailwind.config.ts b/.benchmark/blog-cms/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/blog-cms/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/blog-cms/frontend/tsconfig.json b/.benchmark/blog-cms/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/blog-cms/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/frontend/types/index.ts b/.benchmark/blog-cms/frontend/types/index.ts new file mode 100644 index 0000000..0f93fab --- /dev/null +++ b/.benchmark/blog-cms/frontend/types/index.ts @@ -0,0 +1,130 @@ +// Auto-generated types for SocialApp + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Articles { + id: string; + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + status?: string; + authorId?: string; + category?: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Tags { + id: string; + userId?: string; + name: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Comments { + id: string; + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Media { + id: string; + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/.gitignore b/.benchmark/blog-cms/fullstack/.gitignore new file mode 100644 index 0000000..7488f6e --- /dev/null +++ b/.benchmark/blog-cms/fullstack/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.next/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/blog-cms/fullstack/README.md b/.benchmark/blog-cms/fullstack/README.md new file mode 100644 index 0000000..6341f36 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/README.md @@ -0,0 +1,125 @@ +# SocialApp — Fullstack Project + +> 一款以内容分享和用户互动为核心的社交应用。 + +## Architecture + +``` +apps/ +├── web/ # Next.js 15 + TypeScript + Tailwind CSS +└── api/ # Fastify 5 + TypeScript + SQLite (sql.js) + +packages/ +├── shared-types/ # @shared/types — shared TypeScript interfaces +└── shared-config/ # @shared/config — shared configuration +``` + +## Quick Start + +```bash +# Install all dependencies (root + workspaces) +npm install + +# Start both frontend and backend in dev mode +npm run dev + +# Or start individually +npm run dev:web # http://localhost:3000 +npm run dev:api # http://localhost:3001 +``` + +## Build + +```bash +# Build everything +npm run build + +# Or individually +npm run build:web +npm run build:api +``` + +## Testing + +```bash +# Run API tests +npm test +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/auth/me` + +### Resources +#### users +- `GET /api/users` — List +- `GET /api/users/:id` — Get +- `POST /api/users` — Create +- `PUT /api/users/:id` — Update +- `DELETE /api/users/:id` — Delete + +#### contracts +- `GET /api/contracts` — List +- `GET /api/contracts/:id` — Get +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List +- `GET /api/approvals/:id` — Get +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List +- `GET /api/customers/:id` — Get +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List +- `GET /api/reminders/:id` — Get +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### articles +- `GET /api/articles` — List +- `GET /api/articles/:id` — Get +- `POST /api/articles` — Create +- `PUT /api/articles/:id` — Update +- `DELETE /api/articles/:id` — Delete + +#### tags +- `GET /api/tags` — List +- `GET /api/tags/:id` — Get +- `POST /api/tags` — Create +- `PUT /api/tags/:id` — Update +- `DELETE /api/tags/:id` — Delete + +#### comments +- `GET /api/comments` — List +- `GET /api/comments/:id` — Get +- `POST /api/comments` — Create +- `PUT /api/comments/:id` — Update +- `DELETE /api/comments/:id` — Delete + +#### media +- `GET /api/media` — List +- `GET /api/media/:id` — Get +- `POST /api/media` — Create +- `PUT /api/media/:id` — Update +- `DELETE /api/media/:id` — Delete + +### System +- `GET /api/health` — Health check + +## Environment Variables + +See `.env.example` for all available configuration. diff --git a/.benchmark/blog-cms/fullstack/apps/api/.gitignore b/.benchmark/blog-cms/fullstack/apps/api/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/blog-cms/fullstack/apps/api/README.md b/.benchmark/blog-cms/fullstack/apps/api/README.md new file mode 100644 index 0000000..945cc2f --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/README.md @@ -0,0 +1,118 @@ +# SocialApp — Backend API + +> 一款以内容分享和用户互动为核心的社交应用。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` — List all +- `GET /api/contracts/:id` — Get by ID +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List all +- `GET /api/approvals/:id` — Get by ID +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List all +- `GET /api/customers/:id` — Get by ID +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List all +- `GET /api/reminders/:id` — Get by ID +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### articles +- `GET /api/articles` — List all +- `GET /api/articles/:id` — Get by ID +- `POST /api/articles` — Create +- `PUT /api/articles/:id` — Update +- `DELETE /api/articles/:id` — Delete + +#### tags +- `GET /api/tags` — List all +- `GET /api/tags/:id` — Get by ID +- `POST /api/tags` — Create +- `PUT /api/tags/:id` — Update +- `DELETE /api/tags/:id` — Delete + +#### comments +- `GET /api/comments` — List all +- `GET /api/comments/:id` — Get by ID +- `POST /api/comments` — Create +- `PUT /api/comments/:id` — Update +- `DELETE /api/comments/:id` — Delete + +#### media +- `GET /api/media` — List all +- `GET /api/media/:id` — Get by ID +- `POST /api/media` — Create +- `PUT /api/media/:id` — Update +- `DELETE /api/media/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/blog-cms/fullstack/apps/api/package.json b/.benchmark/blog-cms/fullstack/apps/api/package.json new file mode 100644 index 0000000..7d7e801 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "socialapp-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/auth.test.ts b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/items.test.ts b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/items.test.ts new file mode 100644 index 0000000..7136843 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/items.test.ts @@ -0,0 +1,118 @@ +// Item CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/users.test.ts b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/db/client.ts b/.benchmark/blog-cms/fullstack/apps/api/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/db/schema.ts b/.benchmark/blog-cms/fullstack/apps/api/src/db/schema.ts new file mode 100644 index 0000000..c1e9b27 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/db/schema.ts @@ -0,0 +1,20 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS articles (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n content TEXT,\n excerpt TEXT,\n cover_url TEXT,\n status TEXT,\n author_id TEXT REFERENCES authors(id),\n category TEXT,\n published_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS tags (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL UNIQUE,\n color TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n content TEXT NOT NULL,\n parent_id TEXT REFERENCES parents(id),\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS media (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n filename TEXT NOT NULL,\n url TEXT NOT NULL,\n mime_type TEXT,\n size_bytes INTEGER,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/index.ts b/.benchmark/blog-cms/fullstack/apps/api/src/index.ts new file mode 100644 index 0000000..3592aea --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/index.ts @@ -0,0 +1,79 @@ +// SocialApp — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { articlesRoutes } from "./routes/articles.js"; +import { tagsRoutes } from "./routes/tags.js"; +import { commentsRoutes } from "./routes/comments.js"; +import { mediaRoutes } from "./routes/media.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-8106e913"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(articlesRoutes, { prefix: "/api/articles" }); + await app.register(tagsRoutes, { prefix: "/api/tags" }); + await app.register(commentsRoutes, { prefix: "/api/comments" }); + await app.register(mediaRoutes, { prefix: "/api/media" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/middleware/auth.ts b/.benchmark/blog-cms/fullstack/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/routes/auth.ts b/.benchmark/blog-cms/fullstack/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/routes/items.ts b/.benchmark/blog-cms/fullstack/apps/api/src/routes/items.ts new file mode 100644 index 0000000..2ad945e --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Item routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemService } from "../services/item.js"; +import type { CreateItemInput, UpdateItemInput } from "../types/index.js"; + +const service = new ItemService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items — create + app.post<{ Body: CreateItemInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id — update + app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/routes/users.ts b/.benchmark/blog-cms/fullstack/apps/api/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/services/item.ts b/.benchmark/blog-cms/fullstack/apps/api/src/services/item.ts new file mode 100644 index 0000000..24ec745 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/services/item.ts @@ -0,0 +1,55 @@ +// Auto-generated Item service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js"; + +export class ItemService { + /** List all items */ + list(): Item[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Item | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemInput): Item { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "title", "data", "created_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.data ?? null, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemInput): Item | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/services/user.ts b/.benchmark/blog-cms/fullstack/apps/api/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/types/fastify.d.ts b/.benchmark/blog-cms/fullstack/apps/api/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/types/index.ts b/.benchmark/blog-cms/fullstack/apps/api/src/types/index.ts new file mode 100644 index 0000000..9b200cd --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/types/index.ts @@ -0,0 +1,223 @@ +// Import and re-export shared entity types +import type { + User, + Contract, + Approval, + Customer, + Reminder, + Articles, + Tags, + Comments, + Media, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from "@shared/types"; + +export type { + User, + Contract, + Approval, + Customer, + Reminder, + Articles, + Tags, + Comments, + Media, + ApiResponse, + PaginatedResponse, + ErrorResponse, +}; + +// Auto-generated types (from Model Contract) + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateArticlesInput { + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + authorId?: string; + category?: string; + publishedAt?: string; +} + +export interface CreateTagsInput { + userId?: string; + name: string; + color?: string; +} + +export interface CreateCommentsInput { + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; +} + +export interface CreateMediaInput { + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateArticlesInput { + userId?: string; + title?: string; + content?: string; + excerpt?: string; + coverUrl?: string; + authorId?: string; + category?: string; + publishedAt?: string; +} + +export interface UpdateTagsInput { + userId?: string; + name?: string; + color?: string; +} + +export interface UpdateCommentsInput { + userId?: string; + entityType?: string; + entityId?: string; + content?: string; + parentId?: string; +} + +export interface UpdateMediaInput { + userId?: string; + filename?: string; + url?: string; + mimeType?: string; + sizeBytes?: number; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/types/sql.js.d.ts b/.benchmark/blog-cms/fullstack/apps/api/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/tsconfig.json b/.benchmark/blog-cms/fullstack/apps/api/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/globals.css b/.benchmark/blog-cms/fullstack/apps/web/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/layout.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/layout.tsx new file mode 100644 index 0000000..3613994 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "SocialApp", + description: "一款以内容分享和用户互动为核心的社交应用。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/messages/page.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/messages/page.tsx new file mode 100644 index 0000000..bd9a20e --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/messages/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

消息

+

私信列表、聊天

+
+ +
+ + { +

消息 页面内容

+

路由: /messages

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/page.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/page.tsx new file mode 100644 index 0000000..eb4a2cc --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

SocialApp

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 文章发布 + + + 📅 + 分类标签 + + + 📝 + 评论管理 + + + 📸 + 媒体库 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 SocialApp

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/profile/[userId]/page.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/profile/[userId]/page.tsx new file mode 100644 index 0000000..3933192 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/profile/[userId]/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

个人

+

个人主页

+
+ +
+ + { +

个人 页面内容

+

路由: /profile/:userId

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/publish/page.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/publish/page.tsx new file mode 100644 index 0000000..e09151b --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/publish/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

发布

+

编辑器、相册选择

+
+ +
+ + { +

发布 页面内容

+

路由: /publish

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/layout/BottomNav.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/layout/Sidebar.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/Button.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/Card.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/EmptyState.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/Input.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/Modal.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useComments.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useComments.ts new file mode 100644 index 0000000..b6bfd57 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useComments.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for comments +type Comment = Record; + +/** + * Fetch and manage comments data. + */ +export function useComments() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/comments`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useDebounce.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useFeed.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useFeed.ts new file mode 100644 index 0000000..18364d8 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useFeed.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for feed +type Feed = Record; + +/** + * Fetch and manage feed data. + */ +export function useFeed() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/feed`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useForm.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/usePosts.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/usePosts.ts new file mode 100644 index 0000000..6fa9aae --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/usePosts.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for posts +type Post = Record; + +/** + * Fetch and manage posts data. + */ +export function usePosts() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/posts`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useUsers.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/next.config.ts b/.benchmark/blog-cms/fullstack/apps/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/blog-cms/fullstack/apps/web/package.json b/.benchmark/blog-cms/fullstack/apps/web/package.json new file mode 100644 index 0000000..13d054b --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "socialapp-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/apps/web/postcss.config.js b/.benchmark/blog-cms/fullstack/apps/web/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/api.ts b/.benchmark/blog-cms/fullstack/apps/web/services/api.ts new file mode 100644 index 0000000..cac42f9 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/api.ts @@ -0,0 +1,49 @@ +// Auto-generated API client for SocialApp + +import { API_BASE_URL, API_PREFIX } from "@shared/config"; + +const API_BASE = `${API_BASE_URL}${API_PREFIX}`; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/comments.ts b/.benchmark/blog-cms/fullstack/apps/web/services/comments.ts new file mode 100644 index 0000000..7df69f4 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/comments.ts @@ -0,0 +1,10 @@ +// Auto-generated comments service +import { api } from "./api"; + +export function get() { + return api.get("/api/comments"); +} + +export function post(data: Record) { + return api.post("/api/comments", data); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/feed.ts b/.benchmark/blog-cms/fullstack/apps/web/services/feed.ts new file mode 100644 index 0000000..85b119b --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/feed.ts @@ -0,0 +1,6 @@ +// Auto-generated feed service +import { api } from "./api"; + +export function get() { + return api.get("/api/feed"); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/posts.ts b/.benchmark/blog-cms/fullstack/apps/web/services/posts.ts new file mode 100644 index 0000000..6aeb9b7 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/posts.ts @@ -0,0 +1,10 @@ +// Auto-generated posts service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/posts", data); +} + +export function post_id_like(data: Record) { + return api.post("/api/posts", data); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/users.ts b/.benchmark/blog-cms/fullstack/apps/web/services/users.ts new file mode 100644 index 0000000..4b573b4 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/users.ts @@ -0,0 +1,6 @@ +// Auto-generated users service +import { api } from "./api"; + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/tailwind.config.ts b/.benchmark/blog-cms/fullstack/apps/web/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/blog-cms/fullstack/apps/web/tsconfig.json b/.benchmark/blog-cms/fullstack/apps/web/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/apps/web/types/index.ts b/.benchmark/blog-cms/fullstack/apps/web/types/index.ts new file mode 100644 index 0000000..0f93fab --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/types/index.ts @@ -0,0 +1,130 @@ +// Auto-generated types for SocialApp + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Articles { + id: string; + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + status?: string; + authorId?: string; + category?: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Tags { + id: string; + userId?: string; + name: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Comments { + id: string; + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Media { + id: string; + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/package.json b/.benchmark/blog-cms/fullstack/package.json new file mode 100644 index 0000000..25dab6e --- /dev/null +++ b/.benchmark/blog-cms/fullstack/package.json @@ -0,0 +1,19 @@ +{ + "name": "socialapp", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "node scripts/build.mjs", + "dev:web": "npm run dev -w apps/web", + "dev:api": "npm run dev -w apps/api", + "build:web": "npm run build -w apps/web", + "build:api": "npm run build -w apps/api", + "test": "npm run test -w apps/api", + "lint": "npm run lint -w apps/web" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/packages/shared-config/package.json b/.benchmark/blog-cms/fullstack/packages/shared-config/package.json new file mode 100644 index 0000000..c2f52a9 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/config", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/packages/shared-config/src/index.ts b/.benchmark/blog-cms/fullstack/packages/shared-config/src/index.ts new file mode 100644 index 0000000..abf84f0 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-config/src/index.ts @@ -0,0 +1,16 @@ +// Shared configuration for fullstack project + +/** API base URL — reads from env or defaults */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** API prefix for all endpoints */ +export const API_PREFIX = "/api"; + +/** Full API URL */ +export const API_URL = `${API_BASE_URL}${API_PREFIX}`; + +/** Auth token storage key */ +export const AUTH_TOKEN_KEY = "auth_token"; + +/** Default page size for paginated endpoints */ +export const DEFAULT_PAGE_SIZE = 20; diff --git a/.benchmark/blog-cms/fullstack/packages/shared-config/tsconfig.json b/.benchmark/blog-cms/fullstack/packages/shared-config/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/packages/shared-types/package.json b/.benchmark/blog-cms/fullstack/packages/shared-types/package.json new file mode 100644 index 0000000..d7fb84c --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/types", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/packages/shared-types/src/index.ts b/.benchmark/blog-cms/fullstack/packages/shared-types/src/index.ts new file mode 100644 index 0000000..64821a6 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-types/src/index.ts @@ -0,0 +1,132 @@ +// Shared types for fullstack project +// Auto-generated — used by both apps/web and apps/api + +// ─── Entities ──────────────────────────────────────── +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Articles { + id: string; + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + status?: string; + authorId?: string; + category?: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Tags { + id: string; + userId?: string; + name: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Comments { + id: string; + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Media { + id: string; + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; + createdAt?: string; + updatedAt?: string; +} + +// ─── API Response Wrappers ─────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} diff --git a/.benchmark/blog-cms/fullstack/packages/shared-types/tsconfig.json b/.benchmark/blog-cms/fullstack/packages/shared-types/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/scripts/build.mjs b/.benchmark/blog-cms/fullstack/scripts/build.mjs new file mode 100644 index 0000000..84fa093 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/scripts/build.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Build script — builds both web and api. + * Usage: node scripts/build.mjs + */ + +import { execSync } from "node:child_process"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function run(cmd, cwd) { + console.log(`\n🔨 ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("🏗️ Building fullstack project...\n"); + +try { + run("npm run build", `${ROOT}/apps/api`); + run("npm run build", `${ROOT}/apps/web`); + console.log("\n✅ Build complete!"); +} catch (e) { + console.error("\n❌ Build failed:", e.message); + process.exit(1); +} diff --git a/.benchmark/blog-cms/fullstack/scripts/dev.mjs b/.benchmark/blog-cms/fullstack/scripts/dev.mjs new file mode 100644 index 0000000..dcb8118 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/scripts/dev.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +/** + * Dev script — starts both web and api in parallel. + * Usage: node scripts/dev.mjs + */ + +import { spawn } from "node:child_process"; + +function start(name, command, args, cwd) { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + env: { ...process.env, FORCE_COLOR: "1" }, + }); + child.on("error", (err) => console.error(`[${name}] Failed: ${err.message}`)); + child.on("exit", (code) => { + if (code !== 0 && code !== null) console.error(`[${name}] Exited with code ${code}`); + }); + return child; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +console.log("🚀 Starting fullstack dev servers...\n"); + +const api = start("api", "npm", ["run", "dev"], `${ROOT}/apps/api`); +const web = start("web", "npm", ["run", "dev"], `${ROOT}/apps/web`); + +process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); }); +process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); }); diff --git a/.benchmark/blog-cms/prd.json b/.benchmark/blog-cms/prd.json new file mode 100644 index 0000000..58018ba --- /dev/null +++ b/.benchmark/blog-cms/prd.json @@ -0,0 +1 @@ +{"projectName":"SocialApp","chineseName":"社交平台","domain":"social","matchConfidence":"high","summary":"一款以内容分享和用户互动为核心的社交应用。","personas":[{"name":"内容创作者","description":"发布图文/视频内容的活跃用户","painPoints":["内容曝光不足","互动管理复杂"]},{"name":"普通用户","description":"浏览内容和互动的用户","painPoints":["信息过载","隐私顾虑"]}],"userStories":[{"id":"US-001","as":"内容创作者","want":"文章发布","soThat":"解决痛点:内容曝光不足","priority":"P0"},{"id":"US-002","as":"内容创作者","want":"分类标签","soThat":"解决痛点:内容曝光不足","priority":"P0"},{"id":"US-003","as":"内容创作者","want":"评论管理","soThat":"解决痛点:内容曝光不足","priority":"P0"},{"id":"US-004","as":"普通用户","want":"文章发布","soThat":"解决痛点:信息过载","priority":"P0"},{"id":"US-005","as":"普通用户","want":"分类标签","soThat":"解决痛点:信息过载","priority":"P0"},{"id":"US-006","as":"普通用户","want":"评论管理","soThat":"解决痛点:信息过载","priority":"P0"}],"mvpScope":{"description":"MVP 聚焦 文章发布、分类标签、评论管理、媒体库,包含页面:首页、文章发布、分类标签","features":["文章发布","分类标签","评论管理","媒体库"],"pages":["首页","文章发布","分类标签"],"estimatedWeeks":3},"features":[{"name":"文章发布","description":"文章发布","priority":"P0"},{"name":"分类标签","description":"分类标签","priority":"P0"},{"name":"评论管理","description":"评论管理","priority":"P0"},{"name":"媒体库","description":"媒体库","priority":"P0"}],"pages":[{"name":"首页","route":"/home","description":"应用首页"},{"name":"文章发布","route":"/articles","description":"文章发布"},{"name":"分类标签","route":"/tags","description":"分类标签"},{"name":"评论管理","route":"/comments","description":"评论管理"},{"name":"媒体库","route":"/media","description":"媒体库"}],"apiRequirements":[{"method":"GET","path":"/api/articles","description":"获取文章发布列表"},{"method":"POST","path":"/api/articles","description":"创建文章发布"},{"method":"GET","path":"/api/tags","description":"获取分类标签列表"},{"method":"POST","path":"/api/tags","description":"创建分类标签"},{"method":"GET","path":"/api/comments","description":"获取评论管理列表"},{"method":"POST","path":"/api/comments","description":"创建评论管理"},{"method":"GET","path":"/api/media","description":"获取媒体库列表"},{"method":"POST","path":"/api/media","description":"创建媒体库"},{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter(跨平台)","considerations":["建议跨平台框架减少开发成本"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"项目脚手架搭建","description":"初始化 SocialApp 项目结构,配置构建工具和 CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"数据库设计与初始化","description":"设计数据模型,创建数据库迁移脚本","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"页面开发:首页","description":"开发 首页 页面(/home):应用首页","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"页面开发:文章发布","description":"开发 文章发布 页面(/articles):文章发布","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"页面开发:分类标签","description":"开发 分类标签 页面(/tags):分类标签","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"页面开发:评论管理","description":"开发 评论管理 页面(/comments):评论管理","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API 开发:GET /api/articles","description":"获取文章发布列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API 开发:POST /api/articles","description":"创建文章发布","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API 开发:GET /api/tags","description":"获取分类标签列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API 开发:POST /api/tags","description":"创建分类标签","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"前后端联调","description":"前端页面与后端 API 集成联调","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"测试与修复","description":"功能测试、Bug 修复、性能优化","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T22:08:35.532Z","inputLength":32,"domainMatchCount":1,"extractedFeatureCount":4,"extractedPageCount":5,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/README.md b/.benchmark/blog-cms/release/release/README.md new file mode 100644 index 0000000..eae602e --- /dev/null +++ b/.benchmark/blog-cms/release/release/README.md @@ -0,0 +1,33 @@ +# socialapp — Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T22:08:36.006Z + +## Directory Structure + +``` +release/ +├── version.json — Version metadata +├── build-info.json — Build environment info +├── README.md — This file +├── manifests/ +│ └── manifest.json — Release manifest with file hashes +├── checksums/ +│ └── checksums.txt — SHA256 checksums for verification +├── release-notes/ +│ └── release-notes.md — Human-readable release notes +├── windows/ — Windows installers +├── macos/ — macOS disk images +└── linux/ — Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/build-info.json b/.benchmark/blog-cms/release/release/build-info.json new file mode 100644 index 0000000..d84d88b --- /dev/null +++ b/.benchmark/blog-cms/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T22:08:36.006Z", + "domain": "unknown", + "entityCount": 11, + "routeCount": 1, + "screenCount": 11, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "socialapp", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/checksums/checksums.txt b/.benchmark/blog-cms/release/release/checksums/checksums.txt new file mode 100644 index 0000000..88424d1 --- /dev/null +++ b/.benchmark/blog-cms/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +bb8b8b420d0aa06a4fcede9291db577eebfa1b80def8d70bbbed6c4de79519d0 windows/socialapp-setup-0.1.0.exe +ad0a1855753c7e0fffae07f89042dc7fea92d00d03af3963392d3d975d36e18a windows/socialapp-0.1.0-portable.exe +7d5fb02ede1f0c7c74f5e5b78ddd2096f186cfc5f16421eb8a861739118a8854 macos/socialapp-0.1.0.dmg +d687104970423e01f05af053c12747b5d1f79da5d7ebfbd2a5f924c3aef7bb1a macos/socialapp-0.1.0-arm64.dmg +92ce5ef3f6b3856ac36999ebaff3fe53a0e20ec2fe4d5dc8c47c3562019e1484 linux/socialapp-0.1.0.AppImage +279fd2bc685d8f8d60e556d72f2bd59445c10ee793835297b76279f89cd2ca5a linux/socialapp_0.1.0_amd64.deb diff --git a/.benchmark/blog-cms/release/release/linux/socialapp-0.1.0.AppImage b/.benchmark/blog-cms/release/release/linux/socialapp-0.1.0.AppImage new file mode 100644 index 0000000..7223e9b --- /dev/null +++ b/.benchmark/blog-cms/release/release/linux/socialapp-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/linux/socialapp_0.1.0_amd64.deb b/.benchmark/blog-cms/release/release/linux/socialapp_0.1.0_amd64.deb new file mode 100644 index 0000000..6ec89b5 --- /dev/null +++ b/.benchmark/blog-cms/release/release/linux/socialapp_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0-arm64.dmg b/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0-arm64.dmg new file mode 100644 index 0000000..208f89d --- /dev/null +++ b/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0.dmg b/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0.dmg new file mode 100644 index 0000000..b24c61c --- /dev/null +++ b/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/manifests/manifest.json b/.benchmark/blog-cms/release/release/manifests/manifest.json new file mode 100644 index 0000000..4c7e6cd --- /dev/null +++ b/.benchmark/blog-cms/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "socialapp", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T22:08:36.006Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/socialapp-setup-0.1.0.exe", + "size": 144, + "sha256": "bb8b8b420d0aa06a4fcede9291db577eebfa1b80def8d70bbbed6c4de79519d0" + }, + { + "path": "windows/socialapp-0.1.0-portable.exe", + "size": 148, + "sha256": "ad0a1855753c7e0fffae07f89042dc7fea92d00d03af3963392d3d975d36e18a" + }, + { + "path": "macos/socialapp-0.1.0.dmg", + "size": 140, + "sha256": "7d5fb02ede1f0c7c74f5e5b78ddd2096f186cfc5f16421eb8a861739118a8854" + }, + { + "path": "macos/socialapp-0.1.0-arm64.dmg", + "size": 142, + "sha256": "d687104970423e01f05af053c12747b5d1f79da5d7ebfbd2a5f924c3aef7bb1a" + }, + { + "path": "linux/socialapp-0.1.0.AppImage", + "size": 133, + "sha256": "92ce5ef3f6b3856ac36999ebaff3fe53a0e20ec2fe4d5dc8c47c3562019e1484" + }, + { + "path": "linux/socialapp_0.1.0_amd64.deb", + "size": 143, + "sha256": "279fd2bc685d8f8d60e556d72f2bd59445c10ee793835297b76279f89cd2ca5a" + } + ], + "totalFiles": 6, + "totalSize": 850 +} \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/release-notes/release-notes.md b/.benchmark/blog-cms/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..f05d6bd --- /dev/null +++ b/.benchmark/blog-cms/release/release/release-notes/release-notes.md @@ -0,0 +1,60 @@ +# socialapp v0.1.0 + +> Release Builder Agent — SF-07 +> Generated: 2026-06-05 22:08:36 UTC + +## Version + +- **Name:** socialapp +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- approvals +- articles +- auth +- comments +- contracts +- customers +- items +- media +- reminders +- tags +- users + +## Build Info + +- **Build Time:** 2026-06-05T22:08:36.006Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- ✅ windows +- ✅ macos +- ✅ linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/version.json b/.benchmark/blog-cms/release/release/version.json new file mode 100644 index 0000000..00ba6ca --- /dev/null +++ b/.benchmark/blog-cms/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "socialapp", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/windows/socialapp-0.1.0-portable.exe b/.benchmark/blog-cms/release/release/windows/socialapp-0.1.0-portable.exe new file mode 100644 index 0000000..ca6f54a --- /dev/null +++ b/.benchmark/blog-cms/release/release/windows/socialapp-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/windows/socialapp-setup-0.1.0.exe b/.benchmark/blog-cms/release/release/windows/socialapp-setup-0.1.0.exe new file mode 100644 index 0000000..d087963 --- /dev/null +++ b/.benchmark/blog-cms/release/release/windows/socialapp-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T22:08:36.001Z diff --git a/.benchmark/contract-mgmt/arch.json b/.benchmark/contract-mgmt/arch.json new file mode 100644 index 0000000..2782f82 --- /dev/null +++ b/.benchmark/contract-mgmt/arch.json @@ -0,0 +1 @@ +{"projectName":"OAFlow","domain":"enterprise","contract":{"projectName":"OAFlow","domain":"enterprise","entities":[{"name":"User","table":"users","description":"系统用户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"hasMany","entity":"contracts","via":"user_id"},{"type":"hasMany","entity":"customers","via":"user_id"}]},{"name":"Contract","table":"contracts","description":"合同","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","defaultValue":"draft","enum":["draft","pending","active","expired","terminated"]},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Approval","table":"approvals","description":"审批流程","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"applicant_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"status","type":"VARCHAR(32)","defaultValue":"pending","enum":["pending","approved","rejected"]},{"name":"form_data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"users","via":"applicant_id","as":"applicant"}]},{"name":"Customer","table":"customers","description":"客户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","defaultValue":"[]"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Reminder","table":"reminders","description":"到期提醒","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"remind_at","type":"TIMESTAMP","required":true},{"name":"message","type":"TEXT"},{"name":"sent","type":"BOOLEAN","defaultValue":"false"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Stats","table":"stats","description":"合同金额统计表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"metric","type":"VARCHAR(64)","required":true,"isPrimary":false,"isAuto":false},{"name":"value","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"period","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":false},{"name":"recorded_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"OperationLogs","table":"operation_logs","description":"操作日志表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"data","type":"JSONB","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":8,"requireSpecial":true}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","remind_at":"remindAt","recorded_at":"recordedAt","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","remindAt":"remind_at","recordedAt":"recorded_at","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Contract":{"user_id":{"required":true},"title":{"required":true},"status":{"enum":["draft","pending","active","expired","terminated"],"defaultValue":"draft"}},"Approval":{"user_id":{"required":true},"entity_type":{"required":true},"applicant_id":{"required":true},"status":{"enum":["pending","approved","rejected"],"defaultValue":"pending"}},"Customer":{"user_id":{"required":true},"name":{"required":true},"tags":{"defaultValue":"[]"}},"Reminder":{"user_id":{"required":true},"entity_type":{"required":true},"remind_at":{"required":true},"sent":{"defaultValue":"false"}},"Stats":{"id":{"required":true},"metric":{"required":true}},"OperationLogs":{"id":{"required":true},"title":{"required":true}}}},"techStack":{"frontend":"React 19 + TypeScript + Vite","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7","deployment":"Docker + Nginx","considerations":[]},"architectureDiagram":"┌─────────────────────────────────────────────────────────┐\n│ OAFlow — System Architecture │\n├─────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────────────┐ ┌──────────────────────┐ │\n│ │ Client Layer │ │ Admin / Web │ │\n│ │ React 19 + TypeScript + Vite │ │ React + Vite (Web) │ │\n│ └──────────┬───────────┘ └──────────┬───────────┘ │\n│ │ │ │\n│ └──────────┬────────────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ API Gateway │ │\n│ │ Nginx / Kong │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ Backend Services │ │\n│ │ NestJS + Prisma │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌───────────────┼───────────────┐ │\n│ │ │ │ │\n│ ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ │\n│ │ PostgreSQL 15│ │ Redis Cache │ │ MinIO │ │\n│ │ Primary │ │ Session │ │ Files │ │\n│ └───────────┘ └─────────────┘ └──────────┘ │\n│ │\n├─────────────────────────────────────────────────────────┤\n│ Modules: │\n│ 1 合同创建 │\n│ 2 Workflow │\n│ 3 Notification │\n│ 4 客户管理 │\n│ 5 Analytics │\n│ 6 操作日志 │\n│ │\n└─────────────────────────────────────────────────────────┘","dataFlows":[{"page":"合同创建","route":"/contracts","description":"合同创建","dataFlow":["GET /api/contracts → 获取合同创建列表","POST /api/contracts → 创建合同创建"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"合同审批","route":"/approvals","description":"合同审批","dataFlow":["GET /api/approvals → 获取合同审批列表","POST /api/approvals → 创建合同审批"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"合同到期提醒","route":"/reminders","description":"合同到期提醒","dataFlow":["GET /api/reminders → 获取合同到期提醒列表","POST /api/reminders → 创建合同到期提醒"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"客户管理","route":"/customers","description":"客户管理","dataFlow":["GET /api/customers → 获取客户管理列表","POST /api/customers → 创建客户管理"],"direction":"Client → API Gateway → Backend → Database → Response → Client"}],"modules":[{"name":"合同创建","label":"合同创建","features":["合同创建"],"responsibilities":["提供 合同创建 相关功能"]},{"name":"workflow","label":"Workflow","features":["合同审批"],"responsibilities":["提供 合同审批 相关功能"]},{"name":"notification","label":"Notification","features":["合同到期提醒"],"responsibilities":["提供 合同到期提醒 相关功能"]},{"name":"客户管理","label":"客户管理","features":["客户管理"],"responsibilities":["提供 客户管理 相关功能"]},{"name":"analytics","label":"Analytics","features":["合同金额统计"],"responsibilities":["提供 合同金额统计 相关功能"]},{"name":"操作日志","label":"操作日志","features":["操作日志"],"responsibilities":["提供 操作日志 相关功能"]},{"name":"approval","label":"审批引擎","features":["审批流程"],"responsibilities":["提供 审批流程 相关功能"]},{"name":"attendance","label":"考勤管理","features":["考勤打卡"],"responsibilities":["提供 考勤打卡 相关功能"]},{"name":"department","label":"组织架构","features":["部门管理"],"responsibilities":["提供 部门管理 相关功能"]}],"databaseSchema":[{"table":"users","description":"用户表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"contracts","description":"合同创建表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'draft'"},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_contracts_user ON contracts(user_id)"]},{"table":"approvals","description":"合同审批表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'draft'"},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_approvals_user ON approvals(user_id)"]},{"table":"reminders","description":"合同到期提醒表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'draft'"},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_reminders_user ON reminders(user_id)"]},{"table":"customers","description":"客户管理表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","constraints":"DEFAULT '[]'"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_customers_user ON customers(user_id)"]},{"table":"stats","description":"合同金额统计表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"metric","type":"VARCHAR(64)","constraints":"NOT NULL"},{"name":"value","type":"DECIMAL(12,2)"},{"name":"period","type":"VARCHAR(32)"},{"name":"recorded_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_stats_user ON stats(user_id)"]},{"table":"operation_logs","description":"操作日志表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'active'"},{"name":"data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_operation_logs_user ON operation_logs(user_id)"]}],"apiDesign":[{"resource":"approvals","basePath":"/api/approvals","endpoints":[{"method":"GET","path":"/api/approvals","description":"获取合同审批列表"},{"method":"POST","path":"/api/approvals","description":"创建合同审批"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}]},{"resource":"contracts","basePath":"/api/contracts","endpoints":[{"method":"GET","path":"/api/contracts","description":"获取合同创建列表"},{"method":"POST","path":"/api/contracts","description":"创建合同创建"}]},{"resource":"customers","basePath":"/api/customers","endpoints":[{"method":"GET","path":"/api/customers","description":"获取客户管理列表"},{"method":"POST","path":"/api/customers","description":"创建客户管理"}]},{"resource":"reminders","basePath":"/api/reminders","endpoints":[{"method":"GET","path":"/api/reminders","description":"获取合同到期提醒列表"},{"method":"POST","path":"/api/reminders","description":"创建合同到期提醒"}]}],"directoryStructure":["oaflow/","├── apps/","│ ├── mobile/ # React Native / Flutter 移动端","│ │ ├── src/","│ │ │ ├── screens/ # 页面组件","│ │ │ │ ├── 合同创建/","│ │ │ │ ├── workflow/","│ │ │ │ ├── notification/","│ │ │ │ ├── 客户管理/","│ │ │ ├── components/ # 通用组件","│ │ │ ├── hooks/ # 自定义 Hooks","│ │ │ ├── services/ # API 调用层","│ │ │ ├── store/ # 状态管理","│ │ │ └── utils/ # 工具函数","│ │ ├── app.json","│ │ └── package.json","│ └── web/ # Web 管理后台","│ ├── src/","│ │ ├── pages/","│ │ ├── components/","│ │ └── layouts/","│ └── package.json","├── packages/","│ └── shared/ # 共享类型/常量","├── server/ # NestJS 后端服务","│ ├── src/","│ │ ├── modules/ # 业务模块","│ │ │ ├── 合同创建/","│ │ │ │ ├── 合同创建.controller.ts","│ │ │ │ ├── 合同创建.service.ts","│ │ │ │ ├── 合同创建.module.ts","│ │ │ │ └── dto/","│ │ │ ├── workflow/","│ │ │ │ ├── workflow.controller.ts","│ │ │ │ ├── workflow.service.ts","│ │ │ │ ├── workflow.module.ts","│ │ │ │ └── dto/","│ │ │ ├── notification/","│ │ │ │ ├── notification.controller.ts","│ │ │ │ ├── notification.service.ts","│ │ │ │ ├── notification.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 客户管理/","│ │ │ │ ├── 客户管理.controller.ts","│ │ │ │ ├── 客户管理.service.ts","│ │ │ │ ├── 客户管理.module.ts","│ │ │ │ └── dto/","│ │ │ ├── analytics/","│ │ │ │ ├── analytics.controller.ts","│ │ │ │ ├── analytics.service.ts","│ │ │ │ ├── analytics.module.ts","│ │ │ │ └── dto/","│ │ ├── common/ # 通用(guards, filters, interceptors)","│ │ ├── prisma/ # Prisma ORM","│ │ │ └── schema.prisma","│ │ ├── config/ # 环境配置","│ │ └── main.ts","│ ├── test/","│ ├── Dockerfile","│ └── package.json","├── docker-compose.yml","├── .github/workflows/ # CI/CD","│ └── ci.yml","├── .env.example","├── README.md","└── turbo.json # Monorepo 配置"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker + Nginx","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions → Build → Test → Deploy"},"meta":{"generatedAt":"2026-06-05T13:43:26.925Z","sourceDomain":"enterprise","moduleCount":9,"tableCount":7,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/backend/.gitignore b/.benchmark/contract-mgmt/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/contract-mgmt/backend/README.md b/.benchmark/contract-mgmt/backend/README.md new file mode 100644 index 0000000..d0f7365 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/README.md @@ -0,0 +1,104 @@ +# OAFlow — Backend API + +> 一款支持审批流、考勤管理、部门协作的企业办公自动化系统。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` — List all +- `GET /api/contracts/:id` — Get by ID +- `POST /api/contracts` — Create +- `PUT /api/contracts/:id` — Update +- `DELETE /api/contracts/:id` — Delete + +#### approvals +- `GET /api/approvals` — List all +- `GET /api/approvals/:id` — Get by ID +- `POST /api/approvals` — Create +- `PUT /api/approvals/:id` — Update +- `DELETE /api/approvals/:id` — Delete + +#### customers +- `GET /api/customers` — List all +- `GET /api/customers/:id` — Get by ID +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### reminders +- `GET /api/reminders` — List all +- `GET /api/reminders/:id` — Get by ID +- `POST /api/reminders` — Create +- `PUT /api/reminders/:id` — Update +- `DELETE /api/reminders/:id` — Delete + +#### stats +- `GET /api/stats` — List all +- `GET /api/stats/:id` — Get by ID +- `POST /api/stats` — Create +- `PUT /api/stats/:id` — Update +- `DELETE /api/stats/:id` — Delete + +#### operation_logs +- `GET /api/operation_logs` — List all +- `GET /api/operation_logs/:id` — Get by ID +- `POST /api/operation_logs` — Create +- `PUT /api/operation_logs/:id` — Update +- `DELETE /api/operation_logs/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/contract-mgmt/backend/package.json b/.benchmark/contract-mgmt/backend/package.json new file mode 100644 index 0000000..bd459f1 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "oaflow", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/approvals.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/approvals.test.ts new file mode 100644 index 0000000..392316d --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/approvals.test.ts @@ -0,0 +1,121 @@ +// Approval CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "applicantId": regRes.json().user.id, + "status": "sample-status", + "formData": "sample-formdata" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/approvals", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/approvals", () => { + it("creates a approval", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/approvals/:id", () => { + it("returns the created approval", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/approvals/:id", () => { + it("updates the approval", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/approvals/:id", () => { + it("deletes the approval", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/auth.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/contracts.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..c2e9f16 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/contracts.test.ts @@ -0,0 +1,124 @@ +// Contract CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "partyA": "sample-partya", + "partyB": "sample-partyb", + "amount": 1, + "signedAt": "sample-signedat", + "expiresAt": "sample-expiresat", + "status": "sample-status", + "fileUrl": "sample-fileurl" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/contracts", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/contracts", () => { + it("creates a contract", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/contracts/:id", () => { + it("returns the created contract", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/contracts/:id", () => { + it("updates the contract", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/contracts/:id", () => { + it("deletes the contract", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/customers.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..f3112fa --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/customers.test.ts @@ -0,0 +1,122 @@ +// Customer CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source", + "tags": "sample-tags" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/operation_logs.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/operation_logs.test.ts new file mode 100644 index 0000000..b90eff3 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/operation_logs.test.ts @@ -0,0 +1,119 @@ +// OperationLogs CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "data": "sample-data" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/operation_logs", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/operation_logs", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/operation_logs", () => { + it("creates a operation_log", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/operation_logs", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/operation_logs/:id", () => { + it("returns the created operation_log", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/operation_logs/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/operation_logs/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/operation_logs/:id", () => { + it("updates the operation_log", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/operation_logs/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/operation_logs/:id", () => { + it("deletes the operation_log", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/operation_logs/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/operation_logs/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/reminders.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/reminders.test.ts new file mode 100644 index 0000000..c573030 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/reminders.test.ts @@ -0,0 +1,121 @@ +// Reminder CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "remindAt": "sample-remindat", + "message": "sample-message", + "sent": true + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/reminders", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/reminders", () => { + it("creates a reminder", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/reminders/:id", () => { + it("returns the created reminder", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/reminders/:id", () => { + it("updates the reminder", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/reminders/:id", () => { + it("deletes the reminder", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/stats.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/stats.test.ts new file mode 100644 index 0000000..1b64b6d --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/stats.test.ts @@ -0,0 +1,119 @@ +// Stats CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "metric": "sample-metric", + "value": 1, + "period": "sample-period" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/stats", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/stats", () => { + it("creates a stat", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/stats/:id", () => { + it("returns the created stat", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/stats/:id", () => { + it("updates the stat", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/stats/:id", () => { + it("deletes the stat", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/db/client.ts b/.benchmark/contract-mgmt/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/contract-mgmt/backend/src/db/schema.ts b/.benchmark/contract-mgmt/backend/src/db/schema.ts new file mode 100644 index 0000000..9b7b755 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/db/schema.ts @@ -0,0 +1,18 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n metric TEXT NOT NULL,\n value REAL,\n period TEXT,\n recorded_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS operation_logs (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/contract-mgmt/backend/src/index.ts b/.benchmark/contract-mgmt/backend/src/index.ts new file mode 100644 index 0000000..78b3edd --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/index.ts @@ -0,0 +1,75 @@ +// OAFlow — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { statsRoutes } from "./routes/stats.js"; +import { operation_logsRoutes } from "./routes/operation_logs.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-145dc365"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + await app.register(operation_logsRoutes, { prefix: "/api/operation_logs" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/contract-mgmt/backend/src/middleware/auth.ts b/.benchmark/contract-mgmt/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/approvals.ts b/.benchmark/contract-mgmt/backend/src/routes/approvals.ts new file mode 100644 index 0000000..3a974d2 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/approvals.ts @@ -0,0 +1,51 @@ +// Auto-generated Approval routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ApprovalService } from "../services/approval.js"; +import type { CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +const service = new ApprovalService(); + +export async function approvalsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/approvals — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/approvals/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/approvals — create + app.post<{ Body: CreateApprovalInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/approvals/:id — update + app.put<{ Params: { id: string }; Body: UpdateApprovalInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/approvals/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/auth.ts b/.benchmark/contract-mgmt/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/contracts.ts b/.benchmark/contract-mgmt/backend/src/routes/contracts.ts new file mode 100644 index 0000000..ee83abf --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/contracts.ts @@ -0,0 +1,51 @@ +// Auto-generated Contract routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ContractService } from "../services/contract.js"; +import type { CreateContractInput, UpdateContractInput } from "../types/index.js"; + +const service = new ContractService(); + +export async function contractsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/contracts — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/contracts/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/contracts — create + app.post<{ Body: CreateContractInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/contracts/:id — update + app.put<{ Params: { id: string }; Body: UpdateContractInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/contracts/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/customers.ts b/.benchmark/contract-mgmt/backend/src/routes/customers.ts new file mode 100644 index 0000000..9a6f981 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customer routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomerService } from "../services/customer.js"; +import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +const service = new CustomerService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers — create + app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id — update + app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/operation_logs.ts b/.benchmark/contract-mgmt/backend/src/routes/operation_logs.ts new file mode 100644 index 0000000..518a2b6 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/operation_logs.ts @@ -0,0 +1,51 @@ +// Auto-generated OperationLogs routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { OperationLogsService } from "../services/operation_log.js"; +import type { CreateOperationLogsInput, UpdateOperationLogsInput } from "../types/index.js"; + +const service = new OperationLogsService(); + +export async function operation_logsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/operation_logs — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/operation_logs/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "OperationLogs not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/operation_logs — create + app.post<{ Body: CreateOperationLogsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/operation_logs/:id — update + app.put<{ Params: { id: string }; Body: UpdateOperationLogsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "OperationLogs not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/operation_logs/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "OperationLogs not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/reminders.ts b/.benchmark/contract-mgmt/backend/src/routes/reminders.ts new file mode 100644 index 0000000..ee00754 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/reminders.ts @@ -0,0 +1,51 @@ +// Auto-generated Reminder routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ReminderService } from "../services/reminder.js"; +import type { CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +const service = new ReminderService(); + +export async function remindersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/reminders — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/reminders/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/reminders — create + app.post<{ Body: CreateReminderInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/reminders/:id — update + app.put<{ Params: { id: string }; Body: UpdateReminderInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/reminders/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/stats.ts b/.benchmark/contract-mgmt/backend/src/routes/stats.ts new file mode 100644 index 0000000..92f64ea --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/stats.ts @@ -0,0 +1,51 @@ +// Auto-generated Stats routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { StatsService } from "../services/stat.js"; +import type { CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +const service = new StatsService(); + +export async function statsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/stats — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/stats/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/stats — create + app.post<{ Body: CreateStatsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/stats/:id — update + app.put<{ Params: { id: string }; Body: UpdateStatsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/stats/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/services/approval.ts b/.benchmark/contract-mgmt/backend/src/services/approval.ts new file mode 100644 index 0000000..f40b2a2 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/approval.ts @@ -0,0 +1,56 @@ +// Auto-generated Approval service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Approval, CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +export class ApprovalService { + /** List all approvals */ + list(): Approval[] { + return queryAll("SELECT * FROM approvals ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Approval | undefined { + return queryOne("SELECT * FROM approvals WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateApprovalInput): Approval { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "applicant_id", "status", "form_data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.applicantId ?? null, input.status ?? null, input.formData ?? null, now, now]; + + execute(`INSERT INTO approvals (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateApprovalInput): Approval | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.applicantId !== undefined) { sets.push("applicant_id = ?"); values.push(input.applicantId); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.formData !== undefined) { sets.push("form_data = ?"); values.push(input.formData); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE approvals SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM approvals WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/contract.ts b/.benchmark/contract-mgmt/backend/src/services/contract.ts new file mode 100644 index 0000000..7980109 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/contract.ts @@ -0,0 +1,59 @@ +// Auto-generated Contract service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Contract, CreateContractInput, UpdateContractInput } from "../types/index.js"; + +export class ContractService { + /** List all contracts */ + list(): Contract[] { + return queryAll("SELECT * FROM contracts ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Contract | undefined { + return queryOne("SELECT * FROM contracts WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateContractInput): Contract { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "party_a", "party_b", "amount", "signed_at", "expires_at", "status", "file_url", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.partyA ?? null, input.partyB ?? null, input.amount ?? null, input.signedAt ?? null, input.expiresAt ?? null, input.status ?? null, input.fileUrl ?? null, now, now]; + + execute(`INSERT INTO contracts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateContractInput): Contract | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.partyA !== undefined) { sets.push("party_a = ?"); values.push(input.partyA); } + if (input.partyB !== undefined) { sets.push("party_b = ?"); values.push(input.partyB); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.signedAt !== undefined) { sets.push("signed_at = ?"); values.push(input.signedAt); } + if (input.expiresAt !== undefined) { sets.push("expires_at = ?"); values.push(input.expiresAt); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.fileUrl !== undefined) { sets.push("file_url = ?"); values.push(input.fileUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM contracts WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/customer.ts b/.benchmark/contract-mgmt/backend/src/services/customer.ts new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/customer.ts @@ -0,0 +1,57 @@ +// Auto-generated Customer service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +export class CustomerService { + /** List all customers */ + list(): Customer[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customer | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomerInput): Customer { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "tags", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, input.tags ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomerInput): Customer | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + if (input.tags !== undefined) { sets.push("tags = ?"); values.push(input.tags); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/operation_log.ts b/.benchmark/contract-mgmt/backend/src/services/operation_log.ts new file mode 100644 index 0000000..83854d0 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/operation_log.ts @@ -0,0 +1,54 @@ +// Auto-generated OperationLogs service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { OperationLogs, CreateOperationLogsInput, UpdateOperationLogsInput } from "../types/index.js"; + +export class OperationLogsService { + /** List all operation_logs */ + list(): OperationLogs[] { + return queryAll("SELECT * FROM operation_logs ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): OperationLogs | undefined { + return queryOne("SELECT * FROM operation_logs WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateOperationLogsInput): OperationLogs { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now]; + + execute(`INSERT INTO operation_logs (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateOperationLogsInput): OperationLogs | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE operation_logs SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM operation_logs WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/reminder.ts b/.benchmark/contract-mgmt/backend/src/services/reminder.ts new file mode 100644 index 0000000..8c19dd5 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/reminder.ts @@ -0,0 +1,56 @@ +// Auto-generated Reminder service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Reminder, CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +export class ReminderService { + /** List all reminders */ + list(): Reminder[] { + return queryAll("SELECT * FROM reminders ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Reminder | undefined { + return queryOne("SELECT * FROM reminders WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateReminderInput): Reminder { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "remind_at", "message", "sent", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.remindAt ?? null, input.message ?? null, input.sent ?? null, now, now]; + + execute(`INSERT INTO reminders (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateReminderInput): Reminder | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.remindAt !== undefined) { sets.push("remind_at = ?"); values.push(input.remindAt); } + if (input.message !== undefined) { sets.push("message = ?"); values.push(input.message); } + if (input.sent !== undefined) { sets.push("sent = ?"); values.push(input.sent); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE reminders SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM reminders WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/stat.ts b/.benchmark/contract-mgmt/backend/src/services/stat.ts new file mode 100644 index 0000000..1d4080d --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/stat.ts @@ -0,0 +1,54 @@ +// Auto-generated Stats service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Stats, CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +export class StatsService { + /** List all stats */ + list(): Stats[] { + return queryAll("SELECT * FROM stats ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Stats | undefined { + return queryOne("SELECT * FROM stats WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateStatsInput): Stats { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "metric", "value", "period", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.metric ?? null, input.value ?? null, input.period ?? null, now, now]; + + execute(`INSERT INTO stats (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateStatsInput): Stats | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.metric !== undefined) { sets.push("metric = ?"); values.push(input.metric); } + if (input.value !== undefined) { sets.push("value = ?"); values.push(input.value); } + if (input.period !== undefined) { sets.push("period = ?"); values.push(input.period); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE stats SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM stats WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/types/fastify.d.ts b/.benchmark/contract-mgmt/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/types/index.ts b/.benchmark/contract-mgmt/backend/src/types/index.ts new file mode 100644 index 0000000..a14e7a2 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/types/index.ts @@ -0,0 +1,258 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface OperationLogs { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateStatsInput { + userId?: string; + metric: string; + value?: number; + period?: string; +} + +export interface CreateOperationLogsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateStatsInput { + userId?: string; + metric?: string; + value?: number; + period?: string; +} + +export interface UpdateOperationLogsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/contract-mgmt/backend/src/types/sql.js.d.ts b/.benchmark/contract-mgmt/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/contract-mgmt/backend/tsconfig.json b/.benchmark/contract-mgmt/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/contract-mgmt/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/electron/README.md b/.benchmark/contract-mgmt/electron/README.md new file mode 100644 index 0000000..05c1798 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/README.md @@ -0,0 +1,82 @@ +# oaflow — Desktop + +> Electron desktop application wrapping the oaflow fullstack web project. + +## Architecture + +``` +Web Fullstack Project + × + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** — Window management, menu, tray, IPC, auto-update +- **Preload** — Secure bridge between main and renderer +- **Renderer** — Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +├── main.ts — Main process (BrowserWindow, Menu, lifecycle) +├── preload.ts — Secure IPC bridge (contextBridge) +├── ipc.ts — IPC handlers (dialogs, store, notifications) +├── tray.ts — System tray +├── updater.ts — Auto-update (electron-updater) +└── utils.ts — Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/contract-mgmt/electron/assets/icon.png b/.benchmark/contract-mgmt/electron/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..554e8a31d1f0205fbf5ef853aba9a4fa2fc490dd GIT binary patch literal 66 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv4;KqeCd<5Tr}e}F6o MPgg&ebxsLQ09s=V>;M1& literal 0 HcmV?d00001 diff --git a/.benchmark/contract-mgmt/electron/electron-builder.yml b/.benchmark/contract-mgmt/electron/electron-builder.yml new file mode 100644 index 0000000..cc3c4e8 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml — Auto-generated by SF-06 + +appId: com.oaflow.desktop +productName: oaflow +copyright: "Copyright © 2026 oaflow" + +# ─── Directories ───────────────────────────────── +directories: + output: release + buildResources: assets + +# ─── Files ─────────────────────────────────────── +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# ─── Extra Resources ───────────────────────────── +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# ─── macOS ─────────────────────────────────────── +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# ─── Windows ───────────────────────────────────── +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: oaflow + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: oaflow + +# ─── Linux ─────────────────────────────────────── +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: oaflow + Comment: oaflow Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# ─── Auto Update ───────────────────────────────── +publish: + provider: generic + url: https://releases.example.com/oaflow/ + channel: latest diff --git a/.benchmark/contract-mgmt/electron/electron/ipc.ts b/.benchmark/contract-mgmt/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // ── App Info ── + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // ── Window Controls ── + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // ── File Dialogs ── + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // ── Notifications ── + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // ── Shell ── + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // ── Persistent Store ── + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/contract-mgmt/electron/electron/main.ts b/.benchmark/contract-mgmt/electron/electron/main.ts new file mode 100644 index 0000000..ba02088 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// ─── Constants ─────────────────────────────────── +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "oaflow"; + +// ─── Single Instance Lock ──────────────────────── +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// ─── State ─────────────────────────────────────── +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// ─── Create Window ─────────────────────────────── +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // ── Load content ── + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // ── Show when ready ── + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // ── External links → default browser ── + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // ── Prevent navigation away ── + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// ─── Icon Path ─────────────────────────────────── +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// ─── API Server (Production) ───────────────────── +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// ─── Menu ──────────────────────────────────────── +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// ─── App Lifecycle ─────────────────────────────── +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed — continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/contract-mgmt/electron/electron/preload.ts b/.benchmark/contract-mgmt/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// ─── Expose safe API to renderer ───────────────── +contextBridge.exposeInMainWorld("electronAPI", { + // ── App Info ── + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // ── Window Controls ── + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // ── File Dialogs ── + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // ── Notifications ── + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // ── Shell ── + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // ── Store ── + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // ── Updates ── + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // ── Update Events ── + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // ── Remove listener ── + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// ─── Type declarations for renderer ────────────── +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/contract-mgmt/electron/electron/tray.ts b/.benchmark/contract-mgmt/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/contract-mgmt/electron/electron/updater.ts b/.benchmark/contract-mgmt/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/contract-mgmt/electron/electron/utils.ts b/.benchmark/contract-mgmt/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// ─── Single Instance Lock ──────────────────────── +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// ─── Dev mode detection ────────────────────────── +export function isDev(): boolean { + return !app.isPackaged; +} + +// ─── Web URL ───────────────────────────────────── +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// ─── API Path ──────────────────────────────────── +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// ─── Get API port from env ─────────────────────── +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/contract-mgmt/electron/entitlements.mac.plist b/.benchmark/contract-mgmt/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/contract-mgmt/electron/package.json b/.benchmark/contract-mgmt/electron/package.json new file mode 100644 index 0000000..653d034 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "oaflow-desktop", + "version": "1.0.0", + "private": true, + "description": "oaflow — Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "oaflow", + "appId": "com.oaflow.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/electron/tsconfig.json b/.benchmark/contract-mgmt/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/frontend/app/approvals/page.tsx b/.benchmark/contract-mgmt/frontend/app/approvals/page.tsx new file mode 100644 index 0000000..43808c0 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/approvals/page.tsx @@ -0,0 +1,35 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const approvals = [ + { id: "a1", type: "请假", applicant: "张三", status: "pending", date: "2026-06-05" }, + { id: "a2", type: "报销", applicant: "李四", status: "approved", date: "2026-06-04" }, + { id: "a3", type: "加班", applicant: "王五", status: "rejected", date: "2026-06-03" }, +]; +const statusMap: Record = { + pending: { label: "待审批", color: "bg-yellow-100 text-yellow-700" }, + approved: { label: "已通过", color: "bg-green-100 text-green-700" }, + rejected: { label: "已驳回", color: "bg-red-100 text-red-700" }, +}; + +export default function ApprovalPage() { + return ( +
+

我的审批

+ {approvals.map(a => { + const s = statusMap[a.status]; + return ( + +
+
+

{a.type}申请 — {a.applicant}

+

{a.date}

+
+ {s.label} +
+
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/frontend/app/contracts/page.tsx b/.benchmark/contract-mgmt/frontend/app/contracts/page.tsx new file mode 100644 index 0000000..902f8d6 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/contracts/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useContracts } from "@/hooks/useContracts"; + +export default function PagePage() { + const { data, loading, error, refetch } = useContracts(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

合同创建

+

合同创建

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/customers/page.tsx b/.benchmark/contract-mgmt/frontend/app/customers/page.tsx new file mode 100644 index 0000000..a8cba9e --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/customers/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useCustomers } from "@/hooks/useCustomers"; + +export default function PagePage() { + const { data, loading, error, refetch } = useCustomers(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

客户管理

+

客户管理

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/globals.css b/.benchmark/contract-mgmt/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/contract-mgmt/frontend/app/layout.tsx b/.benchmark/contract-mgmt/frontend/app/layout.tsx new file mode 100644 index 0000000..6faad51 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "OAFlow", + description: "一款支持审批流、考勤管理、部门协作的企业办公自动化系统。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/operation_logs/page.tsx b/.benchmark/contract-mgmt/frontend/app/operation_logs/page.tsx new file mode 100644 index 0000000..d8dce63 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/operation_logs/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

操作日志

+

操作日志

+
+ +
+ + { +

操作日志 页面内容

+

路由: /operation_logs

+
} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/page.tsx b/.benchmark/contract-mgmt/frontend/app/page.tsx new file mode 100644 index 0000000..ae4e833 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

OAFlow

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 合同创建 + + + 📅 + 合同审批 + + + 📝 + 合同到期提醒 + + + 📸 + 客户管理 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 OAFlow

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/reminders/page.tsx b/.benchmark/contract-mgmt/frontend/app/reminders/page.tsx new file mode 100644 index 0000000..156651d --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/reminders/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useReminders } from "@/hooks/useReminders"; + +export default function PagePage() { + const { data, loading, error, refetch } = useReminders(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

合同到期提醒

+

合同到期提醒

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/stats/page.tsx b/.benchmark/contract-mgmt/frontend/app/stats/page.tsx new file mode 100644 index 0000000..fba1ace --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/stats/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

合同金额统计

+

合同金额统计

+
+ +
+ + { +

合同金额统计 页面内容

+

路由: /stats

+
} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/layout/BottomNav.tsx b/.benchmark/contract-mgmt/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/layout/Sidebar.tsx b/.benchmark/contract-mgmt/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/Button.tsx b/.benchmark/contract-mgmt/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/Card.tsx b/.benchmark/contract-mgmt/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/EmptyState.tsx b/.benchmark/contract-mgmt/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/Input.tsx b/.benchmark/contract-mgmt/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/Modal.tsx b/.benchmark/contract-mgmt/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useApprovals.ts b/.benchmark/contract-mgmt/frontend/hooks/useApprovals.ts new file mode 100644 index 0000000..0163a46 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useApprovals.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for approvals +type Approval = Record; + +/** + * Fetch and manage approvals data. + */ +export function useApprovals() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/approvals`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useAuth.ts b/.benchmark/contract-mgmt/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useContracts.ts b/.benchmark/contract-mgmt/frontend/hooks/useContracts.ts new file mode 100644 index 0000000..9fbf845 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useContracts.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for contracts +type Contract = Record; + +/** + * Fetch and manage contracts data. + */ +export function useContracts() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/contracts`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useCustomers.ts b/.benchmark/contract-mgmt/frontend/hooks/useCustomers.ts new file mode 100644 index 0000000..8669d02 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useCustomers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for customers +type Customer = Record; + +/** + * Fetch and manage customers data. + */ +export function useCustomers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/customers`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useDebounce.ts b/.benchmark/contract-mgmt/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useForm.ts b/.benchmark/contract-mgmt/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useReminders.ts b/.benchmark/contract-mgmt/frontend/hooks/useReminders.ts new file mode 100644 index 0000000..7ce891d --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useReminders.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for reminders +type Reminder = Record; + +/** + * Fetch and manage reminders data. + */ +export function useReminders() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/reminders`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/next.config.ts b/.benchmark/contract-mgmt/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/contract-mgmt/frontend/package.json b/.benchmark/contract-mgmt/frontend/package.json new file mode 100644 index 0000000..696da02 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "oaflow", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/frontend/postcss.config.js b/.benchmark/contract-mgmt/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/contract-mgmt/frontend/services/api.ts b/.benchmark/contract-mgmt/frontend/services/api.ts new file mode 100644 index 0000000..db22ccf --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for OAFlow + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/contract-mgmt/frontend/services/approvals.ts b/.benchmark/contract-mgmt/frontend/services/approvals.ts new file mode 100644 index 0000000..65620ab --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/approvals.ts @@ -0,0 +1,10 @@ +// Auto-generated approvals service +import { api } from "./api"; + +export function get() { + return api.get("/api/approvals"); +} + +export function post(data: Record) { + return api.post("/api/approvals", data); +} diff --git a/.benchmark/contract-mgmt/frontend/services/auth.ts b/.benchmark/contract-mgmt/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/contract-mgmt/frontend/services/contracts.ts b/.benchmark/contract-mgmt/frontend/services/contracts.ts new file mode 100644 index 0000000..a2c8c24 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/contracts.ts @@ -0,0 +1,10 @@ +// Auto-generated contracts service +import { api } from "./api"; + +export function get() { + return api.get("/api/contracts"); +} + +export function post(data: Record) { + return api.post("/api/contracts", data); +} diff --git a/.benchmark/contract-mgmt/frontend/services/customers.ts b/.benchmark/contract-mgmt/frontend/services/customers.ts new file mode 100644 index 0000000..c0e2a5e --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/customers.ts @@ -0,0 +1,10 @@ +// Auto-generated customers service +import { api } from "./api"; + +export function get() { + return api.get("/api/customers"); +} + +export function post(data: Record) { + return api.post("/api/customers", data); +} diff --git a/.benchmark/contract-mgmt/frontend/services/reminders.ts b/.benchmark/contract-mgmt/frontend/services/reminders.ts new file mode 100644 index 0000000..6bfa9b5 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/reminders.ts @@ -0,0 +1,10 @@ +// Auto-generated reminders service +import { api } from "./api"; + +export function get() { + return api.get("/api/reminders"); +} + +export function post(data: Record) { + return api.post("/api/reminders", data); +} diff --git a/.benchmark/contract-mgmt/frontend/tailwind.config.ts b/.benchmark/contract-mgmt/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/contract-mgmt/frontend/tsconfig.json b/.benchmark/contract-mgmt/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/frontend/types/index.ts b/.benchmark/contract-mgmt/frontend/types/index.ts new file mode 100644 index 0000000..b49e42e --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/types/index.ts @@ -0,0 +1,106 @@ +// Auto-generated types for OAFlow + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface OperationLogs { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/prd.json b/.benchmark/contract-mgmt/prd.json new file mode 100644 index 0000000..adee471 --- /dev/null +++ b/.benchmark/contract-mgmt/prd.json @@ -0,0 +1 @@ +{"projectName":"OAFlow","chineseName":"企业办公自动化","domain":"enterprise","matchConfidence":"high","summary":"一款支持审批流、考勤管理、部门协作的企业办公自动化系统。","personas":[{"name":"普通员工","description":"需要日常考勤打卡、发起审批的企业员工","painPoints":["审批流程繁琐","考勤异常处理麻烦"]},{"name":"部门主管","description":"负责审批、团队管理的管理者","painPoints":["审批堆积","团队出勤无法一目了然"]},{"name":"HR/行政","description":"负责考勤统计、人事管理的HR","painPoints":["考勤数据手工统计","跨部门流程混乱"]}],"userStories":[{"id":"US-001","as":"普通员工","want":"合同创建","soThat":"解决痛点:审批流程繁琐","priority":"P0"},{"id":"US-002","as":"普通员工","want":"合同审批","soThat":"解决痛点:审批流程繁琐","priority":"P0"},{"id":"US-003","as":"普通员工","want":"合同到期提醒","soThat":"解决痛点:审批流程繁琐","priority":"P0"},{"id":"US-004","as":"部门主管","want":"合同创建","soThat":"解决痛点:审批堆积","priority":"P0"},{"id":"US-005","as":"部门主管","want":"合同审批","soThat":"解决痛点:审批堆积","priority":"P0"},{"id":"US-006","as":"部门主管","want":"合同到期提醒","soThat":"解决痛点:审批堆积","priority":"P0"},{"id":"US-007","as":"HR/行政","want":"合同创建","soThat":"解决痛点:考勤数据手工统计","priority":"P0"},{"id":"US-008","as":"HR/行政","want":"合同审批","soThat":"解决痛点:考勤数据手工统计","priority":"P0"},{"id":"US-009","as":"HR/行政","want":"合同到期提醒","soThat":"解决痛点:考勤数据手工统计","priority":"P0"}],"mvpScope":{"description":"MVP 聚焦 合同创建、合同审批、合同到期提醒、客户管理、合同金额统计、操作日志,包含页面:首页、合同创建、合同审批、合同到期提醒、客户管理","features":["合同创建","合同审批","合同到期提醒","客户管理","合同金额统计","操作日志"],"pages":["首页","合同创建","合同审批","合同到期提醒","客户管理"],"estimatedWeeks":3},"features":[{"name":"合同创建","description":"合同创建","priority":"P0"},{"name":"合同审批","description":"合同审批","priority":"P0"},{"name":"合同到期提醒","description":"合同到期提醒","priority":"P0"},{"name":"客户管理","description":"客户管理","priority":"P0"},{"name":"合同金额统计","description":"合同金额统计","priority":"P0"},{"name":"操作日志","description":"操作日志","priority":"P0"}],"pages":[{"name":"首页","route":"/home","description":"应用首页"},{"name":"合同创建","route":"/contracts","description":"合同创建"},{"name":"合同审批","route":"/approvals","description":"合同审批"},{"name":"合同到期提醒","route":"/reminders","description":"合同到期提醒"},{"name":"客户管理","route":"/customers","description":"客户管理"},{"name":"合同金额统计","route":"/stats","description":"合同金额统计"},{"name":"操作日志","route":"/operation_logs","description":"操作日志"}],"apiRequirements":[{"method":"GET","path":"/api/contracts","description":"获取合同创建列表"},{"method":"POST","path":"/api/contracts","description":"创建合同创建"},{"method":"GET","path":"/api/approvals","description":"获取合同审批列表"},{"method":"POST","path":"/api/approvals","description":"创建合同审批"},{"method":"GET","path":"/api/reminders","description":"获取合同到期提醒列表"},{"method":"POST","path":"/api/reminders","description":"创建合同到期提醒"},{"method":"GET","path":"/api/customers","description":"获取客户管理列表"},{"method":"POST","path":"/api/customers","description":"创建客户管理"},{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}],"techConstraints":{"platforms":["desktop"],"recommendedStack":"React Native / Flutter(跨平台)","considerations":["建议跨平台框架减少开发成本","需支持 RBAC 权限模型","需考虑企业 SSO 集成"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"项目脚手架搭建","description":"初始化 OAFlow 项目结构,配置构建工具和 CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"数据库设计与初始化","description":"设计数据模型,创建数据库迁移脚本","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"页面开发:首页","description":"开发 首页 页面(/home):应用首页","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"页面开发:合同创建","description":"开发 合同创建 页面(/contracts):合同创建","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"页面开发:合同审批","description":"开发 合同审批 页面(/approvals):合同审批","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"页面开发:合同到期提醒","description":"开发 合同到期提醒 页面(/reminders):合同到期提醒","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API 开发:GET /api/contracts","description":"获取合同创建列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API 开发:POST /api/contracts","description":"创建合同创建","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API 开发:GET /api/approvals","description":"获取合同审批列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API 开发:POST /api/approvals","description":"创建合同审批","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"前后端联调","description":"前端页面与后端 API 集成联调","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"测试与修复","description":"功能测试、Bug 修复、性能优化","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T13:43:26.870Z","inputLength":48,"domainMatchCount":1,"extractedFeatureCount":6,"extractedPageCount":7,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/README.md b/.benchmark/contract-mgmt/release/release/README.md new file mode 100644 index 0000000..2688299 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/README.md @@ -0,0 +1,33 @@ +# oaflow — Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T13:43:27.214Z + +## Directory Structure + +``` +release/ +├── version.json — Version metadata +├── build-info.json — Build environment info +├── README.md — This file +├── manifests/ +│ └── manifest.json — Release manifest with file hashes +├── checksums/ +│ └── checksums.txt — SHA256 checksums for verification +├── release-notes/ +│ └── release-notes.md — Human-readable release notes +├── windows/ — Windows installers +├── macos/ — macOS disk images +└── linux/ — Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/build-info.json b/.benchmark/contract-mgmt/release/release/build-info.json new file mode 100644 index 0000000..ef7691f --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T13:43:27.214Z", + "domain": "unknown", + "entityCount": 7, + "routeCount": 1, + "screenCount": 6, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "oaflow", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/checksums/checksums.txt b/.benchmark/contract-mgmt/release/release/checksums/checksums.txt new file mode 100644 index 0000000..a4f150a --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +f5b8f0725308ab4c22da6343cc15692a1803714e53becdbb5151b519c19fab2b windows/oaflow-setup-0.1.0.exe +47a4a3cd0533b44e0253132d04c92fa9c5e657c856ef762520db11c12f0d880a windows/oaflow-0.1.0-portable.exe +248f6af0f2e07555102549ea6624deb2e5c73167db17fd7dbc9fe7046603b53f macos/oaflow-0.1.0.dmg +7b78cfee2917c9053a33bc386d19ff7531fc94366bd5530764399bbbc0a81eaa macos/oaflow-0.1.0-arm64.dmg +8387fdf21838ffff29b78bb8af70a9a0291f3e8f00c3a192df8952baf92b28da linux/oaflow-0.1.0.AppImage +7c0731541f4667ee7c50a31ef4731bf6bf00d30d9e3bc351f34ca21b855ad828 linux/oaflow_0.1.0_amd64.deb diff --git a/.benchmark/contract-mgmt/release/release/linux/oaflow-0.1.0.AppImage b/.benchmark/contract-mgmt/release/release/linux/oaflow-0.1.0.AppImage new file mode 100644 index 0000000..a457331 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/linux/oaflow-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/linux/oaflow_0.1.0_amd64.deb b/.benchmark/contract-mgmt/release/release/linux/oaflow_0.1.0_amd64.deb new file mode 100644 index 0000000..419a570 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/linux/oaflow_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0-arm64.dmg b/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0-arm64.dmg new file mode 100644 index 0000000..38a2028 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0.dmg b/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0.dmg new file mode 100644 index 0000000..9449d3b --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/manifests/manifest.json b/.benchmark/contract-mgmt/release/release/manifests/manifest.json new file mode 100644 index 0000000..479662f --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "oaflow", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T13:43:27.214Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/oaflow-setup-0.1.0.exe", + "size": 141, + "sha256": "f5b8f0725308ab4c22da6343cc15692a1803714e53becdbb5151b519c19fab2b" + }, + { + "path": "windows/oaflow-0.1.0-portable.exe", + "size": 145, + "sha256": "47a4a3cd0533b44e0253132d04c92fa9c5e657c856ef762520db11c12f0d880a" + }, + { + "path": "macos/oaflow-0.1.0.dmg", + "size": 137, + "sha256": "248f6af0f2e07555102549ea6624deb2e5c73167db17fd7dbc9fe7046603b53f" + }, + { + "path": "macos/oaflow-0.1.0-arm64.dmg", + "size": 139, + "sha256": "7b78cfee2917c9053a33bc386d19ff7531fc94366bd5530764399bbbc0a81eaa" + }, + { + "path": "linux/oaflow-0.1.0.AppImage", + "size": 130, + "sha256": "8387fdf21838ffff29b78bb8af70a9a0291f3e8f00c3a192df8952baf92b28da" + }, + { + "path": "linux/oaflow_0.1.0_amd64.deb", + "size": 140, + "sha256": "7c0731541f4667ee7c50a31ef4731bf6bf00d30d9e3bc351f34ca21b855ad828" + } + ], + "totalFiles": 6, + "totalSize": 832 +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/release-notes/release-notes.md b/.benchmark/contract-mgmt/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..885ead4 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/release-notes/release-notes.md @@ -0,0 +1,56 @@ +# oaflow v0.1.0 + +> Release Builder Agent — SF-07 +> Generated: 2026-06-05 13:43:27 UTC + +## Version + +- **Name:** oaflow +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- approvals +- auth +- contracts +- customers +- operation_logs +- reminders +- stats + +## Build Info + +- **Build Time:** 2026-06-05T13:43:27.214Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- ✅ windows +- ✅ macos +- ✅ linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/version.json b/.benchmark/contract-mgmt/release/release/version.json new file mode 100644 index 0000000..1c5bd28 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "oaflow", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/windows/oaflow-0.1.0-portable.exe b/.benchmark/contract-mgmt/release/release/windows/oaflow-0.1.0-portable.exe new file mode 100644 index 0000000..c5888cc --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/windows/oaflow-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/windows/oaflow-setup-0.1.0.exe b/.benchmark/contract-mgmt/release/release/windows/oaflow-setup-0.1.0.exe new file mode 100644 index 0000000..e0276c1 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/windows/oaflow-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/summary.json b/.benchmark/contract-mgmt/summary.json new file mode 100644 index 0000000..9ba6ab6 --- /dev/null +++ b/.benchmark/contract-mgmt/summary.json @@ -0,0 +1,43 @@ +{ + "input": "开发一个合同管理桌面工具,支持合同创建、合同审批、合同到期提醒、客户管理、合同金额统计、操作日志", + "projectName": "OAFlow", + "domain": "enterprise", + "stages": { + "prd": { + "ms": 51 + }, + "arch": { + "ms": 55, + "modules": 9 + }, + "frontend": { + "ms": 51, + "files": 35, + "pass": true + }, + "backend": { + "ms": 54, + "files": 32, + "pass": true + }, + "fullstack": { + "ms": 57, + "files": 79, + "pass": true + }, + "electron": { + "ms": 49, + "files": 12, + "pass": true + }, + "release": { + "ms": 54, + "files": 12, + "pass": true + } + }, + "totalMs": 371, + "totalFiles": 170, + "allPass": true, + "outputDir": "/Users/a1234/.openclaw/workspace/.benchmark/contract-mgmt" +} \ No newline at end of file diff --git a/.benchmark/course/arch.json b/.benchmark/course/arch.json new file mode 100644 index 0000000..67c3b67 --- /dev/null +++ b/.benchmark/course/arch.json @@ -0,0 +1 @@ +{"projectName":"EduPlatform","domain":"education","contract":{"projectName":"EduPlatform","domain":"education","entities":[{"name":"User","table":"users","description":"系统用户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin","instructor"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[]},{"name":"Courses","table":"courses","description":"课程发布表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"instructor","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"price","type":"DECIMAL(10,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"cover_url","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"category","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Chapters","table":"chapters","description":"章节管理表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"course_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"courses","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"sort_order","type":"INTEGER","required":false,"isPrimary":false,"isAuto":true},{"name":"duration_min","type":"INTEGER","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"courses","via":"course_id"}]},{"name":"Students","table":"students","description":"学员进度表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(64)","required":true,"isPrimary":false,"isAuto":false},{"name":"email","type":"VARCHAR(256)","required":false,"isPrimary":false,"isAuto":false},{"name":"enrolled_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"progress","type":"DECIMAL(5,2)","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Assignments","table":"assignments","description":"作业批改表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"course_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"courses","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"due_date","type":"DATE","required":false,"isPrimary":false,"isAuto":false},{"name":"max_score","type":"INTEGER","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"courses","via":"course_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","cover_url":"coverUrl","course_id":"courseId","sort_order":"sortOrder","duration_min":"durationMin","enrolled_at":"enrolledAt","due_date":"dueDate","max_score":"maxScore","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","remind_at":"remindAt","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","coverUrl":"cover_url","courseId":"course_id","sortOrder":"sort_order","durationMin":"duration_min","enrolledAt":"enrolled_at","dueDate":"due_date","maxScore":"max_score","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","remindAt":"remind_at","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin","instructor"],"defaultValue":"user"}},"Courses":{"id":{"required":true},"title":{"required":true}},"Chapters":{"id":{"required":true},"title":{"required":true}},"Students":{"id":{"required":true},"name":{"required":true}},"Assignments":{"id":{"required":true},"title":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO(文件存储)","deployment":"Docker Compose + Nginx / K8s(规模化)","considerations":[]},"architectureDiagram":"┌─────────────────────────────────────────────────────────┐\n│ EduPlatform — System Architecture │\n├─────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────────────┐ ┌──────────────────────┐ │\n│ │ Client Layer │ │ Admin / Web │ │\n│ │ React Native 0.76 + Expo / Flutter 3.x │ │ React + Vite (Web) │ │\n│ └──────────┬───────────┘ └──────────┬───────────┘ │\n│ │ │ │\n│ └──────────┬────────────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ API Gateway │ │\n│ │ Nginx / Kong │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ Backend Services │ │\n│ │ NestJS + Prisma │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌───────────────┼───────────────┐ │\n│ │ │ │ │\n│ ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ │\n│ │ PostgreSQL 15│ │ Redis Cache │ │ MinIO │ │\n│ │ Primary │ │ Session │ │ Files │ │\n│ └───────────┘ └─────────────┘ └──────────┘ │\n│ │\n├─────────────────────────────────────────────────────────┤\n│ Modules: │\n│ 1 课程发布 │\n│ 2 章节管理 │\n│ 3 学员进度 │\n│ 4 作业批改 │\n│ 5 课程管理 │\n│ 6 题库管理 │\n│ │\n└─────────────────────────────────────────────────────────┘","dataFlows":[{"page":"课程发布","route":"/courses","description":"课程发布","dataFlow":["GET /api/courses → 获取课程发布列表","POST /api/courses → 创建课程发布"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"章节管理","route":"/chapters","description":"章节管理","dataFlow":["GET /api/chapters → 获取章节管理列表","POST /api/chapters → 创建章节管理"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"学员进度","route":"/students","description":"学员进度","dataFlow":["GET /api/students → 获取学员进度列表","POST /api/students → 创建学员进度"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"作业批改","route":"/assignments","description":"作业批改","dataFlow":["GET /api/assignments → 获取作业批改列表","POST /api/assignments → 创建作业批改"],"direction":"Client → API Gateway → Backend → Database → Response → Client"}],"modules":[{"name":"课程发布","label":"课程发布","features":["课程发布"],"responsibilities":["提供 课程发布 相关功能"]},{"name":"章节管理","label":"章节管理","features":["章节管理"],"responsibilities":["提供 章节管理 相关功能"]},{"name":"学员进度","label":"学员进度","features":["学员进度"],"responsibilities":["提供 学员进度 相关功能"]},{"name":"作业批改","label":"作业批改","features":["作业批改"],"responsibilities":["提供 作业批改 相关功能"]},{"name":"course","label":"课程管理","features":["课程中心","视频播放"],"responsibilities":["提供 课程中心 相关功能","提供 视频播放 相关功能"]},{"name":"exercise","label":"题库管理","features":["题库练习"],"responsibilities":["提供 题库练习 相关功能"]}],"databaseSchema":[{"table":"users","description":"用户表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"courses","description":"课程发布表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"instructor","type":"VARCHAR(64)"},{"name":"price","type":"DECIMAL(10,2)"},{"name":"cover_url","type":"TEXT"},{"name":"category","type":"VARCHAR(64)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_courses_user ON courses(user_id)"]},{"table":"chapters","description":"章节管理表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"course_id","type":"UUID","constraints":"FK → courses.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"sort_order","type":"INTEGER","constraints":"DEFAULT 0"},{"name":"duration_min","type":"INTEGER"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_chapters_user ON chapters(user_id)"]},{"table":"students","description":"学员进度表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"name","type":"VARCHAR(64)","constraints":"NOT NULL"},{"name":"email","type":"VARCHAR(256)"},{"name":"enrolled_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"progress","type":"DECIMAL(5,2)","constraints":"DEFAULT 0"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_students_user ON students(user_id)"]},{"table":"assignments","description":"作业批改表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"course_id","type":"UUID","constraints":"FK → courses.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"due_date","type":"DATE"},{"name":"max_score","type":"INTEGER","constraints":"DEFAULT 100"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_assignments_user ON assignments(user_id)"]}],"apiDesign":[{"resource":"assignments","basePath":"/api/assignments","endpoints":[{"method":"GET","path":"/api/assignments","description":"获取作业批改列表"},{"method":"POST","path":"/api/assignments","description":"创建作业批改"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}]},{"resource":"chapters","basePath":"/api/chapters","endpoints":[{"method":"GET","path":"/api/chapters","description":"获取章节管理列表"},{"method":"POST","path":"/api/chapters","description":"创建章节管理"}]},{"resource":"courses","basePath":"/api/courses","endpoints":[{"method":"GET","path":"/api/courses","description":"获取课程发布列表"},{"method":"POST","path":"/api/courses","description":"创建课程发布"}]},{"resource":"students","basePath":"/api/students","endpoints":[{"method":"GET","path":"/api/students","description":"获取学员进度列表"},{"method":"POST","path":"/api/students","description":"创建学员进度"}]}],"directoryStructure":["eduplatform/","├── apps/","│ ├── mobile/ # React Native / Flutter 移动端","│ │ ├── src/","│ │ │ ├── screens/ # 页面组件","│ │ │ │ ├── 课程发布/","│ │ │ │ ├── 章节管理/","│ │ │ │ ├── 学员进度/","│ │ │ │ ├── 作业批改/","│ │ │ ├── components/ # 通用组件","│ │ │ ├── hooks/ # 自定义 Hooks","│ │ │ ├── services/ # API 调用层","│ │ │ ├── store/ # 状态管理","│ │ │ └── utils/ # 工具函数","│ │ ├── app.json","│ │ └── package.json","│ └── web/ # Web 管理后台","│ ├── src/","│ │ ├── pages/","│ │ ├── components/","│ │ └── layouts/","│ └── package.json","├── packages/","│ └── shared/ # 共享类型/常量","├── server/ # NestJS 后端服务","│ ├── src/","│ │ ├── modules/ # 业务模块","│ │ │ ├── 课程发布/","│ │ │ │ ├── 课程发布.controller.ts","│ │ │ │ ├── 课程发布.service.ts","│ │ │ │ ├── 课程发布.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 章节管理/","│ │ │ │ ├── 章节管理.controller.ts","│ │ │ │ ├── 章节管理.service.ts","│ │ │ │ ├── 章节管理.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 学员进度/","│ │ │ │ ├── 学员进度.controller.ts","│ │ │ │ ├── 学员进度.service.ts","│ │ │ │ ├── 学员进度.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 作业批改/","│ │ │ │ ├── 作业批改.controller.ts","│ │ │ │ ├── 作业批改.service.ts","│ │ │ │ ├── 作业批改.module.ts","│ │ │ │ └── dto/","│ │ │ ├── course/","│ │ │ │ ├── course.controller.ts","│ │ │ │ ├── course.service.ts","│ │ │ │ ├── course.module.ts","│ │ │ │ └── dto/","│ │ ├── common/ # 通用(guards, filters, interceptors)","│ │ ├── prisma/ # Prisma ORM","│ │ │ └── schema.prisma","│ │ ├── config/ # 环境配置","│ │ └── main.ts","│ ├── test/","│ ├── Dockerfile","│ └── package.json","├── docker-compose.yml","├── .github/workflows/ # CI/CD","│ └── ci.yml","├── .env.example","├── README.md","└── turbo.json # Monorepo 配置"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s(规模化)","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions → Build → Test → Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:37.455Z","sourceDomain":"education","moduleCount":6,"tableCount":5,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/course/backend/.gitignore b/.benchmark/course/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/course/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/course/backend/README.md b/.benchmark/course/backend/README.md new file mode 100644 index 0000000..879ffbb --- /dev/null +++ b/.benchmark/course/backend/README.md @@ -0,0 +1,90 @@ +# EduPlatform — Backend API + +> 一个支持在线课程学习、直播授课、题库练习和学情追踪的教育平台。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### courses +- `GET /api/courses` — List all +- `GET /api/courses/:id` — Get by ID +- `POST /api/courses` — Create +- `PUT /api/courses/:id` — Update +- `DELETE /api/courses/:id` — Delete + +#### chapters +- `GET /api/chapters` — List all +- `GET /api/chapters/:id` — Get by ID +- `POST /api/chapters` — Create +- `PUT /api/chapters/:id` — Update +- `DELETE /api/chapters/:id` — Delete + +#### students +- `GET /api/students` — List all +- `GET /api/students/:id` — Get by ID +- `POST /api/students` — Create +- `PUT /api/students/:id` — Update +- `DELETE /api/students/:id` — Delete + +#### assignments +- `GET /api/assignments` — List all +- `GET /api/assignments/:id` — Get by ID +- `POST /api/assignments` — Create +- `PUT /api/assignments/:id` — Update +- `DELETE /api/assignments/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/course/backend/package.json b/.benchmark/course/backend/package.json new file mode 100644 index 0000000..1493a8d --- /dev/null +++ b/.benchmark/course/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "eduplatform", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/course/backend/src/__tests__/assignments.test.ts b/.benchmark/course/backend/src/__tests__/assignments.test.ts new file mode 100644 index 0000000..3f49cfe --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/assignments.test.ts @@ -0,0 +1,120 @@ +// Assignments CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "courseId": "sample-courseid", + "title": "sample-title", + "description": "sample-description", + "dueDate": "sample-duedate" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/assignments", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/assignments", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/assignments", () => { + it("creates a assignment", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/assignments", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/assignments/:id", () => { + it("returns the created assignment", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assignments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assignments/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/assignments/:id", () => { + it("updates the assignment", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/assignments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/assignments/:id", () => { + it("deletes the assignment", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/assignments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assignments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/auth.test.ts b/.benchmark/course/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/chapters.test.ts b/.benchmark/course/backend/src/__tests__/chapters.test.ts new file mode 100644 index 0000000..e99c835 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/chapters.test.ts @@ -0,0 +1,119 @@ +// Chapters CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "courseId": "sample-courseid", + "title": "sample-title", + "durationMin": 1 + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/chapters", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/chapters", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/chapters", () => { + it("creates a chapter", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/chapters", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/chapters/:id", () => { + it("returns the created chapter", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/chapters/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/chapters/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/chapters/:id", () => { + it("updates the chapter", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/chapters/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/chapters/:id", () => { + it("deletes the chapter", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/chapters/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/chapters/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/courses.test.ts b/.benchmark/course/backend/src/__tests__/courses.test.ts new file mode 100644 index 0000000..5ff6b9f --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/courses.test.ts @@ -0,0 +1,122 @@ +// Courses CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "instructor": "sample-instructor", + "price": 1, + "coverUrl": "sample-coverurl", + "category": "sample-category" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/courses", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/courses", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/courses", () => { + it("creates a cours", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/courses", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/courses/:id", () => { + it("returns the created cours", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/courses/:id", () => { + it("updates the cours", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/courses/:id", () => { + it("deletes the cours", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/exercises.test.ts b/.benchmark/course/backend/src/__tests__/exercises.test.ts new file mode 100644 index 0000000..3845bdf --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/exercises.test.ts @@ -0,0 +1,120 @@ +// Exercis CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/exercises", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/exercises", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/exercises", () => { + it("creates a exercis", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/exercises", + headers: { authorization: `Bearer ${token}` }, + payload: { + "lessonId": "00000000-0000-0000-0000-000000000001", + "question": "sample-question", + "options": "sample-options", + "answer": "sample-answer" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/exercises/:id", () => { + it("returns the created exercis", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/exercises/:id", () => { + it("updates the exercis", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "lessonId": "00000000-0000-0000-0000-000000000001", + "question": "sample-question", + "options": "sample-options", + "answer": "sample-answer" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/exercises/:id", () => { + it("deletes the exercis", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/lessons.test.ts b/.benchmark/course/backend/src/__tests__/lessons.test.ts new file mode 100644 index 0000000..adcd9d7 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/lessons.test.ts @@ -0,0 +1,122 @@ +// Lesson CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/lessons", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/lessons", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/lessons", () => { + it("creates a lesson", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/lessons", + headers: { authorization: `Bearer ${token}` }, + payload: { + "courseId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "videoUrl": "sample-videourl", + "durationSec": 1, + "sortOrder": 1 + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/lessons/:id", () => { + it("returns the created lesson", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/lessons/:id", () => { + it("updates the lesson", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "courseId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "videoUrl": "sample-videourl", + "durationSec": 1, + "sortOrder": 1 + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/lessons/:id", () => { + it("deletes the lesson", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/students.test.ts b/.benchmark/course/backend/src/__tests__/students.test.ts new file mode 100644 index 0000000..945cab6 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/students.test.ts @@ -0,0 +1,118 @@ +// Students CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/students", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/students", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/students", () => { + it("creates a student", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/students", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/students/:id", () => { + it("returns the created student", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/students/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/students/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/students/:id", () => { + it("updates the student", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/students/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/students/:id", () => { + it("deletes the student", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/students/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/students/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/user_progress.test.ts b/.benchmark/course/backend/src/__tests__/user_progress.test.ts new file mode 100644 index 0000000..86811c6 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/user_progress.test.ts @@ -0,0 +1,122 @@ +// UserProgress CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/user_progress", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/user_progress", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/user_progress", () => { + it("creates a user_progress", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/user_progress", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "lessonId": "00000000-0000-0000-0000-000000000001", + "completed": true, + "watchedSec": 1, + "UNIQUE(userId, lessonId)": "sample-unique(userid, lessonid)" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/user_progress/:id", () => { + it("returns the created user_progress", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/user_progress/:id", () => { + it("updates the user_progress", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "lessonId": "00000000-0000-0000-0000-000000000001", + "completed": true, + "watchedSec": 1, + "UNIQUE(userId, lessonId)": "sample-unique(userid, lessonid)" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/user_progress/:id", () => { + it("deletes the user_progress", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/users.test.ts b/.benchmark/course/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/db/client.ts b/.benchmark/course/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/course/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/course/backend/src/db/schema.ts b/.benchmark/course/backend/src/db/schema.ts new file mode 100644 index 0000000..57e05a7 --- /dev/null +++ b/.benchmark/course/backend/src/db/schema.ts @@ -0,0 +1,16 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS courses (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n instructor TEXT,\n price REAL,\n cover_url TEXT,\n category TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS chapters (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n course_id TEXT REFERENCES courses(id),\n title TEXT NOT NULL,\n sort_order INTEGER,\n duration_min INTEGER,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS students (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n enrolled_at TEXT,\n progress REAL,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS assignments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n course_id TEXT REFERENCES courses(id),\n title TEXT NOT NULL,\n description TEXT,\n due_date TEXT,\n max_score INTEGER,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/course/backend/src/index.ts b/.benchmark/course/backend/src/index.ts new file mode 100644 index 0000000..f8dda5f --- /dev/null +++ b/.benchmark/course/backend/src/index.ts @@ -0,0 +1,71 @@ +// EduPlatform — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { coursesRoutes } from "./routes/courses.js"; +import { chaptersRoutes } from "./routes/chapters.js"; +import { studentsRoutes } from "./routes/students.js"; +import { assignmentsRoutes } from "./routes/assignments.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-c08d8e17"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(coursesRoutes, { prefix: "/api/courses" }); + await app.register(chaptersRoutes, { prefix: "/api/chapters" }); + await app.register(studentsRoutes, { prefix: "/api/students" }); + await app.register(assignmentsRoutes, { prefix: "/api/assignments" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/course/backend/src/middleware/auth.ts b/.benchmark/course/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/course/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/course/backend/src/routes/assignments.ts b/.benchmark/course/backend/src/routes/assignments.ts new file mode 100644 index 0000000..155cf1d --- /dev/null +++ b/.benchmark/course/backend/src/routes/assignments.ts @@ -0,0 +1,51 @@ +// Auto-generated Assignments routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { AssignmentsService } from "../services/assignment.js"; +import type { CreateAssignmentsInput, UpdateAssignmentsInput } from "../types/index.js"; + +const service = new AssignmentsService(); + +export async function assignmentsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/assignments — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/assignments/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Assignments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/assignments — create + app.post<{ Body: CreateAssignmentsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/assignments/:id — update + app.put<{ Params: { id: string }; Body: UpdateAssignmentsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Assignments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/assignments/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Assignments not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/auth.ts b/.benchmark/course/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/course/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/course/backend/src/routes/chapters.ts b/.benchmark/course/backend/src/routes/chapters.ts new file mode 100644 index 0000000..0f8bac6 --- /dev/null +++ b/.benchmark/course/backend/src/routes/chapters.ts @@ -0,0 +1,51 @@ +// Auto-generated Chapters routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ChaptersService } from "../services/chapter.js"; +import type { CreateChaptersInput, UpdateChaptersInput } from "../types/index.js"; + +const service = new ChaptersService(); + +export async function chaptersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/chapters — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/chapters/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Chapters not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/chapters — create + app.post<{ Body: CreateChaptersInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/chapters/:id — update + app.put<{ Params: { id: string }; Body: UpdateChaptersInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Chapters not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/chapters/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Chapters not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/courses.ts b/.benchmark/course/backend/src/routes/courses.ts new file mode 100644 index 0000000..4ec97c0 --- /dev/null +++ b/.benchmark/course/backend/src/routes/courses.ts @@ -0,0 +1,51 @@ +// Auto-generated Courses routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CoursesService } from "../services/cours.js"; +import type { CreateCoursesInput, UpdateCoursesInput } from "../types/index.js"; + +const service = new CoursesService(); + +export async function coursesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/courses — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/courses/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/courses — create + app.post<{ Body: CreateCoursesInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/courses/:id — update + app.put<{ Params: { id: string }; Body: UpdateCoursesInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/courses/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/exercises.ts b/.benchmark/course/backend/src/routes/exercises.ts new file mode 100644 index 0000000..352bcb9 --- /dev/null +++ b/.benchmark/course/backend/src/routes/exercises.ts @@ -0,0 +1,51 @@ +// Auto-generated Exercis routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ExercisService } from "../services/exercis.js"; +import type { CreateExercisInput, UpdateExercisInput } from "../types/index.js"; + +const service = new ExercisService(); + +export async function exercisesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/exercises — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/exercises/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/exercises — create + app.post<{ Body: CreateExercisInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/exercises/:id — update + app.put<{ Params: { id: string }; Body: UpdateExercisInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/exercises/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/lessons.ts b/.benchmark/course/backend/src/routes/lessons.ts new file mode 100644 index 0000000..22468bd --- /dev/null +++ b/.benchmark/course/backend/src/routes/lessons.ts @@ -0,0 +1,51 @@ +// Auto-generated Lesson routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { LessonService } from "../services/lesson.js"; +import type { CreateLessonInput, UpdateLessonInput } from "../types/index.js"; + +const service = new LessonService(); + +export async function lessonsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/lessons — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/lessons/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/lessons — create + app.post<{ Body: CreateLessonInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/lessons/:id — update + app.put<{ Params: { id: string }; Body: UpdateLessonInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/lessons/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/students.ts b/.benchmark/course/backend/src/routes/students.ts new file mode 100644 index 0000000..6b97d76 --- /dev/null +++ b/.benchmark/course/backend/src/routes/students.ts @@ -0,0 +1,51 @@ +// Auto-generated Students routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { StudentsService } from "../services/student.js"; +import type { CreateStudentsInput, UpdateStudentsInput } from "../types/index.js"; + +const service = new StudentsService(); + +export async function studentsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/students — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/students/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Students not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/students — create + app.post<{ Body: CreateStudentsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/students/:id — update + app.put<{ Params: { id: string }; Body: UpdateStudentsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Students not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/students/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Students not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/user_progress.ts b/.benchmark/course/backend/src/routes/user_progress.ts new file mode 100644 index 0000000..ac9fec6 --- /dev/null +++ b/.benchmark/course/backend/src/routes/user_progress.ts @@ -0,0 +1,51 @@ +// Auto-generated UserProgress routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserProgressService } from "../services/user_progress.js"; +import type { CreateUserProgressInput, UpdateUserProgressInput } from "../types/index.js"; + +const service = new UserProgressService(); + +export async function user_progressRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/user_progress — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/user_progress/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/user_progress — create + app.post<{ Body: CreateUserProgressInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/user_progress/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserProgressInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/user_progress/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/users.ts b/.benchmark/course/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/course/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/services/assignment.ts b/.benchmark/course/backend/src/services/assignment.ts new file mode 100644 index 0000000..4fbdee4 --- /dev/null +++ b/.benchmark/course/backend/src/services/assignment.ts @@ -0,0 +1,55 @@ +// Auto-generated Assignments service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Assignments, CreateAssignmentsInput, UpdateAssignmentsInput } from "../types/index.js"; + +export class AssignmentsService { + /** List all assignments */ + list(): Assignments[] { + return queryAll("SELECT * FROM assignments ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Assignments | undefined { + return queryOne("SELECT * FROM assignments WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateAssignmentsInput): Assignments { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "course_id", "title", "description", "due_date", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.courseId ?? null, input.title ?? null, input.description ?? null, input.dueDate ?? null, now, now]; + + execute(`INSERT INTO assignments (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateAssignmentsInput): Assignments | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.courseId !== undefined) { sets.push("course_id = ?"); values.push(input.courseId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.dueDate !== undefined) { sets.push("due_date = ?"); values.push(input.dueDate); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE assignments SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM assignments WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/chapter.ts b/.benchmark/course/backend/src/services/chapter.ts new file mode 100644 index 0000000..cb79b82 --- /dev/null +++ b/.benchmark/course/backend/src/services/chapter.ts @@ -0,0 +1,54 @@ +// Auto-generated Chapters service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Chapters, CreateChaptersInput, UpdateChaptersInput } from "../types/index.js"; + +export class ChaptersService { + /** List all chapters */ + list(): Chapters[] { + return queryAll("SELECT * FROM chapters ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Chapters | undefined { + return queryOne("SELECT * FROM chapters WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateChaptersInput): Chapters { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "course_id", "title", "duration_min", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.courseId ?? null, input.title ?? null, input.durationMin ?? null, now, now]; + + execute(`INSERT INTO chapters (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateChaptersInput): Chapters | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.courseId !== undefined) { sets.push("course_id = ?"); values.push(input.courseId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.durationMin !== undefined) { sets.push("duration_min = ?"); values.push(input.durationMin); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE chapters SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM chapters WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/cours.ts b/.benchmark/course/backend/src/services/cours.ts new file mode 100644 index 0000000..38e9af2 --- /dev/null +++ b/.benchmark/course/backend/src/services/cours.ts @@ -0,0 +1,57 @@ +// Auto-generated Courses service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Courses, CreateCoursesInput, UpdateCoursesInput } from "../types/index.js"; + +export class CoursesService { + /** List all courses */ + list(): Courses[] { + return queryAll("SELECT * FROM courses ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Courses | undefined { + return queryOne("SELECT * FROM courses WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCoursesInput): Courses { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "instructor", "price", "cover_url", "category", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.instructor ?? null, input.price ?? null, input.coverUrl ?? null, input.category ?? null, now, now]; + + execute(`INSERT INTO courses (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCoursesInput): Courses | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.instructor !== undefined) { sets.push("instructor = ?"); values.push(input.instructor); } + if (input.price !== undefined) { sets.push("price = ?"); values.push(input.price); } + if (input.coverUrl !== undefined) { sets.push("cover_url = ?"); values.push(input.coverUrl); } + if (input.category !== undefined) { sets.push("category = ?"); values.push(input.category); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE courses SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM courses WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/exercis.ts b/.benchmark/course/backend/src/services/exercis.ts new file mode 100644 index 0000000..c6bef85 --- /dev/null +++ b/.benchmark/course/backend/src/services/exercis.ts @@ -0,0 +1,56 @@ +// Auto-generated Exercis service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Exercis, CreateExercisInput, UpdateExercisInput } from "../types/index.js"; + +export class ExercisService { + /** List all exercises */ + list(): Exercis[] { + return queryAll("SELECT * FROM exercises ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Exercis | undefined { + return queryOne("SELECT * FROM exercises WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateExercisInput): Exercis { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "lesson_id", "question", "options", "answer"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.lessonId ?? null, input.question ?? null, input.options ?? null, input.answer ?? null]; + + execute(`INSERT INTO exercises (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateExercisInput): Exercis | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.lessonId !== undefined) { sets.push("lesson_id = ?"); values.push(input.lessonId); } + if (input.question !== undefined) { sets.push("question = ?"); values.push(input.question); } + if (input.options !== undefined) { sets.push("options = ?"); values.push(input.options); } + if (input.answer !== undefined) { sets.push("answer = ?"); values.push(input.answer); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE exercises SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM exercises WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/lesson.ts b/.benchmark/course/backend/src/services/lesson.ts new file mode 100644 index 0000000..2ddf9ab --- /dev/null +++ b/.benchmark/course/backend/src/services/lesson.ts @@ -0,0 +1,57 @@ +// Auto-generated Lesson service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Lesson, CreateLessonInput, UpdateLessonInput } from "../types/index.js"; + +export class LessonService { + /** List all lessons */ + list(): Lesson[] { + return queryAll("SELECT * FROM lessons ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Lesson | undefined { + return queryOne("SELECT * FROM lessons WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateLessonInput): Lesson { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "course_id", "title", "video_url", "duration_sec", "sort_order"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.courseId ?? null, input.title ?? null, input.videoUrl ?? null, input.durationSec ?? null, input.sortOrder ?? null]; + + execute(`INSERT INTO lessons (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateLessonInput): Lesson | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.courseId !== undefined) { sets.push("course_id = ?"); values.push(input.courseId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.videoUrl !== undefined) { sets.push("video_url = ?"); values.push(input.videoUrl); } + if (input.durationSec !== undefined) { sets.push("duration_sec = ?"); values.push(input.durationSec); } + if (input.sortOrder !== undefined) { sets.push("sort_order = ?"); values.push(input.sortOrder); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE lessons SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM lessons WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/student.ts b/.benchmark/course/backend/src/services/student.ts new file mode 100644 index 0000000..595ecaa --- /dev/null +++ b/.benchmark/course/backend/src/services/student.ts @@ -0,0 +1,53 @@ +// Auto-generated Students service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Students, CreateStudentsInput, UpdateStudentsInput } from "../types/index.js"; + +export class StudentsService { + /** List all students */ + list(): Students[] { + return queryAll("SELECT * FROM students ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Students | undefined { + return queryOne("SELECT * FROM students WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateStudentsInput): Students { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, now, now]; + + execute(`INSERT INTO students (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateStudentsInput): Students | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE students SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM students WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/user.ts b/.benchmark/course/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/course/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/user_progress.ts b/.benchmark/course/backend/src/services/user_progress.ts new file mode 100644 index 0000000..a14a7c8 --- /dev/null +++ b/.benchmark/course/backend/src/services/user_progress.ts @@ -0,0 +1,56 @@ +// Auto-generated UserProgress service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { UserProgress, CreateUserProgressInput, UpdateUserProgressInput } from "../types/index.js"; + +export class UserProgressService { + /** List all user_progress */ + list(): UserProgress[] { + return queryAll("SELECT * FROM user_progress ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): UserProgress | undefined { + return queryOne("SELECT * FROM user_progress WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserProgressInput): UserProgress { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "lesson_id", "completed", "watched_sec"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.lessonId ?? null, input.completed ?? null, input.watchedSec ?? null]; + + execute(`INSERT INTO user_progress (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserProgressInput): UserProgress | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.lessonId !== undefined) { sets.push("lesson_id = ?"); values.push(input.lessonId); } + if (input.completed !== undefined) { sets.push("completed = ?"); values.push(input.completed); } + if (input.watchedSec !== undefined) { sets.push("watched_sec = ?"); values.push(input.watchedSec); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE user_progress SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM user_progress WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/types/fastify.d.ts b/.benchmark/course/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/course/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/course/backend/src/types/index.ts b/.benchmark/course/backend/src/types/index.ts new file mode 100644 index 0000000..0bb3219 --- /dev/null +++ b/.benchmark/course/backend/src/types/index.ts @@ -0,0 +1,185 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Courses { + id: string; + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Chapters { + id: string; + userId?: string; + courseId?: string; + title: string; + sortOrder?: number; + durationMin?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Students { + id: string; + userId?: string; + name: string; + email?: string; + enrolledAt?: string; + progress?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Assignments { + id: string; + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; + maxScore?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateCoursesInput { + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; +} + +export interface CreateChaptersInput { + userId?: string; + courseId?: string; + title: string; + durationMin?: number; +} + +export interface CreateStudentsInput { + userId?: string; + name: string; + email?: string; +} + +export interface CreateAssignmentsInput { + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateCoursesInput { + userId?: string; + title?: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; +} + +export interface UpdateChaptersInput { + userId?: string; + courseId?: string; + title?: string; + durationMin?: number; +} + +export interface UpdateStudentsInput { + userId?: string; + name?: string; + email?: string; +} + +export interface UpdateAssignmentsInput { + userId?: string; + courseId?: string; + title?: string; + description?: string; + dueDate?: string; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/course/backend/src/types/sql.js.d.ts b/.benchmark/course/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/course/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/course/backend/tsconfig.json b/.benchmark/course/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/course/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/course/electron/README.md b/.benchmark/course/electron/README.md new file mode 100644 index 0000000..971613c --- /dev/null +++ b/.benchmark/course/electron/README.md @@ -0,0 +1,82 @@ +# eduplatform — Desktop + +> Electron desktop application wrapping the eduplatform fullstack web project. + +## Architecture + +``` +Web Fullstack Project + × + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** — Window management, menu, tray, IPC, auto-update +- **Preload** — Secure bridge between main and renderer +- **Renderer** — Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +├── main.ts — Main process (BrowserWindow, Menu, lifecycle) +├── preload.ts — Secure IPC bridge (contextBridge) +├── ipc.ts — IPC handlers (dialogs, store, notifications) +├── tray.ts — System tray +├── updater.ts — Auto-update (electron-updater) +└── utils.ts — Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/course/electron/assets/icon.png b/.benchmark/course/electron/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..554e8a31d1f0205fbf5ef853aba9a4fa2fc490dd GIT binary patch literal 66 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv4;KqeCd<5Tr}e}F6o MPgg&ebxsLQ09s=V>;M1& literal 0 HcmV?d00001 diff --git a/.benchmark/course/electron/electron-builder.yml b/.benchmark/course/electron/electron-builder.yml new file mode 100644 index 0000000..806e64c --- /dev/null +++ b/.benchmark/course/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml — Auto-generated by SF-06 + +appId: com.eduplatform.desktop +productName: eduplatform +copyright: "Copyright © 2026 eduplatform" + +# ─── Directories ───────────────────────────────── +directories: + output: release + buildResources: assets + +# ─── Files ─────────────────────────────────────── +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# ─── Extra Resources ───────────────────────────── +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# ─── macOS ─────────────────────────────────────── +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# ─── Windows ───────────────────────────────────── +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: eduplatform + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: eduplatform + +# ─── Linux ─────────────────────────────────────── +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: eduplatform + Comment: eduplatform Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# ─── Auto Update ───────────────────────────────── +publish: + provider: generic + url: https://releases.example.com/eduplatform/ + channel: latest diff --git a/.benchmark/course/electron/electron/ipc.ts b/.benchmark/course/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/course/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // ── App Info ── + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // ── Window Controls ── + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // ── File Dialogs ── + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // ── Notifications ── + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // ── Shell ── + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // ── Persistent Store ── + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/course/electron/electron/main.ts b/.benchmark/course/electron/electron/main.ts new file mode 100644 index 0000000..6cd3381 --- /dev/null +++ b/.benchmark/course/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// ─── Constants ─────────────────────────────────── +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "eduplatform"; + +// ─── Single Instance Lock ──────────────────────── +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// ─── State ─────────────────────────────────────── +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// ─── Create Window ─────────────────────────────── +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // ── Load content ── + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // ── Show when ready ── + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // ── External links → default browser ── + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // ── Prevent navigation away ── + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// ─── Icon Path ─────────────────────────────────── +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// ─── API Server (Production) ───────────────────── +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// ─── Menu ──────────────────────────────────────── +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// ─── App Lifecycle ─────────────────────────────── +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed — continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/course/electron/electron/preload.ts b/.benchmark/course/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/course/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// ─── Expose safe API to renderer ───────────────── +contextBridge.exposeInMainWorld("electronAPI", { + // ── App Info ── + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // ── Window Controls ── + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // ── File Dialogs ── + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // ── Notifications ── + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // ── Shell ── + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // ── Store ── + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // ── Updates ── + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // ── Update Events ── + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // ── Remove listener ── + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// ─── Type declarations for renderer ────────────── +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/course/electron/electron/tray.ts b/.benchmark/course/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/course/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/course/electron/electron/updater.ts b/.benchmark/course/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/course/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/course/electron/electron/utils.ts b/.benchmark/course/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/course/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// ─── Single Instance Lock ──────────────────────── +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// ─── Dev mode detection ────────────────────────── +export function isDev(): boolean { + return !app.isPackaged; +} + +// ─── Web URL ───────────────────────────────────── +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// ─── API Path ──────────────────────────────────── +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// ─── Get API port from env ─────────────────────── +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/course/electron/entitlements.mac.plist b/.benchmark/course/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/course/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/course/electron/package.json b/.benchmark/course/electron/package.json new file mode 100644 index 0000000..d8bbeee --- /dev/null +++ b/.benchmark/course/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "eduplatform-desktop", + "version": "1.0.0", + "private": true, + "description": "eduplatform — Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "eduplatform", + "appId": "com.eduplatform.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/course/electron/tsconfig.json b/.benchmark/course/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/course/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/assignments/page.tsx b/.benchmark/course/frontend/app/assignments/page.tsx new file mode 100644 index 0000000..ed98102 --- /dev/null +++ b/.benchmark/course/frontend/app/assignments/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useAssignments } from "@/hooks/useAssignments"; + +export default function PagePage() { + const { data, loading, error, refetch } = useAssignments(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

作业批改

+

作业批改

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/chapters/page.tsx b/.benchmark/course/frontend/app/chapters/page.tsx new file mode 100644 index 0000000..4d55e33 --- /dev/null +++ b/.benchmark/course/frontend/app/chapters/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useChapters } from "@/hooks/useChapters"; + +export default function PagePage() { + const { data, loading, error, refetch } = useChapters(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

章节管理

+

章节管理

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/course/[id]/page.tsx b/.benchmark/course/frontend/app/course/[id]/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/frontend/app/course/[id]/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 完全指南", instructor: "张老师", price: 199, cover: "📘" }, + { id: "c2", title: "TypeScript 高级编程", instructor: "李老师", price: 149, cover: "📙" }, + { id: "c3", title: "Node.js 后端实战", instructor: "王老师", price: 249, cover: "📗" }, +]; + +export default function CoursePage() { + return ( +
+

课程中心

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

¥{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/courses/page.tsx b/.benchmark/course/frontend/app/courses/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/frontend/app/courses/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 完全指南", instructor: "张老师", price: 199, cover: "📘" }, + { id: "c2", title: "TypeScript 高级编程", instructor: "李老师", price: 149, cover: "📙" }, + { id: "c3", title: "Node.js 后端实战", instructor: "王老师", price: 249, cover: "📗" }, +]; + +export default function CoursePage() { + return ( +
+

课程中心

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

¥{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/exercises/page.tsx b/.benchmark/course/frontend/app/exercises/page.tsx new file mode 100644 index 0000000..15d71e4 --- /dev/null +++ b/.benchmark/course/frontend/app/exercises/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +export default function ExercisePage() { + return ( +
+

题库练习

+

章节练习和模拟考试功能

+
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/globals.css b/.benchmark/course/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/course/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/course/frontend/app/layout.tsx b/.benchmark/course/frontend/app/layout.tsx new file mode 100644 index 0000000..8be9dca --- /dev/null +++ b/.benchmark/course/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "EduPlatform", + description: "一个支持在线课程学习、直播授课、题库练习和学情追踪的教育平台。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/course/frontend/app/learn/[courseId]/[lessonId]/page.tsx b/.benchmark/course/frontend/app/learn/[courseId]/[lessonId]/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/frontend/app/learn/[courseId]/[lessonId]/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 完全指南", instructor: "张老师", price: 199, cover: "📘" }, + { id: "c2", title: "TypeScript 高级编程", instructor: "李老师", price: 149, cover: "📙" }, + { id: "c3", title: "Node.js 后端实战", instructor: "王老师", price: 249, cover: "📗" }, +]; + +export default function CoursePage() { + return ( +
+

课程中心

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

¥{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/live/page.tsx b/.benchmark/course/frontend/app/live/page.tsx new file mode 100644 index 0000000..8679f8f --- /dev/null +++ b/.benchmark/course/frontend/app/live/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +export default function LivePage() { + return ( +
+

直播课堂

+

课程直播功能

+
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/page.tsx b/.benchmark/course/frontend/app/page.tsx new file mode 100644 index 0000000..47bd262 --- /dev/null +++ b/.benchmark/course/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

EduPlatform

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 课程发布 + + + 📅 + 章节管理 + + + 📝 + 学员进度 + + + 📸 + 作业批改 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 EduPlatform

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/course/frontend/app/profile/page.tsx b/.benchmark/course/frontend/app/profile/page.tsx new file mode 100644 index 0000000..ed7c604 --- /dev/null +++ b/.benchmark/course/frontend/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

我的

+

学习报告、已购课程、设置

+
+ +
+ + { +

我的 页面内容

+

路由: /profile

+
} +
+ ); +} diff --git a/.benchmark/course/frontend/app/students/page.tsx b/.benchmark/course/frontend/app/students/page.tsx new file mode 100644 index 0000000..4de9051 --- /dev/null +++ b/.benchmark/course/frontend/app/students/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useStudents } from "@/hooks/useStudents"; + +export default function PagePage() { + const { data, loading, error, refetch } = useStudents(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

学员进度

+

学员进度

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/作业批改/page.tsx b/.benchmark/course/frontend/app/作业批改/page.tsx new file mode 100644 index 0000000..f49e3d1 --- /dev/null +++ b/.benchmark/course/frontend/app/作业批改/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use作业批改 } from "@/hooks/use作业批改"; + +export default function PagePage() { + const { data, loading, error, refetch } = use作业批改(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

作业批改

+

作业批改

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/学员进度/page.tsx b/.benchmark/course/frontend/app/学员进度/page.tsx new file mode 100644 index 0000000..b58312b --- /dev/null +++ b/.benchmark/course/frontend/app/学员进度/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use学员进度 } from "@/hooks/use学员进度"; + +export default function PagePage() { + const { data, loading, error, refetch } = use学员进度(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

学员进度

+

学员进度

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/章节管理/page.tsx b/.benchmark/course/frontend/app/章节管理/page.tsx new file mode 100644 index 0000000..1e8a8fe --- /dev/null +++ b/.benchmark/course/frontend/app/章节管理/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use章节管理 } from "@/hooks/use章节管理"; + +export default function PagePage() { + const { data, loading, error, refetch } = use章节管理(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

章节管理

+

章节管理

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/课程发布/page.tsx b/.benchmark/course/frontend/app/课程发布/page.tsx new file mode 100644 index 0000000..e60d110 --- /dev/null +++ b/.benchmark/course/frontend/app/课程发布/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use课程发布 } from "@/hooks/use课程发布"; + +export default function PagePage() { + const { data, loading, error, refetch } = use课程发布(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

课程发布

+

课程发布

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/components/layout/BottomNav.tsx b/.benchmark/course/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/course/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/course/frontend/components/layout/Sidebar.tsx b/.benchmark/course/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/course/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/course/frontend/components/ui/Button.tsx b/.benchmark/course/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/course/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/course/frontend/components/ui/Card.tsx b/.benchmark/course/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/course/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/course/frontend/components/ui/EmptyState.tsx b/.benchmark/course/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/course/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/course/frontend/components/ui/Input.tsx b/.benchmark/course/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/course/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/course/frontend/components/ui/Modal.tsx b/.benchmark/course/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/course/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/course/frontend/hooks/useAssignments.ts b/.benchmark/course/frontend/hooks/useAssignments.ts new file mode 100644 index 0000000..f0c1577 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useAssignments.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for assignments +type Assignment = Record; + +/** + * Fetch and manage assignments data. + */ +export function useAssignments() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/assignments`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useAuth.ts b/.benchmark/course/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/course/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useChapters.ts b/.benchmark/course/frontend/hooks/useChapters.ts new file mode 100644 index 0000000..4dab116 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useChapters.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for chapters +type Chapter = Record; + +/** + * Fetch and manage chapters data. + */ +export function useChapters() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/chapters`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useCourses.ts b/.benchmark/course/frontend/hooks/useCourses.ts new file mode 100644 index 0000000..1f4fc4a --- /dev/null +++ b/.benchmark/course/frontend/hooks/useCourses.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for courses +type Cour = Record; + +/** + * Fetch and manage courses data. + */ +export function useCourses() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/courses`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useDebounce.ts b/.benchmark/course/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/course/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/course/frontend/hooks/useExercises.ts b/.benchmark/course/frontend/hooks/useExercises.ts new file mode 100644 index 0000000..d2c16ca --- /dev/null +++ b/.benchmark/course/frontend/hooks/useExercises.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for exercises +type Exerci = Record; + +/** + * Fetch and manage exercises data. + */ +export function useExercises() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/exercises`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useForm.ts b/.benchmark/course/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/course/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/course/frontend/hooks/useItem.ts b/.benchmark/course/frontend/hooks/useItem.ts new file mode 100644 index 0000000..06d6f48 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useItem.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for 章节管理 +type Item = Record; + +/** + * Fetch and manage 章节管理 data. + */ +export function useItem() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/章节管理`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useLessons.ts b/.benchmark/course/frontend/hooks/useLessons.ts new file mode 100644 index 0000000..0bf3571 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useLessons.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for lessons +type Lesson = Record; + +/** + * Fetch and manage lessons data. + */ +export function useLessons() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/lessons`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useProgress.ts b/.benchmark/course/frontend/hooks/useProgress.ts new file mode 100644 index 0000000..cdcac25 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useProgress.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for progress +type Progres = Record; + +/** + * Fetch and manage progress data. + */ +export function useProgress() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/progress`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useStudents.ts b/.benchmark/course/frontend/hooks/useStudents.ts new file mode 100644 index 0000000..f000f19 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useStudents.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for students +type Student = Record; + +/** + * Fetch and manage students data. + */ +export function useStudents() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/students`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/next-env.d.ts b/.benchmark/course/frontend/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/.benchmark/course/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/.benchmark/course/frontend/next.config.ts b/.benchmark/course/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/course/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/course/frontend/package.json b/.benchmark/course/frontend/package.json new file mode 100644 index 0000000..653e74f --- /dev/null +++ b/.benchmark/course/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "eduplatform", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/course/frontend/postcss.config.js b/.benchmark/course/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/course/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/course/frontend/services/api.ts b/.benchmark/course/frontend/services/api.ts new file mode 100644 index 0000000..dbab5bc --- /dev/null +++ b/.benchmark/course/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for EduPlatform + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/course/frontend/services/assignments.ts b/.benchmark/course/frontend/services/assignments.ts new file mode 100644 index 0000000..d3f1668 --- /dev/null +++ b/.benchmark/course/frontend/services/assignments.ts @@ -0,0 +1,10 @@ +// Auto-generated assignments service +import { api } from "./api"; + +export function get() { + return api.get("/api/assignments"); +} + +export function post(data: Record) { + return api.post("/api/assignments", data); +} diff --git a/.benchmark/course/frontend/services/auth.ts b/.benchmark/course/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/course/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/course/frontend/services/chapters.ts b/.benchmark/course/frontend/services/chapters.ts new file mode 100644 index 0000000..012b896 --- /dev/null +++ b/.benchmark/course/frontend/services/chapters.ts @@ -0,0 +1,10 @@ +// Auto-generated chapters service +import { api } from "./api"; + +export function get() { + return api.get("/api/chapters"); +} + +export function post(data: Record) { + return api.post("/api/chapters", data); +} diff --git a/.benchmark/course/frontend/services/courses.ts b/.benchmark/course/frontend/services/courses.ts new file mode 100644 index 0000000..6406396 --- /dev/null +++ b/.benchmark/course/frontend/services/courses.ts @@ -0,0 +1,10 @@ +// Auto-generated courses service +import { api } from "./api"; + +export function get() { + return api.get("/api/courses"); +} + +export function post(data: Record) { + return api.post("/api/courses", data); +} diff --git a/.benchmark/course/frontend/services/exercises.ts b/.benchmark/course/frontend/services/exercises.ts new file mode 100644 index 0000000..4bd54bd --- /dev/null +++ b/.benchmark/course/frontend/services/exercises.ts @@ -0,0 +1,10 @@ +// Auto-generated exercises service +import { api } from "./api"; + +export function get() { + return api.get("/api/exercises"); +} + +export function postsubmit(data: Record) { + return api.post("/api/exercises", data); +} diff --git a/.benchmark/course/frontend/services/lessons.ts b/.benchmark/course/frontend/services/lessons.ts new file mode 100644 index 0000000..a6e1315 --- /dev/null +++ b/.benchmark/course/frontend/services/lessons.ts @@ -0,0 +1,6 @@ +// Auto-generated lessons service +import { api } from "./api"; + +export function get_id(id: string) { + return api.get(`/api/lessons/${id}`); +} diff --git a/.benchmark/course/frontend/services/progress.ts b/.benchmark/course/frontend/services/progress.ts new file mode 100644 index 0000000..a88a74b --- /dev/null +++ b/.benchmark/course/frontend/services/progress.ts @@ -0,0 +1,6 @@ +// Auto-generated progress service +import { api } from "./api"; + +export function get() { + return api.get("/api/progress"); +} diff --git a/.benchmark/course/frontend/services/students.ts b/.benchmark/course/frontend/services/students.ts new file mode 100644 index 0000000..ebb3298 --- /dev/null +++ b/.benchmark/course/frontend/services/students.ts @@ -0,0 +1,10 @@ +// Auto-generated students service +import { api } from "./api"; + +export function get() { + return api.get("/api/students"); +} + +export function post(data: Record) { + return api.post("/api/students", data); +} diff --git a/.benchmark/course/frontend/services/作业批改.ts b/.benchmark/course/frontend/services/作业批改.ts new file mode 100644 index 0000000..6ab83a1 --- /dev/null +++ b/.benchmark/course/frontend/services/作业批改.ts @@ -0,0 +1,10 @@ +// Auto-generated 作业批改 service +import { api } from "./api"; + +export function get() { + return api.get("/api/作业批改"); +} + +export function post(data: Record) { + return api.post("/api/作业批改", data); +} diff --git a/.benchmark/course/frontend/services/学员进度.ts b/.benchmark/course/frontend/services/学员进度.ts new file mode 100644 index 0000000..5f252af --- /dev/null +++ b/.benchmark/course/frontend/services/学员进度.ts @@ -0,0 +1,10 @@ +// Auto-generated 学员进度 service +import { api } from "./api"; + +export function get() { + return api.get("/api/学员进度"); +} + +export function post(data: Record) { + return api.post("/api/学员进度", data); +} diff --git a/.benchmark/course/frontend/services/章节管理.ts b/.benchmark/course/frontend/services/章节管理.ts new file mode 100644 index 0000000..2ebdb99 --- /dev/null +++ b/.benchmark/course/frontend/services/章节管理.ts @@ -0,0 +1,10 @@ +// Auto-generated 章节管理 service +import { api } from "./api"; + +export function get() { + return api.get("/api/章节管理"); +} + +export function post(data: Record) { + return api.post("/api/章节管理", data); +} diff --git a/.benchmark/course/frontend/services/课程发布.ts b/.benchmark/course/frontend/services/课程发布.ts new file mode 100644 index 0000000..e1a2cd2 --- /dev/null +++ b/.benchmark/course/frontend/services/课程发布.ts @@ -0,0 +1,10 @@ +// Auto-generated 课程发布 service +import { api } from "./api"; + +export function get() { + return api.get("/api/课程发布"); +} + +export function post(data: Record) { + return api.post("/api/课程发布", data); +} diff --git a/.benchmark/course/frontend/tailwind.config.ts b/.benchmark/course/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/course/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/course/frontend/tsconfig.json b/.benchmark/course/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/course/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/course/frontend/types/index.ts b/.benchmark/course/frontend/types/index.ts new file mode 100644 index 0000000..a445063 --- /dev/null +++ b/.benchmark/course/frontend/types/index.ts @@ -0,0 +1,79 @@ +// Auto-generated types for EduPlatform + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Courses { + id: string; + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Chapters { + id: string; + userId?: string; + courseId?: string; + title: string; + sortOrder?: number; + durationMin?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Students { + id: string; + userId?: string; + name: string; + email?: string; + enrolledAt?: string; + progress?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Assignments { + id: string; + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; + maxScore?: number; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/.gitignore b/.benchmark/course/fullstack/.gitignore new file mode 100644 index 0000000..7488f6e --- /dev/null +++ b/.benchmark/course/fullstack/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.next/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/course/fullstack/README.md b/.benchmark/course/fullstack/README.md new file mode 100644 index 0000000..78eae20 --- /dev/null +++ b/.benchmark/course/fullstack/README.md @@ -0,0 +1,97 @@ +# EduPlatform — Fullstack Project + +> 一个支持在线课程学习、直播授课、题库练习和学情追踪的教育平台。 + +## Architecture + +``` +apps/ +├── web/ # Next.js 15 + TypeScript + Tailwind CSS +└── api/ # Fastify 5 + TypeScript + SQLite (sql.js) + +packages/ +├── shared-types/ # @shared/types — shared TypeScript interfaces +└── shared-config/ # @shared/config — shared configuration +``` + +## Quick Start + +```bash +# Install all dependencies (root + workspaces) +npm install + +# Start both frontend and backend in dev mode +npm run dev + +# Or start individually +npm run dev:web # http://localhost:3000 +npm run dev:api # http://localhost:3001 +``` + +## Build + +```bash +# Build everything +npm run build + +# Or individually +npm run build:web +npm run build:api +``` + +## Testing + +```bash +# Run API tests +npm test +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/auth/me` + +### Resources +#### users +- `GET /api/users` — List +- `GET /api/users/:id` — Get +- `POST /api/users` — Create +- `PUT /api/users/:id` — Update +- `DELETE /api/users/:id` — Delete + +#### courses +- `GET /api/courses` — List +- `GET /api/courses/:id` — Get +- `POST /api/courses` — Create +- `PUT /api/courses/:id` — Update +- `DELETE /api/courses/:id` — Delete + +#### chapters +- `GET /api/chapters` — List +- `GET /api/chapters/:id` — Get +- `POST /api/chapters` — Create +- `PUT /api/chapters/:id` — Update +- `DELETE /api/chapters/:id` — Delete + +#### students +- `GET /api/students` — List +- `GET /api/students/:id` — Get +- `POST /api/students` — Create +- `PUT /api/students/:id` — Update +- `DELETE /api/students/:id` — Delete + +#### assignments +- `GET /api/assignments` — List +- `GET /api/assignments/:id` — Get +- `POST /api/assignments` — Create +- `PUT /api/assignments/:id` — Update +- `DELETE /api/assignments/:id` — Delete + +### System +- `GET /api/health` — Health check + +## Environment Variables + +See `.env.example` for all available configuration. diff --git a/.benchmark/course/fullstack/apps/api/.gitignore b/.benchmark/course/fullstack/apps/api/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/course/fullstack/apps/api/README.md b/.benchmark/course/fullstack/apps/api/README.md new file mode 100644 index 0000000..879ffbb --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/README.md @@ -0,0 +1,90 @@ +# EduPlatform — Backend API + +> 一个支持在线课程学习、直播授课、题库练习和学情追踪的教育平台。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### courses +- `GET /api/courses` — List all +- `GET /api/courses/:id` — Get by ID +- `POST /api/courses` — Create +- `PUT /api/courses/:id` — Update +- `DELETE /api/courses/:id` — Delete + +#### chapters +- `GET /api/chapters` — List all +- `GET /api/chapters/:id` — Get by ID +- `POST /api/chapters` — Create +- `PUT /api/chapters/:id` — Update +- `DELETE /api/chapters/:id` — Delete + +#### students +- `GET /api/students` — List all +- `GET /api/students/:id` — Get by ID +- `POST /api/students` — Create +- `PUT /api/students/:id` — Update +- `DELETE /api/students/:id` — Delete + +#### assignments +- `GET /api/assignments` — List all +- `GET /api/assignments/:id` — Get by ID +- `POST /api/assignments` — Create +- `PUT /api/assignments/:id` — Update +- `DELETE /api/assignments/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/course/fullstack/apps/api/package.json b/.benchmark/course/fullstack/apps/api/package.json new file mode 100644 index 0000000..2fa96e8 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "eduplatform-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/auth.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/courses.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/courses.test.ts new file mode 100644 index 0000000..5ff6b9f --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/courses.test.ts @@ -0,0 +1,122 @@ +// Courses CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "instructor": "sample-instructor", + "price": 1, + "coverUrl": "sample-coverurl", + "category": "sample-category" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/courses", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/courses", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/courses", () => { + it("creates a cours", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/courses", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/courses/:id", () => { + it("returns the created cours", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/courses/:id", () => { + it("updates the cours", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/courses/:id", () => { + it("deletes the cours", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/exercises.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/exercises.test.ts new file mode 100644 index 0000000..3845bdf --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/exercises.test.ts @@ -0,0 +1,120 @@ +// Exercis CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/exercises", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/exercises", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/exercises", () => { + it("creates a exercis", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/exercises", + headers: { authorization: `Bearer ${token}` }, + payload: { + "lessonId": "00000000-0000-0000-0000-000000000001", + "question": "sample-question", + "options": "sample-options", + "answer": "sample-answer" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/exercises/:id", () => { + it("returns the created exercis", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/exercises/:id", () => { + it("updates the exercis", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "lessonId": "00000000-0000-0000-0000-000000000001", + "question": "sample-question", + "options": "sample-options", + "answer": "sample-answer" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/exercises/:id", () => { + it("deletes the exercis", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/lessons.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/lessons.test.ts new file mode 100644 index 0000000..adcd9d7 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/lessons.test.ts @@ -0,0 +1,122 @@ +// Lesson CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/lessons", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/lessons", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/lessons", () => { + it("creates a lesson", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/lessons", + headers: { authorization: `Bearer ${token}` }, + payload: { + "courseId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "videoUrl": "sample-videourl", + "durationSec": 1, + "sortOrder": 1 + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/lessons/:id", () => { + it("returns the created lesson", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/lessons/:id", () => { + it("updates the lesson", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "courseId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "videoUrl": "sample-videourl", + "durationSec": 1, + "sortOrder": 1 + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/lessons/:id", () => { + it("deletes the lesson", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/user_progress.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/user_progress.test.ts new file mode 100644 index 0000000..86811c6 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/user_progress.test.ts @@ -0,0 +1,122 @@ +// UserProgress CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/user_progress", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/user_progress", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/user_progress", () => { + it("creates a user_progress", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/user_progress", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "lessonId": "00000000-0000-0000-0000-000000000001", + "completed": true, + "watchedSec": 1, + "UNIQUE(userId, lessonId)": "sample-unique(userid, lessonid)" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/user_progress/:id", () => { + it("returns the created user_progress", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/user_progress/:id", () => { + it("updates the user_progress", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "lessonId": "00000000-0000-0000-0000-000000000001", + "completed": true, + "watchedSec": 1, + "UNIQUE(userId, lessonId)": "sample-unique(userid, lessonid)" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/user_progress/:id", () => { + it("deletes the user_progress", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/users.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/db/client.ts b/.benchmark/course/fullstack/apps/api/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/course/fullstack/apps/api/src/db/schema.ts b/.benchmark/course/fullstack/apps/api/src/db/schema.ts new file mode 100644 index 0000000..57e05a7 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/db/schema.ts @@ -0,0 +1,16 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS courses (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n instructor TEXT,\n price REAL,\n cover_url TEXT,\n category TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS chapters (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n course_id TEXT REFERENCES courses(id),\n title TEXT NOT NULL,\n sort_order INTEGER,\n duration_min INTEGER,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS students (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n enrolled_at TEXT,\n progress REAL,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS assignments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n course_id TEXT REFERENCES courses(id),\n title TEXT NOT NULL,\n description TEXT,\n due_date TEXT,\n max_score INTEGER,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/index.ts b/.benchmark/course/fullstack/apps/api/src/index.ts new file mode 100644 index 0000000..45ddcfb --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/index.ts @@ -0,0 +1,71 @@ +// EduPlatform — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { coursesRoutes } from "./routes/courses.js"; +import { chaptersRoutes } from "./routes/chapters.js"; +import { studentsRoutes } from "./routes/students.js"; +import { assignmentsRoutes } from "./routes/assignments.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-0b384181"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(coursesRoutes, { prefix: "/api/courses" }); + await app.register(chaptersRoutes, { prefix: "/api/chapters" }); + await app.register(studentsRoutes, { prefix: "/api/students" }); + await app.register(assignmentsRoutes, { prefix: "/api/assignments" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/course/fullstack/apps/api/src/middleware/auth.ts b/.benchmark/course/fullstack/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/auth.ts b/.benchmark/course/fullstack/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/courses.ts b/.benchmark/course/fullstack/apps/api/src/routes/courses.ts new file mode 100644 index 0000000..4ec97c0 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/courses.ts @@ -0,0 +1,51 @@ +// Auto-generated Courses routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CoursesService } from "../services/cours.js"; +import type { CreateCoursesInput, UpdateCoursesInput } from "../types/index.js"; + +const service = new CoursesService(); + +export async function coursesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/courses — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/courses/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/courses — create + app.post<{ Body: CreateCoursesInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/courses/:id — update + app.put<{ Params: { id: string }; Body: UpdateCoursesInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/courses/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/exercises.ts b/.benchmark/course/fullstack/apps/api/src/routes/exercises.ts new file mode 100644 index 0000000..352bcb9 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/exercises.ts @@ -0,0 +1,51 @@ +// Auto-generated Exercis routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ExercisService } from "../services/exercis.js"; +import type { CreateExercisInput, UpdateExercisInput } from "../types/index.js"; + +const service = new ExercisService(); + +export async function exercisesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/exercises — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/exercises/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/exercises — create + app.post<{ Body: CreateExercisInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/exercises/:id — update + app.put<{ Params: { id: string }; Body: UpdateExercisInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/exercises/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/lessons.ts b/.benchmark/course/fullstack/apps/api/src/routes/lessons.ts new file mode 100644 index 0000000..22468bd --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/lessons.ts @@ -0,0 +1,51 @@ +// Auto-generated Lesson routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { LessonService } from "../services/lesson.js"; +import type { CreateLessonInput, UpdateLessonInput } from "../types/index.js"; + +const service = new LessonService(); + +export async function lessonsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/lessons — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/lessons/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/lessons — create + app.post<{ Body: CreateLessonInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/lessons/:id — update + app.put<{ Params: { id: string }; Body: UpdateLessonInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/lessons/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/user_progress.ts b/.benchmark/course/fullstack/apps/api/src/routes/user_progress.ts new file mode 100644 index 0000000..ac9fec6 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/user_progress.ts @@ -0,0 +1,51 @@ +// Auto-generated UserProgress routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserProgressService } from "../services/user_progress.js"; +import type { CreateUserProgressInput, UpdateUserProgressInput } from "../types/index.js"; + +const service = new UserProgressService(); + +export async function user_progressRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/user_progress — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/user_progress/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/user_progress — create + app.post<{ Body: CreateUserProgressInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/user_progress/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserProgressInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/user_progress/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/users.ts b/.benchmark/course/fullstack/apps/api/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/cours.ts b/.benchmark/course/fullstack/apps/api/src/services/cours.ts new file mode 100644 index 0000000..38e9af2 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/cours.ts @@ -0,0 +1,57 @@ +// Auto-generated Courses service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Courses, CreateCoursesInput, UpdateCoursesInput } from "../types/index.js"; + +export class CoursesService { + /** List all courses */ + list(): Courses[] { + return queryAll("SELECT * FROM courses ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Courses | undefined { + return queryOne("SELECT * FROM courses WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCoursesInput): Courses { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "instructor", "price", "cover_url", "category", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.instructor ?? null, input.price ?? null, input.coverUrl ?? null, input.category ?? null, now, now]; + + execute(`INSERT INTO courses (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCoursesInput): Courses | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.instructor !== undefined) { sets.push("instructor = ?"); values.push(input.instructor); } + if (input.price !== undefined) { sets.push("price = ?"); values.push(input.price); } + if (input.coverUrl !== undefined) { sets.push("cover_url = ?"); values.push(input.coverUrl); } + if (input.category !== undefined) { sets.push("category = ?"); values.push(input.category); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE courses SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM courses WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/exercis.ts b/.benchmark/course/fullstack/apps/api/src/services/exercis.ts new file mode 100644 index 0000000..c6bef85 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/exercis.ts @@ -0,0 +1,56 @@ +// Auto-generated Exercis service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Exercis, CreateExercisInput, UpdateExercisInput } from "../types/index.js"; + +export class ExercisService { + /** List all exercises */ + list(): Exercis[] { + return queryAll("SELECT * FROM exercises ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Exercis | undefined { + return queryOne("SELECT * FROM exercises WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateExercisInput): Exercis { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "lesson_id", "question", "options", "answer"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.lessonId ?? null, input.question ?? null, input.options ?? null, input.answer ?? null]; + + execute(`INSERT INTO exercises (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateExercisInput): Exercis | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.lessonId !== undefined) { sets.push("lesson_id = ?"); values.push(input.lessonId); } + if (input.question !== undefined) { sets.push("question = ?"); values.push(input.question); } + if (input.options !== undefined) { sets.push("options = ?"); values.push(input.options); } + if (input.answer !== undefined) { sets.push("answer = ?"); values.push(input.answer); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE exercises SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM exercises WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/lesson.ts b/.benchmark/course/fullstack/apps/api/src/services/lesson.ts new file mode 100644 index 0000000..2ddf9ab --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/lesson.ts @@ -0,0 +1,57 @@ +// Auto-generated Lesson service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Lesson, CreateLessonInput, UpdateLessonInput } from "../types/index.js"; + +export class LessonService { + /** List all lessons */ + list(): Lesson[] { + return queryAll("SELECT * FROM lessons ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Lesson | undefined { + return queryOne("SELECT * FROM lessons WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateLessonInput): Lesson { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "course_id", "title", "video_url", "duration_sec", "sort_order"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.courseId ?? null, input.title ?? null, input.videoUrl ?? null, input.durationSec ?? null, input.sortOrder ?? null]; + + execute(`INSERT INTO lessons (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateLessonInput): Lesson | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.courseId !== undefined) { sets.push("course_id = ?"); values.push(input.courseId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.videoUrl !== undefined) { sets.push("video_url = ?"); values.push(input.videoUrl); } + if (input.durationSec !== undefined) { sets.push("duration_sec = ?"); values.push(input.durationSec); } + if (input.sortOrder !== undefined) { sets.push("sort_order = ?"); values.push(input.sortOrder); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE lessons SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM lessons WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/user.ts b/.benchmark/course/fullstack/apps/api/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/user_progress.ts b/.benchmark/course/fullstack/apps/api/src/services/user_progress.ts new file mode 100644 index 0000000..a14a7c8 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/user_progress.ts @@ -0,0 +1,56 @@ +// Auto-generated UserProgress service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { UserProgress, CreateUserProgressInput, UpdateUserProgressInput } from "../types/index.js"; + +export class UserProgressService { + /** List all user_progress */ + list(): UserProgress[] { + return queryAll("SELECT * FROM user_progress ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): UserProgress | undefined { + return queryOne("SELECT * FROM user_progress WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserProgressInput): UserProgress { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "lesson_id", "completed", "watched_sec"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.lessonId ?? null, input.completed ?? null, input.watchedSec ?? null]; + + execute(`INSERT INTO user_progress (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserProgressInput): UserProgress | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.lessonId !== undefined) { sets.push("lesson_id = ?"); values.push(input.lessonId); } + if (input.completed !== undefined) { sets.push("completed = ?"); values.push(input.completed); } + if (input.watchedSec !== undefined) { sets.push("watched_sec = ?"); values.push(input.watchedSec); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE user_progress SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM user_progress WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/types/fastify.d.ts b/.benchmark/course/fullstack/apps/api/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/types/index.ts b/.benchmark/course/fullstack/apps/api/src/types/index.ts new file mode 100644 index 0000000..16ebe48 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/types/index.ts @@ -0,0 +1,131 @@ +// Import and re-export shared entity types +import type { + User, + Courses, + Chapters, + Students, + Assignments, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from "@shared/types"; + +export type { + User, + Courses, + Chapters, + Students, + Assignments, + ApiResponse, + PaginatedResponse, + ErrorResponse, +}; + +// Auto-generated types (from Model Contract) + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateCoursesInput { + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; +} + +export interface CreateChaptersInput { + userId?: string; + courseId?: string; + title: string; + durationMin?: number; +} + +export interface CreateStudentsInput { + userId?: string; + name: string; + email?: string; +} + +export interface CreateAssignmentsInput { + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateCoursesInput { + userId?: string; + title?: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; +} + +export interface UpdateChaptersInput { + userId?: string; + courseId?: string; + title?: string; + durationMin?: number; +} + +export interface UpdateStudentsInput { + userId?: string; + name?: string; + email?: string; +} + +export interface UpdateAssignmentsInput { + userId?: string; + courseId?: string; + title?: string; + description?: string; + dueDate?: string; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/course/fullstack/apps/api/src/types/sql.js.d.ts b/.benchmark/course/fullstack/apps/api/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/course/fullstack/apps/api/tsconfig.json b/.benchmark/course/fullstack/apps/api/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/course/[id]/page.tsx b/.benchmark/course/fullstack/apps/web/app/course/[id]/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/course/[id]/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 完全指南", instructor: "张老师", price: 199, cover: "📘" }, + { id: "c2", title: "TypeScript 高级编程", instructor: "李老师", price: 149, cover: "📙" }, + { id: "c3", title: "Node.js 后端实战", instructor: "王老师", price: 249, cover: "📗" }, +]; + +export default function CoursePage() { + return ( +
+

课程中心

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

¥{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/exercises/page.tsx b/.benchmark/course/fullstack/apps/web/app/exercises/page.tsx new file mode 100644 index 0000000..15d71e4 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/exercises/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +export default function ExercisePage() { + return ( +
+

题库练习

+

章节练习和模拟考试功能

+
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/globals.css b/.benchmark/course/fullstack/apps/web/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/course/fullstack/apps/web/app/layout.tsx b/.benchmark/course/fullstack/apps/web/app/layout.tsx new file mode 100644 index 0000000..8be9dca --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "EduPlatform", + description: "一个支持在线课程学习、直播授课、题库练习和学情追踪的教育平台。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/course/fullstack/apps/web/app/learn/[courseId]/[lessonId]/page.tsx b/.benchmark/course/fullstack/apps/web/app/learn/[courseId]/[lessonId]/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/learn/[courseId]/[lessonId]/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 完全指南", instructor: "张老师", price: 199, cover: "📘" }, + { id: "c2", title: "TypeScript 高级编程", instructor: "李老师", price: 149, cover: "📙" }, + { id: "c3", title: "Node.js 后端实战", instructor: "王老师", price: 249, cover: "📗" }, +]; + +export default function CoursePage() { + return ( +
+

课程中心

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

¥{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/live/page.tsx b/.benchmark/course/fullstack/apps/web/app/live/page.tsx new file mode 100644 index 0000000..8679f8f --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/live/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +export default function LivePage() { + return ( +
+

直播课堂

+

课程直播功能

+
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/page.tsx b/.benchmark/course/fullstack/apps/web/app/page.tsx new file mode 100644 index 0000000..47bd262 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

EduPlatform

+

应用首页

+
+ + {/* Quick Actions */} +
+

快捷入口

+
+ + 📋 + 课程发布 + + + 📅 + 章节管理 + + + 📝 + 学员进度 + + + 📸 + 作业批改 + +
+
+ + {/* Recent Activity / Stats */} +
+

今日概览

+
+
+
+

欢迎使用 EduPlatform

+

开始探索功能吧

+
+ 👋 +
+
+
+
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/app/profile/page.tsx b/.benchmark/course/fullstack/apps/web/app/profile/page.tsx new file mode 100644 index 0000000..ed7c604 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

我的

+

学习报告、已购课程、设置

+
+ +
+ + { +

我的 页面内容

+

路由: /profile

+
} +
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/layout/BottomNav.tsx b/.benchmark/course/fullstack/apps/web/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/layout/Sidebar.tsx b/.benchmark/course/fullstack/apps/web/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/Button.tsx b/.benchmark/course/fullstack/apps/web/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/Card.tsx b/.benchmark/course/fullstack/apps/web/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/EmptyState.tsx b/.benchmark/course/fullstack/apps/web/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/Input.tsx b/.benchmark/course/fullstack/apps/web/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/Modal.tsx b/.benchmark/course/fullstack/apps/web/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useCourses.ts b/.benchmark/course/fullstack/apps/web/hooks/useCourses.ts new file mode 100644 index 0000000..1f4fc4a --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useCourses.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for courses +type Cour = Record; + +/** + * Fetch and manage courses data. + */ +export function useCourses() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/courses`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useDebounce.ts b/.benchmark/course/fullstack/apps/web/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useExercises.ts b/.benchmark/course/fullstack/apps/web/hooks/useExercises.ts new file mode 100644 index 0000000..d2c16ca --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useExercises.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for exercises +type Exerci = Record; + +/** + * Fetch and manage exercises data. + */ +export function useExercises() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/exercises`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useForm.ts b/.benchmark/course/fullstack/apps/web/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useLessons.ts b/.benchmark/course/fullstack/apps/web/hooks/useLessons.ts new file mode 100644 index 0000000..0bf3571 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useLessons.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for lessons +type Lesson = Record; + +/** + * Fetch and manage lessons data. + */ +export function useLessons() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/lessons`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useProgress.ts b/.benchmark/course/fullstack/apps/web/hooks/useProgress.ts new file mode 100644 index 0000000..cdcac25 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useProgress.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for progress +type Progres = Record; + +/** + * Fetch and manage progress data. + */ +export function useProgress() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/progress`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/fullstack/apps/web/next.config.ts b/.benchmark/course/fullstack/apps/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/course/fullstack/apps/web/package.json b/.benchmark/course/fullstack/apps/web/package.json new file mode 100644 index 0000000..8e6b869 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "eduplatform-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/postcss.config.js b/.benchmark/course/fullstack/apps/web/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/course/fullstack/apps/web/services/api.ts b/.benchmark/course/fullstack/apps/web/services/api.ts new file mode 100644 index 0000000..bb30f14 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/api.ts @@ -0,0 +1,49 @@ +// Auto-generated API client for EduPlatform + +import { API_BASE_URL, API_PREFIX } from "@shared/config"; + +const API_BASE = `${API_BASE_URL}${API_PREFIX}`; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/course/fullstack/apps/web/services/courses.ts b/.benchmark/course/fullstack/apps/web/services/courses.ts new file mode 100644 index 0000000..6406396 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/courses.ts @@ -0,0 +1,10 @@ +// Auto-generated courses service +import { api } from "./api"; + +export function get() { + return api.get("/api/courses"); +} + +export function post(data: Record) { + return api.post("/api/courses", data); +} diff --git a/.benchmark/course/fullstack/apps/web/services/exercises.ts b/.benchmark/course/fullstack/apps/web/services/exercises.ts new file mode 100644 index 0000000..4bd54bd --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/exercises.ts @@ -0,0 +1,10 @@ +// Auto-generated exercises service +import { api } from "./api"; + +export function get() { + return api.get("/api/exercises"); +} + +export function postsubmit(data: Record) { + return api.post("/api/exercises", data); +} diff --git a/.benchmark/course/fullstack/apps/web/services/lessons.ts b/.benchmark/course/fullstack/apps/web/services/lessons.ts new file mode 100644 index 0000000..a6e1315 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/lessons.ts @@ -0,0 +1,6 @@ +// Auto-generated lessons service +import { api } from "./api"; + +export function get_id(id: string) { + return api.get(`/api/lessons/${id}`); +} diff --git a/.benchmark/course/fullstack/apps/web/services/progress.ts b/.benchmark/course/fullstack/apps/web/services/progress.ts new file mode 100644 index 0000000..a88a74b --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/progress.ts @@ -0,0 +1,6 @@ +// Auto-generated progress service +import { api } from "./api"; + +export function get() { + return api.get("/api/progress"); +} diff --git a/.benchmark/course/fullstack/apps/web/tailwind.config.ts b/.benchmark/course/fullstack/apps/web/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/course/fullstack/apps/web/tsconfig.json b/.benchmark/course/fullstack/apps/web/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/types/index.ts b/.benchmark/course/fullstack/apps/web/types/index.ts new file mode 100644 index 0000000..a445063 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/types/index.ts @@ -0,0 +1,79 @@ +// Auto-generated types for EduPlatform + +// ─── Base ─────────────────────────────────────────── + + +// ─── Base ─────────────────────────────────────────── +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Courses { + id: string; + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Chapters { + id: string; + userId?: string; + courseId?: string; + title: string; + sortOrder?: number; + durationMin?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Students { + id: string; + userId?: string; + name: string; + email?: string; + enrolledAt?: string; + progress?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Assignments { + id: string; + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; + maxScore?: number; + createdAt?: string; + updatedAt?: string; +} + + +// ─── API Responses ────────────────────────────────── +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/package.json b/.benchmark/course/fullstack/package.json new file mode 100644 index 0000000..ff9f0bf --- /dev/null +++ b/.benchmark/course/fullstack/package.json @@ -0,0 +1,19 @@ +{ + "name": "eduplatform", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "node scripts/build.mjs", + "dev:web": "npm run dev -w apps/web", + "dev:api": "npm run dev -w apps/api", + "build:web": "npm run build -w apps/web", + "build:api": "npm run build -w apps/api", + "test": "npm run test -w apps/api", + "lint": "npm run lint -w apps/web" + } +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/packages/shared-config/package.json b/.benchmark/course/fullstack/packages/shared-config/package.json new file mode 100644 index 0000000..c2f52a9 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/config", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/packages/shared-config/src/index.ts b/.benchmark/course/fullstack/packages/shared-config/src/index.ts new file mode 100644 index 0000000..abf84f0 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-config/src/index.ts @@ -0,0 +1,16 @@ +// Shared configuration for fullstack project + +/** API base URL — reads from env or defaults */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** API prefix for all endpoints */ +export const API_PREFIX = "/api"; + +/** Full API URL */ +export const API_URL = `${API_BASE_URL}${API_PREFIX}`; + +/** Auth token storage key */ +export const AUTH_TOKEN_KEY = "auth_token"; + +/** Default page size for paginated endpoints */ +export const DEFAULT_PAGE_SIZE = 20; diff --git a/.benchmark/course/fullstack/packages/shared-config/tsconfig.json b/.benchmark/course/fullstack/packages/shared-config/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/packages/shared-types/package.json b/.benchmark/course/fullstack/packages/shared-types/package.json new file mode 100644 index 0000000..d7fb84c --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/types", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/packages/shared-types/src/index.ts b/.benchmark/course/fullstack/packages/shared-types/src/index.ts new file mode 100644 index 0000000..5ef3530 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-types/src/index.ts @@ -0,0 +1,81 @@ +// Shared types for fullstack project +// Auto-generated — used by both apps/web and apps/api + +// ─── Entities ──────────────────────────────────────── +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Courses { + id: string; + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Chapters { + id: string; + userId?: string; + courseId?: string; + title: string; + sortOrder?: number; + durationMin?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Students { + id: string; + userId?: string; + name: string; + email?: string; + enrolledAt?: string; + progress?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Assignments { + id: string; + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; + maxScore?: number; + createdAt?: string; + updatedAt?: string; +} + +// ─── API Response Wrappers ─────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} diff --git a/.benchmark/course/fullstack/packages/shared-types/tsconfig.json b/.benchmark/course/fullstack/packages/shared-types/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/scripts/build.mjs b/.benchmark/course/fullstack/scripts/build.mjs new file mode 100644 index 0000000..84fa093 --- /dev/null +++ b/.benchmark/course/fullstack/scripts/build.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Build script — builds both web and api. + * Usage: node scripts/build.mjs + */ + +import { execSync } from "node:child_process"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function run(cmd, cwd) { + console.log(`\n🔨 ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("🏗️ Building fullstack project...\n"); + +try { + run("npm run build", `${ROOT}/apps/api`); + run("npm run build", `${ROOT}/apps/web`); + console.log("\n✅ Build complete!"); +} catch (e) { + console.error("\n❌ Build failed:", e.message); + process.exit(1); +} diff --git a/.benchmark/course/fullstack/scripts/dev.mjs b/.benchmark/course/fullstack/scripts/dev.mjs new file mode 100644 index 0000000..dcb8118 --- /dev/null +++ b/.benchmark/course/fullstack/scripts/dev.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +/** + * Dev script — starts both web and api in parallel. + * Usage: node scripts/dev.mjs + */ + +import { spawn } from "node:child_process"; + +function start(name, command, args, cwd) { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + env: { ...process.env, FORCE_COLOR: "1" }, + }); + child.on("error", (err) => console.error(`[${name}] Failed: ${err.message}`)); + child.on("exit", (code) => { + if (code !== 0 && code !== null) console.error(`[${name}] Exited with code ${code}`); + }); + return child; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +console.log("🚀 Starting fullstack dev servers...\n"); + +const api = start("api", "npm", ["run", "dev"], `${ROOT}/apps/api`); +const web = start("web", "npm", ["run", "dev"], `${ROOT}/apps/web`); + +process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); }); +process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); }); diff --git a/.benchmark/course/prd.json b/.benchmark/course/prd.json new file mode 100644 index 0000000..affd9d4 --- /dev/null +++ b/.benchmark/course/prd.json @@ -0,0 +1 @@ +{"projectName":"EduPlatform","chineseName":"在线教育平台","domain":"education","matchConfidence":"high","summary":"一个支持在线课程学习、直播授课、题库练习和学情追踪的教育平台。","personas":[{"name":"学生","description":"希望通过线上学习提升技能的在校生或职场人","painPoints":["课程质量参差不齐","缺乏学习动力","疑惑无人解答"]},{"name":"讲师","description":"发布课程内容的专业讲师","painPoints":["课件管理不便","学生互动效率低"]}],"userStories":[{"id":"US-001","as":"学生","want":"课程发布","soThat":"解决痛点:课程质量参差不齐","priority":"P0"},{"id":"US-002","as":"学生","want":"章节管理","soThat":"解决痛点:课程质量参差不齐","priority":"P0"},{"id":"US-003","as":"学生","want":"学员进度","soThat":"解决痛点:课程质量参差不齐","priority":"P0"},{"id":"US-004","as":"讲师","want":"课程发布","soThat":"解决痛点:课件管理不便","priority":"P0"},{"id":"US-005","as":"讲师","want":"章节管理","soThat":"解决痛点:课件管理不便","priority":"P0"},{"id":"US-006","as":"讲师","want":"学员进度","soThat":"解决痛点:课件管理不便","priority":"P0"}],"mvpScope":{"description":"MVP 聚焦 课程发布、章节管理、学员进度、作业批改,包含页面:首页、课程发布、章节管理","features":["课程发布","章节管理","学员进度","作业批改"],"pages":["首页","课程发布","章节管理"],"estimatedWeeks":3},"features":[{"name":"课程发布","description":"课程发布","priority":"P0"},{"name":"章节管理","description":"章节管理","priority":"P0"},{"name":"学员进度","description":"学员进度","priority":"P0"},{"name":"作业批改","description":"作业批改","priority":"P0"}],"pages":[{"name":"首页","route":"/home","description":"应用首页"},{"name":"课程发布","route":"/courses","description":"课程发布"},{"name":"章节管理","route":"/chapters","description":"章节管理"},{"name":"学员进度","route":"/students","description":"学员进度"},{"name":"作业批改","route":"/assignments","description":"作业批改"}],"apiRequirements":[{"method":"GET","path":"/api/courses","description":"获取课程发布列表"},{"method":"POST","path":"/api/courses","description":"创建课程发布"},{"method":"GET","path":"/api/chapters","description":"获取章节管理列表"},{"method":"POST","path":"/api/chapters","description":"创建章节管理"},{"method":"GET","path":"/api/students","description":"获取学员进度列表"},{"method":"POST","path":"/api/students","description":"创建学员进度"},{"method":"GET","path":"/api/assignments","description":"获取作业批改列表"},{"method":"POST","path":"/api/assignments","description":"创建作业批改"},{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter(跨平台)","considerations":["建议跨平台框架减少开发成本"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"项目脚手架搭建","description":"初始化 EduPlatform 项目结构,配置构建工具和 CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"数据库设计与初始化","description":"设计数据模型,创建数据库迁移脚本","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"页面开发:首页","description":"开发 首页 页面(/home):应用首页","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"页面开发:课程发布","description":"开发 课程发布 页面(/courses):课程发布","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"页面开发:章节管理","description":"开发 章节管理 页面(/chapters):章节管理","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"页面开发:学员进度","description":"开发 学员进度 页面(/students):学员进度","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API 开发:GET /api/courses","description":"获取课程发布列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API 开发:POST /api/courses","description":"创建课程发布","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API 开发:GET /api/chapters","description":"获取章节管理列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API 开发:POST /api/chapters","description":"创建章节管理","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"前后端联调","description":"前端页面与后端 API 集成联调","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"测试与修复","description":"功能测试、Bug 修复、性能优化","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T22:08:37.397Z","inputLength":33,"domainMatchCount":2,"extractedFeatureCount":4,"extractedPageCount":5,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/course/release/release/README.md b/.benchmark/course/release/release/README.md new file mode 100644 index 0000000..6e568e8 --- /dev/null +++ b/.benchmark/course/release/release/README.md @@ -0,0 +1,33 @@ +# eduplatform — Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T22:08:37.740Z + +## Directory Structure + +``` +release/ +├── version.json — Version metadata +├── build-info.json — Build environment info +├── README.md — This file +├── manifests/ +│ └── manifest.json — Release manifest with file hashes +├── checksums/ +│ └── checksums.txt — SHA256 checksums for verification +├── release-notes/ +│ └── release-notes.md — Human-readable release notes +├── windows/ — Windows installers +├── macos/ — macOS disk images +└── linux/ — Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/course/release/release/build-info.json b/.benchmark/course/release/release/build-info.json new file mode 100644 index 0000000..11eb08b --- /dev/null +++ b/.benchmark/course/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T22:08:37.740Z", + "domain": "unknown", + "entityCount": 9, + "routeCount": 1, + "screenCount": 13, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "eduplatform", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/course/release/release/checksums/checksums.txt b/.benchmark/course/release/release/checksums/checksums.txt new file mode 100644 index 0000000..99ff694 --- /dev/null +++ b/.benchmark/course/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +79f6d32cc3a0acd8ad736e978e3a0b209ffb7008f08b87b19e29d31accc0c8e2 windows/eduplatform-setup-0.1.0.exe +0d545bb9fe7696f633360c5c7aef906d39d85fae332c687ba03e0146ab9815e6 windows/eduplatform-0.1.0-portable.exe +a3eefa70bb8f4d7d3dd0d837742859d6a928da07054b38f3be46fccaaceb1cc3 macos/eduplatform-0.1.0.dmg +d7028300e13c25b3253059ba4ce56568083f77fc0d671e0266068d4e971acb6e macos/eduplatform-0.1.0-arm64.dmg +b20e30fb4add3e328351bceff0fd3d82c0c80a503fa478b2d9da4758f0060491 linux/eduplatform-0.1.0.AppImage +61616501f0dc42b4f6ab776a45b3c08e9116eb606389653045adad8e77b0811d linux/eduplatform_0.1.0_amd64.deb diff --git a/.benchmark/course/release/release/linux/eduplatform-0.1.0.AppImage b/.benchmark/course/release/release/linux/eduplatform-0.1.0.AppImage new file mode 100644 index 0000000..25e2ef2 --- /dev/null +++ b/.benchmark/course/release/release/linux/eduplatform-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/linux/eduplatform_0.1.0_amd64.deb b/.benchmark/course/release/release/linux/eduplatform_0.1.0_amd64.deb new file mode 100644 index 0000000..089fc5a --- /dev/null +++ b/.benchmark/course/release/release/linux/eduplatform_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/macos/eduplatform-0.1.0-arm64.dmg b/.benchmark/course/release/release/macos/eduplatform-0.1.0-arm64.dmg new file mode 100644 index 0000000..902319a --- /dev/null +++ b/.benchmark/course/release/release/macos/eduplatform-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/macos/eduplatform-0.1.0.dmg b/.benchmark/course/release/release/macos/eduplatform-0.1.0.dmg new file mode 100644 index 0000000..0f0dcb8 --- /dev/null +++ b/.benchmark/course/release/release/macos/eduplatform-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/manifests/manifest.json b/.benchmark/course/release/release/manifests/manifest.json new file mode 100644 index 0000000..c7b94d9 --- /dev/null +++ b/.benchmark/course/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "eduplatform", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T22:08:37.739Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/eduplatform-setup-0.1.0.exe", + "size": 146, + "sha256": "79f6d32cc3a0acd8ad736e978e3a0b209ffb7008f08b87b19e29d31accc0c8e2" + }, + { + "path": "windows/eduplatform-0.1.0-portable.exe", + "size": 150, + "sha256": "0d545bb9fe7696f633360c5c7aef906d39d85fae332c687ba03e0146ab9815e6" + }, + { + "path": "macos/eduplatform-0.1.0.dmg", + "size": 142, + "sha256": "a3eefa70bb8f4d7d3dd0d837742859d6a928da07054b38f3be46fccaaceb1cc3" + }, + { + "path": "macos/eduplatform-0.1.0-arm64.dmg", + "size": 144, + "sha256": "d7028300e13c25b3253059ba4ce56568083f77fc0d671e0266068d4e971acb6e" + }, + { + "path": "linux/eduplatform-0.1.0.AppImage", + "size": 135, + "sha256": "b20e30fb4add3e328351bceff0fd3d82c0c80a503fa478b2d9da4758f0060491" + }, + { + "path": "linux/eduplatform_0.1.0_amd64.deb", + "size": 145, + "sha256": "61616501f0dc42b4f6ab776a45b3c08e9116eb606389653045adad8e77b0811d" + } + ], + "totalFiles": 6, + "totalSize": 862 +} \ No newline at end of file diff --git a/.benchmark/course/release/release/release-notes/release-notes.md b/.benchmark/course/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..73796f9 --- /dev/null +++ b/.benchmark/course/release/release/release-notes/release-notes.md @@ -0,0 +1,58 @@ +# eduplatform v0.1.0 + +> Release Builder Agent — SF-07 +> Generated: 2026-06-05 22:08:37 UTC + +## Version + +- **Name:** eduplatform +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- assignments +- auth +- chapters +- courses +- exercises +- lessons +- students +- user_progress +- users + +## Build Info + +- **Build Time:** 2026-06-05T22:08:37.740Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- ✅ windows +- ✅ macos +- ✅ linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent — SF-07* \ No newline at end of file diff --git a/.benchmark/course/release/release/version.json b/.benchmark/course/release/release/version.json new file mode 100644 index 0000000..0bc24d6 --- /dev/null +++ b/.benchmark/course/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "eduplatform", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/course/release/release/windows/eduplatform-0.1.0-portable.exe b/.benchmark/course/release/release/windows/eduplatform-0.1.0-portable.exe new file mode 100644 index 0000000..a0032d5 --- /dev/null +++ b/.benchmark/course/release/release/windows/eduplatform-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/windows/eduplatform-setup-0.1.0.exe b/.benchmark/course/release/release/windows/eduplatform-setup-0.1.0.exe new file mode 100644 index 0000000..d12a641 --- /dev/null +++ b/.benchmark/course/release/release/windows/eduplatform-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/crm/arch.json b/.benchmark/crm/arch.json new file mode 100644 index 0000000..974e900 --- /dev/null +++ b/.benchmark/crm/arch.json @@ -0,0 +1 @@ +{"projectName":"NoteApp","domain":"note","contract":{"projectName":"NoteApp","domain":"note","entities":[{"name":"User","table":"users","description":"系统用户","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[]},{"name":"Customers","table":"customers","description":"客户管理表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true,"isPrimary":false,"isAuto":false},{"name":"email","type":"VARCHAR(256)","required":false,"isPrimary":false,"isAuto":false},{"name":"phone","type":"VARCHAR(20)","required":false,"isPrimary":false,"isAuto":false},{"name":"company","type":"VARCHAR(128)","required":false,"isPrimary":false,"isAuto":false},{"name":"source","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"tags","type":"JSONB","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"SalesPipeline","table":"sales_pipeline","description":"销售漏斗表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"customer_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"customers","fkColumn":"id"},{"name":"stage","type":"VARCHAR(32)","required":true,"isPrimary":false,"isAuto":false},{"name":"amount","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"probability","type":"INTEGER","required":false,"isPrimary":false,"isAuto":false},{"name":"expected_close","type":"DATE","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"customers","via":"customer_id"}]},{"name":"FollowUps","table":"follow_ups","description":"跟进记录表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"customer_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"customers","fkColumn":"id"},{"name":"type","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":false},{"name":"content","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"next_action","type":"VARCHAR(256)","required":false,"isPrimary":false,"isAuto":false},{"name":"next_action_date","type":"DATE","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"customers","via":"customer_id"}]},{"name":"Dashboard","table":"dashboard","description":"数据分析仪表盘表","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"color","type":"VARCHAR(7)","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","customer_id":"customerId","expected_close":"expectedClose","next_action":"nextAction","next_action_date":"nextActionDate","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","remind_at":"remindAt","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","customerId":"customer_id","expectedClose":"expected_close","nextAction":"next_action","nextActionDate":"next_action_date","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","remindAt":"remind_at","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Customers":{"id":{"required":true},"name":{"required":true}},"SalesPipeline":{"id":{"required":true},"stage":{"required":true}},"FollowUps":{"id":{"required":true}},"Dashboard":{"id":{"required":true},"name":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO(文件存储)","deployment":"Docker Compose + Nginx / K8s(规模化)","considerations":[]},"architectureDiagram":"┌─────────────────────────────────────────────────────────┐\n│ NoteApp — System Architecture │\n├─────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────────────┐ ┌──────────────────────┐ │\n│ │ Client Layer │ │ Admin / Web │ │\n│ │ React Native 0.76 + Expo / Flutter 3.x │ │ React + Vite (Web) │ │\n│ └──────────┬───────────┘ └──────────┬───────────┘ │\n│ │ │ │\n│ └──────────┬────────────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ API Gateway │ │\n│ │ Nginx / Kong │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌─────────▼──────────┐ │\n│ │ Backend Services │ │\n│ │ NestJS + Prisma │ │\n│ └─────────┬──────────┘ │\n│ │ │\n│ ┌───────────────┼───────────────┐ │\n│ │ │ │ │\n│ ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ │\n│ │ PostgreSQL 15│ │ Redis Cache │ │ MinIO │ │\n│ │ Primary │ │ Session │ │ Files │ │\n│ └───────────┘ └─────────────┘ └──────────┘ │\n│ │\n├─────────────────────────────────────────────────────────┤\n│ Modules: │\n│ 1 客户管理 │\n│ 2 销售漏斗 │\n│ 3 跟进记录 │\n│ 4 Analytics │\n│ 5 编辑器 │\n│ 6 搜索引擎 │\n│ │\n└─────────────────────────────────────────────────────────┘","dataFlows":[{"page":"客户管理","route":"/customers","description":"客户管理","dataFlow":["GET /api/customers → 获取客户管理列表","POST /api/customers → 创建客户管理"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"销售漏斗","route":"/sales_pipeline","description":"销售漏斗","dataFlow":["GET /api/sales_pipeline → 获取销售漏斗列表","POST /api/sales_pipeline → 创建销售漏斗"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"跟进记录","route":"/follow_ups","description":"跟进记录","dataFlow":["GET /api/follow_ups → 获取跟进记录列表","POST /api/follow_ups → 创建跟进记录"],"direction":"Client → API Gateway → Backend → Database → Response → Client"},{"page":"数据分析仪表盘","route":"/dashboard","description":"数据分析仪表盘","dataFlow":["GET /api/dashboard → 获取数据分析仪表盘列表","POST /api/dashboard → 创建数据分析仪表盘"],"direction":"Client → API Gateway → Backend → Database → Response → Client"}],"modules":[{"name":"客户管理","label":"客户管理","features":["客户管理"],"responsibilities":["提供 客户管理 相关功能"]},{"name":"销售漏斗","label":"销售漏斗","features":["销售漏斗"],"responsibilities":["提供 销售漏斗 相关功能"]},{"name":"跟进记录","label":"跟进记录","features":["跟进记录"],"responsibilities":["提供 跟进记录 相关功能"]},{"name":"analytics","label":"Analytics","features":["数据分析仪表盘"],"responsibilities":["提供 数据分析仪表盘 相关功能"]},{"name":"editor","label":"编辑器","features":["笔记编辑"],"responsibilities":["提供 笔记编辑 相关功能"]},{"name":"search","label":"搜索引擎","features":["全文搜索"],"responsibilities":["提供 全文搜索 相关功能"]}],"databaseSchema":[{"table":"users","description":"用户表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"customers","description":"客户管理表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","constraints":"DEFAULT '[]'"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_customers_user ON customers(user_id)"]},{"table":"sales_pipeline","description":"销售漏斗表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"customer_id","type":"UUID","constraints":"FK → customers.id"},{"name":"stage","type":"VARCHAR(32)","constraints":"NOT NULL"},{"name":"amount","type":"DECIMAL(12,2)"},{"name":"probability","type":"INTEGER"},{"name":"expected_close","type":"DATE"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_sales_pipeline_user ON sales_pipeline(user_id)"]},{"table":"follow_ups","description":"跟进记录表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"customer_id","type":"UUID","constraints":"FK → customers.id"},{"name":"type","type":"VARCHAR(32)"},{"name":"content","type":"TEXT"},{"name":"next_action","type":"VARCHAR(256)"},{"name":"next_action_date","type":"DATE"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_follow_ups_user ON follow_ups(user_id)"]},{"table":"dashboard","description":"数据分析仪表盘表","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK → users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"color","type":"VARCHAR(7)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_dashboard_user ON dashboard(user_id)"]}],"apiDesign":[{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"用户登录"},{"method":"GET","path":"/api/auth/me","description":"获取当前用户"}]},{"resource":"customers","basePath":"/api/customers","endpoints":[{"method":"GET","path":"/api/customers","description":"获取客户管理列表"},{"method":"POST","path":"/api/customers","description":"创建客户管理"}]},{"resource":"dashboard","basePath":"/api/dashboard","endpoints":[{"method":"GET","path":"/api/dashboard","description":"获取数据分析仪表盘列表"},{"method":"POST","path":"/api/dashboard","description":"创建数据分析仪表盘"}]},{"resource":"follow_ups","basePath":"/api/follow_ups","endpoints":[{"method":"GET","path":"/api/follow_ups","description":"获取跟进记录列表"},{"method":"POST","path":"/api/follow_ups","description":"创建跟进记录"}]},{"resource":"sales_pipeline","basePath":"/api/sales_pipeline","endpoints":[{"method":"GET","path":"/api/sales_pipeline","description":"获取销售漏斗列表"},{"method":"POST","path":"/api/sales_pipeline","description":"创建销售漏斗"}]}],"directoryStructure":["noteapp/","├── apps/","│ ├── mobile/ # React Native / Flutter 移动端","│ │ ├── src/","│ │ │ ├── screens/ # 页面组件","│ │ │ │ ├── 客户管理/","│ │ │ │ ├── 销售漏斗/","│ │ │ │ ├── 跟进记录/","│ │ │ │ ├── analytics/","│ │ │ ├── components/ # 通用组件","│ │ │ ├── hooks/ # 自定义 Hooks","│ │ │ ├── services/ # API 调用层","│ │ │ ├── store/ # 状态管理","│ │ │ └── utils/ # 工具函数","│ │ ├── app.json","│ │ └── package.json","│ └── web/ # Web 管理后台","│ ├── src/","│ │ ├── pages/","│ │ ├── components/","│ │ └── layouts/","│ └── package.json","├── packages/","│ └── shared/ # 共享类型/常量","├── server/ # NestJS 后端服务","│ ├── src/","│ │ ├── modules/ # 业务模块","│ │ │ ├── 客户管理/","│ │ │ │ ├── 客户管理.controller.ts","│ │ │ │ ├── 客户管理.service.ts","│ │ │ │ ├── 客户管理.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 销售漏斗/","│ │ │ │ ├── 销售漏斗.controller.ts","│ │ │ │ ├── 销售漏斗.service.ts","│ │ │ │ ├── 销售漏斗.module.ts","│ │ │ │ └── dto/","│ │ │ ├── 跟进记录/","│ │ │ │ ├── 跟进记录.controller.ts","│ │ │ │ ├── 跟进记录.service.ts","│ │ │ │ ├── 跟进记录.module.ts","│ │ │ │ └── dto/","│ │ │ ├── analytics/","│ │ │ │ ├── analytics.controller.ts","│ │ │ │ ├── analytics.service.ts","│ │ │ │ ├── analytics.module.ts","│ │ │ │ └── dto/","│ │ │ ├── editor/","│ │ │ │ ├── editor.controller.ts","│ │ │ │ ├── editor.service.ts","│ │ │ │ ├── editor.module.ts","│ │ │ │ └── dto/","│ │ ├── common/ # 通用(guards, filters, interceptors)","│ │ ├── prisma/ # Prisma ORM","│ │ │ └── schema.prisma","│ │ ├── config/ # 环境配置","│ │ └── main.ts","│ ├── test/","│ ├── Dockerfile","│ └── package.json","├── docker-compose.yml","├── .github/workflows/ # CI/CD","│ └── ci.yml","├── .env.example","├── README.md","└── turbo.json # Monorepo 配置"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s(规模化)","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions → Build → Test → Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:34.079Z","sourceDomain":"note","moduleCount":6,"tableCount":5,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/crm/backend/.gitignore b/.benchmark/crm/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/crm/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/crm/backend/README.md b/.benchmark/crm/backend/README.md new file mode 100644 index 0000000..00690e5 --- /dev/null +++ b/.benchmark/crm/backend/README.md @@ -0,0 +1,90 @@ +# NoteApp — Backend API + +> 一款支持多端同步、Markdown 编辑和标签管理的笔记应用。 + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +├── index.ts # Server entry point +├── db/ +│ ├── schema.ts # SQLite schema +│ └── client.ts # Database client +├── routes/ +│ ├── auth.ts # Auth routes (register/login/me) +│ └── *.ts # CRUD routes +├── services/ +│ └── *.ts # Business logic +├── middleware/ +│ └── auth.ts # JWT middleware +├── types/ +│ └── index.ts # TypeScript types +└── __tests__/ + └── *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` — Register +- `POST /api/auth/login` — Login +- `GET /api/auth/me` — Current user (auth required) + +### Resources +#### customers +- `GET /api/customers` — List all +- `GET /api/customers/:id` — Get by ID +- `POST /api/customers` — Create +- `PUT /api/customers/:id` — Update +- `DELETE /api/customers/:id` — Delete + +#### sales_pipeline +- `GET /api/sales_pipeline` — List all +- `GET /api/sales_pipeline/:id` — Get by ID +- `POST /api/sales_pipeline` — Create +- `PUT /api/sales_pipeline/:id` — Update +- `DELETE /api/sales_pipeline/:id` — Delete + +#### follow_ups +- `GET /api/follow_ups` — List all +- `GET /api/follow_ups/:id` — Get by ID +- `POST /api/follow_ups` — Create +- `PUT /api/follow_ups/:id` — Update +- `DELETE /api/follow_ups/:id` — Delete + +#### dashboard +- `GET /api/dashboard` — List all +- `GET /api/dashboard/:id` — Get by ID +- `POST /api/dashboard` — Create +- `PUT /api/dashboard/:id` — Update +- `DELETE /api/dashboard/:id` — Delete + +### System +- `GET /api/health` — Health check diff --git a/.benchmark/crm/backend/package.json b/.benchmark/crm/backend/package.json new file mode 100644 index 0000000..6fa3b11 --- /dev/null +++ b/.benchmark/crm/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "noteapp", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/crm/backend/src/__tests__/auth.test.ts b/.benchmark/crm/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/customers.test.ts b/.benchmark/crm/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..8425a02 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/customers.test.ts @@ -0,0 +1,121 @@ +// Customers CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/dashboard.test.ts b/.benchmark/crm/backend/src/__tests__/dashboard.test.ts new file mode 100644 index 0000000..4199529 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/dashboard.test.ts @@ -0,0 +1,119 @@ +// Dashboard CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "description": "sample-description", + "color": "sample-color" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/dashboard", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/dashboard", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/dashboard", () => { + it("creates a dashboard", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/dashboard", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/dashboard/:id", () => { + it("returns the created dashboard", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/dashboard/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/dashboard/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/dashboard/:id", () => { + it("updates the dashboard", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/dashboard/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/dashboard/:id", () => { + it("deletes the dashboard", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/dashboard/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/dashboard/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/follow_ups.test.ts b/.benchmark/crm/backend/src/__tests__/follow_ups.test.ts new file mode 100644 index 0000000..de5e1ef --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/follow_ups.test.ts @@ -0,0 +1,121 @@ +// FollowUps CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "customerId": "sample-customerid", + "type": "sample-type", + "content": "sample-content", + "nextAction": "sample-nextaction", + "nextActionDate": "sample-nextactiondate" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/follow_ups", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/follow_ups", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/follow_ups", () => { + it("creates a follow_up", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/follow_ups", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/follow_ups/:id", () => { + it("returns the created follow_up", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/follow_ups/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/follow_ups/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/follow_ups/:id", () => { + it("updates the follow_up", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/follow_ups/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/follow_ups/:id", () => { + it("deletes the follow_up", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/follow_ups/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/follow_ups/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/note_tags.test.ts b/.benchmark/crm/backend/src/__tests__/note_tags.test.ts new file mode 100644 index 0000000..8544923 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/note_tags.test.ts @@ -0,0 +1,118 @@ +// NoteTag CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/note_tags", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/note_tags", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/note_tags", () => { + it("creates a note_tag", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/note_tags", + headers: { authorization: `Bearer ${token}` }, + payload: { + "noteId": "00000000-0000-0000-0000-000000000001", + "tagId": "00000000-0000-0000-0000-000000000001", + "PRIMARY KEY (noteId, tagId)": "sample-primary key (noteid, tagid)" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/note_tags/:id", () => { + it("returns the created note_tag", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/note_tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/note_tags/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/note_tags/:id", () => { + it("updates the note_tag", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/note_tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "noteId": "00000000-0000-0000-0000-000000000001", + "tagId": "00000000-0000-0000-0000-000000000001", + "PRIMARY KEY (noteId, tagId)": "sample-primary key (noteid, tagid)" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/note_tags/:id", () => { + it("deletes the note_tag", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/note_tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/note_tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/notes.test.ts b/.benchmark/crm/backend/src/__tests__/notes.test.ts new file mode 100644 index 0000000..4e329a7 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/notes.test.ts @@ -0,0 +1,122 @@ +// Note CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/notes", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/notes", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/notes", () => { + it("creates a note", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/notes", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "content": "sample-content", + "isMarkdown": true, + "folderId": "00000000-0000-0000-0000-000000000001" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/notes/:id", () => { + it("returns the created note", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/notes/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/notes/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/notes/:id", () => { + it("updates the note", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/notes/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "content": "sample-content", + "isMarkdown": true, + "folderId": "00000000-0000-0000-0000-000000000001" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/notes/:id", () => { + it("deletes the note", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/notes/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/notes/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/sales_pipeline.test.ts b/.benchmark/crm/backend/src/__tests__/sales_pipeline.test.ts new file mode 100644 index 0000000..9bd83c7 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/sales_pipeline.test.ts @@ -0,0 +1,121 @@ +// SalesPipeline CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "customerId": "sample-customerid", + "stage": "sample-stage", + "amount": 1, + "probability": 1, + "expectedClose": "sample-expectedclose" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/sales_pipeline", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/sales_pipeline", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/sales_pipeline", () => { + it("creates a sales_pipeline", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/sales_pipeline", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/sales_pipeline/:id", () => { + it("returns the created sales_pipeline", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/sales_pipeline/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/sales_pipeline/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/sales_pipeline/:id", () => { + it("updates the sales_pipeline", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/sales_pipeline/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/sales_pipeline/:id", () => { + it("deletes the sales_pipeline", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/sales_pipeline/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/sales_pipeline/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/tags.test.ts b/.benchmark/crm/backend/src/__tests__/tags.test.ts new file mode 100644 index 0000000..8505912 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/tags.test.ts @@ -0,0 +1,116 @@ +// Tag CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/tags", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/tags", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/tags", () => { + it("creates a tag", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/tags", + headers: { authorization: `Bearer ${token}` }, + payload: { + "name": "sample-name", + "color": "sample-color" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/tags/:id", () => { + it("returns the created tag", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/tags/:id", () => { + it("updates the tag", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "name": "sample-name", + "color": "sample-color" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/tags/:id", () => { + it("deletes the tag", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/users.test.ts b/.benchmark/crm/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/db/client.ts b/.benchmark/crm/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/crm/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js — pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet — start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/crm/backend/src/db/schema.ts b/.benchmark/crm/backend/src/db/schema.ts new file mode 100644 index 0000000..2bebc78 --- /dev/null +++ b/.benchmark/crm/backend/src/db/schema.ts @@ -0,0 +1,16 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS sales_pipeline (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n customer_id TEXT REFERENCES customers(id),\n stage TEXT NOT NULL,\n amount REAL,\n probability INTEGER,\n expected_close TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS follow_ups (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n customer_id TEXT REFERENCES customers(id),\n type TEXT,\n content TEXT,\n next_action TEXT,\n next_action_date TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS dashboard (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n description TEXT,\n color TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/crm/backend/src/index.ts b/.benchmark/crm/backend/src/index.ts new file mode 100644 index 0000000..b45ebd8 --- /dev/null +++ b/.benchmark/crm/backend/src/index.ts @@ -0,0 +1,71 @@ +// NoteApp — Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { customersRoutes } from "./routes/customers.js"; +import { sales_pipelineRoutes } from "./routes/sales_pipeline.js"; +import { follow_upsRoutes } from "./routes/follow_ups.js"; +import { dashboardRoutes } from "./routes/dashboard.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-80f3b14d"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(sales_pipelineRoutes, { prefix: "/api/sales_pipeline" }); + await app.register(follow_upsRoutes, { prefix: "/api/follow_ups" }); + await app.register(dashboardRoutes, { prefix: "/api/dashboard" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/crm/backend/src/middleware/auth.ts b/.benchmark/crm/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/crm/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid — continue without user + } +} diff --git a/.benchmark/crm/backend/src/routes/auth.ts b/.benchmark/crm/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/crm/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me — current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/crm/backend/src/routes/customers.ts b/.benchmark/crm/backend/src/routes/customers.ts new file mode 100644 index 0000000..e22abcc --- /dev/null +++ b/.benchmark/crm/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customers routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomersService } from "../services/customer.js"; +import type { CreateCustomersInput, UpdateCustomersInput } from "../types/index.js"; + +const service = new CustomersService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customers not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers — create + app.post<{ Body: CreateCustomersInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id — update + app.put<{ Params: { id: string }; Body: UpdateCustomersInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customers not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customers not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/dashboard.ts b/.benchmark/crm/backend/src/routes/dashboard.ts new file mode 100644 index 0000000..d7828c0 --- /dev/null +++ b/.benchmark/crm/backend/src/routes/dashboard.ts @@ -0,0 +1,51 @@ +// Auto-generated Dashboard routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { DashboardService } from "../services/dashboard.js"; +import type { CreateDashboardInput, UpdateDashboardInput } from "../types/index.js"; + +const service = new DashboardService(); + +export async function dashboardRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/dashboard — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/dashboard/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Dashboard not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/dashboard — create + app.post<{ Body: CreateDashboardInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/dashboard/:id — update + app.put<{ Params: { id: string }; Body: UpdateDashboardInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Dashboard not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/dashboard/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Dashboard not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/follow_ups.ts b/.benchmark/crm/backend/src/routes/follow_ups.ts new file mode 100644 index 0000000..fabb3f8 --- /dev/null +++ b/.benchmark/crm/backend/src/routes/follow_ups.ts @@ -0,0 +1,51 @@ +// Auto-generated FollowUps routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { FollowUpsService } from "../services/follow_up.js"; +import type { CreateFollowUpsInput, UpdateFollowUpsInput } from "../types/index.js"; + +const service = new FollowUpsService(); + +export async function follow_upsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/follow_ups — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/follow_ups/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "FollowUps not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/follow_ups — create + app.post<{ Body: CreateFollowUpsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/follow_ups/:id — update + app.put<{ Params: { id: string }; Body: UpdateFollowUpsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "FollowUps not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/follow_ups/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "FollowUps not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/note_tags.ts b/.benchmark/crm/backend/src/routes/note_tags.ts new file mode 100644 index 0000000..5faa25a --- /dev/null +++ b/.benchmark/crm/backend/src/routes/note_tags.ts @@ -0,0 +1,51 @@ +// Auto-generated NoteTag routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { NoteTagService } from "../services/note_tag.js"; +import type { CreateNoteTagInput, UpdateNoteTagInput } from "../types/index.js"; + +const service = new NoteTagService(); + +export async function note_tagsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/note_tags — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/note_tags/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/note_tags — create + app.post<{ Body: CreateNoteTagInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/note_tags/:id — update + app.put<{ Params: { id: string }; Body: UpdateNoteTagInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/note_tags/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/notes.ts b/.benchmark/crm/backend/src/routes/notes.ts new file mode 100644 index 0000000..db544f1 --- /dev/null +++ b/.benchmark/crm/backend/src/routes/notes.ts @@ -0,0 +1,51 @@ +// Auto-generated Note routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { NoteService } from "../services/note.js"; +import type { CreateNoteInput, UpdateNoteInput } from "../types/index.js"; + +const service = new NoteService(); + +export async function notesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/notes — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/notes/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/notes — create + app.post<{ Body: CreateNoteInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/notes/:id — update + app.put<{ Params: { id: string }; Body: UpdateNoteInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/notes/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/sales_pipeline.ts b/.benchmark/crm/backend/src/routes/sales_pipeline.ts new file mode 100644 index 0000000..4a5f3cf --- /dev/null +++ b/.benchmark/crm/backend/src/routes/sales_pipeline.ts @@ -0,0 +1,51 @@ +// Auto-generated SalesPipeline routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { SalesPipelineService } from "../services/sales_pipeline.js"; +import type { CreateSalesPipelineInput, UpdateSalesPipelineInput } from "../types/index.js"; + +const service = new SalesPipelineService(); + +export async function sales_pipelineRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/sales_pipeline — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/sales_pipeline/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "SalesPipeline not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/sales_pipeline — create + app.post<{ Body: CreateSalesPipelineInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/sales_pipeline/:id — update + app.put<{ Params: { id: string }; Body: UpdateSalesPipelineInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "SalesPipeline not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/sales_pipeline/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "SalesPipeline not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/tags.ts b/.benchmark/crm/backend/src/routes/tags.ts new file mode 100644 index 0000000..70cb8ba --- /dev/null +++ b/.benchmark/crm/backend/src/routes/tags.ts @@ -0,0 +1,51 @@ +// Auto-generated Tag routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { TagService } from "../services/tag.js"; +import type { CreateTagInput, UpdateTagInput } from "../types/index.js"; + +const service = new TagService(); + +export async function tagsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/tags — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/tags/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/tags — create + app.post<{ Body: CreateTagInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/tags/:id — update + app.put<{ Params: { id: string }; Body: UpdateTagInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/tags/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/users.ts b/.benchmark/crm/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/crm/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users — list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id — get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users — create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id — update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id — delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/services/customer.ts b/.benchmark/crm/backend/src/services/customer.ts new file mode 100644 index 0000000..fe775ea --- /dev/null +++ b/.benchmark/crm/backend/src/services/customer.ts @@ -0,0 +1,56 @@ +// Auto-generated Customers service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customers, CreateCustomersInput, UpdateCustomersInput } from "../types/index.js"; + +export class CustomersService { + /** List all customers */ + list(): Customers[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customers | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomersInput): Customers { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomersInput): Customers | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/dashboard.ts b/.benchmark/crm/backend/src/services/dashboard.ts new file mode 100644 index 0000000..6beae29 --- /dev/null +++ b/.benchmark/crm/backend/src/services/dashboard.ts @@ -0,0 +1,54 @@ +// Auto-generated Dashboard service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Dashboard, CreateDashboardInput, UpdateDashboardInput } from "../types/index.js"; + +export class DashboardService { + /** List all dashboard */ + list(): Dashboard[] { + return queryAll("SELECT * FROM dashboard ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Dashboard | undefined { + return queryOne("SELECT * FROM dashboard WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateDashboardInput): Dashboard { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "description", "color", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.description ?? null, input.color ?? null, now, now]; + + execute(`INSERT INTO dashboard (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateDashboardInput): Dashboard | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.color !== undefined) { sets.push("color = ?"); values.push(input.color); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE dashboard SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM dashboard WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/follow_up.ts b/.benchmark/crm/backend/src/services/follow_up.ts new file mode 100644 index 0000000..e4e2715 --- /dev/null +++ b/.benchmark/crm/backend/src/services/follow_up.ts @@ -0,0 +1,56 @@ +// Auto-generated FollowUps service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { FollowUps, CreateFollowUpsInput, UpdateFollowUpsInput } from "../types/index.js"; + +export class FollowUpsService { + /** List all follow_ups */ + list(): FollowUps[] { + return queryAll("SELECT * FROM follow_ups ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): FollowUps | undefined { + return queryOne("SELECT * FROM follow_ups WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateFollowUpsInput): FollowUps { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "customer_id", "type", "content", "next_action", "next_action_date", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.customerId ?? null, input.type ?? null, input.content ?? null, input.nextAction ?? null, input.nextActionDate ?? null, now, now]; + + execute(`INSERT INTO follow_ups (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateFollowUpsInput): FollowUps | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.customerId !== undefined) { sets.push("customer_id = ?"); values.push(input.customerId); } + if (input.type !== undefined) { sets.push("type = ?"); values.push(input.type); } + if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); } + if (input.nextAction !== undefined) { sets.push("next_action = ?"); values.push(input.nextAction); } + if (input.nextActionDate !== undefined) { sets.push("next_action_date = ?"); values.push(input.nextActionDate); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE follow_ups SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM follow_ups WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/note.ts b/.benchmark/crm/backend/src/services/note.ts new file mode 100644 index 0000000..a206fa0 --- /dev/null +++ b/.benchmark/crm/backend/src/services/note.ts @@ -0,0 +1,58 @@ +// Auto-generated Note service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Note, CreateNoteInput, UpdateNoteInput } from "../types/index.js"; + +export class NoteService { + /** List all notes */ + list(): Note[] { + return queryAll("SELECT * FROM notes ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Note | undefined { + return queryOne("SELECT * FROM notes WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateNoteInput): Note { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "user_id", "title", "content", "is_markdown", "folder_id", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.content ?? null, input.isMarkdown ?? null, input.folderId ?? null, now, now]; + + execute(`INSERT INTO notes (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateNoteInput): Note | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); } + if (input.isMarkdown !== undefined) { sets.push("is_markdown = ?"); values.push(input.isMarkdown); } + if (input.folderId !== undefined) { sets.push("folder_id = ?"); values.push(input.folderId); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE notes SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM notes WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/note_tag.ts b/.benchmark/crm/backend/src/services/note_tag.ts new file mode 100644 index 0000000..91a5652 --- /dev/null +++ b/.benchmark/crm/backend/src/services/note_tag.ts @@ -0,0 +1,54 @@ +// Auto-generated NoteTag service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { NoteTag, CreateNoteTagInput, UpdateNoteTagInput } from "../types/index.js"; + +export class NoteTagService { + /** List all note_tags */ + list(): NoteTag[] { + return queryAll("SELECT * FROM note_tags ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): NoteTag | undefined { + return queryOne("SELECT * FROM note_tags WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateNoteTagInput): NoteTag { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "note_id", "tag_id"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.noteId ?? null, input.tagId ?? null]; + + execute(`INSERT INTO note_tags (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateNoteTagInput): NoteTag | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.noteId !== undefined) { sets.push("note_id = ?"); values.push(input.noteId); } + if (input.tagId !== undefined) { sets.push("tag_id = ?"); values.push(input.tagId); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE note_tags SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM note_tags WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/sales_pipeline.ts b/.benchmark/crm/backend/src/services/sales_pipeline.ts new file mode 100644 index 0000000..543e5c8 --- /dev/null +++ b/.benchmark/crm/backend/src/services/sales_pipeline.ts @@ -0,0 +1,56 @@ +// Auto-generated SalesPipeline service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { SalesPipeline, CreateSalesPipelineInput, UpdateSalesPipelineInput } from "../types/index.js"; + +export class SalesPipelineService { + /** List all sales_pipeline */ + list(): SalesPipeline[] { + return queryAll("SELECT * FROM sales_pipeline ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): SalesPipeline | undefined { + return queryOne("SELECT * FROM sales_pipeline WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateSalesPipelineInput): SalesPipeline { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "customer_id", "stage", "amount", "probability", "expected_close", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.customerId ?? null, input.stage ?? null, input.amount ?? null, input.probability ?? null, input.expectedClose ?? null, now, now]; + + execute(`INSERT INTO sales_pipeline (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateSalesPipelineInput): SalesPipeline | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.customerId !== undefined) { sets.push("customer_id = ?"); values.push(input.customerId); } + if (input.stage !== undefined) { sets.push("stage = ?"); values.push(input.stage); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.probability !== undefined) { sets.push("probability = ?"); values.push(input.probability); } + if (input.expectedClose !== undefined) { sets.push("expected_close = ?"); values.push(input.expectedClose); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE sales_pipeline SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM sales_pipeline WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/tag.ts b/.benchmark/crm/backend/src/services/tag.ts new file mode 100644 index 0000000..267443d --- /dev/null +++ b/.benchmark/crm/backend/src/services/tag.ts @@ -0,0 +1,54 @@ +// Auto-generated Tag service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Tag, CreateTagInput, UpdateTagInput } from "../types/index.js"; + +export class TagService { + /** List all tags */ + list(): Tag[] { + return queryAll("SELECT * FROM tags ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Tag | undefined { + return queryOne("SELECT * FROM tags WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateTagInput): Tag { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "name", "color"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.name ?? null, input.color ?? null]; + + execute(`INSERT INTO tags (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateTagInput): Tag | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.color !== undefined) { sets.push("color = ?"); values.push(input.color); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE tags SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM tags WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/user.ts b/.benchmark/crm/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/crm/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/types/fastify.d.ts b/.benchmark/crm/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/crm/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/crm/backend/src/types/index.ts b/.benchmark/crm/backend/src/types/index.ts new file mode 100644 index 0000000..fe2dee4 --- /dev/null +++ b/.benchmark/crm/backend/src/types/index.ts @@ -0,0 +1,191 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Customers { + id: string; + userId?: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface SalesPipeline { + id: string; + userId?: string; + customerId?: string; + stage: string; + amount?: number; + probability?: number; + expectedClose?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface FollowUps { + id: string; + userId?: string; + customerId?: string; + type?: string; + content?: string; + nextAction?: string; + nextActionDate?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Dashboard { + id: string; + userId?: string; + name: string; + description?: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateCustomersInput { + userId?: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; +} + +export interface CreateSalesPipelineInput { + userId?: string; + customerId?: string; + stage: string; + amount?: number; + probability?: number; + expectedClose?: string; +} + +export interface CreateFollowUpsInput { + userId?: string; + customerId?: string; + type?: string; + content?: string; + nextAction?: string; + nextActionDate?: string; +} + +export interface CreateDashboardInput { + userId?: string; + name: string; + description?: string; + color?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateCustomersInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; +} + +export interface UpdateSalesPipelineInput { + userId?: string; + customerId?: string; + stage?: string; + amount?: number; + probability?: number; + expectedClose?: string; +} + +export interface UpdateFollowUpsInput { + userId?: string; + customerId?: string; + type?: string; + content?: string; + nextAction?: string; + nextActionDate?: string; +} + +export interface UpdateDashboardInput { + userId?: string; + name?: string; + description?: string; + color?: string; +} + +// ─── Auth ─────────────────────────────────────────── +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// ─── API ──────────────────────────────────────────── +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// ─── JWT ──────────────────────────────────────────── +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/crm/backend/src/types/sql.js.d.ts b/.benchmark/crm/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/crm/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/crm/backend/tsconfig.json b/.benchmark/crm/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/crm/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/crm/electron/README.md b/.benchmark/crm/electron/README.md new file mode 100644 index 0000000..4372420 --- /dev/null +++ b/.benchmark/crm/electron/README.md @@ -0,0 +1,82 @@ +# noteapp — Desktop + +> Electron desktop application wrapping the noteapp fullstack web project. + +## Architecture + +``` +Web Fullstack Project + × + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** — Window management, menu, tray, IPC, auto-update +- **Preload** — Secure bridge between main and renderer +- **Renderer** — Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +├── main.ts — Main process (BrowserWindow, Menu, lifecycle) +├── preload.ts — Secure IPC bridge (contextBridge) +├── ipc.ts — IPC handlers (dialogs, store, notifications) +├── tray.ts — System tray +├── updater.ts — Auto-update (electron-updater) +└── utils.ts — Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/crm/electron/assets/icon.png b/.benchmark/crm/electron/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..554e8a31d1f0205fbf5ef853aba9a4fa2fc490dd GIT binary patch literal 66 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv4;KqeCd<5Tr}e}F6o MPgg&ebxsLQ09s=V>;M1& literal 0 HcmV?d00001 diff --git a/.benchmark/crm/electron/electron-builder.yml b/.benchmark/crm/electron/electron-builder.yml new file mode 100644 index 0000000..8f64fd1 --- /dev/null +++ b/.benchmark/crm/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml — Auto-generated by SF-06 + +appId: com.noteapp.desktop +productName: noteapp +copyright: "Copyright © 2026 noteapp" + +# ─── Directories ───────────────────────────────── +directories: + output: release + buildResources: assets + +# ─── Files ─────────────────────────────────────── +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# ─── Extra Resources ───────────────────────────── +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# ─── macOS ─────────────────────────────────────── +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# ─── Windows ───────────────────────────────────── +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: noteapp + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: noteapp + +# ─── Linux ─────────────────────────────────────── +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: noteapp + Comment: noteapp Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# ─── Auto Update ───────────────────────────────── +publish: + provider: generic + url: https://releases.example.com/noteapp/ + channel: latest diff --git a/.benchmark/crm/electron/electron/ipc.ts b/.benchmark/crm/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/crm/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // ── App Info ── + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // ── Window Controls ── + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // ── File Dialogs ── + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // ── Notifications ── + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // ── Shell ── + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // ── Persistent Store ── + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/crm/electron/electron/main.ts b/.benchmark/crm/electron/electron/main.ts new file mode 100644 index 0000000..6f03c94 --- /dev/null +++ b/.benchmark/crm/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// ─── Constants ─────────────────────────────────── +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "noteapp"; + +// ─── Single Instance Lock ──────────────────────── +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// ─── State ─────────────────────────────────────── +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// ─── Create Window ─────────────────────────────── +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // ── Load content ── + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // ── Show when ready ── + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // ── External links → default browser ── + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // ── Prevent navigation away ── + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// ─── Icon Path ─────────────────────────────────── +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// ─── API Server (Production) ───────────────────── +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// ─── Menu ──────────────────────────────────────── +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// ─── App Lifecycle ─────────────────────────────── +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed — continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/crm/electron/electron/preload.ts b/.benchmark/crm/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/crm/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// ─── Expose safe API to renderer ───────────────── +contextBridge.exposeInMainWorld("electronAPI", { + // ── App Info ── + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // ── Window Controls ── + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // ── File Dialogs ── + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // ── Notifications ── + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // ── Shell ── + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // ── Store ── + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // ── Updates ── + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // ── Update Events ── + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // ── Remove listener ── + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// ─── Type declarations for renderer ────────────── +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/crm/electron/electron/tray.ts b/.benchmark/crm/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/crm/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/crm/electron/electron/updater.ts b/.benchmark/crm/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/crm/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/crm/electron/electron/utils.ts b/.benchmark/crm/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/crm/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// ─── Single Instance Lock ──────────────────────── +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// ─── Dev mode detection ────────────────────────── +export function isDev(): boolean { + return !app.isPackaged; +} + +// ─── Web URL ───────────────────────────────────── +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// ─── API Path ──────────────────────────────────── +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// ─── Get API port from env ─────────────────────── +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/crm/electron/entitlements.mac.plist b/.benchmark/crm/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/crm/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/crm/electron/package.json b/.benchmark/crm/electron/package.json new file mode 100644 index 0000000..c3e0509 --- /dev/null +++ b/.benchmark/crm/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "noteapp-desktop", + "version": "1.0.0", + "private": true, + "description": "noteapp — Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "noteapp", + "appId": "com.noteapp.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/crm/electron/tsconfig.json b/.benchmark/crm/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/crm/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/crm/frontend/app/customers/page.tsx b/.benchmark/crm/frontend/app/customers/page.tsx new file mode 100644 index 0000000..a8cba9e --- /dev/null +++ b/.benchmark/crm/frontend/app/customers/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useCustomers } from "@/hooks/useCustomers"; + +export default function PagePage() { + const { data, loading, error, refetch } = useCustomers(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

客户管理

+

客户管理

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/crm/frontend/app/dashboard/page.tsx b/.benchmark/crm/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..9e19a76 --- /dev/null +++ b/.benchmark/crm/frontend/app/dashboard/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useDashboard } from "@/hooks/useDashboard"; + +export default function PagePage() { + const { data, loading, error, refetch } = useDashboard(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

数据分析仪表盘

+

数据分析仪表盘

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/crm/frontend/app/follow_ups/page.tsx b/.benchmark/crm/frontend/app/follow_ups/page.tsx new file mode 100644 index 0000000..0a81bc3 --- /dev/null +++ b/.benchmark/crm/frontend/app/follow_ups/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useFollow_ups } from "@/hooks/useFollow_ups"; + +export default function PagePage() { + const { data, loading, error, refetch } = useFollow_ups(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

跟进记录

+

跟进记录

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">创建第一条} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/crm/frontend/app/globals.css b/.benchmark/crm/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/crm/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/crm/frontend/app/layout.tsx b/.benchmark/crm/frontend/app/layout.tsx new file mode 100644 index 0000000..61e9ee6 --- /dev/null +++ b/.benchmark/crm/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "NoteApp", + description: "一款支持多端同步、Markdown 编辑和标签管理的笔记应用。", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/crm/frontend/app/note/[id]/page.tsx b/.benchmark/crm/frontend/app/note/[id]/page.tsx new file mode 100644 index 0000000..0698205 --- /dev/null +++ b/.benchmark/crm/frontend/app/note/[id]/page.tsx @@ -0,0 +1,25 @@ +"use client"; +import { useState } from "react"; +import { Input } from "@/components/ui/Input"; +import { Button } from "@/components/ui/Button"; + +export default function NoteEditorPage() { + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + + return ( +
+
+

编辑笔记

+ +
+ setTitle(e.target.value)} placeholder="笔记标题" className="text-lg font-medium border-0 px-0" /> +