dialog-survey 1.11.4 → 1.13.0

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 (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -3
  3. package/README.zh-CN.md +16 -3
  4. package/VERSION +1 -1
  5. package/dist/src/api/admin/analytics.d.ts +12 -0
  6. package/dist/src/api/admin/analytics.d.ts.map +1 -0
  7. package/dist/src/api/admin/analytics.js +105 -0
  8. package/dist/src/api/admin/analytics.js.map +1 -0
  9. package/dist/src/api/admin/html.d.ts +13 -0
  10. package/dist/src/api/admin/html.d.ts.map +1 -0
  11. package/dist/src/api/admin/html.js +23 -0
  12. package/dist/src/api/admin/html.js.map +1 -0
  13. package/dist/src/api/admin/plans.d.ts +12 -0
  14. package/dist/src/api/admin/plans.d.ts.map +1 -0
  15. package/dist/src/api/admin/plans.js +141 -0
  16. package/dist/src/api/admin/plans.js.map +1 -0
  17. package/dist/src/api/admin/report-view.d.ts +29 -0
  18. package/dist/src/api/admin/report-view.d.ts.map +1 -0
  19. package/dist/src/api/admin/report-view.js +54 -0
  20. package/dist/src/api/admin/report-view.js.map +1 -0
  21. package/dist/src/api/admin/reports.d.ts +12 -0
  22. package/dist/src/api/admin/reports.d.ts.map +1 -0
  23. package/dist/src/api/admin/reports.js +178 -0
  24. package/dist/src/api/admin/reports.js.map +1 -0
  25. package/dist/src/api/admin/shared.d.ts +37 -0
  26. package/dist/src/api/admin/shared.d.ts.map +1 -0
  27. package/dist/src/api/admin/shared.js +12 -0
  28. package/dist/src/api/admin/shared.js.map +1 -0
  29. package/dist/src/api/admin/template-form.d.ts +60 -0
  30. package/dist/src/api/admin/template-form.d.ts.map +1 -0
  31. package/dist/src/api/admin/template-form.js +169 -0
  32. package/dist/src/api/admin/template-form.js.map +1 -0
  33. package/dist/src/api/admin/templates.d.ts +12 -0
  34. package/dist/src/api/admin/templates.d.ts.map +1 -0
  35. package/dist/src/api/admin/templates.js +274 -0
  36. package/dist/src/api/admin/templates.js.map +1 -0
  37. package/dist/src/api/admin/view-models.d.ts +34 -0
  38. package/dist/src/api/admin/view-models.d.ts.map +1 -0
  39. package/dist/src/api/admin/view-models.js +78 -0
  40. package/dist/src/api/admin/view-models.js.map +1 -0
  41. package/dist/src/api/admin-templates.d.ts +22 -18
  42. package/dist/src/api/admin-templates.d.ts.map +1 -1
  43. package/dist/src/api/admin-templates.js +22 -844
  44. package/dist/src/api/admin-templates.js.map +1 -1
  45. package/dist/src/api/health.d.ts +23 -0
  46. package/dist/src/api/health.d.ts.map +1 -1
  47. package/dist/src/api/health.js +137 -67
  48. package/dist/src/api/health.js.map +1 -1
  49. package/dist/src/api/plans.d.ts.map +1 -1
  50. package/dist/src/api/plans.js +422 -125
  51. package/dist/src/api/plans.js.map +1 -1
  52. package/dist/src/api/templates.d.ts.map +1 -1
  53. package/dist/src/api/templates.js +7 -29
  54. package/dist/src/api/templates.js.map +1 -1
  55. package/dist/src/core/graph.d.ts +14 -1
  56. package/dist/src/core/graph.d.ts.map +1 -1
  57. package/dist/src/core/graph.js +77 -40
  58. package/dist/src/core/graph.js.map +1 -1
  59. package/dist/src/core/nodes/interviewing.d.ts.map +1 -1
  60. package/dist/src/core/nodes/interviewing.js +93 -37
  61. package/dist/src/core/nodes/interviewing.js.map +1 -1
  62. package/dist/src/core/nodes/planning.d.ts.map +1 -1
  63. package/dist/src/core/nodes/planning.js +4 -1
  64. package/dist/src/core/nodes/planning.js.map +1 -1
  65. package/dist/src/core/nodes/template-utils.d.ts.map +1 -1
  66. package/dist/src/core/nodes/template-utils.js +8 -2
  67. package/dist/src/core/nodes/template-utils.js.map +1 -1
  68. package/dist/src/core/types/index.d.ts +9 -1
  69. package/dist/src/core/types/index.d.ts.map +1 -1
  70. package/dist/src/core/types/index.js +10 -0
  71. package/dist/src/core/types/index.js.map +1 -1
  72. package/dist/src/generated/prisma/internal/class.js +4 -4
  73. package/dist/src/generated/prisma/internal/class.js.map +1 -1
  74. package/dist/src/generated/prisma/internal/prismaNamespace.d.ts +0 -1
  75. package/dist/src/generated/prisma/internal/prismaNamespace.d.ts.map +1 -1
  76. package/dist/src/generated/prisma/internal/prismaNamespace.js +0 -1
  77. package/dist/src/generated/prisma/internal/prismaNamespace.js.map +1 -1
  78. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts +0 -1
  79. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts.map +1 -1
  80. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js +0 -1
  81. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js.map +1 -1
  82. package/dist/src/generated/prisma/models/InterviewPlan.d.ts +1 -50
  83. package/dist/src/generated/prisma/models/InterviewPlan.d.ts.map +1 -1
  84. package/dist/src/integrations/dingtalk/client.d.ts +14 -1
  85. package/dist/src/integrations/dingtalk/client.d.ts.map +1 -1
  86. package/dist/src/integrations/dingtalk/client.js +29 -8
  87. package/dist/src/integrations/dingtalk/client.js.map +1 -1
  88. package/dist/src/integrations/dingtalk/stream-client.d.ts +7 -11
  89. package/dist/src/integrations/dingtalk/stream-client.d.ts.map +1 -1
  90. package/dist/src/integrations/dingtalk/stream-client.js +7 -4
  91. package/dist/src/integrations/dingtalk/stream-client.js.map +1 -1
  92. package/dist/src/integrations/dingtalk/token-manager.d.ts.map +1 -1
  93. package/dist/src/integrations/dingtalk/token-manager.js +3 -4
  94. package/dist/src/integrations/dingtalk/token-manager.js.map +1 -1
  95. package/dist/src/integrations/llm/openai-compatible.d.ts +4 -0
  96. package/dist/src/integrations/llm/openai-compatible.d.ts.map +1 -1
  97. package/dist/src/integrations/llm/openai-compatible.js +47 -39
  98. package/dist/src/integrations/llm/openai-compatible.js.map +1 -1
  99. package/dist/src/repositories/interview-plan.repository.d.ts +0 -1
  100. package/dist/src/repositories/interview-plan.repository.d.ts.map +1 -1
  101. package/dist/src/repositories/interview-state.repository.d.ts.map +1 -1
  102. package/dist/src/repositories/interview-state.repository.js +16 -12
  103. package/dist/src/repositories/interview-state.repository.js.map +1 -1
  104. package/dist/src/repositories/interview.repository.d.ts.map +1 -1
  105. package/dist/src/repositories/interview.repository.js +2 -3
  106. package/dist/src/repositories/interview.repository.js.map +1 -1
  107. package/dist/src/repositories/status-counts.d.ts +22 -0
  108. package/dist/src/repositories/status-counts.d.ts.map +1 -0
  109. package/dist/src/repositories/status-counts.js +27 -0
  110. package/dist/src/repositories/status-counts.js.map +1 -0
  111. package/dist/src/repositories/template.repository.d.ts +40 -1
  112. package/dist/src/repositories/template.repository.d.ts.map +1 -1
  113. package/dist/src/repositories/template.repository.js +78 -72
  114. package/dist/src/repositories/template.repository.js.map +1 -1
  115. package/dist/src/routes/admin-auth.d.ts.map +1 -1
  116. package/dist/src/routes/admin-auth.js +13 -0
  117. package/dist/src/routes/admin-auth.js.map +1 -1
  118. package/dist/src/server-lifecycle.d.ts.map +1 -1
  119. package/dist/src/server-lifecycle.js +25 -28
  120. package/dist/src/server-lifecycle.js.map +1 -1
  121. package/dist/src/server.d.ts +9 -0
  122. package/dist/src/server.d.ts.map +1 -1
  123. package/dist/src/server.js +74 -4
  124. package/dist/src/server.js.map +1 -1
  125. package/dist/src/services/analysis.service.d.ts +10 -0
  126. package/dist/src/services/analysis.service.d.ts.map +1 -1
  127. package/dist/src/services/analysis.service.js +42 -49
  128. package/dist/src/services/analysis.service.js.map +1 -1
  129. package/dist/src/services/export.service.d.ts +30 -1
  130. package/dist/src/services/export.service.d.ts.map +1 -1
  131. package/dist/src/services/export.service.js +172 -75
  132. package/dist/src/services/export.service.js.map +1 -1
  133. package/dist/src/services/followup.service.d.ts +48 -0
  134. package/dist/src/services/followup.service.d.ts.map +1 -1
  135. package/dist/src/services/followup.service.js +226 -85
  136. package/dist/src/services/followup.service.js.map +1 -1
  137. package/dist/src/services/interview-plan-base.service.d.ts +0 -5
  138. package/dist/src/services/interview-plan-base.service.d.ts.map +1 -1
  139. package/dist/src/services/interview-plan-base.service.js +2 -3
  140. package/dist/src/services/interview-plan-base.service.js.map +1 -1
  141. package/dist/src/services/interview-plan-members.service.d.ts +24 -1
  142. package/dist/src/services/interview-plan-members.service.d.ts.map +1 -1
  143. package/dist/src/services/interview-plan-members.service.js +105 -76
  144. package/dist/src/services/interview-plan-members.service.js.map +1 -1
  145. package/dist/src/services/interview-plan-send.service.d.ts +14 -1
  146. package/dist/src/services/interview-plan-send.service.d.ts.map +1 -1
  147. package/dist/src/services/interview-plan-send.service.js +112 -71
  148. package/dist/src/services/interview-plan-send.service.js.map +1 -1
  149. package/dist/src/services/member-verification.service.d.ts +14 -4
  150. package/dist/src/services/member-verification.service.d.ts.map +1 -1
  151. package/dist/src/services/member-verification.service.js +55 -9
  152. package/dist/src/services/member-verification.service.js.map +1 -1
  153. package/dist/src/services/plan-completion.service.d.ts +38 -0
  154. package/dist/src/services/plan-completion.service.d.ts.map +1 -0
  155. package/dist/src/services/plan-completion.service.js +120 -0
  156. package/dist/src/services/plan-completion.service.js.map +1 -0
  157. package/dist/src/services/prompt.service.d.ts.map +1 -1
  158. package/dist/src/services/prompt.service.js +9 -5
  159. package/dist/src/services/prompt.service.js.map +1 -1
  160. package/dist/src/services/report.service.d.ts +6 -0
  161. package/dist/src/services/report.service.d.ts.map +1 -1
  162. package/dist/src/services/report.service.js +79 -71
  163. package/dist/src/services/report.service.js.map +1 -1
  164. package/dist/src/services/stream-message.service.d.ts +30 -2
  165. package/dist/src/services/stream-message.service.d.ts.map +1 -1
  166. package/dist/src/services/stream-message.service.js +182 -132
  167. package/dist/src/services/stream-message.service.js.map +1 -1
  168. package/dist/src/utils/app-version.d.ts +0 -4
  169. package/dist/src/utils/app-version.d.ts.map +1 -1
  170. package/dist/src/utils/app-version.js +35 -41
  171. package/dist/src/utils/app-version.js.map +1 -1
  172. package/dist/src/utils/metrics.d.ts.map +1 -1
  173. package/dist/src/utils/metrics.js +14 -10
  174. package/dist/src/utils/metrics.js.map +1 -1
  175. package/dist/src/utils/pii-anonymizer.d.ts +6 -0
  176. package/dist/src/utils/pii-anonymizer.d.ts.map +1 -1
  177. package/dist/src/utils/pii-anonymizer.js +11 -0
  178. package/dist/src/utils/pii-anonymizer.js.map +1 -1
  179. package/dist/src/utils/security.d.ts.map +1 -1
  180. package/dist/src/utils/security.js +28 -7
  181. package/dist/src/utils/security.js.map +1 -1
  182. package/dist/src/utils/startup-config-check.d.ts +21 -0
  183. package/dist/src/utils/startup-config-check.d.ts.map +1 -0
  184. package/dist/src/utils/startup-config-check.js +96 -0
  185. package/dist/src/utils/startup-config-check.js.map +1 -0
  186. package/dist/src/utils/template-content.d.ts +15 -0
  187. package/dist/src/utils/template-content.d.ts.map +1 -0
  188. package/dist/src/utils/template-content.js +30 -0
  189. package/dist/src/utils/template-content.js.map +1 -0
  190. package/package.json +9 -7
  191. package/prisma/migrations/20261023000000_drop_interview_plan_completed_count/migration.sql +20 -0
  192. package/prisma/schema.prisma +0 -1
  193. package/scripts/ccn-report.cjs +685 -0
  194. package/scripts/deploy.sh +26 -0
  195. package/scripts/migrate-deploy.mjs +224 -0
  196. package/scripts/mutation-test.mjs +173 -42
  197. package/scripts/prune-runtime-deps.mjs +308 -0
  198. package/scripts/push-dt.ps1.example +81 -0
  199. package/scripts/run-xp-gate-native.ps1 +64 -0
  200. package/scripts/run-xp-gate-native.sh +136 -0
  201. package/scripts/scan-git-history-secrets.mjs +356 -0
  202. package/scripts/verify-font-asset.mjs +108 -0
  203. package/scripts/verify-xp-gate-patches.cjs +225 -0
  204. package/src/views/admin/content/plan-detail.njk +11 -9
  205. package/src/views/layouts/admin-tree.njk +1 -1
  206. package/src/views/layouts/admin.njk +2 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 boyingliu01
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -99,12 +99,27 @@ This generates `.env`, applies database migrations with `prisma migrate deploy`
99
99
  #### Option B: Docker Compose (recommended for evaluation)
100
100
 
101
101
  ```bash
102
+ git lfs install && git lfs pull # the CJK font is a Git LFS object (see below)
102
103
  cp .env.example .env
103
- # Fill in your DingTalk + LLM config
104
+ # Fill in your DingTalk + LLM config, and set POSTGRES_PASSWORD
104
105
  docker compose up -d
105
- curl http://localhost:3001/health
106
+ curl -i http://localhost:3001/health/ready # 200 only when fully configured
106
107
  ```
107
108
 
109
+ Three notes that save an evening:
110
+
111
+ - **`ADMIN_PASSWORD_HASH` needs `$$` in `.env`, not `$`.** `docker compose`
112
+ interpolates `env_file` values, so `$2b$12$...` is truncated to `$2b$12` and the
113
+ admin login answers 401 for a correct password. Write
114
+ `ADMIN_PASSWORD_HASH=$$2b$$12$$<salt><digest>`. A direct install (`npx
115
+ dialog-survey install`, PM2, `docker run -e`) takes the single-`$` form.
116
+ - **`git lfs pull` is not optional.** Without it the repository checkout contains
117
+ a 133-byte pointer instead of the font, and every exported PDF renders Chinese
118
+ as blank boxes. The Docker build now refuses to proceed in that state.
119
+ - **`/health` answers 200 for a degraded deployment** (alive, but missing
120
+ credentials); `/health/ready` is the one to poll. See
121
+ [DEPLOY.md](./DEPLOY.md#health-checks).
122
+
108
123
  #### Option C: Manual setup (development)
109
124
 
110
125
  ```bash
@@ -247,7 +262,9 @@ docker compose up -d
247
262
  docker compose logs -f app
248
263
  ```
249
264
 
250
- Health check: `curl http://localhost:3001/health`
265
+ Health check: `curl http://localhost:3001/health` (liveness) and
266
+ `curl -i http://localhost:3001/health/ready` (readiness — a container healthcheck
267
+ should use this one).
251
268
 
252
269
  > **Security note**: `pm2 save` persists the full process environment to
253
270
  > `~/.pm2/dump.pm2` (mode follows umask — usually 0644, world-readable). The
package/README.zh-CN.md CHANGED
@@ -99,12 +99,24 @@ CLI 会自动生成 `.env`、通过 `prisma migrate deploy` 应用数据库迁
99
99
  #### 方式 B:Docker Compose(评估推荐)
100
100
 
101
101
  ```bash
102
+ git lfs install && git lfs pull # 中文字体是 Git LFS 对象(见下)
102
103
  cp .env.example .env
103
- # 填入钉钉 + LLM 配置
104
+ # 填入钉钉 + LLM 配置,并设置 POSTGRES_PASSWORD
104
105
  docker compose up -d
105
- curl http://localhost:3001/health
106
+ curl -i http://localhost:3001/health/ready # 配置完整才返回 200
106
107
  ```
107
108
 
109
+ 三个能省下一个晚上的提示:
110
+
111
+ - **`.env` 里的 `ADMIN_PASSWORD_HASH` 要用 `$$` 而不是 `$`。** compose 会对 `env_file`
112
+ 的值做变量插值,`$2b$12$...` 被截断成 `$2b$12`,管理员登录对**正确**的口令也返回 401。
113
+ 应写成 `ADMIN_PASSWORD_HASH=$$2b$$12$$<salt><digest>`。直接安装(`npx dialog-survey
114
+ install`、PM2、`docker run -e`)不走插值,用的是单个 `$` 形式。
115
+ - **`git lfs pull` 不是可选项。** 不执行时检出的只是 133 字节的指针文件,导出的 PDF
116
+ 中文全是空白方框。现在 Docker 构建遇到这种状态会直接失败。
117
+ - **`/health` 对 degraded 部署仍返回 200**(进程活着,但凭据缺失),要探活请看
118
+ `/health/ready`。详见 [DEPLOY.md](./DEPLOY.md#health-checks)。
119
+
108
120
  #### 方式 C:手动安装(开发环境)
109
121
 
110
122
  ```bash
@@ -173,7 +185,8 @@ docker compose up -d
173
185
  docker compose logs -f app
174
186
  ```
175
187
 
176
- 健康检查:`curl http://localhost:3001/health`
188
+ 健康检查:`curl http://localhost:3001/health`(存活)与
189
+ `curl -i http://localhost:3001/health/ready`(就绪,容器健康检查应当用这个)。
177
190
 
178
191
  > **安全提示**: `pm2 save` 会把进程的完整环境变量持久化到 `~/.pm2/dump.pm2`
179
192
  > (权限跟随 umask,通常为 0644,全局可读)。安装器与 `npx dialog-survey start`
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.11.4
1
+ 1.13.0
@@ -0,0 +1,12 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AdminRoutesDeps } from './shared.js';
3
+ /**
4
+ * The admin shell entry point, the plan-progress dashboard fragment and the
5
+ * analytics surfaces (page + JSON data endpoint).
6
+ *
7
+ * These stand apart from the resource CRUD modules: none of them mutates
8
+ * anything, and the analytics page is the only place that composes four
9
+ * `analyticsService` reads plus the template/plan selects in one response.
10
+ */
11
+ export declare function registerAnalyticsRoutes(fastify: FastifyInstance, opts: AdminRoutesDeps): Promise<void>;
12
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../../src/api/admin/analytics.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAI7E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CA2Gf"}
@@ -0,0 +1,105 @@
1
+ import { adminAuth } from '../../middleware/admin-auth.js';
2
+ import { error } from '../../utils/logger.js';
3
+ import { htmlEscape } from './html.js';
4
+ const BASE_PATH = '/admin';
5
+ const API_PATH = '/admin/api';
6
+ /**
7
+ * The admin shell entry point, the plan-progress dashboard fragment and the
8
+ * analytics surfaces (page + JSON data endpoint).
9
+ *
10
+ * These stand apart from the resource CRUD modules: none of them mutates
11
+ * anything, and the analytics page is the only place that composes four
12
+ * `analyticsService` reads plus the template/plan selects in one response.
13
+ */
14
+ export async function registerAnalyticsRoutes(fastify, opts) {
15
+ const { templateRepo, analyticsService, interviewPlanService } = opts;
16
+ // GET /admin — Tree view main entry (new hierarchical UI)
17
+ fastify.get(BASE_PATH, { preHandler: adminAuth }, async (_request, reply) => {
18
+ try {
19
+ const templates = await templateRepo.findAllForAdminTree();
20
+ const templatesWithPlans = templates.map((t) => ({
21
+ ...t,
22
+ _plans: t.interviewPlans.map((p) => ({
23
+ ...p,
24
+ _interviews: p.interviews,
25
+ _interviewCount: p._count.interviews,
26
+ _completedCount: p.interviews.filter((i) => i.status === 'COMPLETED').length,
27
+ })),
28
+ _planCount: t._count.interviewPlans,
29
+ _interviewCount: t._count.interviews,
30
+ interviewPlans: undefined,
31
+ }));
32
+ return reply.view('layouts/admin-tree.njk', {
33
+ templates: templatesWithPlans,
34
+ adminUser: _request.user ? _request.user['userId'] : undefined,
35
+ csrfToken: reply.generateCsrf(),
36
+ });
37
+ }
38
+ catch (e) {
39
+ const errMsg = e instanceof Error ? e.message : 'Failed to load admin tree';
40
+ error('Failed to load admin tree', { error: errMsg });
41
+ return reply.status(500).view('error.njk', { message: errMsg });
42
+ }
43
+ });
44
+ // GET /admin/content/dashboard — Plan Progress Dashboard (HTMX fragment)
45
+ fastify.get(`${BASE_PATH}/content/dashboard`, { preHandler: adminAuth }, async (_request, reply) => {
46
+ try {
47
+ const plans = await analyticsService.getDashboardPlanProgress();
48
+ return reply.view('admin/content/dashboard.njk', {
49
+ plans,
50
+ });
51
+ }
52
+ catch (e) {
53
+ const errMsg = e instanceof Error ? e.message : 'Failed to load dashboard';
54
+ error('Failed to load dashboard', { error: errMsg });
55
+ return reply.status(500).send(`<div class="text-red-600">${htmlEscape(errMsg)}</div>`);
56
+ }
57
+ });
58
+ // GET /admin/analytics - Analytics dashboard
59
+ fastify.get(`${BASE_PATH}/analytics`, { preHandler: adminAuth }, async (_r, reply) => {
60
+ try {
61
+ const [kpis, statusDistribution, planCompletionRates, weeklyTrend, templates, plans] = await Promise.all([
62
+ analyticsService.getKPIs(),
63
+ analyticsService.getStatusDistribution(),
64
+ analyticsService.getPlanCompletionRates(),
65
+ analyticsService.getWeeklyTrend(),
66
+ templateRepo.findAllForSelect(),
67
+ interviewPlanService.findAllForSelect(),
68
+ ]);
69
+ return reply.view('analytics/index.njk', {
70
+ kpis,
71
+ statusDistribution,
72
+ planCompletionRates,
73
+ weeklyTrend,
74
+ templates,
75
+ plans,
76
+ });
77
+ }
78
+ catch (e) {
79
+ const errMsg = e instanceof Error ? e.message : 'Failed to load analytics';
80
+ error('Failed to load analytics', { error: errMsg });
81
+ return reply.status(500).view('error.njk', { message: errMsg });
82
+ }
83
+ });
84
+ // GET /admin/api/analytics/data - Analytics data API
85
+ fastify.get(`${API_PATH}/analytics/data`, { preHandler: adminAuth }, async (_r, reply) => {
86
+ try {
87
+ const [kpis, statusDistribution, planCompletionRates, weeklyTrend] = await Promise.all([
88
+ analyticsService.getKPIs(),
89
+ analyticsService.getStatusDistribution(),
90
+ analyticsService.getPlanCompletionRates(),
91
+ analyticsService.getWeeklyTrend(),
92
+ ]);
93
+ return reply.send({
94
+ success: true,
95
+ data: { kpis, statusDistribution, planCompletionRates, weeklyTrend },
96
+ });
97
+ }
98
+ catch (e) {
99
+ const errMsg = e instanceof Error ? e.message : 'Failed to fetch analytics data';
100
+ error('Failed to fetch analytics data', { error: errMsg });
101
+ return reply.status(500).send({ success: false, error: errMsg });
102
+ }
103
+ });
104
+ }
105
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../../../src/api/admin/analytics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * HTML fragment helpers shared by every admin route module.
3
+ *
4
+ * Admin routes return HTMX fragments, so error reporting is markup, not JSON:
5
+ * these two functions are the single escaping boundary for user-visible error
6
+ * text. They are imported by nearly every admin module, which is why they live
7
+ * in their own leaf file instead of a catch-all "helpers" module.
8
+ */
9
+ /** Escape the five characters that would otherwise close out of an HTML text node. */
10
+ export declare function htmlEscape(text: string): string;
11
+ /** The red error banner an admin fragment shows in place of its normal content. */
12
+ export declare function htmlError(message: string): string;
13
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../../src/api/admin/html.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD"}
@@ -0,0 +1,23 @@
1
+ // @no-test-required: covered by tests/admin-error-branches.test.ts
2
+ /**
3
+ * HTML fragment helpers shared by every admin route module.
4
+ *
5
+ * Admin routes return HTMX fragments, so error reporting is markup, not JSON:
6
+ * these two functions are the single escaping boundary for user-visible error
7
+ * text. They are imported by nearly every admin module, which is why they live
8
+ * in their own leaf file instead of a catch-all "helpers" module.
9
+ */
10
+ /** Escape the five characters that would otherwise close out of an HTML text node. */
11
+ export function htmlEscape(text) {
12
+ return text
13
+ .replace(/&/g, '&amp;')
14
+ .replace(/</g, '&lt;')
15
+ .replace(/>/g, '&gt;')
16
+ .replace(/"/g, '&quot;')
17
+ .replace(/'/g, '&#x27;');
18
+ }
19
+ /** The red error banner an admin fragment shows in place of its normal content. */
20
+ export function htmlError(message) {
21
+ return `<div class="text-red-600">${htmlEscape(message)}</div>`;
22
+ }
23
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../../../../src/api/admin/html.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE;;;;;;;GAOG;AAEH,sFAAsF;AACtF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,OAAO,6BAA6B,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC;AAClE,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { type AdminRoutesDeps } from './shared.js';
3
+ /**
4
+ * Plan management pages under `/admin/content/plans*`.
5
+ *
6
+ * These render the plan form and the two plan-detail variants (with and without
7
+ * the analytics summary), plus the delete endpoint the tree view calls. The
8
+ * template select is loaded here too because a plan form cannot render without
9
+ * one — that dependency is what ties these routes together.
10
+ */
11
+ export declare function registerPlanPageRoutes(fastify: FastifyInstance, opts: AdminRoutesDeps): Promise<void>;
12
+ //# sourceMappingURL=plans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../../../src/api/admin/plans.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAI7E,OAAO,EAAE,KAAK,eAAe,EAAoB,MAAM,aAAa,CAAC;AAKrE;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CA4Jf"}
@@ -0,0 +1,141 @@
1
+ import { adminAuth } from '../../middleware/admin-auth.js';
2
+ import { error, info } from '../../utils/logger.js';
3
+ import { htmlError } from './html.js';
4
+ import { createAdminGuard } from './shared.js';
5
+ import { buildInviteeNameMap, parseTemplateContent } from './view-models.js';
6
+ const API_PATH = '/admin/api';
7
+ /**
8
+ * Plan management pages under `/admin/content/plans*`.
9
+ *
10
+ * These render the plan form and the two plan-detail variants (with and without
11
+ * the analytics summary), plus the delete endpoint the tree view calls. The
12
+ * template select is loaded here too because a plan form cannot render without
13
+ * one — that dependency is what ties these routes together.
14
+ */
15
+ export async function registerPlanPageRoutes(fastify, opts) {
16
+ const { templateRepo, interviewPlanService, interviewRepo, analyticsService } = opts;
17
+ const adminMutationGuard = createAdminGuard(fastify);
18
+ // GET /admin/content/plans/new — HTMX fragment for new plan form
19
+ fastify.get('/admin/content/plans/new', { preHandler: adminAuth }, async (request, reply) => {
20
+ const { templateId } = request.query || {};
21
+ const templates = await templateRepo.findAllForSelect();
22
+ return reply.view('admin/content/plan-form.njk', {
23
+ templates,
24
+ selectedTemplateId: templateId || null,
25
+ });
26
+ });
27
+ // GET /admin/content/plans/:id/edit — HTMX fragment for edit plan form
28
+ fastify.get('/admin/content/plans/:id/edit', { preHandler: adminAuth }, async (request, reply) => {
29
+ const { id } = request.params;
30
+ const plan = await interviewPlanService.findByIdWithTemplate(id);
31
+ if (!plan)
32
+ return reply.status(404).type('text/html').send('计划不存在');
33
+ const templates = await templateRepo.findAllForSelect();
34
+ return reply.view('admin/content/plan-form.njk', {
35
+ templates,
36
+ plan,
37
+ selectedTemplateId: plan.templateId,
38
+ isEdit: true,
39
+ });
40
+ });
41
+ // GET /admin/content/templates/:id — the template's detail (info) page.
42
+ // It lives with the plan pages because the tree opens it from a plan row and
43
+ // it renders the same counts the plan detail shows.
44
+ fastify.get('/admin/content/templates/:id', { preHandler: adminAuth }, async (request, reply) => {
45
+ const { id } = request.params;
46
+ const template = await templateRepo.findByIdWithCounts(id);
47
+ if (!template)
48
+ return reply.status(404).type('text/html').send('模板不存在');
49
+ const { content: parsedContent, questions } = parseTemplateContent(template.content);
50
+ const tpl = {
51
+ ...template,
52
+ _planCount: template._count.interviewPlans,
53
+ _interviewCount: template._count.interviews,
54
+ };
55
+ return reply.view('admin/content/template-info.njk', {
56
+ template: tpl,
57
+ questions,
58
+ invitationPrompt: parsedContent['invitationPrompt'],
59
+ closingMessage: parsedContent['closingMessage'],
60
+ });
61
+ });
62
+ // GET /admin/content/plans/:id
63
+ fastify.get('/admin/content/plans/:id', { preHandler: adminAuth }, async (request, reply) => {
64
+ const { id } = request.params;
65
+ const plan = await interviewPlanService.findByIdWithInterviewsAndCounts(id);
66
+ if (!plan)
67
+ return reply.status(404).type('text/html').send('计划不存在');
68
+ const completed = plan.interviews.filter((i) => i.status === 'COMPLETED');
69
+ let planStats = null;
70
+ try {
71
+ planStats = await analyticsService.getPlanStats(id);
72
+ }
73
+ catch (e) {
74
+ const errMsg = e instanceof Error ? e.message : 'Failed to compute plan stats';
75
+ error('Failed to load plan stats for plan-detail render', { planId: id, error: errMsg });
76
+ }
77
+ const inviteeMap = buildInviteeNameMap(plan.inviteeData);
78
+ return reply.view('admin/content/plan-detail.njk', {
79
+ plan,
80
+ template: plan.template,
81
+ interviews: plan.interviews,
82
+ totalInterviews: plan._count.interviews,
83
+ completedCount: completed.length,
84
+ completionRate: plan.sentCount > 0 ? Math.round((completed.length / plan.sentCount) * 100) : 0,
85
+ planStats,
86
+ inviteeMap,
87
+ });
88
+ });
89
+ // GET /admin/content/plans/:id/all-interviews
90
+ fastify.get('/admin/content/plans/:id/all-interviews', { preHandler: adminAuth }, async (request, reply) => {
91
+ const { id } = request.params;
92
+ const interviews = await interviewRepo.findByPlanId(id);
93
+ const plan = await interviewPlanService.findByIdWithTemplate(id);
94
+ if (!plan)
95
+ return reply.status(404).type('text/html').send('计划不存在');
96
+ const completed = interviews.filter((i) => i.status === 'COMPLETED');
97
+ return reply.view('admin/content/plan-detail.njk', {
98
+ plan: { ...plan, sentCount: 0 },
99
+ template: plan.template,
100
+ interviews,
101
+ totalInterviews: interviews.length,
102
+ completedCount: completed.length,
103
+ completionRate: 0,
104
+ });
105
+ });
106
+ // DELETE /admin/api/plans/:id — with `?force=true` the interviews go too
107
+ fastify.delete(`${API_PATH}/plans/:id`, { preHandler: adminMutationGuard }, async (request, reply) => {
108
+ const { id } = request.params;
109
+ const query = request.query;
110
+ try {
111
+ const plan = await interviewPlanService.findByIdWithDeleteChecks(id);
112
+ if (!plan)
113
+ return reply.status(404).type('text/html').send('计划不存在');
114
+ const hasInterviews = plan._count.interviews > 0;
115
+ const hasBatchReports = plan._count.batchReports > 0;
116
+ if (query.force === 'true') {
117
+ const result = await interviewPlanService.deletePlanWithInterviews(id);
118
+ return reply.send(`<div class="text-green-600">计划「${result.planName}」已删除,同时删除 ${result.interviewCount} 条访谈记录和 ${result.batchReportCount} 个批量报告</div>`);
119
+ }
120
+ if (hasInterviews || hasBatchReports) {
121
+ const parts = [];
122
+ if (hasInterviews)
123
+ parts.push(`${plan._count.interviews} 个访谈记录`);
124
+ if (hasBatchReports)
125
+ parts.push(`${plan._count.batchReports} 个批量报告`);
126
+ return reply
127
+ .status(409)
128
+ .send(`<div class="text-red-600" data-delete-warning="true" data-count-interviews="${plan._count.interviews}" data-count-reports="${plan._count.batchReports}">该计划关联了 ${parts.join('、')},删除将一并删除这些数据</div>`);
129
+ }
130
+ await interviewPlanService.deletePlan(id);
131
+ info('Admin plan deleted', { planId: id });
132
+ return reply.send('<div class="text-green-600">计划已删除</div>');
133
+ }
134
+ catch (e) {
135
+ const errMsg = e instanceof Error ? e.message : 'Failed to delete plan';
136
+ error('Failed to delete admin plan', { error: errMsg, planId: id });
137
+ return reply.status(500).send(htmlError(errMsg));
138
+ }
139
+ });
140
+ }
141
+ //# sourceMappingURL=plans.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plans.js","sourceRoot":"","sources":["../../../../src/api/admin/plans.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * View-model builders for the interview report pages.
3
+ *
4
+ * The report pages render three shapes: the analysis section, the full
5
+ * transcript and the Markdown download. Each builder returns the exact lines or
6
+ * fields the template/destination expects, so the Markdown export and the HTML
7
+ * page cannot drift apart.
8
+ */
9
+ /** Header block of the Markdown report: who was interviewed, and the status. */
10
+ export declare function reportHeaderLines(interview: {
11
+ userId: string;
12
+ status: string;
13
+ }, inviteeInfo: {
14
+ name?: string;
15
+ phone?: string;
16
+ }): string[];
17
+ /** The transcript, one labelled block per message. */
18
+ export declare function transcriptLines(messages: Array<{
19
+ role: string;
20
+ content: string;
21
+ }>): string[];
22
+ /** The analysis section, omitted entirely when no report exists yet. */
23
+ export declare function analysisSectionLines(report: {
24
+ content?: string | null;
25
+ sentiment?: string | null;
26
+ keyFindings?: string[] | null;
27
+ recommendations?: string[] | null;
28
+ }): string[];
29
+ //# sourceMappingURL=report-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-view.d.ts","sourceRoot":"","sources":["../../../../src/api/admin/report-view.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAQH,gFAAgF;AAChF,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC7C,WAAW,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,MAAM,EAAE,CAMV;AAED,sDAAsD;AACtD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,EAAE,CAO5F;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACnC,GAAG,MAAM,EAAE,CAgBX"}
@@ -0,0 +1,54 @@
1
+ // @no-test-required: covered by tests/admin-templates-extra.test.ts and tests/admin-route-coverage.test.ts
2
+ /**
3
+ * View-model builders for the interview report pages.
4
+ *
5
+ * The report pages render three shapes: the analysis section, the full
6
+ * transcript and the Markdown download. Each builder returns the exact lines or
7
+ * fields the template/destination expects, so the Markdown export and the HTML
8
+ * page cannot drift apart.
9
+ */
10
+ const SENTIMENT_LABELS = {
11
+ positive: '积极',
12
+ negative: '消极',
13
+ neutral: '中性',
14
+ };
15
+ /** Header block of the Markdown report: who was interviewed, and the status. */
16
+ export function reportHeaderLines(interview, inviteeInfo) {
17
+ const lines = [`- **用户**: ${interview.userId}`];
18
+ if (inviteeInfo.name)
19
+ lines.push(`- **姓名**: ${inviteeInfo.name}`);
20
+ if (inviteeInfo.phone)
21
+ lines.push(`- **电话**: ${inviteeInfo.phone}`);
22
+ lines.push(`- **状态**: ${interview.status}`, '', '---', '', '## 访谈对话记录', '');
23
+ return lines;
24
+ }
25
+ /** The transcript, one labelled block per message. */
26
+ export function transcriptLines(messages) {
27
+ const lines = [];
28
+ for (const m of messages) {
29
+ const roleLabel = m.role === 'assistant' ? '🤖 机器人' : '👤 受访者';
30
+ lines.push(`**${roleLabel}**:`, '', m.content, '');
31
+ }
32
+ return lines;
33
+ }
34
+ /** The analysis section, omitted entirely when no report exists yet. */
35
+ export function analysisSectionLines(report) {
36
+ const lines = ['---', '', '## 访谈分析总结', '', report.content ?? '', ''];
37
+ if (report.sentiment) {
38
+ lines.push(`**情感分析**: ${SENTIMENT_LABELS[report.sentiment] ?? report.sentiment}`, '');
39
+ }
40
+ if (report.keyFindings?.length) {
41
+ lines.push('**关键发现**:', '');
42
+ for (const f of report.keyFindings)
43
+ lines.push(`- ${f}`);
44
+ lines.push('');
45
+ }
46
+ if (report.recommendations?.length) {
47
+ lines.push('**建议**:', '');
48
+ for (const r of report.recommendations)
49
+ lines.push(`- ${r}`);
50
+ lines.push('');
51
+ }
52
+ return lines;
53
+ }
54
+ //# sourceMappingURL=report-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-view.js","sourceRoot":"","sources":["../../../../src/api/admin/report-view.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G;;;;;;;GAOG;AAEH,MAAM,gBAAgB,GAA2B;IAC/C,QAAQ,EAAE,IAAI;IACd,QAAQ,EAAE,IAAI;IACd,OAAO,EAAE,IAAI;CACd,CAAC;AAEF,gFAAgF;AAChF,MAAM,UAAU,iBAAiB,CAC/B,SAA6C,EAC7C,WAA8C;IAE9C,MAAM,KAAK,GAAG,CAAC,aAAa,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IAChD,IAAI,WAAW,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;IAClE,IAAI,WAAW,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC;IACpE,KAAK,CAAC,IAAI,CAAC,aAAa,SAAS,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;IAC5E,OAAO,KAAK,CAAC;AACf,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,eAAe,CAAC,QAAkD;IAChF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC/D,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,oBAAoB,CAAC,MAKpC;IACC,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACrE,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,KAAK,CAAC,IAAI,CAAC,aAAa,gBAAgB,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,IAAI,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,WAAW;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACzD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IACD,IAAI,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAC1B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,eAAe;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC7D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { type AdminRoutesDeps } from './shared.js';
3
+ /**
4
+ * Interview/report viewing: the reports list and detail shell pages, the
5
+ * report-detail fragment, the analysis-view page and the download/export
6
+ * endpoints (`Markdown`, `PDF`, `Excel`) plus re-analysis.
7
+ *
8
+ * Everything here reads an interview through `interviewRepo`'s report readers
9
+ * and renders or streams it; nothing writes template or plan state.
10
+ */
11
+ export declare function registerReportRoutes(fastify: FastifyInstance, opts: AdminRoutesDeps): Promise<void>;
12
+ //# sourceMappingURL=reports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reports.d.ts","sourceRoot":"","sources":["../../../../src/api/admin/reports.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAK7E,OAAO,EAAE,KAAK,eAAe,EAAoB,MAAM,aAAa,CAAC;AAMrE;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAwMf"}