@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.68

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.
Files changed (198) hide show
  1. package/README.en.md +86 -241
  2. package/README.ja.md +86 -241
  3. package/README.md +86 -241
  4. package/dist/{src-RDQ2EIBT.js → chunk-FI56GONY.js} +32 -2669
  5. package/dist/chunk-FI56GONY.js.map +1 -0
  6. package/dist/chunk-MBPB37FR.js +1348 -0
  7. package/dist/chunk-MBPB37FR.js.map +1 -0
  8. package/dist/{chunk-QYX6V7H2.js → chunk-ZFNBUF77.js} +347 -276
  9. package/dist/chunk-ZFNBUF77.js.map +1 -0
  10. package/dist/cli-config-templates-W2LLPMB6.js +5 -0
  11. package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
  12. package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-US5CCAJ4.js} +3 -3
  13. package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
  14. package/dist/index.js +1304 -532
  15. package/dist/index.js.map +1 -1
  16. package/dist/metafile-esm.json +1 -0
  17. package/dist/src-DSJJXM2F.js +3849 -0
  18. package/dist/src-DSJJXM2F.js.map +1 -0
  19. package/package.json +9 -8
  20. package/templates/README.md +48 -145
  21. package/templates/apps/admin/messages/ja-JP.json +38 -0
  22. package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
  23. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
  24. package/templates/apps/api/.dockerignore.hbs +14 -7
  25. package/templates/apps/api/.gitignore.hbs +7 -0
  26. package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
  27. package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
  28. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
  29. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +33 -8
  30. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
  31. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
  32. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
  33. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
  34. package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
  35. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
  36. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
  37. package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
  38. package/templates/apps/api/hai.build.json.hbs +1 -1
  39. package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
  40. package/templates/apps/api/package.json.hbs +6 -1
  41. package/templates/apps/api/playwright.config.ts.hbs +15 -7
  42. package/templates/apps/fullstack/.dockerignore.hbs +14 -9
  43. package/templates/apps/fullstack/.gitignore.hbs +7 -0
  44. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +24 -6
  45. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
  46. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
  47. package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
  48. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
  49. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +2 -2
  50. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +23 -5
  51. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
  52. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
  53. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
  54. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
  55. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +24 -6
  56. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
  57. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +2 -2
  58. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +30 -9
  59. package/templates/apps/fullstack/hai.build.json.hbs +2 -2
  60. package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
  61. package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
  62. package/templates/apps/fullstack/package.json.hbs +3 -1
  63. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
  64. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
  65. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
  66. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
  67. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +32 -8
  68. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
  69. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
  70. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
  71. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
  72. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
  73. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
  74. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
  75. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
  76. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
  77. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
  78. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
  79. package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
  80. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +40 -14
  81. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
  82. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
  83. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +6 -6
  84. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
  85. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +4 -4
  86. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
  87. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +13 -14
  88. package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
  89. package/templates/apps/h5/messages/ja-JP.json +40 -0
  90. package/templates/apps/h5/src/routes/+layout.svelte.hbs +16 -5
  91. package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
  92. package/templates/apps/mobile-app/src/App.svelte.hbs +15 -5
  93. package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
  94. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
  95. package/templates/apps/website/messages/ja-JP.json +32 -0
  96. package/templates/apps/website/src/routes/+layout.svelte.hbs +13 -5
  97. package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
  98. package/templates/base/.dockerignore.hbs +14 -8
  99. package/templates/base/.gitignore.hbs +7 -0
  100. package/templates/base/e2e/app.spec.ts.hbs +23 -8
  101. package/templates/base/hai.build.json.hbs +1 -1
  102. package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
  103. package/templates/base/package.json.hbs +11 -3
  104. package/templates/base/playwright.config.ts +15 -4
  105. package/templates/base/src/hooks.server.ts.hbs +15 -6
  106. package/templates/base/src/lib/server/init.ts.hbs +139 -30
  107. package/templates/base/vite.config.ts.hbs +5 -3
  108. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
  109. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
  110. package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
  111. package/templates/i18n/project.inlang/settings.json +1 -1
  112. package/templates/shared/.gitattributes +6 -0
  113. package/templates/shared/e2e-env.ts +98 -0
  114. package/templates/shared/run-e2e.mjs +51 -12
  115. package/templates/skills/AGENTS.md +7 -3
  116. package/templates/skills/bridges/admin/AGENTS.md +6 -2
  117. package/templates/skills/bridges/api/AGENTS.md +3 -1
  118. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +5 -1
  119. package/templates/skills/bridges/h5/AGENTS.md +6 -2
  120. package/templates/skills/bridges/mobile-app/AGENTS.md +6 -2
  121. package/templates/skills/bridges/website/AGENTS.md +6 -2
  122. package/templates/skills/hai-ai/SKILL.md +8 -2
  123. package/templates/skills/hai-ai/guide.md +134 -60
  124. package/templates/skills/hai-ai/reference.md +51 -11
  125. package/templates/skills/hai-api-client/SKILL.md +15 -160
  126. package/templates/skills/hai-api-client/reference.md +178 -0
  127. package/templates/skills/hai-api-contract/SKILL.md +16 -273
  128. package/templates/skills/hai-api-contract/reference.md +280 -0
  129. package/templates/skills/hai-app-create/SKILL.md +6 -2
  130. package/templates/skills/hai-app-create/development.md +24 -8
  131. package/templates/skills/hai-app-create/reference.md +7 -5
  132. package/templates/skills/hai-app-review/SKILL.md +7 -3
  133. package/templates/skills/hai-app-review/differential.md +34 -0
  134. package/templates/skills/hai-app-tests/SKILL.md +6 -3
  135. package/templates/skills/hai-app-tests/reference.md +5 -5
  136. package/templates/skills/hai-app-tests/validation.md +38 -0
  137. package/templates/skills/hai-audit/SKILL.md +24 -147
  138. package/templates/skills/hai-audit/reference.md +148 -0
  139. package/templates/skills/hai-build/SKILL.md +14 -8
  140. package/templates/skills/hai-build/reference.md +16 -13
  141. package/templates/skills/hai-cache/SKILL.md +20 -181
  142. package/templates/skills/hai-cache/reference.md +229 -0
  143. package/templates/skills/hai-capacitor/SKILL.md +7 -4
  144. package/templates/skills/hai-capacitor/reference.md +15 -9
  145. package/templates/skills/hai-ci/SKILL.md +2 -0
  146. package/templates/skills/hai-core/SKILL.md +7 -3
  147. package/templates/skills/hai-core/reference.md +25 -5
  148. package/templates/skills/hai-crypto/SKILL.md +22 -250
  149. package/templates/skills/hai-crypto/reference.md +310 -0
  150. package/templates/skills/hai-datapipe/SKILL.md +16 -195
  151. package/templates/skills/hai-datapipe/reference.md +204 -0
  152. package/templates/skills/hai-deploy/SKILL.md +24 -54
  153. package/templates/skills/hai-deploy/middleware.md +51 -43
  154. package/templates/skills/hai-framework-sync/SKILL.md +2 -2
  155. package/templates/skills/hai-framework-sync/sync.md +1 -1
  156. package/templates/skills/hai-iam/SKILL.md +25 -11
  157. package/templates/skills/hai-iam/reference.md +181 -154
  158. package/templates/skills/hai-kit/SKILL.md +35 -174
  159. package/templates/skills/hai-kit/reference.md +190 -0
  160. package/templates/skills/hai-payment/SKILL.md +33 -263
  161. package/templates/skills/hai-payment/reference.md +295 -0
  162. package/templates/skills/hai-pr-review/SKILL.md +21 -88
  163. package/templates/skills/hai-reach/SKILL.md +6 -3
  164. package/templates/skills/hai-reach/reference.md +83 -22
  165. package/templates/skills/hai-reldb/SKILL.md +6 -3
  166. package/templates/skills/hai-reldb/reference.md +68 -25
  167. package/templates/skills/hai-scheduler/SKILL.md +18 -195
  168. package/templates/skills/hai-scheduler/reference.md +216 -0
  169. package/templates/skills/hai-serv/SKILL.md +25 -10
  170. package/templates/skills/hai-serv/reference.md +123 -87
  171. package/templates/skills/hai-spec-implement/SKILL.md +5 -1
  172. package/templates/skills/hai-spec-implement/evaluation.md +18 -0
  173. package/templates/skills/hai-spec-implement/handoff.md +47 -0
  174. package/templates/skills/hai-spec-implement/templates.md +5 -0
  175. package/templates/skills/hai-spec-implement/workflow.md +5 -3
  176. package/templates/skills/hai-spec-review/SKILL.md +4 -0
  177. package/templates/skills/hai-storage/SKILL.md +23 -238
  178. package/templates/skills/hai-storage/reference.md +282 -0
  179. package/templates/skills/hai-ui/SKILL.md +17 -3
  180. package/templates/skills/hai-ui/guide.md +3 -3
  181. package/templates/skills/hai-ui/reference.md +26 -11
  182. package/templates/skills/hai-ui-design/SKILL.md +31 -0
  183. package/templates/skills/hai-ui-design/patterns.md +26 -0
  184. package/templates/skills/hai-ui-review/SKILL.md +30 -0
  185. package/templates/skills/hai-ui-review/checklist.md +22 -0
  186. package/templates/skills/hai-vecdb/SKILL.md +14 -10
  187. package/templates/skills/hai-vecdb/reference.md +33 -3
  188. package/dist/chunk-FWN5B54L.js +0 -468
  189. package/dist/chunk-FWN5B54L.js.map +0 -1
  190. package/dist/chunk-QYX6V7H2.js.map +0 -1
  191. package/dist/cli-config-templates-3XSFIXBP.js +0 -5
  192. package/dist/src-RDQ2EIBT.js.map +0 -1
  193. package/templates/apps/api/Dockerfile.hbs +0 -35
  194. package/templates/apps/fullstack/Dockerfile.hbs +0 -57
  195. /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
  196. /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
  197. /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
  198. /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
@@ -2,6 +2,9 @@ import type { CoreConfig, HaiResult } from '@h-ai/core'
2
2
  import { mkdir } from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
  import process from 'node:process'
5
+ {{#if features.ai}}
6
+ import { ai, AIConfigSchema } from '@h-ai/ai'
7
+ {{/if}}
5
8
  import { audit, AuditInitConfigSchema } from '@h-ai/audit'
6
9
  import { cache, CacheConfigSchema } from '@h-ai/cache'
7
10
  import { core } from '@h-ai/core'
@@ -11,13 +14,20 @@ import { reach, ReachConfigSchema } from '@h-ai/reach'
11
14
  import { reldb, ReldbConfigSchema } from '@h-ai/reldb'
12
15
  import { ServConfigSchema } from '@h-ai/serv'
13
16
  import { storage, StorageConfigSchema } from '@h-ai/storage'
17
+ {{#if features.vecdb}}
18
+ import { vecdb, VecdbConfigSchema } from '@h-ai/vecdb'
19
+ {{/if}}
14
20
  import { serverM } from './messages.js'
15
21
 
16
22
  let initialized = false
17
23
  let initializing: Promise<void> | undefined
24
+ let closing: Promise<void> | undefined
25
+ let cleanupPending = false
18
26
 
19
27
  /** 初始化失败停止启动并反序释放资源;不把功能缺失降级为成功。 */
20
28
  export async function initializeModules(): Promise<void> {
29
+ if (closing)
30
+ await closing
21
31
  if (initialized)
22
32
  return
23
33
  initializing ??= runInitialization().finally(() => {
@@ -27,6 +37,9 @@ export async function initializeModules(): Promise<void> {
27
37
  }
28
38
 
29
39
  async function runInitialization(): Promise<void> {
40
+ if (cleanupPending)
41
+ await cleanupModules()
42
+ cleanupPending = true
30
43
  try {
31
44
  requireInitialized(core.config.validate('serv', ServConfigSchema), 'serv-config')
32
45
  const db = ReldbConfigSchema.parse(core.config.getOrThrow('db'))
@@ -38,6 +51,10 @@ async function runInitialization(): Promise<void> {
38
51
  await mkdir(path.dirname(db.database), { recursive: true })
39
52
  requireInitialized(await reldb.init(db), 'reldb')
40
53
  requireInitialized(await cache.init(cacheConfig), 'cache')
54
+ {{#if features.vecdb}}
55
+ const vectorConfig = VecdbConfigSchema.parse(core.config.getOrThrow('vecdb'))
56
+ requireInitialized(await vecdb.init(vectorConfig), 'vecdb')
57
+ {{/if}}
41
58
  requireInitialized(await crypto.init(), 'crypto')
42
59
  requireInitialized(await storage.init(storageConfig), 'storage')
43
60
  const reachConfig = core.config.get('reach')
@@ -45,13 +62,23 @@ async function runInitialization(): Promise<void> {
45
62
  requireInitialized(await reach.init(ReachConfigSchema.parse(reachConfig)), 'reach')
46
63
  requireInitialized(await iam.init({ ...iamConfig, reach }), 'iam')
47
64
  requireInitialized(await audit.init(auditConfig), 'audit')
65
+ {{#if features.ai}}
66
+ const aiConfig = AIConfigSchema.parse(core.config.getOrThrow('ai'))
67
+ requireInitialized(await ai.init(aiConfig), 'ai')
68
+ {{/if}}
48
69
  await ensureRolesAndAdmin()
49
70
  initialized = true
71
+ cleanupPending = false
50
72
  const app = core.config.getOrThrow<CoreConfig>('core')
51
73
  core.logger.info('Application initialized', { name: app.name, version: app.version, environment: app.env })
52
74
  }
53
75
  catch (error) {
54
- await closeModules()
76
+ try {
77
+ await cleanupModules()
78
+ }
79
+ catch (cleanupError) {
80
+ throw new AggregateError([error, cleanupError], 'Application startup and cleanup failed')
81
+ }
55
82
  throw error
56
83
  }
57
84
  }
@@ -91,12 +118,36 @@ async function ensureRolesAndAdmin(): Promise<void> {
91
118
 
92
119
  /** 关闭顺序与依赖初始化顺序相反,同时覆盖启动失败路径。 */
93
120
  export async function closeModules(): Promise<void> {
94
- for (const [name, module] of [['audit', audit], ['iam', iam], ['reach', reach], ['storage', storage], ['crypto', crypto], ['cache', cache], ['reldb', reldb]] as const) {
95
- if (!module.isInitialized)
96
- continue
97
- const result = await module.close()
98
- if (result && !result.success)
99
- core.logger.error('Failed to close application module', { module: name, error: result.error.code })
121
+ if (!closing) {
122
+ const pending = initializing
123
+ closing = (async () => {
124
+ if (pending)
125
+ await pending.catch(() => undefined)
126
+ await cleanupModules()
127
+ })().finally(() => {
128
+ closing = undefined
129
+ })
130
+ }
131
+ return closing
132
+ }
133
+
134
+ /** 不依赖 isInitialized 推断资源是否释放;关闭失败必须保留义务供重试。 */
135
+ async function cleanupModules(): Promise<void> {
136
+ const failures: unknown[] = []
137
+ for (const [name, module] of [{{#if features.ai}}['ai', ai], {{/if}}['audit', audit], ['iam', iam], ['reach', reach], ['storage', storage], ['crypto', crypto], {{#if features.vecdb}}['vecdb', vecdb], {{/if}}['cache', cache], ['reldb', reldb]] as const) {
138
+ try {
139
+ const result = await module.close()
140
+ if (result && !result.success) {
141
+ core.logger.error('Failed to close application module', { module: name, error: result.error.code })
142
+ failures.push(result.error)
143
+ }
144
+ }
145
+ catch (error) {
146
+ failures.push(error)
147
+ }
100
148
  }
101
149
  initialized = false
150
+ cleanupPending = failures.length > 0
151
+ if (failures.length > 0)
152
+ throw new AggregateError(failures, 'Application cleanup failed; retry closeModules()')
102
153
  }
@@ -46,6 +46,10 @@ beforeAll(async () => {
46
46
  vi.stubEnv('HAI_ADMIN_DEFAULT_PASSWORD', TEST_PASSWORD)
47
47
  core.init({ configDir: './config' })
48
48
  await initializeModules()
49
+ const key = crypto.asymmetric.generateKeyPair()
50
+ if (!key.success)
51
+ throw new Error(key.error.message)
52
+ vi.stubEnv('HAI_CRYPTO_TRANSPORT_SERVERKEYRING', JSON.stringify({ activeKeyId: 'test-v1', keys: { 'test-v1': key.data } }))
49
53
  app = createServerApp()
50
54
  transport = crypto.transport.createClient({
51
55
  keyExchangeUrl: `http://localhost${PREFIX}/_hai/key-exchange`,
@@ -339,7 +343,10 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
339
343
  })
340
344
 
341
345
  it('rejects stale administrator snapshots and reports fresh user roles', async () => {
342
- const stale = await iam.session.update(userToken, { roles: ['admin'], permissions: ['*'] })
346
+ const snapshot = await iam.session.get(userToken)
347
+ if (!snapshot.success || !snapshot.data)
348
+ throw new Error('Session unavailable')
349
+ const stale = await iam.session.update(userToken, { roles: ['admin'], permissions: ['*'] }, snapshot.data.version)
343
350
  expect(stale.success).toBe(true)
344
351
  for (const [route, input] of [['/admin/users/list', { page: 1, pageSize: 20 }], ['/admin/roles/list', undefined], ['/admin/audit/list', { page: 1, pageSize: 20 }]] as const) {
345
352
  const denied = await call(route, input, userToken)
@@ -354,8 +361,11 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
354
361
  })
355
362
 
356
363
  it('returns a conflict when another administrator holds the mutation lock and resumes after release', async () => {
357
- const locked = await cache.lock.acquire('hai:app:admin-mutation', { owner: 'test-owner', ttl: 10 })
364
+ const locked = await cache.lock.acquire('hai:app:admin-mutation', { ttl: 10 })
358
365
  expect(locked.success).toBe(true)
366
+ if (!locked.success || !locked.data)
367
+ throw new Error('Mutation lock unavailable')
368
+ const owner = locked.data
359
369
  try {
360
370
  const result = await call('/admin/users/update', { id: user.id, displayName: 'Locked' }, adminToken)
361
371
  expect(result.success).toBe(false)
@@ -363,7 +373,7 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
363
373
  expect(result.error.httpStatus).toBe(409)
364
374
  }
365
375
  finally {
366
- expect((await cache.lock.release('hai:app:admin-mutation', 'test-owner')).success).toBe(true)
376
+ expect((await cache.lock.release('hai:app:admin-mutation', owner)).success).toBe(true)
367
377
  }
368
378
  expect((await call('/admin/users/update', { id: user.id, displayName: 'Unlocked' }, adminToken)).success).toBe(true)
369
379
  })
@@ -1,3 +1,10 @@
1
1
  node_modules
2
2
  .turbo
3
3
  src/lib/paraglide
4
+
5
+ # 构建器生成的安装输入快照
6
+ **/images/dependencies/
7
+
8
+ # 项目本地缓存
9
+ **/.pnpm-store/
10
+ **/.cache/
@@ -1,58 +1,84 @@
1
1
  # {{fullstack.sharedPackageName}}
2
2
 
3
+ ## Introduction
4
+
3
5
  Shared mini administration workspace for **{{projectName}}**. Web, Capacitor App and Tauri desktop use the same `AdminWorkspace`, responsive `AppShell`, typed API client and management store.
4
6
 
7
+ ## Core features
8
+
9
+ - Shared management pages, forms, navigation and application state.
10
+ - Typed encrypted API client and platform-specific token storage.
11
+ - Themes, language preferences, onboarding and help.
12
+
13
+ ## Getting started
14
+
15
+ ### Behavior and configuration
16
+
5
17
  The behavior below describes the initial example. Roles, permissions, workflows, limits, preferences and audit policies can change with the user's requirements. Update conflicting implementation, tests and guidance together; preserve the applicable public API contracts and server security checks.
6
18
 
7
19
  - Administrators manage users, upload user avatars, assign the built-in administrator/user roles, edit role descriptions and query audit logs.
8
20
  - By default, ordinary users update their own profile, avatar, language, appearance and password. The server enforces the actual business permissions on every request.
21
+ - The workspace uses the available window width. Tables scroll within their own area; settings panels form two columns on large screens and stack on phones.
9
22
  - `ManagementStore` owns API calls and business state; components render the state and forward user interactions.
10
23
  - Application name, version, environment and default locale come from the backend `app.info` contract, which reads `core` configuration. Do not duplicate these values in translations or frontend constants.
11
24
  - Requests, token refresh and JSON responses use the same `@h-ai/api-client` + `@h-ai/crypto` transport. Avatar uploads use encrypted JSON and validate PNG/JPEG/WebP content, with a 512 KiB limit.
12
25
  - Read metadata through the same initialized `managementApi.app.info()` client; inspect `success` before using `data`.
13
26
 
14
- All three Vite apps read the workspace root .env. Development and preview servers proxy /api to the local backend when no explicit API base is configured, so browser clients can use a single origin. In deployments, configure PUBLIC_API_BASE or the Web container runtime API origin for the actual service.
27
+ Vite development and preview servers read the workspace root environment files and proxy /api to the backend. Builds do not load deployment environment files. Web containers receive their public API address at startup; native apps follow the platform configuration below.
15
28
 
16
29
  An origin-only API base uses `/api/v1`. For a custom service `http.apiPrefix`, include that path in `PUBLIC_API_BASE` or the runtime API base; the client preserves the configured path and restricts avatar URLs to its matching resource prefix.
17
30
 
18
- ## Authentication and preferences
31
+ ### Authentication and preferences
19
32
 
20
33
  Web and browser previews keep access tokens in memory and refresh tokens in the server's httpOnly Cookie. Requests include credentials; reload restores the session through the encrypted refresh flow. Never store authentication tokens in localStorage.
21
34
 
22
- Capacitor native uses `createCapacitorTokenStorage()` and the trusted native token channel. Tauri native uses memory token storage and requires a new login after restarting the application. Native builds require a reachable `PUBLIC_API_BASE` in the workspace root `.env`; missing configuration produces a connection configuration error. Both send their frontend `clientName` and `X-HAI-Token-Transport: body`; the service verifies the allowed native origin before returning a refresh token in JSON.
35
+ Capacitor native uses `createCapacitorTokenStorage()` and the trusted native token channel. Set its public API address in the App's `public/runtime-config.js` before compiling and syncing native assets. Tauri uses memory token storage and requires a new login after restart; set process `PUBLIC_API_BASE` before launching the installed application, and its Rust host injects the address. Both send `clientName` and `X-HAI-Token-Transport: body`; the service verifies the trusted native origin before returning a refresh token in JSON. Missing API configuration is an error, not a successful connection.
23
36
 
24
37
  Language and theme preferences are saved in IAM user metadata, restored after login and synchronized with the shared/host Paraglide runtimes and `core.i18n`. The small reactive theme store delegates DOM updates and safe appearance caching to `@h-ai/ui/theme` (`applyTheme` / `getSavedTheme`); the framework uses the `theme` localStorage key. Appearance selectors live in personal settings and save through the authenticated profile contract.
25
38
 
26
- ## Onboarding and help
39
+ ### Onboarding and help
27
40
 
28
41
  AdminWorkspace mounts the shared `@h-ai/ui/guide` controller and GuideHost after authentication. AppShell includes a help button; first-use invitations offer start, later and dismiss. Web stores preferences in Cookies, while App/Desktop use localStorage, scoped by the current user and tour version. Help articles link to the overview and profile tours.
29
42
 
30
- Extend `src/lib/stores/guide-store.ts` and the shared Chinese/English messages, then add stable data-guide attributes to existing components. Navigation uses the same ManagementStore. Keep business text as i18n callbacks and preserve the host's locale/scope binding. Browser tests cover completion, reload, replay, settings navigation and mobile help.
43
+ Extend `src/lib/stores/guide-store.ts` and the shared Chinese/English/Japanese messages, then add stable data-guide attributes to existing components. Navigation uses the same ManagementStore. Keep business text as i18n callbacks and preserve the host's locale/scope binding. Browser tests cover completion, reload, replay, settings navigation and mobile help.
31
44
 
32
- ## i18n and themes
45
+ ### i18n and themes
33
46
 
34
- Shared page copy belongs in `messages/{zh-CN,en-US}.json`. Frontend-specific copy belongs in each app's own messages directory. Keep `baseLocale` and `locales` aligned across all enabled projects, then run root `pnpm i18n:compile`; do not edit generated Paraglide files.
47
+ Shared page copy belongs in `messages/{zh-CN,en-US,ja-JP}.json`. Frontend-specific copy belongs in each app's own messages directory. Keep `baseLocale` and `locales` aligned across all enabled projects, then run root `pnpm i18n:compile`; do not edit generated Paraglide files.
35
48
 
36
49
  All frontend and shared projects load the installed message-format plugin from the workspace root `node_modules`, matching the scaffold's hoisted dependency layout. Compilation uses the workspace dependency version and does not fetch a plugin from a CDN.
37
50
 
38
51
  The scaffold theme configuration determines the themes compiled into frontend CSS. Personal settings offer only those available themes. Customize the common layout in `src/lib/components/AppShell.svelte`, the management pages in adjacent components, and business behavior in `src/lib/stores/management-store.svelte.ts`.
39
52
 
40
- ## Validation
53
+ ### Validation
41
54
 
42
55
  Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Shared API tests exercise real transport key exchange, encrypted request/response handling, configured core metadata and credential forwarding. Root browser tests cover administrator and ordinary-user workflows, avatar rendering, persisted preferences, password/session invalidation and narrow-screen layout. All selected Web/App/Desktop previews also exercise login, tours, help and profile navigation. Clean screenshots, reports, temporary projects, containers and helper processes in finally/teardown, including failure paths. These browser checks do not replace native-shell or miniapp device acceptance.
43
56
 
44
- ## IAM configuration
57
+ ### IAM configuration
45
58
 
46
- The service's `config/_iam.yml` controls login methods, registration, password reset, agreements and `ui` branding. Email and SMS use providers/templates in `_reach.yml` or custom callbacks. `AuthPanel` uses the public configuration and framework auth actions to manage tokens, resend countdowns and errors. All service nodes must share the same database and Redis; memory cache is for a single process.
59
+ The service's `config/_iam.yml` controls login methods, registration, password reset, agreements and `ui` branding. Email and SMS use providers/templates in `_reach.yml` or custom callbacks. `AuthPanel` uses the public configuration and framework auth actions to manage tokens, resend countdowns and errors. A single process can use memory cache. Multiple instances need shared database/cache, write routing and a shared managed transport key ring and registry as described in the project README.
47
60
 
48
61
  Authentication defaults to a split layout. Set `ui.variant`, `title`, `subtitle`, `brandTitle`, `brandText`, `logoUrl` and `illustrationUrl` to customize login and registration. Without a `brandTitle`, the displayed application name comes from `app.info` and follows `core` configuration.
49
62
 
50
- LDAP and Reach are optional. Install ldapts in the IAM service workspace only when using its default LDAP client, then enable login.ldap and set connection settings. Custom onOtpSendEmail/onOtpSendSms/onPasswordResetRequest callbacks work without Reach and must reject on delivery failure. Agreements open in a Modal. LoginForm/RegisterForm support header/extraFields/footer snippets; AuthPanel forwards loginHeader/loginFields/loginFooter and registerHeader/registerFields/registerFooter. See the generated hai-iam/hai-ui skills for configuration and examples.
63
+ LDAP and Reach are optional. Install ldapts in the IAM service workspace only when using its default LDAP client, then enable login.ldap and set connection settings. Custom onOtpSendEmail/onOtpSendSms/onPasswordResetRequest callbacks work without Reach and must reject on delivery failure. Their third context argument supplies expiresAt/idempotencyKey; senders durably deduplicate that key, and errors/timeouts mean unknown delivery with no automatic resend. Agreements open in a Modal. LoginForm/RegisterForm support header/extraFields/footer snippets; AuthPanel forwards loginHeader/loginFields/loginFooter and registerHeader/registerFields/registerFooter. See the generated hai-iam/hai-ui skills for configuration and examples.
51
64
 
52
- 协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
65
+ Agreements accept Markdown text or a URL. Markdown takes precedence and opens in a safely rendered dialog.
53
66
 
54
67
  See `.agents/skills/hai-iam/reference.md` in the generated application for the complete contract.
55
68
 
56
- ## Form layouts
69
+ ### Form layouts
70
+
71
+ The user editor uses `FormLayout columns={2} labelPosition="left"`. Personal settings fill the available width and use two columns on wide screens, with stacked labels and a single avatar editor. Narrow containers automatically stack cards, fields and labels. UI controls and CRUD forms show client validation on blur, update errors while correcting input and validate untouched fields on submit. Field errors follow the current UI language. The management store syncs locale through the lightweight `@h-ai/ui/i18n` entry, without loading components or tour CSS. Configure `colSpan` on FormField to override automatic spans for textareas, uploads and multiple choices.
72
+
73
+ ## Development and testing
74
+
75
+ Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Browser checks cover shared workflows; test native shells on their target platforms separately.
76
+
77
+ ## 界面设计与验收
78
+
79
+ 共享管理界面的 Web/App/Desktop 使用同一套 UI 组件和状态。设计、实现、审查分别参考根目录 `.agents/skills/hai-ui-design/`、`hai-ui/`、`hai-ui-review/`;正常、加载、空态、错误恢复、键盘、语言/主题和窄容器均按实际场景验收。组件的筛选、排序和分页仍由业务数据回调接入。
80
+
81
+
82
+ ## License
57
83
 
58
- The user editor uses `FormLayout columns={2} labelPosition="left"`. Personal settings use a bounded page width, stacked labels and a single avatar editor. Narrow containers automatically stack fields and labels. UI controls and CRUD forms show client validation on blur, update errors while correcting input and validate untouched fields on submit. Field errors follow the current UI language. The management store syncs locale through the lightweight `@h-ai/ui/i18n` entry, without loading components or tour CSS. Configure `colSpan` on FormField to override automatic spans for textareas, uploads and multiple choices.
84
+ Apache-2.0
@@ -0,0 +1,105 @@
1
+ {{! @skipIfEmpty }}
2
+ {
3
+ "$schema": "https://inlang.com/schema/inlang-message-format",
4
+ "settings_title": "個人設定",
5
+ "settings_language": "言語",
6
+ "settings_theme": "テーマ",
7
+ "home_refresh": "更新",
8
+ "home_loading": "読み込み中…",
9
+ "home_error": "APIサービスに接続できません",
10
+ "nav_users": "ユーザー",
11
+ "nav_roles": "ロール",
12
+ "nav_audit": "監査ログ",
13
+ "navigation": "メインナビゲーション",
14
+ "open_menu": "ナビゲーションを開く",
15
+ "close_menu": "ナビゲーションを閉じる",
16
+ "logout": "ログアウト",
17
+ "login_title": "ログイン",
18
+ "login_description": "ログインしてアカウントとワークスペースを管理します",
19
+ "login_invalid": "アカウントまたはパスワードが正しくありません。再試行してください。",
20
+ "session_expired": "セッションの有効期限が切れています。再度ログインしてください。",
21
+ "saved": "変更を保存しました",
22
+ "save": "保存",
23
+ "edit": "編集",
24
+ "delete": "削除",
25
+ "cancel": "キャンセル",
26
+ "search": "検索",
27
+ "details": "詳細",
28
+ "created_at": "作成日時",
29
+ "enabled": "有効",
30
+ "disabled": "無効",
31
+ "all_statuses": "すべての状態",
32
+ "operation_failed": "操作を完了できませんでした。再試行してください。",
33
+ "network_error": "接続に失敗しました。ネットワークを確認して再試行してください。",
34
+ "forbidden": "この操作を行う権限がありません。",
35
+ "conflict": "このアカウントは既に存在するか、変更が現在のデータと競合しています。",
36
+ "validation_failed": "入力内容を確認して再試行してください。",
37
+ "settings_description": "プロフィール、環境設定、アカウントのセキュリティを管理します",
38
+ "profile_title": "プロフィール",
39
+ "preferences_title": "環境設定",
40
+ "preferences_description": "言語と外観の設定は、端末にかかわらずアカウントに適用されます。",
41
+ "password_title": "パスワードを変更",
42
+ "password_rules": "現在と異なるパスワードを選んでください。",
43
+ "password_changed": "パスワードを更新しました。新しいパスワードでログインしてください。",
44
+ "avatar_hint": "PNG、JPEG、WebPの画像をアップロードしてください。最大512KBです。",
45
+ "avatar_invalid": "512KB以下のPNG、JPEG、WebP画像を選択してください。",
46
+ "avatar_saved": "アバターを更新しました",
47
+ "avatar_upload": "アバターをアップロード",
48
+ "users_description": "アカウントの作成、ロールの割り当て、アクセスを管理します",
49
+ "users_search": "ユーザー名、名前、メールアドレスを検索",
50
+ "user_account": "アカウント",
51
+ "user_username": "ユーザー名",
52
+ "user_display_name": "表示名",
53
+ "user_email": "メールアドレス",
54
+ "user_password": "初期パスワード",
55
+ "user_role": "ロール",
56
+ "user_status": "状態",
57
+ "user_create": "ユーザーを追加",
58
+ "initial_password_rules": "{min}文字以上で入力してください。",
59
+ "account_enabled": "ログインを許可",
60
+ "users_empty": "一致するアカウントがありません。検索条件を変更するか、ユーザーを作成してください。",
61
+ "user_edit": "ユーザーを編集",
62
+ "user_deleted": "ユーザーを削除しました",
63
+ "user_delete_title": "ユーザーを削除",
64
+ "user_delete_confirm": "{name}を削除しますか?アカウントとアクセス権は完全に削除されます。",
65
+ "roles_description": "2つのロールでアクセスを管理します。ロールはユーザー管理から割り当ててください。",
66
+ "role_admin": "管理者",
67
+ "role_user": "ユーザー",
68
+ "role_admin_capabilities": "ユーザー、ロールの説明、監査ログを管理し、個人設定を更新できます。",
69
+ "role_user_capabilities": "プロフィール、アバター、言語、外観、パスワードを更新できます。",
70
+ "role_members": "メンバー{count}人",
71
+ "role_edit": "説明を編集",
72
+ "role_description": "説明",
73
+ "audit_description": "アカウントと管理操作の履歴を確認します",
74
+ "audit_actor": "実行者",
75
+ "audit_action": "操作",
76
+ "audit_resource": "リソース",
77
+ "audit_resource_id": "リソースID",
78
+ "audit_ip": "IPアドレス",
79
+ "audit_start": "開始",
80
+ "audit_end": "終了",
81
+ "audit_action_placeholder": "すべての操作",
82
+ "audit_resource_placeholder": "すべてのリソース",
83
+ "audit_system": "システム",
84
+ "audit_details": "監査ログの詳細",
85
+ "audit_date_invalid": "終了日は開始日以降にしてください。",
86
+ "api_not_configured": "サービスへの接続が設定されていません。管理者にお問い合わせください。",
87
+ "guide_overview_title": "管理ワークスペースを見てみましょう",
88
+ "guide_overview_description": "簡単な手順でワークスペース、個人設定、ヘルプを紹介します。いつでも終了でき、ヘルプセンターから再開できます。",
89
+ "guide_content_title": "ワークスペース",
90
+ "guide_content_description": "現在のページがここに表示されます。ナビゲーションで機能を切り替えます。利用できる操作は権限に応じて変わります。",
91
+ "guide_theme_title": "外観",
92
+ "guide_theme_description": "ライトとダークの表示を切り替えます。設定でさらに多くのテーマを選択できます。",
93
+ "guide_profile_title": "プロフィール",
94
+ "guide_profile_description": "アバターからプロフィールや設定を開き、個人情報やパスワードを管理できます。",
95
+ "guide_help_title": "ヘルプセンター",
96
+ "guide_help_description": "困ったときは、ここで説明を検索したり、製品ツアーを再生したりできます。",
97
+ "guide_profile_tour_title": "プロフィールを設定",
98
+ "guide_profile_tour_description": "個人情報、環境設定、パスワード設定の場所を紹介します。ツアーでは変更を送信しません。",
99
+ "guide_profile_form_description": "プロフィールの表示や編集、アバターの選択、変更の保存ができます。アップロードするファイルは、画面に表示された形式とサイズの条件を満たす必要があります。",
100
+ "guide_preferences_description": "言語とテーマの設定を保存します。画面とヘルプの言語は選択に従います。",
101
+ "guide_password_title": "パスワードを変更",
102
+ "guide_password_description": "現在のパスワード、新しいパスワード、確認用パスワードを入力します。送信前にパスワードのルールを確認してください。",
103
+ "guide_overview_article": "## はじめに\n\n- ナビゲーションから各機能を開きます。メニューと操作は現在の権限に応じて変わります。\n- ワークスペースには一覧、詳細、設定が表示されます。画面の説明に従い、送信前に入力内容を確認してください。\n- アバターからプロフィールと設定を開きます。\n- ヘルプボタンで説明を検索したり、ツアーを再生したりできます。\n\n## 初回ツアー\n\n「後で通知」を選ぶと1日間通知を停止します。「今後通知しない」は設定を保存します。完了したツアーは自動で表示されなくなりますが、手動で再生できます。",
104
+ "guide_profile_article": "## プロフィールと環境設定\n\nプロフィールを開くと個人情報やアバターを確認・更新できます。ファイルの条件を確認し、変更を保存して結果のメッセージを確認してください。外観と言語は設定で変更できます。\n\n## パスワード\n\nパスワードの変更には、現在のパスワード、新しいパスワード、確認用パスワードが必要です。画面のルールに従い、送信後に結果を確認してください。\n\n「ツアーを開始」で関連するページを開きます。ツアーが自動でフォームを保存したり、アカウントを変更したりすることはありません。"
105
+ }
@@ -59,7 +59,7 @@
59
59
 
60
60
  {#key store.locale}
61
61
  {#if store.initializing}
62
- <div class='grid min-h-dvh place-items-center bg-base-200/50'><div class='w-72 space-y-4'><Skeleton variant='title' /><Skeleton count={3} /><p class='text-center text-sm text-base-content/50'>{m.home_loading()}</p></div></div>
62
+ <div role='status' aria-live='polite' class='grid min-h-dvh place-items-center bg-base-200/50'><div class='w-72 space-y-4'><Skeleton variant='title' /><Skeleton count={3} /><p class='text-center text-sm text-base-content/50'>{m.home_loading()}</p></div></div>
63
63
  {:else if !store.user || resetToken}
64
64
  <header class='border-b border-base-content/8 bg-base-100 px-6 py-3 text-sm font-medium'>{store.info?.name}</header>
65
65
  <main data-testid='login-page'>
@@ -78,11 +78,11 @@
78
78
  </main>
79
79
  {:else}
80
80
  <AppShell {platform} {store} onhelp={() => guide.openHelp()}>
81
- <div class='space-y-5'>
81
+ <div class='flex min-h-0 flex-1 flex-col gap-5'>
82
82
  {#if store.error}<Alert variant='error' data-testid='error'>{store.error}</Alert>{/if}
83
83
  {#if store.notice}<Alert variant='success' data-testid='notice'>{store.notice}</Alert>{/if}
84
84
  {#await pageComponent}
85
- <div class='space-y-4'><Skeleton variant='title' /><Skeleton count={3} /></div>
85
+ <div role='status' aria-label={m.home_loading()} class='space-y-4'><Skeleton variant='title' /><Skeleton count={3} /></div>
86
86
  {:then { default: Page }}
87
87
  <Page {store} />
88
88
  {/await}
@@ -30,11 +30,11 @@
30
30
  }
31
31
  </script>
32
32
 
33
- <div class="flex min-h-dvh bg-base-200/50 text-base-content" class:hai-safe-area={platform === 'app'}>
33
+ <div class="flex h-dvh overflow-hidden bg-base-200/50 text-base-content" class:hai-safe-area={platform === 'app'}>
34
34
  {#if menuOpen}
35
35
  <BareButton class="fixed inset-0 z-30 bg-black/30 lg:hidden" ariaLabel={m.close_menu()} onclick={() => { menuOpen = false }} />
36
36
  {/if}
37
- <aside class="fixed inset-y-0 left-0 z-40 flex w-60 flex-col border-r border-base-content/8 bg-base-100 transition-transform duration-200 lg:sticky lg:top-0 lg:h-dvh lg:translate-x-0 {menuOpen ? 'translate-x-0' : '-translate-x-full'}">
37
+ <aside class="fixed inset-y-0 left-0 z-40 flex w-60 shrink-0 flex-col border-r border-base-content/8 bg-base-100 transition-transform duration-200 lg:sticky lg:top-0 lg:h-dvh lg:translate-x-0 {menuOpen ? 'translate-x-0' : '-translate-x-full'}">
38
38
  <div class="flex h-16 items-center gap-3 border-b border-base-content/8 px-5">
39
39
  <span class="grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-primary-content"><span class="icon-[tabler--hexagon-letter-h] size-5"></span></span>
40
40
  <span class="truncate text-sm font-semibold" data-testid="app-name">{store.info?.name}</span>
@@ -48,8 +48,8 @@
48
48
  </nav>
49
49
  <div class="border-t border-base-content/8 p-4 text-xs text-base-content/40"><span data-testid="app-version">v{store.info?.version}</span></div>
50
50
  </aside>
51
- <div class="flex min-w-0 flex-1 flex-col">
52
- <header class="sticky top-0 z-20 flex h-16 items-center justify-between gap-2 border-b border-base-content/8 bg-base-100/95 px-3 backdrop-blur sm:px-4 lg:px-6">
51
+ <div class="flex min-h-0 min-w-0 flex-1 flex-col">
52
+ <header class="sticky top-0 z-20 flex h-16 shrink-0 items-center justify-between gap-2 border-b border-base-content/8 bg-base-100/95 px-3 backdrop-blur sm:px-4 lg:px-6">
53
53
  <div class="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
54
54
  <BareButton class="rounded-lg p-2 hover:bg-base-content/5 lg:hidden" ariaLabel={m.open_menu()} onclick={() => { menuOpen = !menuOpen }} data-testid="mobile-menu"><span class="icon-[tabler--menu-2] size-5"></span></BareButton>
55
55
  <span class="truncate text-sm font-medium">{title}</span>
@@ -66,7 +66,7 @@
66
66
  <Button variant="ghost" size="sm" loading={store.saving} onclick={() => void store.logout()} data-testid="logout">{m.logout()}</Button>
67
67
  </div>
68
68
  </header>
69
- <main class="mx-auto w-full max-w-7xl flex-1 p-4 pb-8 sm:p-6 lg:p-8" data-guide="admin-content">{@render children()}</main>
70
- <footer class="border-t border-base-content/8 px-6 py-3 text-center text-xs text-base-content/35">{store.info?.name} · v{store.info?.version}</footer>
69
+ <main class="flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-auto p-4 pb-8 sm:p-6 lg:p-8" data-guide="admin-content">{@render children()}</main>
70
+ <footer class="shrink-0 border-t border-base-content/8 px-6 py-3 text-center text-xs text-base-content/35">{store.info?.name} · v{store.info?.version}</footer>
71
71
  </div>
72
72
  </div>
@@ -17,10 +17,10 @@
17
17
  ])
18
18
  </script>
19
19
 
20
- <section class="space-y-5" data-testid="audit-page">
21
- <div><h1 class="text-xl font-semibold tracking-tight">{m.nav_audit()}</h1><p class="mt-1 text-sm text-base-content/50">{m.audit_description()}</p></div>
22
- <div class="rounded-xl border border-base-content/8 bg-base-100">
23
- <form class="grid items-end gap-3 border-b border-base-content/8 p-4 sm:grid-cols-2 xl:grid-cols-[repeat(4,minmax(0,1fr))_auto] [&_.fieldset]:p-0" onsubmit={(event) => {
20
+ <section class="flex min-h-0 flex-1 flex-col gap-5" data-testid="audit-page">
21
+ <div class="shrink-0"><h1 class="text-xl font-semibold tracking-tight">{m.nav_audit()}</h1><p class="mt-1 text-sm text-base-content/50">{m.audit_description()}</p></div>
22
+ <div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-base-content/8 bg-base-100">
23
+ <form class="grid shrink-0 items-end gap-3 border-b border-base-content/8 p-4 sm:grid-cols-2 xl:grid-cols-[repeat(4,minmax(0,1fr))_auto] [&_.fieldset]:p-0" onsubmit={(event) => {
24
24
  event.preventDefault()
25
25
  void store.loadAudit()
26
26
  }}>
@@ -30,8 +30,8 @@
30
30
  <div class="min-w-0 space-y-1"><label class="block text-xs text-base-content/60" for="audit-end">{m.audit_end()}</label><input id="audit-end" type="date" class="input block h-10 min-w-0 w-full" bind:value={store.auditEndDate} /></div>
31
31
  <Button type="submit" variant="outline" disabled={store.loading} class="h-10 w-full">{m.search()}</Button>
32
32
  </form>
33
- <div class="overflow-x-auto">
34
- <DataTable data={store.audit} {columns} keyField="id" loading={store.loading} striped={false}>
33
+ <div class="flex min-h-0 flex-1 flex-col overflow-auto">
34
+ <DataTable data={store.audit} {columns} keyField="id" loading={store.loading} striped={false} class="min-h-0 flex-1 overflow-auto">
35
35
  {#snippet cell(entry, key)}
36
36
  {#if key === 'createdAt'}<span class="whitespace-nowrap text-xs">{store.formatDate(entry.createdAt)}</span>
37
37
  {:else if key === 'username'}{entry.username || m.audit_system()}
@@ -42,7 +42,7 @@
42
42
  {#snippet actions(entry)}<Button size="xs" variant="ghost" onclick={() => { selected = entry }}>{m.details()}</Button>{/snippet}
43
43
  </DataTable>
44
44
  </div>
45
- <div class="border-t border-base-content/8 p-4"><Pagination page={store.auditPage} total={store.auditTotal} pageSize={store.pageSize} onchange={page => void store.loadAudit(page)} /></div>
45
+ <div class="mt-auto shrink-0 border-t border-base-content/8 p-4"><Pagination page={store.auditPage} total={store.auditTotal} pageSize={store.pageSize} onchange={page => void store.loadAudit(page)} /></div>
46
46
  </div>
47
47
  </section>
48
48
 
@@ -10,17 +10,17 @@
10
10
 
11
11
  interface Props { store: ManagementStore }
12
12
  const { store }: Props = $props()
13
- const languageOptions = $derived(locales.filter(locale => store.info?.supportedLocales.includes(locale)).map(locale => ({ value: locale, label: locale === 'zh-CN' ? '简体中文' : 'English' })))
13
+ const languageOptions = $derived(locales.filter(locale => store.info?.supportedLocales.includes(locale)).map(locale => ({ value: locale, label: locale === 'zh-CN' ? '简体中文' : locale === 'ja-JP' ? '日本語' : 'English' })))
14
14
  const themeOptions = AVAILABLE_THEMES.map(theme => ({ value: theme, label: getThemeInfo(theme)?.name ?? theme }))
15
15
  </script>
16
16
 
17
- <section class='mx-auto w-full max-w-4xl space-y-6' data-testid='settings-page'>
18
- <div>
17
+ <section class='grid w-full min-w-0 gap-6 xl:grid-cols-2' data-testid='settings-page'>
18
+ <div class='xl:col-span-2'>
19
19
  <h1 class='text-xl font-semibold tracking-tight'>{m.settings_title()}</h1>
20
20
  <p class='mt-1 text-sm text-base-content/50'>{m.settings_description()}</p>
21
21
  </div>
22
22
  {#if store.user}
23
- <Card title={m.profile_title()} bordered shadow={false} data-guide='admin-profile-form'>
23
+ <Card class='xl:row-span-2' title={m.profile_title()} bordered shadow={false} data-guide='admin-profile-form'>
24
24
  <div class='mb-5 flex flex-wrap items-center gap-3 border-b border-base-content/8 pb-4'>
25
25
  <p class='min-w-0 break-all text-sm text-base-content/55'>@{store.user.username}</p>
26
26
  <Badge size='sm' variant='ghost'>{store.isAdmin ? m.role_admin() : m.role_user()}</Badge>
@@ -63,16 +63,16 @@
63
63
  }
64
64
  </script>
65
65
 
66
- <section class="space-y-5" data-testid="users-page">
67
- <div class="flex flex-wrap items-center justify-between gap-3">
66
+ <section class="flex min-h-0 flex-1 flex-col gap-5" data-testid="users-page">
67
+ <div class="flex shrink-0 flex-wrap items-center justify-between gap-3">
68
68
  <div>
69
69
  <h1 class="text-xl font-semibold tracking-tight">{m.nav_users()}</h1>
70
70
  <p class="mt-1 text-sm text-base-content/50">{m.users_description()}</p>
71
71
  </div>
72
72
  <Button variant="primary" onclick={() => openEditor(null)} data-testid="create-user">{m.user_create()}</Button>
73
73
  </div>
74
- <div class="rounded-xl border border-base-content/8 bg-base-100">
75
- <form class="flex flex-wrap items-center gap-3 border-b border-base-content/8 p-4" onsubmit={(event) => {
74
+ <div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-base-content/8 bg-base-100">
75
+ <form class="flex shrink-0 flex-wrap items-center gap-3 border-b border-base-content/8 p-4" onsubmit={(event) => {
76
76
  event.preventDefault()
77
77
  void store.loadUsers()
78
78
  }}>
@@ -82,8 +82,8 @@
82
82
  <div class="w-40"><Select bind:value={store.usersStatus} options={statusOptions} /></div>
83
83
  <Button type="submit" variant="outline" disabled={store.loading}>{m.search()}</Button>
84
84
  </form>
85
- <div class="hidden overflow-x-auto md:block">
86
- <DataTable data={store.users} {columns} keyField="id" loading={store.loading} striped={false}>
85
+ <div class="hidden min-h-0 flex-1 overflow-auto md:flex md:flex-col">
86
+ <DataTable data={store.users} {columns} keyField="id" loading={store.loading} striped={false} class="min-h-0 flex-1 overflow-auto">
87
87
  {#snippet cell(user, key)}
88
88
  {#if key === 'username'}
89
89
  <div class="flex items-center gap-3">
@@ -105,7 +105,7 @@
105
105
  {/snippet}
106
106
  </DataTable>
107
107
  </div>
108
- <div class="space-y-3 p-4 md:hidden" data-testid="user-cards">
108
+ <div class="min-h-0 flex-1 space-y-3 overflow-auto p-4 md:hidden" data-testid="user-cards">
109
109
  {#if store.loading}
110
110
  <Skeleton count={3} />
111
111
  {:else if store.users.length === 0}
@@ -127,7 +127,7 @@
127
127
  {/each}
128
128
  {/if}
129
129
  </div>
130
- <div class="border-t border-base-content/8 p-4">
130
+ <div class="mt-auto shrink-0 border-t border-base-content/8 p-4">
131
131
  <Pagination page={store.usersPage} total={store.usersTotal} pageSize={store.pageSize} onchange={page => void store.loadUsers(page)} />
132
132
  </div>
133
133
  </div>