co-maintainer 0.4.0-beta.1

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 (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/main.d.ts +2 -0
  4. package/dist/main.js +9 -0
  5. package/dist/mod.d.ts +4 -0
  6. package/dist/mod.js +2 -0
  7. package/dist/package.json +54 -0
  8. package/dist/src/ai/batch.d.ts +16 -0
  9. package/dist/src/ai/batch.js +117 -0
  10. package/dist/src/ai/fake.d.ts +7 -0
  11. package/dist/src/ai/fake.js +40 -0
  12. package/dist/src/ai/hetzner.d.ts +16 -0
  13. package/dist/src/ai/hetzner.js +69 -0
  14. package/dist/src/ai/mermaid.d.ts +27 -0
  15. package/dist/src/ai/mermaid.js +457 -0
  16. package/dist/src/ai/mermaid_loop.d.ts +12 -0
  17. package/dist/src/ai/mermaid_loop.js +100 -0
  18. package/dist/src/ai/openrouter.d.ts +9 -0
  19. package/dist/src/ai/openrouter.js +29 -0
  20. package/dist/src/ai/provider.d.ts +4 -0
  21. package/dist/src/ai/provider.js +63 -0
  22. package/dist/src/cli/args.d.ts +5 -0
  23. package/dist/src/cli/args.js +252 -0
  24. package/dist/src/cli/commands/probe.d.ts +2 -0
  25. package/dist/src/cli/commands/probe.js +122 -0
  26. package/dist/src/cli/commands/review.d.ts +3 -0
  27. package/dist/src/cli/commands/review.js +84 -0
  28. package/dist/src/cli/commands/serve.d.ts +18 -0
  29. package/dist/src/cli/commands/serve.js +183 -0
  30. package/dist/src/cli/commands/set.d.ts +7 -0
  31. package/dist/src/cli/commands/set.js +171 -0
  32. package/dist/src/cli/main.d.ts +1 -0
  33. package/dist/src/cli/main.js +27 -0
  34. package/dist/src/cli/prompt.d.ts +4 -0
  35. package/dist/src/cli/prompt.js +26 -0
  36. package/dist/src/cli/review_args.d.ts +28 -0
  37. package/dist/src/cli/review_args.js +84 -0
  38. package/dist/src/cli/review_output.d.ts +2 -0
  39. package/dist/src/cli/review_output.js +10 -0
  40. package/dist/src/cli/review_result.d.ts +81 -0
  41. package/dist/src/cli/review_result.js +281 -0
  42. package/dist/src/config.d.ts +108 -0
  43. package/dist/src/config.js +138 -0
  44. package/dist/src/git/default_branch.d.ts +5 -0
  45. package/dist/src/git/default_branch.js +22 -0
  46. package/dist/src/github/app.d.ts +43 -0
  47. package/dist/src/github/app.js +120 -0
  48. package/dist/src/github/client.d.ts +17 -0
  49. package/dist/src/github/client.js +66 -0
  50. package/dist/src/github/collect.d.ts +7 -0
  51. package/dist/src/github/collect.js +368 -0
  52. package/dist/src/github/gh.d.ts +8 -0
  53. package/dist/src/github/gh.js +63 -0
  54. package/dist/src/github/jwt.d.ts +12 -0
  55. package/dist/src/github/jwt.js +84 -0
  56. package/dist/src/github/oauth.d.ts +9 -0
  57. package/dist/src/github/oauth.js +51 -0
  58. package/dist/src/github/pat.d.ts +10 -0
  59. package/dist/src/github/pat.js +60 -0
  60. package/dist/src/knowledge/facts.d.ts +3 -0
  61. package/dist/src/knowledge/facts.js +325 -0
  62. package/dist/src/knowledge/guide.d.ts +6 -0
  63. package/dist/src/knowledge/guide.js +82 -0
  64. package/dist/src/knowledge/probe.d.ts +12 -0
  65. package/dist/src/knowledge/probe.js +144 -0
  66. package/dist/src/knowledge/sections.d.ts +2 -0
  67. package/dist/src/knowledge/sections.js +13 -0
  68. package/dist/src/knowledge/skill.d.ts +10 -0
  69. package/dist/src/knowledge/skill.js +106 -0
  70. package/dist/src/knowledge/synthesis.d.ts +7 -0
  71. package/dist/src/knowledge/synthesis.js +315 -0
  72. package/dist/src/knowledge/types.d.ts +49 -0
  73. package/dist/src/knowledge/types.js +1 -0
  74. package/dist/src/knowledge/validate.d.ts +6 -0
  75. package/dist/src/knowledge/validate.js +134 -0
  76. package/dist/src/local/carry_over_store.d.ts +18 -0
  77. package/dist/src/local/carry_over_store.js +40 -0
  78. package/dist/src/local/codegraph_exclude.d.ts +3 -0
  79. package/dist/src/local/codegraph_exclude.js +26 -0
  80. package/dist/src/local/codegraph_prepare.d.ts +17 -0
  81. package/dist/src/local/codegraph_prepare.js +51 -0
  82. package/dist/src/local/git_ops.d.ts +12 -0
  83. package/dist/src/local/git_ops.js +98 -0
  84. package/dist/src/local/git_parse.d.ts +27 -0
  85. package/dist/src/local/git_parse.js +131 -0
  86. package/dist/src/local/git_revision.d.ts +15 -0
  87. package/dist/src/local/git_revision.js +141 -0
  88. package/dist/src/local/git_untracked.d.ts +21 -0
  89. package/dist/src/local/git_untracked.js +79 -0
  90. package/dist/src/local/review_local.d.ts +4 -0
  91. package/dist/src/local/review_local.js +273 -0
  92. package/dist/src/local/review_lock.d.ts +7 -0
  93. package/dist/src/local/review_lock.js +45 -0
  94. package/dist/src/pr/checkout.d.ts +25 -0
  95. package/dist/src/pr/checkout.js +136 -0
  96. package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
  97. package/dist/src/pr/codegraph_tool_args.js +34 -0
  98. package/dist/src/pr/codegraph_tools.d.ts +15 -0
  99. package/dist/src/pr/codegraph_tools.js +408 -0
  100. package/dist/src/pr/diff_summary.d.ts +6 -0
  101. package/dist/src/pr/diff_summary.js +52 -0
  102. package/dist/src/pr/findings.d.ts +14 -0
  103. package/dist/src/pr/findings.js +94 -0
  104. package/dist/src/pr/github_revision.d.ts +4 -0
  105. package/dist/src/pr/github_revision.js +39 -0
  106. package/dist/src/pr/hunks.d.ts +18 -0
  107. package/dist/src/pr/hunks.js +73 -0
  108. package/dist/src/pr/reviewer.d.ts +40 -0
  109. package/dist/src/pr/reviewer.js +582 -0
  110. package/dist/src/pr/rounds.d.ts +17 -0
  111. package/dist/src/pr/rounds.js +43 -0
  112. package/dist/src/pr/scope.d.ts +35 -0
  113. package/dist/src/pr/scope.js +84 -0
  114. package/dist/src/pr/snapshot.d.ts +11 -0
  115. package/dist/src/pr/snapshot.js +1 -0
  116. package/dist/src/pr/suggestion.d.ts +17 -0
  117. package/dist/src/pr/suggestion.js +70 -0
  118. package/dist/src/remote/client.d.ts +4 -0
  119. package/dist/src/remote/client.js +199 -0
  120. package/dist/src/remote/client_tools.d.ts +13 -0
  121. package/dist/src/remote/client_tools.js +30 -0
  122. package/dist/src/remote/fixtures.d.ts +1 -0
  123. package/dist/src/remote/fixtures.js +30 -0
  124. package/dist/src/remote/revision_from_submit.d.ts +2 -0
  125. package/dist/src/remote/revision_from_submit.js +26 -0
  126. package/dist/src/remote/schema.d.ts +14 -0
  127. package/dist/src/remote/schema.js +30 -0
  128. package/dist/src/remote/server/body.d.ts +18 -0
  129. package/dist/src/remote/server/body.js +73 -0
  130. package/dist/src/remote/server/routes.d.ts +1 -0
  131. package/dist/src/remote/server/routes.js +150 -0
  132. package/dist/src/remote/server/sessions.d.ts +16 -0
  133. package/dist/src/remote/server/sessions.js +48 -0
  134. package/dist/src/remote/server/sync.d.ts +3 -0
  135. package/dist/src/remote/server/sync.js +90 -0
  136. package/dist/src/remote/tool_bridge.d.ts +15 -0
  137. package/dist/src/remote/tool_bridge.js +84 -0
  138. package/dist/src/remote/validate.d.ts +8 -0
  139. package/dist/src/remote/validate.js +248 -0
  140. package/dist/src/review/carry_over.d.ts +63 -0
  141. package/dist/src/review/carry_over.js +372 -0
  142. package/dist/src/review/engine.d.ts +13 -0
  143. package/dist/src/review/engine.js +4 -0
  144. package/dist/src/review/guides.d.ts +12 -0
  145. package/dist/src/review/guides.js +44 -0
  146. package/dist/src/review/revision.d.ts +26 -0
  147. package/dist/src/review/revision.js +57 -0
  148. package/dist/src/server/api/activity.d.ts +1 -0
  149. package/dist/src/server/api/activity.js +12 -0
  150. package/dist/src/server/api/analytics.d.ts +1 -0
  151. package/dist/src/server/api/analytics.js +9 -0
  152. package/dist/src/server/api/installations.d.ts +4 -0
  153. package/dist/src/server/api/installations.js +24 -0
  154. package/dist/src/server/api/jobs.d.ts +1 -0
  155. package/dist/src/server/api/jobs.js +86 -0
  156. package/dist/src/server/api/json_body.d.ts +1 -0
  157. package/dist/src/server/api/json_body.js +14 -0
  158. package/dist/src/server/api/remote_tokens.d.ts +1 -0
  159. package/dist/src/server/api/remote_tokens.js +71 -0
  160. package/dist/src/server/api/repos.d.ts +4 -0
  161. package/dist/src/server/api/repos.js +136 -0
  162. package/dist/src/server/api/settings.d.ts +1 -0
  163. package/dist/src/server/api/settings.js +210 -0
  164. package/dist/src/server/app.d.ts +27 -0
  165. package/dist/src/server/app.js +147 -0
  166. package/dist/src/server/assets.d.ts +3 -0
  167. package/dist/src/server/assets.js +21 -0
  168. package/dist/src/server/auth.d.ts +44 -0
  169. package/dist/src/server/auth.js +188 -0
  170. package/dist/src/server/errors.d.ts +8 -0
  171. package/dist/src/server/errors.js +10 -0
  172. package/dist/src/server/http.d.ts +8 -0
  173. package/dist/src/server/http.js +117 -0
  174. package/dist/src/server/logo.d.ts +1 -0
  175. package/dist/src/server/logo.js +2 -0
  176. package/dist/src/server/pages/activity.d.ts +4 -0
  177. package/dist/src/server/pages/activity.js +200 -0
  178. package/dist/src/server/pages/add_repo.d.ts +9 -0
  179. package/dist/src/server/pages/add_repo.js +56 -0
  180. package/dist/src/server/pages/analytics.d.ts +2 -0
  181. package/dist/src/server/pages/analytics.js +193 -0
  182. package/dist/src/server/pages/client.d.ts +2 -0
  183. package/dist/src/server/pages/client.js +2 -0
  184. package/dist/src/server/pages/home.d.ts +16 -0
  185. package/dist/src/server/pages/home.js +87 -0
  186. package/dist/src/server/pages/knowledge.d.ts +2 -0
  187. package/dist/src/server/pages/knowledge.js +64 -0
  188. package/dist/src/server/pages/layout.d.ts +32 -0
  189. package/dist/src/server/pages/layout.js +230 -0
  190. package/dist/src/server/pages/pr.d.ts +2 -0
  191. package/dist/src/server/pages/pr.js +109 -0
  192. package/dist/src/server/pages/repo.d.ts +2 -0
  193. package/dist/src/server/pages/repo.js +92 -0
  194. package/dist/src/server/pages/repo_prs.d.ts +2 -0
  195. package/dist/src/server/pages/repo_prs.js +75 -0
  196. package/dist/src/server/pages/repo_remote.d.ts +2 -0
  197. package/dist/src/server/pages/repo_remote.js +38 -0
  198. package/dist/src/server/pages/repo_settings.d.ts +3 -0
  199. package/dist/src/server/pages/repo_settings.js +127 -0
  200. package/dist/src/server/pages/router.d.ts +17 -0
  201. package/dist/src/server/pages/router.js +312 -0
  202. package/dist/src/server/pages/settings.d.ts +2 -0
  203. package/dist/src/server/pages/settings.js +337 -0
  204. package/dist/src/server/pages/setup.d.ts +5 -0
  205. package/dist/src/server/pages/setup.js +38 -0
  206. package/dist/src/server/pages/styles.d.ts +2 -0
  207. package/dist/src/server/pages/styles.js +2 -0
  208. package/dist/src/server/webhook/index.d.ts +1 -0
  209. package/dist/src/server/webhook/index.js +64 -0
  210. package/dist/src/server/webhook/signature.d.ts +7 -0
  211. package/dist/src/server/webhook/signature.js +41 -0
  212. package/dist/src/services/credentials.d.ts +19 -0
  213. package/dist/src/services/credentials.js +146 -0
  214. package/dist/src/services/dashboard.d.ts +165 -0
  215. package/dist/src/services/dashboard.js +200 -0
  216. package/dist/src/services/drift.d.ts +13 -0
  217. package/dist/src/services/drift.js +76 -0
  218. package/dist/src/services/jobs.d.ts +34 -0
  219. package/dist/src/services/jobs.js +222 -0
  220. package/dist/src/services/remote_review.d.ts +10 -0
  221. package/dist/src/services/remote_review.js +274 -0
  222. package/dist/src/services/remote_review_abort.d.ts +3 -0
  223. package/dist/src/services/remote_review_abort.js +11 -0
  224. package/dist/src/services/remote_token_jobs.d.ts +1 -0
  225. package/dist/src/services/remote_token_jobs.js +20 -0
  226. package/dist/src/services/remote_tokens.d.ts +13 -0
  227. package/dist/src/services/remote_tokens.js +57 -0
  228. package/dist/src/services/replies.d.ts +7 -0
  229. package/dist/src/services/replies.js +248 -0
  230. package/dist/src/services/review.d.ts +35 -0
  231. package/dist/src/services/review.js +671 -0
  232. package/dist/src/services/setup.d.ts +29 -0
  233. package/dist/src/services/setup.js +364 -0
  234. package/dist/src/services/webhook.d.ts +22 -0
  235. package/dist/src/services/webhook.js +401 -0
  236. package/dist/src/store/app_db.d.ts +10 -0
  237. package/dist/src/store/app_db.js +85 -0
  238. package/dist/src/store/cache_db.d.ts +4 -0
  239. package/dist/src/store/cache_db.js +63 -0
  240. package/dist/src/store/deliveries.d.ts +12 -0
  241. package/dist/src/store/deliveries.js +23 -0
  242. package/dist/src/store/drift.d.ts +4 -0
  243. package/dist/src/store/drift.js +22 -0
  244. package/dist/src/store/findings.d.ts +31 -0
  245. package/dist/src/store/findings.js +64 -0
  246. package/dist/src/store/installations.d.ts +9 -0
  247. package/dist/src/store/installations.js +35 -0
  248. package/dist/src/store/job_logs.d.ts +5 -0
  249. package/dist/src/store/job_logs.js +17 -0
  250. package/dist/src/store/jobs.d.ts +27 -0
  251. package/dist/src/store/jobs.js +86 -0
  252. package/dist/src/store/migrations.d.ts +5 -0
  253. package/dist/src/store/migrations.js +258 -0
  254. package/dist/src/store/remote_review_inputs.d.ts +11 -0
  255. package/dist/src/store/remote_review_inputs.js +25 -0
  256. package/dist/src/store/remote_tokens.d.ts +10 -0
  257. package/dist/src/store/remote_tokens.js +45 -0
  258. package/dist/src/store/replies.d.ts +24 -0
  259. package/dist/src/store/replies.js +79 -0
  260. package/dist/src/store/repos.d.ts +11 -0
  261. package/dist/src/store/repos.js +59 -0
  262. package/dist/src/store/reviews.d.ts +86 -0
  263. package/dist/src/store/reviews.js +220 -0
  264. package/dist/src/store/rows.d.ts +179 -0
  265. package/dist/src/store/rows.js +4 -0
  266. package/dist/src/store/sessions.d.ts +8 -0
  267. package/dist/src/store/sessions.js +34 -0
  268. package/dist/src/store/skill_state.d.ts +3 -0
  269. package/dist/src/store/skill_state.js +24 -0
  270. package/dist/src/store/sqlite.d.ts +39 -0
  271. package/dist/src/store/sqlite.js +77 -0
  272. package/dist/src/store/subjects.d.ts +15 -0
  273. package/dist/src/store/subjects.js +78 -0
  274. package/dist/src/tools/codegraph.d.ts +58 -0
  275. package/dist/src/tools/codegraph.js +165 -0
  276. package/dist/src/tools/codegraph_exec.d.ts +13 -0
  277. package/dist/src/tools/codegraph_exec.js +66 -0
  278. package/dist/src/types.d.ts +89 -0
  279. package/dist/src/types.js +1 -0
  280. package/dist/src/util/atomic.d.ts +4 -0
  281. package/dist/src/util/atomic.js +78 -0
  282. package/dist/src/util/keyed_lock.d.ts +3 -0
  283. package/dist/src/util/keyed_lock.js +33 -0
  284. package/dist/src/util/log.d.ts +11 -0
  285. package/dist/src/util/log.js +42 -0
  286. package/dist/src/util/redact.d.ts +5 -0
  287. package/dist/src/util/redact.js +30 -0
  288. package/dist/src/util/runtime.d.ts +90 -0
  289. package/dist/src/util/runtime.js +206 -0
  290. package/dist/src/util/time.d.ts +9 -0
  291. package/dist/src/util/time.js +36 -0
  292. package/dist/src/version.d.ts +1 -0
  293. package/dist/src/version.js +5 -0
  294. package/package.json +54 -0
@@ -0,0 +1,193 @@
1
+ import { bars, count, delta, duration, empty, html, layout, money, text, } from "./layout.js";
2
+ import { listJobs } from "../../store/jobs.js";
3
+ export function renderAnalytics(username, range, data) {
4
+ const failed = listJobs({ status: "failed" }).slice(0, 5);
5
+ const totalFindings = data.bySeverity.reduce((sum, row) => sum + row.findings, 0);
6
+ const byRepo = data.byRepo.length === 0
7
+ ? empty("No usage yet", "Cost and findings show up after reviews run.")
8
+ : `<table>
9
+ <thead><tr><th>Repository</th><th class="num">Pull requests</th><th class="num">Findings</th>
10
+ <th class="num">Per pull request</th><th class="num">Tokens</th>
11
+ <th class="num">Average review</th><th class="num">Cost</th></tr></thead>
12
+ <tbody>
13
+ ${data.byRepo
14
+ .map((row) => `<tr><td><a href="/repos/${text(row.repo)}">${text(row.repo)}</a></td>
15
+ <td class="num">${row.pullRequests}</td>
16
+ <td class="num">${row.findings}</td>
17
+ <td class="num">${row.pullRequests
18
+ ? (row.findings / row.pullRequests).toFixed(1)
19
+ : "0"}</td>
20
+ <td class="num">${count(row.tokensIn + row.tokensOut)}</td>
21
+ <td class="num">${duration(row.avgDurationMs)}</td>
22
+ <td class="num">${money(row.cost)}</td></tr>`)
23
+ .join("")}
24
+ </tbody>
25
+ </table>`;
26
+ const byModel = data.byModel.length === 0
27
+ ? empty("No models yet", "Each review records the model that wrote it.")
28
+ : `<table>
29
+ <thead><tr><th>Model</th><th class="num">Reviews</th><th class="num">Tokens in</th>
30
+ <th class="num">Tokens out</th><th class="num">Average review</th>
31
+ <th class="num">Per review</th><th class="num">Cost</th></tr></thead>
32
+ <tbody>
33
+ ${data.byModel
34
+ .map((row) => `<tr><td>${text(row.model)}</td>
35
+ <td class="num">${row.reviews}</td>
36
+ <td class="num">${count(row.tokensIn)}</td>
37
+ <td class="num">${count(row.tokensOut)}</td>
38
+ <td class="num">${duration(row.avgDurationMs)}</td>
39
+ <td class="num">${money(row.reviews ? row.cost / row.reviews : 0)}</td>
40
+ <td class="num">${money(row.cost)}</td></tr>`)
41
+ .join("")}
42
+ </tbody>
43
+ </table>`;
44
+ const byRemoteToken = (data.byRemoteToken ?? []).length === 0
45
+ ? empty("No remote reviews yet", "Usage from CLI tokens appears here.")
46
+ : `<table>
47
+ <thead><tr><th>Token</th><th class="num">Reviews</th><th class="num">Findings</th>
48
+ <th class="num">Cost</th></tr></thead>
49
+ <tbody>
50
+ ${(data.byRemoteToken ?? [])
51
+ .map((row) => `<tr><td>${text(row.tokenName)}</td>
52
+ <td class="num">${row.reviews}</td>
53
+ <td class="num">${row.findings}</td>
54
+ <td class="num">${money(row.cost)}</td></tr>`)
55
+ .join("")}
56
+ </tbody>
57
+ </table>`;
58
+ const bySeverity = data.bySeverity.length === 0
59
+ ? empty("No findings yet", "Severity shows how much of the noise matters.")
60
+ : `<table>
61
+ <thead><tr><th>Severity</th><th class="num">Findings</th><th class="num">Share</th>
62
+ <th class="num">Posted</th><th class="num">Raised again</th></tr></thead>
63
+ <tbody>
64
+ ${data.bySeverity
65
+ .map((row) => `<tr><td>${text(row.severity)}</td>
66
+ <td class="num">${row.findings}</td>
67
+ <td class="num">${totalFindings
68
+ ? Math.round((row.findings / totalFindings) * 100)
69
+ : 0}%</td>
70
+ <td class="num">${row.posted}</td>
71
+ <td class="num">${row.repeats}</td></tr>`)
72
+ .join("")}
73
+ </tbody>
74
+ </table>`;
75
+ const busiest = data.byDay.reduce((best, row) => (row.cost > best.cost ? row : best), {
76
+ day: "",
77
+ cost: 0,
78
+ reviews: 0,
79
+ findings: 0,
80
+ });
81
+ const looks = failed.length === 0
82
+ ? empty("Nothing stands out", "Failed jobs will be listed here.")
83
+ : failed
84
+ .map((job) => `<div class="notice bad worth-item" data-worth-id="${text(job.id)}" style="margin-bottom:12px">
85
+ <div class="txt"><b>${text(job.repo)}</b>. ${text(job.type)} failed${job.error ? `. ${text(job.error)}` : ""}.</div>
86
+ <a class="btn" href="/activity">See why</a>
87
+ <button class="dismiss-worth" type="button" data-dismiss-worth="${text(job.id)}" aria-label="Dismiss">×</button>
88
+ </div>`)
89
+ .join("");
90
+ return html(layout({
91
+ title: "Usage · co-maintainer",
92
+ username,
93
+ active: "analytics",
94
+ body: `<div class="wrap">
95
+ <div class="pagehead">
96
+ <div><h1>Usage</h1>
97
+ <p class="lead">Last ${data.days} days</p></div>
98
+ <div class="actions">
99
+ <select id="range" style="width:150px">
100
+ <option value="7d"${range === "7d" ? " selected" : ""}>Last 7 days</option>
101
+ <option value="30d"${range === "30d" ? " selected" : ""}>Last 30 days</option>
102
+ <option value="90d"${range === "90d" ? " selected" : ""}>Last 90 days</option>
103
+ </select>
104
+ </div>
105
+ </div>
106
+ <div class="card"><div class="bd">
107
+ <div class="kfig wrap4">
108
+ <div><div class="k">Pull requests reviewed</div><div class="big">${data.totals.pullRequests}</div>${delta(data.change.pullRequests, data.days)}</div>
109
+ <div><div class="k">Findings</div><div class="big">${data.totals.findings}</div>${delta(data.change.findings, data.days)}</div>
110
+ <div><div class="k">Cost</div><div class="big">${money(data.totals.cost)}</div>${delta(data.change.cost, data.days, true)}</div>
111
+ <div><div class="k">Tokens</div><div class="big">${count(data.totals.tokensIn + data.totals.tokensOut)}</div><div class="trend">${count(data.totals.tokensIn)} in and ${count(data.totals.tokensOut)} out</div></div>
112
+ <div><div class="k">Average review</div><div class="big">${duration(data.totals.avgDurationMs)}</div></div>
113
+ <div><div class="k">Rounds per pull request</div><div class="big">${data.totals.pullRequests
114
+ ? (data.totals.reviews / data.totals.pullRequests).toFixed(1)
115
+ : "0"}</div>${data.totals.failed
116
+ ? `<div class="trend down">${data.totals.failed} failed</div>`
117
+ : ""}</div>
118
+ </div>
119
+ </div></div>
120
+ <div class="card">
121
+ <div class="hd"><h2>Cost per day</h2></div>
122
+ <div class="bd">
123
+ ${bars(data.byDay.map((row) => ({
124
+ label: `${row.day} ${money(row.cost)} over ${row.reviews} reviews`,
125
+ value: row.cost,
126
+ })))}
127
+ <p class="muted" style="margin:10px 0 0">${busiest.cost
128
+ ? `Busiest day was ${text(busiest.day)} at ${money(busiest.cost)}`
129
+ : "No spend in this range"}</p>
130
+ </div>
131
+ </div>
132
+ <div class="card">
133
+ <div class="hd"><h2>By repository</h2></div>
134
+ <div class="bd flush">${byRepo}</div>
135
+ </div>
136
+ <div class="card">
137
+ <div class="hd"><h2>By model</h2></div>
138
+ <div class="bd flush">${byModel}</div>
139
+ </div>
140
+ <div class="card">
141
+ <div class="hd"><h2>By remote token</h2></div>
142
+ <div class="bd flush">${byRemoteToken}</div>
143
+ </div>
144
+ <div class="card">
145
+ <div class="hd"><h2>Findings by severity</h2></div>
146
+ <div class="bd flush">${bySeverity}</div>
147
+ </div>
148
+ <div class="card" id="worth-a-look">
149
+ <div class="hd"><h2>Worth a look</h2></div>
150
+ <div class="bd">${looks}</div>
151
+ </div>
152
+ </div>
153
+ <script>
154
+ document.getElementById("range").addEventListener("change", function() {
155
+ location.href = "/analytics?range=" + this.value;
156
+ });
157
+ (function() {
158
+ var key = "co-maintainer:dismissed-worth";
159
+ var read = function() {
160
+ try {
161
+ var value = JSON.parse(localStorage.getItem(key) || "[]");
162
+ return Array.isArray(value) ? value : [];
163
+ } catch (_) {
164
+ return [];
165
+ }
166
+ };
167
+ var dismissed = read();
168
+ document.querySelectorAll("[data-worth-id]").forEach(function(item) {
169
+ if (dismissed.indexOf(item.getAttribute("data-worth-id")) !== -1) {
170
+ item.remove();
171
+ }
172
+ });
173
+ document.addEventListener("click", function(event) {
174
+ var button = event.target.closest("[data-dismiss-worth]");
175
+ if (!button) return;
176
+ var id = button.getAttribute("data-dismiss-worth");
177
+ if (!id) return;
178
+ var next = read();
179
+ if (next.indexOf(id) === -1) next.push(id);
180
+ try {
181
+ localStorage.setItem(key, JSON.stringify(next));
182
+ } catch (_) {}
183
+ var item = button.closest("[data-worth-id]");
184
+ if (item) item.remove();
185
+ if (!document.querySelector("[data-worth-id]")) {
186
+ document.querySelector("#worth-a-look .bd").textContent =
187
+ "Nothing stands out.";
188
+ }
189
+ });
190
+ })();
191
+ </script>`,
192
+ }));
193
+ }
@@ -0,0 +1,2 @@
1
+ /** Generated from src/server/pages/client.js by scripts/embed_assets.mjs — do not edit. */
2
+ export declare const client = "function toast(message) {\n const host = document.getElementById(\"toasts\");\n if (!host) return;\n const el = document.createElement(\"div\");\n el.className = \"toast\";\n el.setAttribute(\"role\", \"status\");\n el.textContent = message;\n host.appendChild(el);\n setTimeout(function () {\n el.remove();\n }, 5000);\n}\n\nfunction fail(card, message, retry) {\n toast(message);\n if (!card) return;\n let box = card.querySelector(\"[data-fail]\");\n if (!box) {\n box = document.createElement(\"div\");\n box.setAttribute(\"data-fail\", \"1\");\n box.className = \"fail\";\n card.appendChild(box);\n }\n box.replaceChildren();\n const p = document.createElement(\"p\");\n p.textContent = message;\n const btn = document.createElement(\"button\");\n btn.className = \"btn sm\";\n btn.type = \"button\";\n btn.textContent = \"Retry\";\n btn.addEventListener(\"click\", retry);\n box.appendChild(p);\n box.appendChild(btn);\n}\n\nfunction clearFail(card) {\n const box = card && card.querySelector(\"[data-fail]\");\n if (box) box.remove();\n}\n\nfunction skeleton(card, on) {\n if (!card) return;\n card.classList.toggle(\"busy\", on);\n}\n\nasync function api(method, url, body) {\n const res = await fetch(url, {\n method: method,\n headers: {\n \"content-type\": \"application/json\",\n \"x-requested-with\": \"co-maintainer\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) {\n let message = \"Request failed\";\n try {\n const data = await res.json();\n if (data.error && data.error.message) message = data.error.message;\n } catch {\n // body was not JSON; keep the generic message\n }\n throw new Error(message);\n }\n if (res.status === 204) return null;\n return await res.json();\n}\n\nasync function run(btn, card, fn) {\n if (btn) btn.disabled = true;\n skeleton(card, true);\n clearFail(card);\n try {\n await fn();\n } catch (err) {\n fail(card, err.message, function () {\n run(btn, card, fn);\n });\n } finally {\n if (btn) btn.disabled = false;\n skeleton(card, false);\n }\n}\n\nasync function postAndGo(url, body, next, btn) {\n const card = btn && btn.closest(\"[data-async]\");\n await run(btn, card, async function () {\n await api(\"POST\", url, body);\n location.href = next;\n });\n}\n\n// Called from the inline scripts on the repo list and repo pages, which deno lint\n// cannot see, so it looks unused from inside this file.\n// deno-lint-ignore no-unused-vars\nfunction bindToggle(btn, url, field) {\n btn.addEventListener(\"click\", async function () {\n if (btn.disabled) return;\n const on = !btn.classList.contains(\"on\");\n btn.classList.toggle(\"on\", on);\n btn.setAttribute(\"data-on\", on ? \"1\" : \"0\");\n btn.disabled = true;\n const card = btn.closest(\"[data-async]\");\n try {\n const payload = {};\n payload[field] = on;\n await api(\"PATCH\", url, payload);\n } catch (err) {\n btn.classList.toggle(\"on\", !on);\n btn.setAttribute(\"data-on\", on ? \"0\" : \"1\");\n fail(card, err.message, function () {\n btn.click();\n });\n } finally {\n btn.disabled = false;\n }\n });\n}\n\naddEventListener(\"error\", function (ev) {\n toast((ev.error && ev.error.message) || \"Something broke\");\n});\naddEventListener(\"unhandledrejection\", function (ev) {\n toast((ev.reason && ev.reason.message) || \"Something broke\");\n});\n\ndocument.addEventListener(\"click\", function (ev) {\n const btn =\n ev.target && ev.target.closest\n ? ev.target.closest(\"[data-cancel], [data-post]\")\n : null;\n if (!btn) return;\n ev.preventDefault();\n if (btn.hasAttribute(\"data-cancel\")) {\n postAndGo(\n \"/api/jobs/\" + btn.getAttribute(\"data-cancel\") + \"/cancel\",\n {},\n \"/activity\",\n btn,\n );\n return;\n }\n const body = btn.getAttribute(\"data-body\");\n postAndGo(\n btn.getAttribute(\"data-post\"),\n body ? JSON.parse(body) : {},\n \"/activity\",\n btn,\n );\n});\n\n(function pollActivity() {\n if (!document.getElementById(\"activity-root\")) return;\n setInterval(async function () {\n if (document.hidden) return;\n if (document.querySelector(\"[data-async].busy\")) return;\n const root = document.getElementById(\"activity-root\");\n if (!root) return;\n try {\n const res = await fetch(location.pathname + location.search, {\n headers: { accept: \"text/html\" },\n });\n if (!res.ok) return;\n const doc = new DOMParser().parseFromString(\n await res.text(),\n \"text/html\",\n );\n const next = doc.getElementById(\"activity-root\");\n if (!next || next.innerHTML === root.innerHTML) return;\n root.replaceWith(next);\n } catch {\n // a failed poll just waits for the next tick\n }\n }, 5000);\n})();\n";
@@ -0,0 +1,2 @@
1
+ /** Generated from src/server/pages/client.js by scripts/embed_assets.mjs — do not edit. */
2
+ export const client = "function toast(message) {\n const host = document.getElementById(\"toasts\");\n if (!host) return;\n const el = document.createElement(\"div\");\n el.className = \"toast\";\n el.setAttribute(\"role\", \"status\");\n el.textContent = message;\n host.appendChild(el);\n setTimeout(function () {\n el.remove();\n }, 5000);\n}\n\nfunction fail(card, message, retry) {\n toast(message);\n if (!card) return;\n let box = card.querySelector(\"[data-fail]\");\n if (!box) {\n box = document.createElement(\"div\");\n box.setAttribute(\"data-fail\", \"1\");\n box.className = \"fail\";\n card.appendChild(box);\n }\n box.replaceChildren();\n const p = document.createElement(\"p\");\n p.textContent = message;\n const btn = document.createElement(\"button\");\n btn.className = \"btn sm\";\n btn.type = \"button\";\n btn.textContent = \"Retry\";\n btn.addEventListener(\"click\", retry);\n box.appendChild(p);\n box.appendChild(btn);\n}\n\nfunction clearFail(card) {\n const box = card && card.querySelector(\"[data-fail]\");\n if (box) box.remove();\n}\n\nfunction skeleton(card, on) {\n if (!card) return;\n card.classList.toggle(\"busy\", on);\n}\n\nasync function api(method, url, body) {\n const res = await fetch(url, {\n method: method,\n headers: {\n \"content-type\": \"application/json\",\n \"x-requested-with\": \"co-maintainer\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) {\n let message = \"Request failed\";\n try {\n const data = await res.json();\n if (data.error && data.error.message) message = data.error.message;\n } catch {\n // body was not JSON; keep the generic message\n }\n throw new Error(message);\n }\n if (res.status === 204) return null;\n return await res.json();\n}\n\nasync function run(btn, card, fn) {\n if (btn) btn.disabled = true;\n skeleton(card, true);\n clearFail(card);\n try {\n await fn();\n } catch (err) {\n fail(card, err.message, function () {\n run(btn, card, fn);\n });\n } finally {\n if (btn) btn.disabled = false;\n skeleton(card, false);\n }\n}\n\nasync function postAndGo(url, body, next, btn) {\n const card = btn && btn.closest(\"[data-async]\");\n await run(btn, card, async function () {\n await api(\"POST\", url, body);\n location.href = next;\n });\n}\n\n// Called from the inline scripts on the repo list and repo pages, which deno lint\n// cannot see, so it looks unused from inside this file.\n// deno-lint-ignore no-unused-vars\nfunction bindToggle(btn, url, field) {\n btn.addEventListener(\"click\", async function () {\n if (btn.disabled) return;\n const on = !btn.classList.contains(\"on\");\n btn.classList.toggle(\"on\", on);\n btn.setAttribute(\"data-on\", on ? \"1\" : \"0\");\n btn.disabled = true;\n const card = btn.closest(\"[data-async]\");\n try {\n const payload = {};\n payload[field] = on;\n await api(\"PATCH\", url, payload);\n } catch (err) {\n btn.classList.toggle(\"on\", !on);\n btn.setAttribute(\"data-on\", on ? \"0\" : \"1\");\n fail(card, err.message, function () {\n btn.click();\n });\n } finally {\n btn.disabled = false;\n }\n });\n}\n\naddEventListener(\"error\", function (ev) {\n toast((ev.error && ev.error.message) || \"Something broke\");\n});\naddEventListener(\"unhandledrejection\", function (ev) {\n toast((ev.reason && ev.reason.message) || \"Something broke\");\n});\n\ndocument.addEventListener(\"click\", function (ev) {\n const btn =\n ev.target && ev.target.closest\n ? ev.target.closest(\"[data-cancel], [data-post]\")\n : null;\n if (!btn) return;\n ev.preventDefault();\n if (btn.hasAttribute(\"data-cancel\")) {\n postAndGo(\n \"/api/jobs/\" + btn.getAttribute(\"data-cancel\") + \"/cancel\",\n {},\n \"/activity\",\n btn,\n );\n return;\n }\n const body = btn.getAttribute(\"data-body\");\n postAndGo(\n btn.getAttribute(\"data-post\"),\n body ? JSON.parse(body) : {},\n \"/activity\",\n btn,\n );\n});\n\n(function pollActivity() {\n if (!document.getElementById(\"activity-root\")) return;\n setInterval(async function () {\n if (document.hidden) return;\n if (document.querySelector(\"[data-async].busy\")) return;\n const root = document.getElementById(\"activity-root\");\n if (!root) return;\n try {\n const res = await fetch(location.pathname + location.search, {\n headers: { accept: \"text/html\" },\n });\n if (!res.ok) return;\n const doc = new DOMParser().parseFromString(\n await res.text(),\n \"text/html\",\n );\n const next = doc.getElementById(\"activity-root\");\n if (!next || next.innerHTML === root.innerHTML) return;\n root.replaceWith(next);\n } catch {\n // a failed poll just waits for the next tick\n }\n }, 5000);\n})();\n";
@@ -0,0 +1,16 @@
1
+ export declare function renderLogin(opts: {
2
+ next: string;
3
+ error?: string;
4
+ showPassword: boolean;
5
+ showGithub: boolean;
6
+ }): Response;
7
+ export declare function renderHome(username: string, rows: {
8
+ fullName: string;
9
+ statusKind: "ok" | "warn" | "err" | "run";
10
+ status: string;
11
+ knowledge: string;
12
+ knowledgeAt?: string;
13
+ autoOn: boolean;
14
+ reviews: string;
15
+ cost: string;
16
+ }[]): Response;
@@ -0,0 +1,87 @@
1
+ import { empty, html, layout, skSlot, text, when } from "./layout.js";
2
+ export function renderLogin(opts) {
3
+ const err = opts.error
4
+ ? `<p class="notice bad"><span class="txt">${text(opts.error)}</span></p>`
5
+ : "";
6
+ const passwordForm = opts.showPassword
7
+ ? `<form method="post" action="/login">
8
+ <input type="hidden" name="next" value="${text(opts.next)}">
9
+ <div class="field wide"><label>Password</label>
10
+ <input type="password" name="password" autofocus required></div>
11
+ <button class="primary" type="submit">Sign in</button>
12
+ </form>`
13
+ : "";
14
+ const githubButton = opts.showGithub
15
+ ? `<a class="btn primary" style="display:block;text-align:center" href="/auth/github?next=${encodeURIComponent(opts.next)}">Continue with GitHub</a>`
16
+ : "";
17
+ const divider = opts.showPassword && opts.showGithub
18
+ ? `<p class="muted" style="text-align:center;margin:16px 0">or</p>`
19
+ : "";
20
+ const lead = opts.showPassword
21
+ ? "Use the dashboard password printed when serve started."
22
+ : "Sign in with your GitHub account.";
23
+ return html(layout({
24
+ title: "Sign in · co-maintainer",
25
+ body: `<div class="wrap" style="max-width:420px">
26
+ <img class="brand" src="/logo.png" alt="co-maintainer">
27
+ <div class="pagehead"><div><h1>Sign in</h1>
28
+ <p class="lead">${text(lead)}</p></div></div>
29
+ ${err}
30
+ <div class="card"><div class="bd">
31
+ ${passwordForm}${divider}${githubButton}
32
+ </div></div>
33
+ </div>`,
34
+ }));
35
+ }
36
+ export function renderHome(username, rows) {
37
+ const autoCount = rows.filter((row) => row.autoOn).length;
38
+ const lead = rows.length === 0
39
+ ? "No repositories yet. Add one to start."
40
+ : `${rows.length} repositories · ${autoCount} reviewing pull requests automatically`;
41
+ const body = rows.length === 0
42
+ ? empty("No repositories", "Add a repository to generate knowledge and review pull requests.")
43
+ : `<div class="card" data-async>
44
+ ${skSlot("table")}
45
+ <div class="bd flush">
46
+ <table>
47
+ <thead><tr>
48
+ <th>Repository</th><th>Status</th><th>Knowledge</th><th>Auto review</th>
49
+ <th class="num">Reviews</th><th class="num">Cost (30d)</th>
50
+ </tr></thead>
51
+ <tbody>
52
+ ${rows
53
+ .map((row) => `<tr>
54
+ <td><a href="/repos/${text(row.fullName)}">${text(row.fullName)}</a></td>
55
+ <td><span class="st ${row.statusKind}">${text(row.status)}</span></td>
56
+ <td>${text(row.knowledge)}${row.knowledgeAt
57
+ ? `<div class="dim" style="font-size:13px">${when(row.knowledgeAt)}</div>`
58
+ : ""}</td>
59
+ <td>${row.autoOn
60
+ ? `<button class="tg on" data-repo="${text(row.fullName)}" data-on="1"></button>`
61
+ : `<button class="tg" data-repo="${text(row.fullName)}" data-on="0"></button>`}</td>
62
+ <td class="num">${text(row.reviews)}</td>
63
+ <td class="num">${text(row.cost)}</td>
64
+ </tr>`)
65
+ .join("")}
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ <div class="ft"><span>Showing ${rows.length} repositories</span></div>
70
+ </div>`;
71
+ return html(layout({
72
+ title: "Repositories · co-maintainer",
73
+ username,
74
+ body: `<div class="wrap">
75
+ <div class="pagehead">
76
+ <div><h1>Repositories</h1><p class="lead">${text(lead)}</p></div>
77
+ <div class="actions">${rows.length === 0 ? `<a class="btn" href="/setup">Get started</a>` : ""}<a class="btn primary" href="/repos/new">Add repository</a></div>
78
+ </div>
79
+ ${body}
80
+ </div>
81
+ <script>
82
+ document.querySelectorAll(".tg[data-repo]").forEach(function(btn) {
83
+ bindToggle(btn, "/api/repos/" + btn.getAttribute("data-repo"), "autoReview");
84
+ });
85
+ </script>`,
86
+ }));
87
+ }
@@ -0,0 +1,2 @@
1
+ import type { repoKnowledge } from "../../services/dashboard.ts";
2
+ export declare function renderKnowledge(username: string, data: Awaited<ReturnType<typeof repoKnowledge>>, viewId?: string): Response;
@@ -0,0 +1,64 @@
1
+ import { capped, empty, html, layout, repoNav, skSlot, text, when, } from "./layout.js";
2
+ import { COMPARE_FILE_CAP, MAX_COMMITS_COUNTED } from "../../services/drift.js";
3
+ function sizeLabel(bytes) {
4
+ if (bytes < 1024)
5
+ return `${bytes} B`;
6
+ return `${(bytes / 1024).toFixed(1)} KB`;
7
+ }
8
+ export function renderKnowledge(username, data, viewId) {
9
+ const name = data.repo.full_name;
10
+ const viewing = data.docs.find((doc) => doc.id === viewId);
11
+ const drift = data.drift;
12
+ const notice = data.repo.knowledge_built_at
13
+ ? `<div class="notice"><div class="txt">Built ${when(data.repo.knowledge_built_at)}.${drift
14
+ ? ` Since then: <b>${drift.prs_since} new pull requests</b>, <b>${drift.prs_updated} changed pull requests</b>, <b>${capped(drift.commits_since, MAX_COMMITS_COUNTED)} new commits</b> and <b>${capped(drift.files_changed, COMPARE_FILE_CAP)} changed files</b>. <span class="muted">Checked ${when(drift.as_of)}.</span>`
15
+ : ""}</div></div>`
16
+ : `<div class="notice info"><div class="txt">Knowledge has not been built yet. Run init from the repository list.</div></div>`;
17
+ const viewer = viewing
18
+ ? `<div class="card"><div class="hd"><h2>${text(viewing.title)}</h2>
19
+ <a class="btn sm" style="margin-left:auto" href="/repos/${text(name)}/knowledge">Close</a></div>
20
+ <div class="bd"><pre class="log" style="max-height:none">${text(viewing.text)}</pre></div></div>`
21
+ : "";
22
+ const table = data.docs.length === 0
23
+ ? empty("No documents", "Init this repository to generate the review guide.")
24
+ : `<table>
25
+ <thead><tr><th>Document</th><th>What it covers</th><th class="num">Size</th><th></th></tr></thead>
26
+ <tbody>
27
+ ${data.docs
28
+ .map((doc) => `<tr><td><b>${text(doc.title)}</b></td>
29
+ <td class="muted">${text(doc.covers)}</td>
30
+ <td class="num">${text(sizeLabel(doc.bytes))}</td>
31
+ <td style="width:1%"><a class="btn sm" href="?view=${text(doc.id)}">View</a></td></tr>`)
32
+ .join("")}
33
+ </tbody>
34
+ </table>`;
35
+ return html(layout({
36
+ title: `Knowledge · ${name}`,
37
+ username,
38
+ body: `<div class="wrap side">
39
+ ${repoNav(name, "knowledge")}
40
+ <div data-async>
41
+ ${skSlot()}
42
+ <div class="crumbs"><a href="/">Repositories</a> /
43
+ <a href="/repos/${text(name)}">${text(name)}</a> / Knowledge</div>
44
+ <div class="pagehead">
45
+ <div><h1>Knowledge</h1>
46
+ <p class="lead">What co-maintainer learned about this repository, and checks every pull request against.</p></div>
47
+ <div class="actions"><button class="primary" id="update">Update</button></div>
48
+ </div>
49
+ ${notice}
50
+ <div class="card"><div class="bd flush">${table}</div></div>
51
+ ${viewer}
52
+ <div class="card">
53
+ <div class="hd"><h2>History</h2></div>
54
+ <div class="bd">${empty("Only the current knowledge is kept", "Older versions are not stored yet.")}</div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ <script>
59
+ document.getElementById("update").addEventListener("click", function() {
60
+ postAndGo("/api/repos/" + ${JSON.stringify(name)} + "/remake", {}, "/activity", this);
61
+ });
62
+ </script>`,
63
+ }));
64
+ }
@@ -0,0 +1,32 @@
1
+ export declare function escapeHtml(value: unknown): string;
2
+ export declare function text(value: unknown): string;
3
+ export declare function markdown(value: unknown): string;
4
+ export declare function money(value: number): string;
5
+ export declare function count(value: number): string;
6
+ export declare function duration(ms: number | null | undefined): string;
7
+ /** Rendered only when the earlier window had something to compare against,
8
+ * so an empty first month stays quiet rather than claiming a jump. */
9
+ export declare function delta(change: number | undefined, days: number, lowerIsBetter?: boolean): string;
10
+ /** A flex row of divs beats pulling in a chart library for one series, and
11
+ * it stays readable when a range has a single day in it. Each column is full
12
+ * height with the bar painted as a gradient stop, because a bar sized by its
13
+ * own height leaves nothing to hover over on a quiet day. */
14
+ export declare function bars(items: {
15
+ label: string;
16
+ value: number;
17
+ }[]): string;
18
+ /** GitHub truncates long responses, so a count that reached its ceiling is
19
+ * a floor rather than a total and has to read as one. */
20
+ export declare function capped(value: number, cap: number): string;
21
+ export declare function when(iso: string | null | undefined): string;
22
+ export declare function skSlot(kind?: "table" | "block"): string;
23
+ export declare function layout(opts: {
24
+ title: string;
25
+ username?: string;
26
+ active?: "activity" | "analytics" | "settings";
27
+ body: string;
28
+ }): string;
29
+ export declare function html(body: string, status?: number): Response;
30
+ export declare function repoNav(fullName: string, on: string): string;
31
+ export declare function empty(title: string, lead: string): string;
32
+ export declare function statusClass(kind: "ok" | "warn" | "err" | "run", label: string): string;