create-fullstack-scaffold 0.4.23 → 0.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,154 +1,281 @@
1
- # create-biomimic-app
1
+ # create-fullstack-scaffold
2
2
 
3
- A full-stack React + Hono application template with TypeScript, demonstrating best practices for monorepo-style architecture with single-port development.
3
+ [![npm version](https://img.shields.io/npm/v/create-fullstack-scaffold.svg)](https://www.npmjs.com/package/create-fullstack-scaffold)
4
4
 
5
- ## Features
5
+ Zero-config fullstack app generator with type-safe RPC, 15+ modules, and 8 production-ready presets. One command, zero `.env`, instant `npm run dev`.
6
6
 
7
- - **Frontend**: React with TypeScript, Vite
8
- - **Backend**: Hono with TypeScript
9
- - **Database**: SQLite with Drizzle ORM
10
- - **State Management**: Zustand
11
- - **Real-time**: WebSocket + SSE support
12
- - **Testing**: Vitest (unit + integration tests)
13
- - **Code Quality**: ESLint, Prettier, pre-commit hooks
14
- - **Type Safety**: End-to-end type safety with Hono RPC
7
+ ## Quick Start
15
8
 
16
- ## Architecture
17
-
18
- ```
19
- src/
20
- ├── client/ # React frontend
21
- │ ├── components/ # UI components
22
- │ ├── stores/ # Zustand state management
23
- │ ├── services/ # API clients (apiClient)
24
- │ ├── hooks/ # Custom hooks
25
- │ ├── pages/ # Page components
26
- │ └── App.tsx
27
- ├── server/ # Hono backend
28
- │ ├── module-todos/ # Todo module
29
- │ ├── module-chat/ # WebSocket chat module
30
- │ ├── module-notifications/ # SSE notifications module
31
- │ ├── core/ # Core services (runtime, realtime)
32
- │ ├── middleware/ # Express middleware
33
- │ ├── test-utils/ # Test utilities
34
- │ └── entries/ # Entry points (node.ts, cloudflare.ts)
35
- └── shared/ # Shared types
36
- ├── core/ # Framework layer (ws-client, sse-client)
37
- ├── modules/ # Business layer (chat, todos, notifications)
38
- └── schemas/ # Unified exports
9
+ ```bash
10
+ npx create-fullstack-scaffold@latest my-app
11
+ cd my-app && npm install && npm run dev
39
12
  ```
40
13
 
41
- ## Getting Started
14
+ Open http://localhost:3010 — that's it. No `.env` required.
42
15
 
43
- ### Installation
16
+ ## Tech Stack
44
17
 
45
- ```bash
46
- npm install
47
- ```
18
+ React + Hono + Vite + Zustand + TypeScript + Ant Design + Zod
48
19
 
49
- ### Development
20
+ | Layer | Technology | Purpose |
21
+ | ---------- | -------------------- | ----------------------------------- |
22
+ | Frontend | React 19 + Vite | Client SPA with HMR |
23
+ | Admin | Ant Design 5 | Admin/merchant/tenant dashboards |
24
+ | Backend | Hono (OpenAPI) | Type-safe RPC server |
25
+ | State | Zustand | Client-side state management |
26
+ | Validation | Zod | Shared schemas, end-to-end |
27
+ | DB | Drizzle ORM + SQLite | Pluggable data layer |
28
+ | Realtime | WebSocket + SSE | Built-in typed protocols |
29
+ | CLI | Commander | `biomimic` CLI for agent automation |
50
30
 
51
- ```bash
52
- npm run dev
53
- ```
31
+ ## Presets
54
32
 
55
- The application will be available at http://localhost:3010
33
+ 8 presets. Each generates a different app by including/excluding modules.
56
34
 
57
- ### Build
35
+ | Preset | Modules | Use Case |
36
+ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
37
+ | `fullstack-admin` | todos, chat, notifications, file, captcha, permission, admin, auth, plugin, tenant, order, ticket, dispute, content, merchant (15) | Full-featured admin platform |
38
+ | `todo-app` | todos, chat, notifications, auth (4) | Learning / simple app |
39
+ | `ecommerce` | todos, chat, notifications, file, permission, order, ticket, dispute, content (9) | E-commerce store |
40
+ | `xbrowser-marketplace` | notifications, file, captcha, auth, permission, admin, plugin, order, ticket, dispute, content (11) | Plugin marketplace |
41
+ | `forum` | content, auth, permission, admin, notifications (5) | Community forum |
42
+ | `cli-only` | todos, chat, notifications, auth (4) | CLI agent / no browser UI |
43
+ | `minimal` | todos (1) | Bare minimum starting point |
44
+ | `saas` | todos, notifications, file, captcha, permission, auth, tenant, content (8) | Multi-tenant SaaS |
58
45
 
59
46
  ```bash
60
- npm run build
47
+ # Choose a preset
48
+ npx create-fullstack-scaffold@latest my-app --preset ecommerce
61
49
  ```
62
50
 
63
- ### Testing
51
+ ## Type-Safe RPC
64
52
 
65
- ```bash
66
- # Run all tests
67
- npm test
53
+ Every API call is fully typed — zero code generation, powered by Hono RPC.
54
+
55
+ ```typescript
56
+ import { apiClient } from '@client/services/apiClient'
57
+
58
+ // HTTP — typed request + response
59
+ const res = await apiClient.api.todos.$get()
60
+ const { data } = await res.json() // data: Todo[]
68
61
 
69
- # Run unit tests only
70
- npm run test:unit
62
+ // WebSocket typed RPC + events
63
+ const ws = apiClient.api.chat.ws.$ws()
64
+ const result = await ws.call('echo', { message: 'hello' })
71
65
 
72
- # Run integration tests only
73
- npm run test:integration
66
+ // SSE typed server-push
67
+ const conn = await apiClient.api.notifications.stream.$sse()
68
+ conn.on('notification', n => console.log(n.title))
69
+
70
+ // Media — typed binary responses
71
+ const blob = await apiClient.api.avatar[':id'].$image({ param: { id: '123' } })
72
+ const svg = await apiClient.api.icon[':name'].$svg({ param: { name: 'home' } })
73
+ const file = await apiClient.api.export.$download()
74
74
  ```
75
75
 
76
- ## Key Concepts
76
+ | Protocol | Method | Return Type |
77
+ | ------------- | ------------------------------------------ | --------------------- |
78
+ | HTTP JSON | `$get()`, `$post()`, `$put()`, `$delete()` | `ClientResponse<T>` |
79
+ | WebSocket | `$ws()` | `WSClient<Protocol>` |
80
+ | SSE | `$sse()` | `SSEClient<Protocol>` |
81
+ | Image | `$image()` | `Promise<Blob>` |
82
+ | SVG | `$svg()` | `Promise<string>` |
83
+ | File Download | `$download()` | `Promise<Blob>` |
77
84
 
78
- ### Path Aliases
85
+ ## Architecture
79
86
 
80
- - `@shared/*` → src/shared/\*
81
- - `@client/*` → src/client/\*
82
- - `@server/*` → src/server/\*
87
+ ```
88
+ ┌─────────────────────────────────────────────────────────┐
89
+ │ Generated App │
90
+ │ │
91
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
92
+ │ │ Client │ │ Admin │ │ Tenant │ │Merchant │ │
93
+ │ │ (React) │ │(Ant Design)│ │(Ant Design)│ │(Ant Design)│
94
+ │ │ index.html│ │admin.html│ │tenant.html│ │merchant │ │
95
+ │ └─────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬────┘ │
96
+ │ │ │ │ │ │
97
+ │ └──────────────┴──────────────┴──────────────┘ │
98
+ │ │ │
99
+ │ apiClient (hc) │
100
+ │ type-safe RPC │
101
+ │ │ │
102
+ │ ┌───────────────────────────┴───────────────────────────┐ │
103
+ │ │ Hono Server (Single Port) │ │
104
+ │ │ │ │
105
+ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │ │
106
+ │ │ │module- │ │module- │ │module- │ │module- │ │ │
107
+ │ │ │todos │ │chat │ │notifi- │ │admin │ │ │
108
+ │ │ │ │ │ │ │cations │ │ │ │ │
109
+ │ │ │routes/ │ │routes/ │ │routes/ │ │routes/ │ │ │
110
+ │ │ │services/│ │services/│ │services/│ │services/ │ │ │
111
+ │ │ │__tests__/│ │__tests__/│ │__tests__/│ │__tests__/ │ │ │
112
+ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────────┘ │ │
113
+ │ │ + 11 more modules (order, ticket, dispute, ...) │ │
114
+ │ └───────────────────────────────────────────────────────┘ │
115
+ │ │ │
116
+ │ ┌────────┴────────┐ │
117
+ │ │ Shared / │ │
118
+ │ │ core/ (Zod) │ │
119
+ │ │ modules/ (types)│ │
120
+ │ └─────────────────┘ │
121
+ └─────────────────────────────────────────────────────────────┘
122
+ ```
83
123
 
84
- ### Single-Port Development
124
+ ### Module System
85
125
 
86
- Uses "@hono/vite-dev-server" to run both frontend and backend on port 3010.
126
+ 15 modules with declarative manifests (`module.ts`). Each declares routes, dependencies, DB schemas, CLI commands, and pages.
87
127
 
88
- ### Framework Layer vs Business Layer
128
+ | Category | Modules |
129
+ | ------------- | -------------------------------------------------------------------- |
130
+ | Core | `todos` |
131
+ | Communication | `chat`, `notifications` |
132
+ | System | `permission`, `admin`, `auth`, `captcha`, `file`, `tenant`, `plugin` |
133
+ | Business | `order`, `ticket`, `dispute`, `content`, `merchant` |
89
134
 
90
- The project has clear separation between framework and business layers:
135
+ ### Multi-Entry HTML
91
136
 
92
- - **Framework Layer** (`src/shared/core/`): Generic, reusable infrastructure
93
- - **Business Layer** (`src/shared/modules/`): Business-specific schemas and protocols
137
+ Up to 4 independent SPAs, generated based on preset modules:
94
138
 
95
- ### Hono RPC
139
+ | Entry | File | Included When |
140
+ | -------- | --------------- | -------------------------------------- |
141
+ | Client | `index.html` | Always |
142
+ | Admin | `admin.html` | `admin` or `permission` module present |
143
+ | Tenant | `tenant.html` | `tenant` module present |
144
+ | Merchant | `merchant.html` | `merchant` module present |
96
145
 
97
- Provides type-safe API calls from frontend to backend:
146
+ Each entry has its own `App.tsx`, router, and layout — fully isolated.
98
147
 
99
- ```typescript
100
- import { apiClient } from '@client/services/apiClient'
148
+ ### Module Dependency Graph
101
149
 
102
- // HTTP API
103
- const response = await apiClient.api.todos.$get()
104
- const result = await response.json()
150
+ ```
151
+ todos ──── (standalone)
152
+ chat ──── (standalone)
153
+ notifications ──── (standalone)
154
+ file ──── (standalone)
155
+ captcha ──── (standalone)
156
+ auth ──── (standalone)
157
+ permission ──── (standalone, foundational)
158
+ admin ────→ permission + notifications
159
+ plugin ────→ auth + permission + notifications
160
+ tenant ────→ auth + permission
161
+ order ────→ permission
162
+ ticket ────→ permission
163
+ dispute ────→ permission
164
+ content ────→ permission
165
+ merchant ────→ auth + permission
166
+ ```
105
167
 
106
- // WebSocket
107
- const ws = apiClient.api.chat.ws.$ws()
108
- const result = await ws.call('echo', { message: 'hello' })
168
+ ## Deployment
109
169
 
110
- // SSE
111
- const conn = await apiClient.api.notifications.stream.$sse()
112
- conn.on('notification', n => console.log(n))
170
+ | Platform | Entry | Command |
171
+ | ---------------------- | ---------------------------------- | ------------------------------------------------------ |
172
+ | **Cloudflare Workers** | `src/server/entries/cloudflare.ts` | `wrangler deploy` |
173
+ | **Node.js** | `src/server/entries/node.ts` | `node dist/server/entries/node.js` |
174
+ | **shanbox** | Dockerfile included | `docker build -t app . && docker run -p 3010:3010 app` |
175
+
176
+ ```bash
177
+ # Build for production
178
+ npm run build
179
+
180
+ # Preview production build
181
+ npm run preview
113
182
  ```
114
183
 
115
- ### Real-time Features
184
+ ## Development
116
185
 
117
- | Feature | Method | Type Safety | Testing |
118
- | --------- | ------------------- | ----------- | ---------------- |
119
- | HTTP API | `$get()`, `$post()` | ✅ | No server needed |
120
- | WebSocket | `$ws()` | ✅ | Requires server |
121
- | SSE | `$sse()` | ✅ | No server needed |
186
+ ```bash
187
+ npm run dev # Start dev server on :3010 (no .env needed)
188
+ npm run build # Production build
189
+ npm run typecheck # TypeScript type check
190
+ npm run lint # ESLint
191
+ npm run test # Vitest (all tests)
192
+ npm run test:unit # Unit tests only
193
+ npm run test:integration # Integration tests only
194
+ npm run validate:modules # Validate module manifests
195
+ ```
196
+
197
+ ## Project Structure
122
198
 
123
- ### Module Structure
199
+ ```
200
+ template/src/
201
+ ├── client/ # React SPA (index.html)
202
+ │ ├── components/ # UI components
203
+ │ ├── stores/ # Zustand state
204
+ │ ├── services/ # apiClient
205
+ │ ├── hooks/ # Custom hooks
206
+ │ └── pages/ # Page components
207
+ ├── admin/ # Admin dashboard (admin.html, Ant Design)
208
+ │ ├── components/
209
+ │ ├── stores/
210
+ │ ├── layouts/
211
+ │ └── pages/
212
+ ├── tenant/ # Tenant dashboard (tenant.html, Ant Design)
213
+ │ └── ...
214
+ ├── merchant/ # Merchant dashboard (merchant.html, Ant Design)
215
+ │ └── ...
216
+ ├── server/ # Hono backend
217
+ │ ├── module-{name}/ # Feature modules (15+)
218
+ │ │ ├── module.ts # Declarative manifest
219
+ │ │ ├── routes/ # API endpoints
220
+ │ │ ├── services/ # Business logic
221
+ │ │ └── __tests__/ # Module tests
222
+ │ ├── core/ # Runtime, realtime scanner
223
+ │ ├── middleware/ # Auth, CORS, logger, captcha
224
+ │ ├── db/ # Drizzle schema + migrations
225
+ │ ├── entries/ # node.ts, cloudflare.ts
226
+ │ └── test-utils/ # createTestClient, createTestServer
227
+ ├── shared/ # Shared types (client + server)
228
+ │ ├── core/ # Framework: ws-client, sse-client, api-schemas
229
+ │ ├── modules/ # Business: todos, chat, notifications, ...
230
+ │ └── schemas/ # Unified re-exports
231
+ └── cli/ # CLI agent (biomimic command)
232
+ ├── modules/ # todo, notification, config, ...
233
+ └── rpc/ # hc RPC client
234
+ ```
124
235
 
125
- Backend is organized by feature modules:
236
+ ### Path Aliases
126
237
 
127
- - `module-todos/` - Todo CRUD
128
- - `module-chat/` - WebSocket chat
129
- - `module-notifications/` - SSE notifications
238
+ | Alias | Resolves To |
239
+ | ----------- | -------------- |
240
+ | `@shared/*` | `src/shared/*` |
241
+ | `@client/*` | `src/client/*` |
242
+ | `@server/*` | `src/server/*` |
243
+ | `@admin/*` | `src/admin/*` |
130
244
 
131
- Each module contains:
245
+ ## Testing
132
246
 
133
- - `routes/` - API endpoints
134
- - `services/` - Business logic
135
- - `__tests__/` - Unit tests
247
+ ```typescript
248
+ import { createTestClient } from '@server/test-utils/test-client'
136
249
 
137
- ## Pre-commit Hooks
250
+ const client = createTestClient() // No server needed for HTTP/SSE
138
251
 
139
- The project uses Husky for Git hooks:
252
+ const res = await client.api.todos.$get() // Fully typed
253
+ const { data } = await res.json()
254
+ ```
140
255
 
141
- - **lint-staged** - Format staged files
142
- - **npm test** - Run test suite
143
- - **validate-all** - Custom validation script
256
+ | Test Type | Needs Server | Tool |
257
+ | --------- | ------------ | ---------------------------------------------- |
258
+ | HTTP API | No | `createTestClient()` |
259
+ | SSE | No | `$sse()` via `createTestClient()` |
260
+ | WebSocket | Yes | `createTestServer()` + `createTestClient(url)` |
261
+ | E2E | Yes | Playwright |
144
262
 
145
- ## Environment Variables
263
+ ## Quality Gates
146
264
 
147
- See `.env.example` for required environment variables.
265
+ - **TypeScript strict mode** — no `any`, explicit return types
266
+ - **ESLint** — 17 custom rules (chain syntax, layer boundaries, no inline schemas, ...)
267
+ - **Pre-commit hooks** — typecheck + lint-staged + smart tests
268
+ - **Module validation** — `npm run validate:modules` checks manifests
269
+ - **Production verified** — all 7 client presets pass `typecheck` + `build`
148
270
 
149
271
  ## Documentation
150
272
 
151
- - `QUICKSTART.md` - Quick start guide
152
- - `DESIGN.md` - Technical architecture
153
- - `CLAUDE.md` - Development guidelines
154
- - `.claude/rules/` - Detailed development constraints
273
+ | File | Content |
274
+ | -------------------- | ----------------------------------------- |
275
+ | `CLAUDE.md` | Development guidelines for AI agents |
276
+ | `.claude/rules/` | 20+ detailed development constraint files |
277
+ | `template/CLAUDE.md` | Template-specific development guide |
278
+
279
+ ## License
280
+
281
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fullstack-scaffold",
3
- "version": "0.4.23",
3
+ "version": "0.4.24",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-fullstack-scaffold": "dist/cli/index.js"
@@ -15,6 +15,7 @@
15
15
  "dev:forum": "VITE_PRESET=community vite",
16
16
  "dev:saas": "VITE_PRESET=saas vite",
17
17
  "build": "npm run build:client && npm run build:server",
18
+ "build:analyze": "ANALYZE=true vite build",
18
19
  "build:client": "vite build",
19
20
  "build:server": "tsup",
20
21
  "build:cli": "tsup --config tsup.config.ts",
@@ -131,7 +131,16 @@
131
131
  "confirmNewPassword": "Confirm New Password",
132
132
  "confirmNewPasswordPlaceholder": "Confirm new password",
133
133
  "saveChanges": "Save Changes",
134
- "saved": "Settings saved successfully!"
134
+ "saved": "Settings saved successfully!",
135
+ "email": "Email Settings",
136
+ "smtpHost": "SMTP Host",
137
+ "smtpHostPlaceholder": "e.g. smtp.example.com",
138
+ "smtpPort": "SMTP Port",
139
+ "emailFrom": "From Address",
140
+ "emailFromPlaceholder": "e.g. noreply@example.com",
141
+ "sessionTimeout": "Session Timeout",
142
+ "maxLoginAttempts": "Max Login Attempts",
143
+ "minutes": "minutes"
135
144
  },
136
145
  "roles": {
137
146
  "title": "Role Management",
@@ -131,7 +131,16 @@
131
131
  "confirmNewPassword": "确认新密码",
132
132
  "confirmNewPasswordPlaceholder": "请确认新密码",
133
133
  "saveChanges": "保存更改",
134
- "saved": "设置保存成功!"
134
+ "saved": "设置保存成功!",
135
+ "email": "邮件设置",
136
+ "smtpHost": "SMTP 主机",
137
+ "smtpHostPlaceholder": "例如 smtp.example.com",
138
+ "smtpPort": "SMTP 端口",
139
+ "emailFrom": "发件地址",
140
+ "emailFromPlaceholder": "例如 noreply@example.com",
141
+ "sessionTimeout": "会话超时",
142
+ "maxLoginAttempts": "最大登录尝试次数",
143
+ "minutes": "分钟"
135
144
  },
136
145
  "roles": {
137
146
  "title": "角色管理",
@@ -1,73 +1,122 @@
1
- import { Card, Form, Input, Button, Switch, Divider, message } from 'antd'
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { Card, Form, Input, Button, Switch, Divider, message, InputNumber } from 'antd'
3
+ import { apiClient, api } from '../services/apiClient'
2
4
  import { useLanguage } from '../i18n/useLanguage'
3
-
4
- interface SettingsFormValues {
5
- siteName?: string
6
- siteDescription?: string
7
- currentPassword?: string
8
- newPassword?: string
9
- confirmPassword?: string
10
- }
5
+ import type { Settings } from '@shared/modules/admin'
11
6
 
12
7
  export const SettingsPage: React.FC = () => {
13
8
  const { t } = useLanguage()
14
- const [form] = Form.useForm<SettingsFormValues>()
9
+ const [form] = Form.useForm<Settings>()
10
+ const [loading, setLoading] = useState(false)
11
+
12
+ const fetchSettings = useCallback(async () => {
13
+ try {
14
+ const data = await api(apiClient.api.admin.settings.$get())
15
+ .withLoading(t('common.loading'))
16
+ .json()
17
+ form.setFieldsValue(data)
18
+ } catch {
19
+ // handled by api-request
20
+ }
21
+ }, [form, t])
15
22
 
16
- const handleSave = () => {
17
- message.success(t('settings.saved'))
23
+ useEffect(() => {
24
+ fetchSettings()
25
+ }, [fetchSettings])
26
+
27
+ const handleSave = async (values: Settings) => {
28
+ setLoading(true)
29
+ try {
30
+ await api(
31
+ apiClient.api.admin.settings.$put({
32
+ json: values,
33
+ })
34
+ )
35
+ .withLoading(t('common.loading'))
36
+ .json()
37
+ message.success(t('settings.saved'))
38
+ } catch {
39
+ // handled by api-request
40
+ } finally {
41
+ setLoading(false)
42
+ }
18
43
  }
19
44
 
20
45
  return (
21
46
  <div>
22
47
  <h1 className="text-2xl font-bold text-gray-900 mb-6">{t('settings.title')}</h1>
23
48
 
24
- <Card title={t('settings.general')} className="mb-6">
25
- <Form form={form} layout="vertical">
26
- <Form.Item label={t('settings.siteName')} name="siteName">
49
+ <Form form={form} layout="vertical" onFinish={handleSave}>
50
+ <Card title={t('settings.general')} className="mb-6">
51
+ <Form.Item label={t('settings.siteName')} name="siteName" rules={[{ required: true }]}>
27
52
  <Input placeholder={t('settings.siteNamePlaceholder')} />
28
53
  </Form.Item>
29
54
  <Form.Item label={t('settings.siteDescription')} name="siteDescription">
30
55
  <Input.TextArea rows={4} placeholder={t('settings.siteDescriptionPlaceholder')} />
31
56
  </Form.Item>
32
- </Form>
33
- </Card>
57
+ </Card>
34
58
 
35
- <Card title={t('settings.notification')} className="mb-6">
36
- <div className="space-y-4">
37
- <div className="flex items-center justify-between">
38
- <div>
39
- <p className="font-medium">{t('settings.emailNotifications')}</p>
40
- <p className="text-sm text-gray-500">{t('settings.emailNotificationsDesc')}</p>
59
+ <Card title={t('settings.email')} className="mb-6">
60
+ <Form.Item label={t('settings.smtpHost')} name="smtpHost" rules={[{ required: true }]}>
61
+ <Input placeholder={t('settings.smtpHostPlaceholder')} />
62
+ </Form.Item>
63
+ <Form.Item label={t('settings.smtpPort')} name="smtpPort" rules={[{ required: true }]}>
64
+ <InputNumber min={1} max={65535} className="w-full" />
65
+ </Form.Item>
66
+ <Form.Item
67
+ label={t('settings.emailFrom')}
68
+ name="emailFrom"
69
+ rules={[{ required: true, type: 'email' }]}
70
+ >
71
+ <Input placeholder={t('settings.emailFromPlaceholder')} />
72
+ </Form.Item>
73
+ </Card>
74
+
75
+ <Card title={t('settings.notification')} className="mb-6">
76
+ <div className="space-y-4">
77
+ <div className="flex items-center justify-between">
78
+ <div>
79
+ <p className="font-medium">{t('settings.emailNotifications')}</p>
80
+ <p className="text-sm text-gray-500">{t('settings.emailNotificationsDesc')}</p>
81
+ </div>
82
+ <Form.Item name="emailNotifications" valuePropName="checked" noStyle>
83
+ <Switch />
84
+ </Form.Item>
41
85
  </div>
42
- <Switch defaultChecked />
43
- </div>
44
- <Divider />
45
- <div className="flex items-center justify-between">
46
- <div>
47
- <p className="font-medium">{t('settings.pushNotifications')}</p>
48
- <p className="text-sm text-gray-500">{t('settings.pushNotificationsDesc')}</p>
86
+ <Divider />
87
+ <div className="flex items-center justify-between">
88
+ <div>
89
+ <p className="font-medium">{t('settings.pushNotifications')}</p>
90
+ <p className="text-sm text-gray-500">{t('settings.pushNotificationsDesc')}</p>
91
+ </div>
92
+ <Form.Item name="pushNotifications" valuePropName="checked" noStyle>
93
+ <Switch />
94
+ </Form.Item>
49
95
  </div>
50
- <Switch />
51
96
  </div>
52
- </div>
53
- </Card>
97
+ </Card>
54
98
 
55
- <Card title={t('settings.security')}>
56
- <Form form={form} layout="vertical">
57
- <Form.Item label={t('settings.currentPassword')} name="currentPassword">
58
- <Input.Password placeholder={t('settings.currentPasswordPlaceholder')} />
99
+ <Card title={t('settings.security')} className="mb-6">
100
+ <Form.Item
101
+ label={t('settings.sessionTimeout')}
102
+ name="sessionTimeout"
103
+ rules={[{ required: true }]}
104
+ >
105
+ <InputNumber min={5} max={1440} addonAfter={t('settings.minutes')} className="w-full" />
59
106
  </Form.Item>
60
- <Form.Item label={t('settings.newPassword')} name="newPassword">
61
- <Input.Password placeholder={t('settings.newPasswordPlaceholder')} />
107
+ <Form.Item
108
+ label={t('settings.maxLoginAttempts')}
109
+ name="maxLoginAttempts"
110
+ rules={[{ required: true }]}
111
+ >
112
+ <InputNumber min={1} max={20} className="w-full" />
62
113
  </Form.Item>
63
- <Form.Item label={t('settings.confirmNewPassword')} name="confirmPassword">
64
- <Input.Password placeholder={t('settings.confirmNewPasswordPlaceholder')} />
65
- </Form.Item>
66
- <Button type="primary" onClick={handleSave}>
67
- {t('settings.saveChanges')}
68
- </Button>
69
- </Form>
70
- </Card>
114
+ </Card>
115
+
116
+ <Button type="primary" htmlType="submit" loading={loading} size="large">
117
+ {t('settings.saveChanges')}
118
+ </Button>
119
+ </Form>
71
120
  </div>
72
121
  )
73
122
  }
@@ -9,6 +9,8 @@ import {
9
9
  HealthCheckSchema,
10
10
  RecentActivitySchema,
11
11
  ClearTodosResultSchema,
12
+ SettingsSchema,
13
+ UpdateSettingsSchema,
12
14
  } from '@shared/modules/admin'
13
15
 
14
16
  const getStatsRoute = createRoute({
@@ -68,7 +70,58 @@ const clearAllTodosRoute = createRoute({
68
70
  },
69
71
  })
70
72
 
73
+ const getSettingsRoute = createRoute({
74
+ method: 'get',
75
+ path: '/admin/settings',
76
+ tags: ['admin'],
77
+ security: [{ Bearer: [] }],
78
+ middleware: [authMiddleware({ requiredRole: Role.SUPER_ADMIN })],
79
+ responses: {
80
+ 200: successResponse(SettingsSchema, 'Get system settings'),
81
+ 401: errorResponse('Unauthorized'),
82
+ 403: errorResponse('Forbidden'),
83
+ },
84
+ })
85
+
86
+ const updateSettingsRoute = createRoute({
87
+ method: 'put',
88
+ path: '/admin/settings',
89
+ tags: ['admin'],
90
+ security: [{ Bearer: [] }],
91
+ middleware: [authMiddleware({ requiredRole: Role.SUPER_ADMIN })],
92
+ request: {
93
+ body: {
94
+ content: { 'application/json': { schema: UpdateSettingsSchema } },
95
+ },
96
+ },
97
+ responses: {
98
+ 200: successResponse(SettingsSchema, 'Settings updated'),
99
+ 401: errorResponse('Unauthorized'),
100
+ 403: errorResponse('Forbidden'),
101
+ },
102
+ })
103
+
104
+ let settingsState: z.infer<typeof SettingsSchema> = {
105
+ siteName: 'Biomimic Admin',
106
+ siteDescription: 'A full-stack admin dashboard',
107
+ smtpHost: 'smtp.example.com',
108
+ smtpPort: 587,
109
+ emailFrom: 'noreply@example.com',
110
+ sessionTimeout: 30,
111
+ maxLoginAttempts: 5,
112
+ emailNotifications: true,
113
+ pushNotifications: false,
114
+ }
115
+
71
116
  export const systemRoutes = new OpenAPIHono<{ Variables: { authUser: AuthUser } }>()
117
+ .openapi(getSettingsRoute, async c => {
118
+ return c.json(success(settingsState), 200)
119
+ })
120
+ .openapi(updateSettingsRoute, async c => {
121
+ const body = c.req.valid('json')
122
+ settingsState = { ...settingsState, ...body }
123
+ return c.json(success(settingsState), 200)
124
+ })
72
125
  .openapi(getStatsRoute, async c => {
73
126
  const stats = await adminService.getSystemStats()
74
127
  return c.json(success(stats), 200)
@@ -2,19 +2,36 @@
2
2
  import { describe, it, expect } from 'vitest'
3
3
  import { profileRoutes } from '../routes/profile-routes'
4
4
 
5
+ interface ProfileResponse {
6
+ success: boolean
7
+ data: {
8
+ id: string
9
+ username: string
10
+ email: string
11
+ bio: string
12
+ joinDate: string
13
+ stats: { posts: number; followers: number; following: number }
14
+ }
15
+ timestamp: string
16
+ }
17
+
18
+ async function fetchProfile(): Promise<ProfileResponse> {
19
+ const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
20
+ return (await res.json()) as ProfileResponse
21
+ }
22
+
5
23
  describe('Profile Routes', () => {
6
24
  describe('GET /profile', () => {
7
25
  it('should return profile with 200', async () => {
8
26
  const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
9
27
 
10
28
  expect(res.status).toBe(200)
11
- const data = (await res.json()) as Record<string, any>
29
+ const data = await fetchProfile()
12
30
  expect(data.success).toBe(true)
13
31
  })
14
32
 
15
33
  it('should return profile with required fields', async () => {
16
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
17
- const data = (await res.json()) as Record<string, any>
34
+ const data = await fetchProfile()
18
35
 
19
36
  expect(data.success).toBe(true)
20
37
  expect(data.data.id).toBeDefined()
@@ -25,16 +42,14 @@ describe('Profile Routes', () => {
25
42
  })
26
43
 
27
44
  it('should return valid email format', async () => {
28
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
29
- const data = (await res.json()) as Record<string, any>
45
+ const data = await fetchProfile()
30
46
 
31
47
  expect(data.success).toBe(true)
32
48
  expect(data.data.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
33
49
  })
34
50
 
35
51
  it('should return stats with numeric values', async () => {
36
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
37
- const data = (await res.json()) as Record<string, any>
52
+ const data = await fetchProfile()
38
53
 
39
54
  expect(data.success).toBe(true)
40
55
  expect(typeof data.data.stats.posts).toBe('number')
@@ -43,18 +58,15 @@ describe('Profile Routes', () => {
43
58
  })
44
59
 
45
60
  it('should return a valid ISO date for joinDate', async () => {
46
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
47
- const data = (await res.json()) as Record<string, any>
61
+ const data = await fetchProfile()
48
62
 
49
63
  expect(data.success).toBe(true)
50
64
  expect(data.data.joinDate).toMatch(/^\d{4}-\d{2}-\d{2}T/)
51
65
  })
52
66
 
53
67
  it('should return consistent data across requests', async () => {
54
- const res1 = await profileRoutes.fetch(new Request('http://localhost/profile'))
55
- const data1 = (await res1.json()) as Record<string, any>
56
- const res2 = await profileRoutes.fetch(new Request('http://localhost/profile'))
57
- const data2 = (await res2.json()) as Record<string, any>
68
+ const data1 = await fetchProfile()
69
+ const data2 = await fetchProfile()
58
70
 
59
71
  expect(data1.success).toBe(true)
60
72
  expect(data2.success).toBe(true)
@@ -64,8 +76,7 @@ describe('Profile Routes', () => {
64
76
  })
65
77
 
66
78
  it('should include timestamp in response', async () => {
67
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
68
- const data = (await res.json()) as Record<string, any>
79
+ const data = await fetchProfile()
69
80
 
70
81
  expect(data.success).toBe(true)
71
82
  expect(data.timestamp).toBeDefined()
@@ -73,8 +84,7 @@ describe('Profile Routes', () => {
73
84
  })
74
85
 
75
86
  it('should return bio field', async () => {
76
- const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
77
- const data = (await res.json()) as Record<string, any>
87
+ const data = await fetchProfile()
78
88
 
79
89
  expect(data.success).toBe(true)
80
90
  expect(data.data.bio).toBeDefined()
@@ -79,6 +79,20 @@ export const ClearTodosResultSchema = z.object({
79
79
  deletedCount: z.number(),
80
80
  })
81
81
 
82
+ export const SettingsSchema = z.object({
83
+ siteName: z.string(),
84
+ siteDescription: z.string(),
85
+ smtpHost: z.string(),
86
+ smtpPort: z.number(),
87
+ emailFrom: z.string(),
88
+ sessionTimeout: z.number(),
89
+ maxLoginAttempts: z.number(),
90
+ emailNotifications: z.boolean(),
91
+ pushNotifications: z.boolean(),
92
+ })
93
+
94
+ export const UpdateSettingsSchema = SettingsSchema
95
+
82
96
  export const AdminSuccessSchema = z.object({})
83
97
 
84
98
  export const DownloadTokenSchema = z.object({
@@ -98,3 +112,5 @@ export type RegisterRequest = z.infer<typeof RegisterRequestSchema>
98
112
  export type User = z.infer<typeof UserSchema>
99
113
  export type UpdateUserRequest = z.infer<typeof UpdateUserRequestSchema>
100
114
  export type ClearTodosResult = z.infer<typeof ClearTodosResultSchema>
115
+ export type Settings = z.infer<typeof SettingsSchema>
116
+ export type UpdateSettings = z.infer<typeof UpdateSettingsSchema>
@@ -1,8 +1,25 @@
1
1
  import path from 'path'
2
2
  import { existsSync } from 'fs'
3
- import { defineConfig } from 'vite'
3
+ import { defineConfig, type Plugin } from 'vite'
4
4
  import devServer from '@hono/vite-dev-server'
5
5
  import { websocketPlugin, dbPlugin } from './vite-plugins'
6
+ // Bundle analysis: npm install -D rollup-plugin-visualizer && npm run build:analyze
7
+ let visualizerPlugin: (() => Plugin) | undefined
8
+ if (process.env.ANALYZE === 'true') {
9
+ try {
10
+ // @ts-expect-error — optional dev dependency, installed via: npm install -D rollup-plugin-visualizer
11
+ const mod = await import('rollup-plugin-visualizer')
12
+ visualizerPlugin = () =>
13
+ mod.visualizer({
14
+ open: true,
15
+ filename: 'stats.html',
16
+ gzipSize: true,
17
+ brotliSize: true,
18
+ }) as Plugin
19
+ } catch {
20
+ // rollup-plugin-visualizer not installed — run: npm install -D rollup-plugin-visualizer
21
+ }
22
+ }
6
23
  // Prerender is optional — only needed during production build with puppeteer + Chrome installed
7
24
  let prerender: typeof import('@prerenderer/rollup-plugin').default | undefined
8
25
  let puppeteerRenderer: typeof import('@prerenderer/renderer-puppeteer').default | undefined
@@ -85,6 +102,7 @@ export default defineConfig({
85
102
  ],
86
103
  build: {
87
104
  outDir: 'dist/client',
105
+ chunkSizeWarningLimit: 1000,
88
106
  rollupOptions: {
89
107
  input: {
90
108
  main: path.resolve(__dirname, 'index.html'),
@@ -93,6 +111,12 @@ export default defineConfig({
93
111
  merchant: path.resolve(__dirname, 'merchant.html'),
94
112
  },
95
113
  output: {
114
+ manualChunks: {
115
+ 'vendor-react': ['react', 'react-dom', 'react-router-dom'],
116
+ 'vendor-antd': ['antd', '@ant-design/icons'],
117
+ 'vendor-hono': ['hono'],
118
+ 'vendor-zustand': ['zustand'],
119
+ },
96
120
  plugins: [
97
121
  ...(prerender && puppeteerRenderer
98
122
  ? [
@@ -105,6 +129,7 @@ export default defineConfig({
105
129
  }),
106
130
  ]
107
131
  : []),
132
+ ...(visualizerPlugin ? [visualizerPlugin()] : []),
108
133
  ],
109
134
  },
110
135
  onwarn(warning, defaultHandler) {