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

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 (125) hide show
  1. package/README.en.md +66 -257
  2. package/README.ja.md +66 -257
  3. package/README.md +66 -257
  4. package/dist/{chunk-QYX6V7H2.js → chunk-7DSURTJ4.js} +22 -79
  5. package/dist/chunk-7DSURTJ4.js.map +1 -0
  6. package/dist/{chunk-FWN5B54L.js → chunk-AOQ4N657.js} +182 -10
  7. package/dist/chunk-AOQ4N657.js.map +1 -0
  8. package/dist/cli-config-templates-6AHNUGBN.js +5 -0
  9. package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-6AHNUGBN.js.map} +1 -1
  10. package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-FC3QRKDO.js} +3 -3
  11. package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-FC3QRKDO.js.map} +1 -1
  12. package/dist/index.js +435 -247
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-RDQ2EIBT.js → src-2V6VP4I5.js} +553 -119
  15. package/dist/src-2V6VP4I5.js.map +1 -0
  16. package/package.json +8 -8
  17. package/templates/README.md +48 -145
  18. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +1 -1
  19. package/templates/apps/api/.dockerignore.hbs +14 -7
  20. package/templates/apps/api/.gitignore.hbs +7 -0
  21. package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
  22. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +28 -7
  23. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
  24. package/templates/apps/api/hai.build.json.hbs +1 -1
  25. package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
  26. package/templates/apps/api/package.json.hbs +5 -0
  27. package/templates/apps/fullstack/.dockerignore.hbs +14 -9
  28. package/templates/apps/fullstack/.gitignore.hbs +7 -0
  29. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +23 -5
  30. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
  31. package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
  32. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +1 -1
  33. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +22 -4
  34. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
  35. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +23 -5
  36. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +1 -1
  37. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +11 -1
  38. package/templates/apps/fullstack/hai.build.json.hbs +2 -2
  39. package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
  40. package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
  41. package/templates/apps/fullstack/package.json.hbs +2 -0
  42. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
  43. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +27 -6
  44. package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
  45. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +32 -11
  46. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +2 -2
  47. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +3 -3
  48. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +2 -2
  49. package/templates/apps/h5/src/routes/+layout.svelte.hbs +2 -2
  50. package/templates/apps/mobile-app/src/App.svelte.hbs +2 -2
  51. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
  52. package/templates/apps/website/src/routes/+layout.svelte.hbs +3 -3
  53. package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
  54. package/templates/base/.dockerignore.hbs +14 -8
  55. package/templates/base/.gitignore.hbs +7 -0
  56. package/templates/base/e2e/app.spec.ts.hbs +10 -0
  57. package/templates/base/hai.build.json.hbs +1 -1
  58. package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
  59. package/templates/base/package.json.hbs +8 -3
  60. package/templates/base/vite.config.ts.hbs +3 -1
  61. package/templates/shared/.gitattributes +6 -0
  62. package/templates/skills/AGENTS.md +3 -3
  63. package/templates/skills/bridges/admin/AGENTS.md +1 -1
  64. package/templates/skills/bridges/api/AGENTS.md +1 -1
  65. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +1 -1
  66. package/templates/skills/bridges/h5/AGENTS.md +1 -1
  67. package/templates/skills/bridges/mobile-app/AGENTS.md +1 -1
  68. package/templates/skills/bridges/website/AGENTS.md +1 -1
  69. package/templates/skills/hai-api-client/SKILL.md +14 -161
  70. package/templates/skills/hai-api-client/reference.md +166 -0
  71. package/templates/skills/hai-api-contract/SKILL.md +15 -273
  72. package/templates/skills/hai-api-contract/reference.md +278 -0
  73. package/templates/skills/hai-app-create/SKILL.md +2 -2
  74. package/templates/skills/hai-app-create/development.md +4 -4
  75. package/templates/skills/hai-app-create/reference.md +5 -4
  76. package/templates/skills/hai-app-review/SKILL.md +1 -1
  77. package/templates/skills/hai-app-tests/SKILL.md +4 -3
  78. package/templates/skills/hai-app-tests/reference.md +2 -2
  79. package/templates/skills/hai-app-tests/validation.md +31 -0
  80. package/templates/skills/hai-audit/SKILL.md +17 -142
  81. package/templates/skills/hai-audit/reference.md +145 -0
  82. package/templates/skills/hai-build/SKILL.md +14 -8
  83. package/templates/skills/hai-build/reference.md +10 -9
  84. package/templates/skills/hai-cache/SKILL.md +15 -182
  85. package/templates/skills/hai-cache/reference.md +187 -0
  86. package/templates/skills/hai-capacitor/SKILL.md +4 -3
  87. package/templates/skills/hai-capacitor/reference.md +6 -4
  88. package/templates/skills/hai-ci/SKILL.md +2 -0
  89. package/templates/skills/hai-core/SKILL.md +3 -3
  90. package/templates/skills/hai-crypto/SKILL.md +16 -252
  91. package/templates/skills/hai-crypto/reference.md +256 -0
  92. package/templates/skills/hai-datapipe/SKILL.md +15 -196
  93. package/templates/skills/hai-datapipe/reference.md +202 -0
  94. package/templates/skills/hai-deploy/SKILL.md +20 -55
  95. package/templates/skills/hai-deploy/middleware.md +5 -11
  96. package/templates/skills/hai-framework-sync/SKILL.md +2 -2
  97. package/templates/skills/hai-framework-sync/sync.md +1 -1
  98. package/templates/skills/hai-iam/SKILL.md +1 -3
  99. package/templates/skills/hai-kit/SKILL.md +12 -167
  100. package/templates/skills/hai-kit/reference.md +172 -0
  101. package/templates/skills/hai-payment/SKILL.md +19 -251
  102. package/templates/skills/hai-payment/reference.md +256 -0
  103. package/templates/skills/hai-pr-review/SKILL.md +4 -4
  104. package/templates/skills/hai-reach/SKILL.md +2 -2
  105. package/templates/skills/hai-reldb/SKILL.md +1 -1
  106. package/templates/skills/hai-scheduler/SKILL.md +15 -195
  107. package/templates/skills/hai-scheduler/reference.md +202 -0
  108. package/templates/skills/hai-serv/SKILL.md +2 -2
  109. package/templates/skills/hai-spec-implement/SKILL.md +1 -1
  110. package/templates/skills/hai-spec-implement/workflow.md +3 -3
  111. package/templates/skills/hai-storage/SKILL.md +17 -238
  112. package/templates/skills/hai-storage/reference.md +243 -0
  113. package/templates/skills/hai-ui/SKILL.md +1 -1
  114. package/templates/skills/hai-ui/reference.md +9 -5
  115. package/templates/skills/hai-vecdb/SKILL.md +2 -2
  116. package/dist/chunk-FWN5B54L.js.map +0 -1
  117. package/dist/chunk-QYX6V7H2.js.map +0 -1
  118. package/dist/cli-config-templates-3XSFIXBP.js +0 -5
  119. package/dist/src-RDQ2EIBT.js.map +0 -1
  120. package/templates/apps/api/Dockerfile.hbs +0 -35
  121. package/templates/apps/fullstack/Dockerfile.hbs +0 -57
  122. /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
  123. /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
  124. /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
  125. /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
@@ -2,9 +2,19 @@
2
2
  {{#if fullstack.frontends.desktop}}
3
3
  # {{projectName}}-desktop
4
4
 
5
+ ## 简介
6
+
5
7
  桌面端工程,参考 `apps/desktop-app` 使用 Tauri v2 + Svelte 5 + Vite,前端通过共享 contract 调用 `{{fullstack.servPackageName}}`。
6
8
 
7
- ## i18n 与 shared 协同
9
+ ## 核心功能清单
10
+
11
+ - Tauri v2 桌面壳与共享管理界面。
12
+ - 运行时配置 API 地址,无需按环境重复打包。
13
+ - 原生令牌仅存内存,重启后重新登录。
14
+
15
+ ## 使用入门
16
+
17
+ ### i18n 与 shared 协同
8
18
 
9
19
  - 本端页面文案:`messages/{locale}.json` + `project.inlang/settings.json`
10
20
  - 共享 Shell/主题/语言/API:`packages/{{fullstack.sharedPackageName}}`
@@ -14,7 +24,7 @@
14
24
 
15
25
  自定义建议:桌面端菜单、状态、快捷操作文案放在本目录 `messages/`;跨端通用能力放到 shared。
16
26
 
17
- ## 编译、打包与部署
27
+ ### 编译、打包与部署
18
28
 
19
29
  ```bash
20
30
  # 仅前端开发,默认端口 {{fullstack.ports.desktop}}
@@ -32,13 +42,21 @@ pnpm build --target desktop --platform windows --arch x64
32
42
 
33
43
  打包前需要安装 Rust 工具链和 Tauri 平台依赖。构建不读取 .env.prod/.env.local;启动已安装程序前设置进程 PUBLIC_API_BASE(HTTP(S),不含凭据、查询和片段)。Rust 会在页面加载前注入公开地址,仅将该 Origin 加入 CSP;不需要为 test/prod 重打安装包。缺省连接 http://localhost:3000/api/v1。
34
44
 
35
- ## IAM 认证配置
45
+ ### IAM 认证配置
36
46
 
37
- 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。所有节点共享同一 DB 与 Redis;Memory 仅用于单进程。
47
+ 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。单实例可使用 Memory;多实例需共享数据库与 Redis,并按工程根 README 配置写入路由和传输会话。
38
48
 
39
49
  LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
40
50
 
41
51
  协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
42
52
 
43
53
  完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
54
+
55
+ ## 开发测试
56
+
57
+ 从工程根执行 `pnpm typecheck`、`pnpm lint`、`pnpm build`、`pnpm test` 和 `pnpm test:e2e`。浏览器预览通过后,原生壳仍需在对应系统或设备单独验收。
58
+
59
+ ## 许可证
60
+
61
+ Apache-2.0
44
62
  {{/if}}
@@ -2,14 +2,24 @@
2
2
  {{#if fullstack.hasMiniapp}}
3
3
  # {{projectName}}-miniapp
4
4
 
5
- 小程序端当前为预留占位,不参与 workspace 构建、单元测试或 E2E,但已预置 inlang 标准目录:
5
+ ## 简介
6
6
 
7
- - `project.inlang/settings.json`:小程序端 locale 配置
8
- - `messages/zh-CN.json`、`messages/en-US.json`:小程序端自有文案
7
+ 小程序预留目录,目前没有可运行的小程序应用。
9
8
 
10
- 消息格式插件复用 workspace 已安装的依赖,从根目录 `node_modules` 加载;生成后先在 workspace 根目录安装依赖,无需从 CDN 加载插件。
9
+ ## 核心功能清单
11
10
 
12
- 后续实现时建议复用 `packages/{{fullstack.contractPackageName}}` 中的 Schema 与路径常量,保持接口真相源一致。
11
+ - 已提供 `project.inlang/settings.json` 与中英文 `messages/`。
12
+ - 可复用 `packages/{{fullstack.contractPackageName}}` 的接口类型。
13
13
 
14
- 协同边界:跨端通用文案/组件放 `packages/{{fullstack.sharedPackageName}}`,小程序独有页面文案放本目录 `messages/`。
14
+ ## 使用入门
15
+
16
+ 选定小程序框架后再实现页面、请求适配和认证。小程序文案放本目录 `messages/`,跨端通用内容放 `packages/{{fullstack.sharedPackageName}}`;消息格式插件使用工程根已安装的依赖。
17
+
18
+ ## 开发测试
19
+
20
+ 本目录不参与 workspace 构建、单元测试或 E2E。接入实际框架后增加对应命令,并在目标平台验证登录、请求、导航与发布包。
21
+
22
+ ## 许可证
23
+
24
+ Apache-2.0
15
25
  {{/if}}
@@ -2,9 +2,19 @@
2
2
  {{#if fullstack.frontends.web}}
3
3
  # {{projectName}}-web
4
4
 
5
+ ## 简介
6
+
5
7
  响应式 mini 后台 Web 前端,通过 `packages/{{fullstack.sharedPackageName}}` 调用后端 API,并通过 `@h-ai/ui` 组件搭建页面。
6
8
 
7
- ## i18n 与 shared 协同
9
+ ## 核心功能清单
10
+
11
+ - 浏览器访问共享管理界面与个人设置。
12
+ - 与 API 共用接口类型、登录状态、主题和语言。
13
+ - 可单独构建 Web 镜像并增量更新。
14
+
15
+ ## 使用入门
16
+
17
+ ### i18n 与 shared 协同
8
18
 
9
19
  - 本端页面文案:`messages/{locale}.json` + `project.inlang/settings.json`
10
20
  - 共享 Shell/主题/语言/API:`packages/{{fullstack.sharedPackageName}}`
@@ -12,7 +22,7 @@
12
22
 
13
23
  自定义建议:Web 独有文案放在本目录 `messages/`;跨端通用文案或组件放到 shared。
14
24
 
15
- ## 编译、打包与部署
25
+ ### 编译、打包与部署
16
26
 
17
27
  ```bash
18
28
  # 开发预览,默认端口 {{fullstack.ports.web}}
@@ -22,15 +32,23 @@ pnpm --filter {{projectName}}-web dev
22
32
  pnpm --filter {{projectName}}-web build
23
33
  ```
24
34
 
25
- 从工程根运行 `pnpm build --target docker` 构建并导出镜像。启用 deploy 功能后,运行根命令 `pnpm deploy --env test`,一次发布 API/Web;网络不通时用 `pnpm deploy --env prod --bundle-only`,按输出说明手工搬运 tar。部署不重新构建。
35
+ 从工程根运行 `pnpm build --target docker` 导出镜像。默认带部署工具,可运行 `pnpm run deploy --env test --build` 一次完成构建发布,或 `pnpm run deploy --env prod --bundle-only` 交付已有镜像及跨平台脚本。完整包包含自建中间件,人工部署与按服务更新见工程根 README。
26
36
 
27
- ## IAM 认证配置
37
+ ### IAM 认证配置
28
38
 
29
- 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。所有节点共享同一 DB 与 Redis;Memory 仅用于单进程。
39
+ 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。单实例可使用 Memory;多实例需共享数据库与 Redis,并按工程根 README 配置写入路由和传输会话。
30
40
 
31
41
  LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
32
42
 
33
43
  协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
34
44
 
35
45
  完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
46
+
47
+ ## 开发测试
48
+
49
+ 从工程根执行 `pnpm typecheck`、`pnpm lint`、`pnpm build`、`pnpm test` 和 `pnpm test:e2e`。浏览器预览通过后,原生壳仍需在对应系统或设备单独验收。
50
+
51
+ ## 许可证
52
+
53
+ Apache-2.0
36
54
  {{/if}}
@@ -51,7 +51,7 @@ export default defineConfig(({ mode, command }) => {
51
51
  },
52
52
  envPrefix: ['VITE_', 'PUBLIC_'],
53
53
  ssr: {
54
- noExternal: [/@h-ai\//, /{{fullstack.sharedPackageName}}/],
54
+ noExternal: ['@h-ai/ui', /{{fullstack.sharedPackageName}}/],
55
55
  },
56
56
  test: {
57
57
  environment: 'node',
@@ -61,7 +61,7 @@ test('service rejects plaintext business requests', async ({ request }) => {
61
61
  })
62
62
 
63
63
  {{#if fullstack.e2eFrontend}}
64
- test('hash routing restores direct links, refresh and browser history', async ({ page }) => {
64
+ test('hash routing and responsive workspace preserve usable content width', async ({ page }, testInfo) => {
65
65
  await page.goto('/#/roles?source=direct')
66
66
  await login(page, 'admin', adminPassword)
67
67
  await expect(page.getByTestId('roles-page')).toBeVisible()
@@ -70,6 +70,16 @@ test('hash routing restores direct links, refresh and browser history', async ({
70
70
  await expect(page.getByTestId('roles-page')).toBeVisible()
71
71
  await expect(page.locator('header')).toBeVisible()
72
72
  await expect(page.locator('footer')).toBeVisible()
73
+ for (const width of [1440, 1920, 2560, 3840, 390]) {
74
+ await page.setViewportSize({ width, height: 960 })
75
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
76
+ const main = await page.locator('main').boundingBox()
77
+ const content = await page.getByTestId('roles-page').boundingBox()
78
+ expect(content!.width).toBeGreaterThan(main!.width - 80)
79
+ if (width === 1920)
80
+ await capture(page, '{{projectName}}-wide.png', testInfo.outputPath('wide-1920.png'))
81
+ }
82
+ await page.setViewportSize({ width: 1440, height: 960 })
73
83
  await page.getByTestId('nav-settings').click()
74
84
  await expect(page).toHaveURL(/#\/settings$/)
75
85
  await page.goBack()
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "images": [
3
- { "name": "app", "target": "runtime", "containerPort": 3000 }{{#if fullstack.frontends.web}},
4
- { "name": "web", "target": "web", "containerPort": 8080 }{{/if}}
3
+ { "name": "app", "dockerfile": "images/app.Dockerfile", "containerPort": 3000 }{{#if fullstack.frontends.web}},
4
+ { "name": "web", "dockerfile": "images/web.Dockerfile", "containerPort": 8080 }{{/if}}
5
5
  ]{{#if fullstack.frontends.desktop}},
6
6
  "desktop": ["apps/{{projectName}}-desktop"]{{/if}}
7
7
  }
@@ -0,0 +1,36 @@
1
+ FROM node:24.22.1-bookworm-slim AS build
2
+ ARG SQLITE_PREBUILD_MIRROR=
3
+ ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
4
+
5
+ ENV PNPM_HOME=/pnpm
6
+ ENV PATH=$PNPM_HOME:$PATH
7
+ ENV CI=true
8
+ ENV ONNXRUNTIME_NODE_INSTALL_CUDA=skip
9
+ WORKDIR /workspace
10
+
11
+ RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
12
+ COPY pnpm-lock.yaml pnpm-workspace.yaml ./
13
+ COPY images/dependencies/ ./
14
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
15
+ COPY . .
16
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
17
+ RUN pnpm --filter {{fullstack.servPackageName}}... --if-present build
18
+
19
+ RUN --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds if [ -d node_modules/better-sqlite3 ]; then cd node_modules/better-sqlite3; for attempt in 1 2 3; do node -e "require('node:child_process').execFileSync(process.execPath,[require.resolve('prebuild-install/bin.js'),'--verbose'],{stdio:'inherit'})" && break; [ "$attempt" = 3 ] && exit 1; sleep 3; done; fi
20
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds pnpm pm deploy --filter {{fullstack.servPackageName}} --prod /prod/service
21
+
22
+ FROM node:24.22.1-bookworm-slim AS service
23
+
24
+ ENV NODE_ENV=production
25
+ ENV HOST=0.0.0.0
26
+ ENV PORT=3000
27
+ WORKDIR /app
28
+
29
+ COPY --from=build --chown=node:node /prod/service ./
30
+ RUN mkdir -p /app/data && chown node:node /app/data
31
+
32
+ USER node
33
+ EXPOSE 3000
34
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
35
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
36
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,34 @@
1
+ {{#if fullstack.frontends.web}}
2
+ FROM node:24.22.1-bookworm-slim AS build
3
+ ARG SQLITE_PREBUILD_MIRROR=
4
+ ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
5
+
6
+ ENV PNPM_HOME=/pnpm
7
+ ENV PATH=$PNPM_HOME:$PATH
8
+ ENV CI=true
9
+ WORKDIR /workspace
10
+
11
+ RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
12
+ COPY pnpm-lock.yaml pnpm-workspace.yaml ./
13
+ COPY images/dependencies/ ./
14
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
15
+ COPY . .
16
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
17
+ RUN pnpm --filter {{fullstack.sharedPackageName}} paraglide:compile && pnpm --filter {{projectName}}-web... --if-present build
18
+
19
+ FROM nginxinc/nginx-unprivileged:1.29.8-alpine AS web
20
+
21
+ ENV PUBLIC_API_BASE=
22
+ ENV NGINX_ENVSUBST_FILTER=PUBLIC_API_BASE_BASE64
23
+ ENV NGINX_ENVSUBST_OUTPUT_DIR=/tmp/hai-runtime-config
24
+
25
+ RUN mkdir -p /tmp/hai-runtime-config
26
+ COPY images/web/nginx.conf /etc/nginx/conf.d/default.conf
27
+ COPY --chmod=755 images/web/19-runtime-config.envsh /docker-entrypoint.d/19-runtime-config.envsh
28
+ COPY images/web/runtime-config.js.template /etc/nginx/templates/runtime-config.js.template
29
+ COPY --from=build /workspace/apps/{{projectName}}-web/dist /usr/share/nginx/html
30
+
31
+ EXPOSE 8080
32
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
33
+ CMD wget -Y off -q -O /dev/null http://127.0.0.1:8080/health || exit 1
34
+ {{/if}}
@@ -17,7 +17,9 @@
17
17
  "dev:prod": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev:prod",
18
18
  "compile": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present build",
19
19
  "build": "hai build",
20
+ {{#if features.deploy}}
20
21
  "deploy": "hai deploy packages/{{projectName}}-serv",
22
+ {{/if}}
21
23
  "check": "pnpm typecheck",
22
24
  "typecheck": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present typecheck",
23
25
  "lint": "eslint *.js *.ts e2e && pnpm -r --if-present lint",
@@ -1,8 +1,18 @@
1
1
  # {{fullstack.contractPackageName}}
2
2
 
3
+ ## 简介
4
+
3
5
  共享 API 契约包,包含 Zod Schema、`apiContract.route()` 业务接口定义与工程级 contract。业务 schema、角色、流程及限额是模板初始方案,按用户需求修改,并同步服务端授权、调用方、测试与说明;不要求需求沿用默认业务。
4
6
 
5
- ## 骨架说明
7
+ ## 核心功能清单
8
+
9
+ - 共享接口路径、输入输出 Schema 和类型。
10
+ - 客户端与服务端使用同一份 contract。
11
+ - 不包含服务端业务实现,也不加载私有配置。
12
+
13
+ ## 使用入门
14
+
15
+ ### 骨架说明
6
16
 
7
17
  - `src/app-schemas.ts`:跨接口、跨层复用的输入/数据 Schema 与类型。
8
18
  - `src/app-contract.ts`:app 领域 contract;每个业务 path 直接写在对应接口定义中。
@@ -13,7 +23,18 @@
13
23
  API 前缀、健康检查与跨端 Header 等传输配置位于 serv 包的 `config/_serv.yml`。
14
24
  `SUPPORTED_LOCALES` 为生成时实际编译的语言列表;`app.info` 中名称、版本、环境、默认语言来自后端 core 配置。更新公开契约时同时更新 serv、shared typed API client、各端 UI 与测试。
15
25
 
16
- ## 开发
26
+ ### 开发
17
27
 
18
28
  - `pnpm --filter {{fullstack.contractPackageName}} typecheck`
19
29
  - `pnpm --filter {{fullstack.contractPackageName}} test`
30
+
31
+ ## 开发测试
32
+
33
+ ```sh
34
+ pnpm --filter {{fullstack.contractPackageName}} typecheck
35
+ pnpm --filter {{fullstack.contractPackageName}} test
36
+ ```
37
+
38
+ ## 许可证
39
+
40
+ Apache-2.0
@@ -1,8 +1,18 @@
1
1
  # {{fullstack.servPackageName}}
2
2
 
3
+ ## 简介
4
+
3
5
  后端 API Service,基于 `@h-ai/serv` 将共享 contract 装配为最小 HTTP App 抽象,不在应用代码中直接暴露 Hono。
4
6
 
5
- ## 默认功能与配置
7
+ ## 核心功能清单
8
+
9
+ - HTTP 服务、健康检查和共享 contract 的 procedure 实现。
10
+ - 用户、角色、审计与个人设置的示例业务。
11
+ - 模块配置、加密传输与服务端权限校验。
12
+
13
+ ## 使用入门
14
+
15
+ ### 默认功能与配置
6
16
 
7
17
  以下是可运行示例的初始实现,角色/权限、管理流程、头像限额、分页、审计策略及部署方案均可按用户需求调整。需求与代码或模板规则不一致时,以需求为业务基线同步实现、测试与过期说明;实际框架 API 契约和服务端安全校验仍需满足。
8
18
 
@@ -24,7 +34,7 @@ CLI 已生成根目录 `.env.local`,`HAI_ADMIN_DEFAULT_PASSWORD` 为每个项
24
34
 
25
35
  业务修改提交后的审计记录采用最大努力策略:写入失败输出包含操作及资源的结构化错误日志,业务仍返回原操作结果,避免将已经完成的变更误报为失败;应用启动时 audit 初始化失败则停止启动。
26
36
 
27
- ## 开发
37
+ ### 开发
28
38
 
29
39
  所有配置字段以模块 Schema 为准;未使用字段和替代 Provider 用注释保留。叶子环境变量只覆盖已有 YAML 路径,先取消相应键的注释,或用 `HAI_DB` / `HAI_CACHE` 等整体对象覆盖;`connectTimeout` 对应 `HAI_CACHE_CONNECTTIMEOUT`。凭据示例在根 `.env.example`,实际密钥保存在忽略的 `.env.local` 或部署环境。`.gitignore` 的 `**/data/` 忽略所有运行数据及 `.gitkeep`;运行时会创建目录。`crypto.init()` 无配置参数,datapipe 为纯函数,不提供虚构配置。
30
40
 
@@ -33,20 +43,31 @@ CLI 已生成根目录 `.env.local`,`HAI_ADMIN_DEFAULT_PASSWORD` 为每个项
33
43
  - `pnpm --filter {{fullstack.servPackageName}} dev`
34
44
  - `pnpm --filter {{fullstack.servPackageName}} test`
35
45
 
36
- ## 容器运行
46
+ ### 容器运行
37
47
 
38
- 从工程根运行 `pnpm build --target docker` 构建并导出镜像。启用 deploy 功能后,运行根命令 `pnpm deploy --env test`;含 Web 镜像时一并发布 API/Web,部署不重新构建。网络不通时用 `pnpm deploy --env prod --bundle-only`,按说明手工搬运 tar。
48
+ 从工程根运行 `pnpm build --target docker` 导出镜像。默认带部署工具,可运行 `pnpm run deploy --env test --build` 一次完成构建发布,或 `pnpm run deploy --env prod --bundle-only` 交付已有镜像及跨平台脚本。完整包包含自建中间件,人工部署与按服务更新见工程根 README。
39
49
 
40
50
  - 服务镜像直接由 `@h-ai/serv` 对外监听,配置挂载点为 `/app/config`,数据目录为 `/app/data`,不包含额外转发进程。
41
51
  - Web 镜像不烘焙 API 地址;启动时使用 `-e PUBLIC_API_BASE=https://api.example.com` 注入,同一镜像可部署到不同环境。未设置时 Web 默认请求自身 origin。
42
52
  - YAML 配置使用 `${ENV_NAME:default}` 接收运行时环境变量;不要把密钥写入镜像。
43
53
 
44
- ## IAM 认证配置
54
+ ### IAM 认证配置
45
55
 
46
- 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。所有节点共享同一 DB 与 Redis;Memory 仅用于单进程。
56
+ 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。单实例可使用 Memory;多实例需共享数据库与 Redis,并按工程根 README 配置写入路由和传输会话。
47
57
 
48
58
  LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
49
59
 
50
60
  协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
51
61
 
52
62
  完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
63
+
64
+ ## 开发测试
65
+
66
+ ```sh
67
+ pnpm --filter {{fullstack.servPackageName}} typecheck
68
+ pnpm --filter {{fullstack.servPackageName}} test
69
+ ```
70
+
71
+ ## 许可证
72
+
73
+ Apache-2.0
@@ -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,35 +1,48 @@
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
43
  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.
31
44
 
32
- ## i18n and themes
45
+ ### i18n and themes
33
46
 
34
47
  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.
35
48
 
@@ -37,22 +50,30 @@ All frontend and shared projects load the installed message-format plugin from t
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 transport session affinity 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
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. 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
+ ## License
57
78
 
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.
79
+ Apache-2.0
@@ -34,7 +34,7 @@
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>
@@ -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>
69
+ <main class="w-full min-w-0 flex-1 p-4 pb-8 sm:p-6 lg:p-8" data-guide="admin-content">{@render children()}</main>
70
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>
71
71
  </div>
72
72
  </div>
@@ -14,13 +14,13 @@
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>
@@ -75,11 +75,11 @@ describe('shared API transport', () => {
75
75
  expect(server.requests.at(-1)?.headers.get('x-request-client')).toBe('configured-client')
76
76
  })
77
77
 
78
- it('uses the API origin injected at container startup', async () => {
78
+ it.each([false, true])('uses the injected API origin when native configuration is required: %s', async (requireApiBase) => {
79
79
  Object.assign(globalThis, { __HAI_PUBLIC_CONFIG__: { apiBase: 'https://runtime.example.com' } })
80
80
  const server = createServerFetch()
81
81
  try {
82
- expect((await initializeManagementApi({ fetchFn: server.fetchFn })).success).toBe(true)
82
+ expect((await initializeManagementApi({ fetchFn: server.fetchFn, requireApiBase })).success).toBe(true)
83
83
  expect((await managementApi.app.info()).success).toBe(true)
84
84
  expect(server.requests.every(request => request.url.startsWith('https://runtime.example.com/api/v1/'))).toBe(true)
85
85
  }
@@ -61,14 +61,14 @@
61
61
  </svelte:head>
62
62
 
63
63
  <div class='hai-mobile-viewport'>
64
- <div class='hai-mobile-shell max-w-lg'>
64
+ <div class='hai-mobile-shell min-w-0'>
65
65
  <AppBar title={page.data.appConfig.name} safeArea fixed={false} />
66
66
  <main class='hai-mobile-main'>{@render children()}</main>
67
67
  <footer class='border-t border-base-300 bg-base-100 p-2 text-center text-xs text-base-content/60'>
68
68
  {page.data.appConfig.name} · {page.data.appConfig.version} · {page.data.appConfig.env}
69
69
  </footer>
70
70
  {#if !isAuthPage}
71
- <BottomNav items={navItems} active={activeNav} onchange={handleNavigation} centered maxWidth='lg' safeArea />
71
+ <BottomNav items={navItems} active={activeNav} onchange={handleNavigation} centered maxWidth='none' safeArea />
72
72
  {/if}
73
73
  </div>
74
74
  </div>
@@ -100,7 +100,7 @@
100
100
  </script>
101
101
 
102
102
  <div class='hai-mobile-viewport'>
103
- <div class='hai-mobile-shell max-w-lg'>
103
+ <div class='hai-mobile-shell min-w-0'>
104
104
  <AppBar title={appConfig.name} safeArea fixed={false} class='bg-base-100/95 backdrop-blur border-base-content/8' />
105
105
  <main class='hai-mobile-main'>
106
106
  {#if booting}
@@ -129,7 +129,7 @@
129
129
  {{#if features.iam}}
130
130
  {#if authenticated && !booting && !bootError}
131
131
  {{/if}}
132
- <BottomNav items={navItems} active={router.path.slice(1)} onchange={handleTabChange} safeArea centered maxWidth='lg' />
132
+ <BottomNav items={navItems} active={router.path.slice(1)} onchange={handleTabChange} safeArea centered maxWidth='none' />
133
133
  {{#if features.iam}}
134
134
  {/if}
135
135
  {{/if}}
@@ -12,8 +12,8 @@
12
12
  const { nativeRuntime, nativeTokenStorage }: Props = $props()
13
13
  </script>
14
14
 
15
- <section class='space-y-4 p-4 pb-6'>
16
- <div class='bg-primary text-primary-content rounded-2xl p-5 shadow-sm'>
15
+ <section class='grid gap-4 p-4 pb-6 lg:grid-cols-2'>
16
+ <div class='lg:col-span-2 bg-primary text-primary-content rounded-2xl p-5 shadow-sm'>
17
17
  <p class='text-sm opacity-75'>{m.hero_eyebrow()}</p>
18
18
  <h1 class='mt-1 text-2xl font-bold'>{m.hero_title()}</h1>
19
19
  <p class='mt-2 text-sm opacity-80'>{m.hero_subtitle()}</p>
@@ -49,7 +49,7 @@
49
49
  </svelte:head>
50
50
 
51
51
  <div class='min-h-screen flex flex-col'>
52
- <header class='navbar bg-base-100 shadow-sm'>
52
+ <header class='navbar flex-wrap bg-base-100 shadow-sm'>
53
53
  <div class='flex-1'>
54
54
  {{#if hasI18n}}
55
55
  <a href={resolve('/', {})} class='btn btn-ghost text-xl font-black tracking-tight'>{page.data.appConfig.name}</a>
@@ -57,8 +57,8 @@
57
57
  <a href={resolve('/', {})} class='btn btn-ghost text-xl font-black tracking-tight'>{page.data.appConfig.name}</a>
58
58
  {{/if}}
59
59
  </div>
60
- <nav class='flex-none'>
61
- <ul class='menu menu-horizontal px-1'>
60
+ <nav class='w-full flex-none sm:w-auto'>
61
+ <ul class='menu menu-horizontal flex-wrap px-1'>
62
62
  {#each navItems as item (item.path)}
63
63
  <li><a href={resolve(item.path, {})}>{item.label}</a></li>
64
64
  {/each}