co-maintainer 0.5.0 → 0.5.1-beta.2

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 (177) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +5 -3
  3. package/dist/package.json +1 -1
  4. package/dist/src/ai/anthropic.d.ts +55 -0
  5. package/dist/src/ai/anthropic.js +262 -0
  6. package/dist/src/ai/batch.d.ts +2 -2
  7. package/dist/src/ai/batch.js +1 -1
  8. package/dist/src/ai/estimate.d.ts +4 -0
  9. package/dist/src/ai/estimate.js +17 -2
  10. package/dist/src/ai/mermaid_loop.js +7 -4
  11. package/dist/src/ai/openai.d.ts +45 -0
  12. package/dist/src/ai/openai.js +206 -0
  13. package/dist/src/ai/opencode.d.ts +10 -0
  14. package/dist/src/ai/opencode.js +76 -0
  15. package/dist/src/ai/openrouter.d.ts +23 -7
  16. package/dist/src/ai/openrouter.js +39 -23
  17. package/dist/src/ai/pricing.js +1 -1
  18. package/dist/src/ai/provider.d.ts +9 -0
  19. package/dist/src/ai/provider.js +42 -3
  20. package/dist/src/ai/verify.d.ts +9 -1
  21. package/dist/src/ai/verify.js +81 -3
  22. package/dist/src/cli/args.d.ts +1 -2
  23. package/dist/src/cli/args.js +38 -26
  24. package/dist/src/cli/commands/config.d.ts +1 -1
  25. package/dist/src/cli/commands/config.js +10 -4
  26. package/dist/src/cli/commands/probe.js +3 -11
  27. package/dist/src/cli/commands/registry.d.ts +1 -9
  28. package/dist/src/cli/commands/registry.js +75 -10
  29. package/dist/src/cli/commands/review.js +18 -13
  30. package/dist/src/cli/commands/rollback.d.ts +1 -0
  31. package/dist/src/cli/commands/rollback.js +42 -0
  32. package/dist/src/cli/commands/serve.js +2 -3
  33. package/dist/src/cli/commands/set.js +71 -7
  34. package/dist/src/cli/commands/view.js +10 -9
  35. package/dist/src/cli/error.d.ts +4 -4
  36. package/dist/src/cli/error.js +4 -4
  37. package/dist/src/cli/main.d.ts +4 -4
  38. package/dist/src/cli/main.js +13 -8
  39. package/dist/src/cli/prompt.js +1 -1
  40. package/dist/src/cli/review_args.d.ts +9 -3
  41. package/dist/src/cli/review_args.js +28 -5
  42. package/dist/src/cli/review_github.d.ts +31 -0
  43. package/dist/src/cli/review_github.js +128 -0
  44. package/dist/src/cli/review_result.d.ts +27 -16
  45. package/dist/src/cli/review_result.js +7 -7
  46. package/dist/src/config.d.ts +28 -6
  47. package/dist/src/config.js +15 -1
  48. package/dist/src/git/default_branch.d.ts +1 -1
  49. package/dist/src/git/default_branch.js +1 -1
  50. package/dist/src/github/app.js +2 -2
  51. package/dist/src/github/app_manifest.d.ts +1 -7
  52. package/dist/src/github/app_manifest.js +2 -7
  53. package/dist/src/github/client.d.ts +4 -1
  54. package/dist/src/github/client.js +14 -2
  55. package/dist/src/github/gh.js +10 -7
  56. package/dist/src/github/pat.js +4 -4
  57. package/dist/src/github/permissions.d.ts +36 -0
  58. package/dist/src/github/permissions.js +334 -0
  59. package/dist/src/knowledge/facts.js +1 -1
  60. package/dist/src/knowledge/guide.d.ts +3 -3
  61. package/dist/src/knowledge/guide.js +5 -5
  62. package/dist/src/knowledge/probe.js +1 -2
  63. package/dist/src/knowledge/sections.d.ts +3 -3
  64. package/dist/src/knowledge/sections.js +3 -3
  65. package/dist/src/knowledge/skill.d.ts +1 -1
  66. package/dist/src/knowledge/skill.js +4 -4
  67. package/dist/src/knowledge/synthesis.d.ts +1 -1
  68. package/dist/src/knowledge/synthesis.js +5 -5
  69. package/dist/src/knowledge/types.d.ts +1 -1
  70. package/dist/src/local/codegraph_exclude.d.ts +1 -1
  71. package/dist/src/local/codegraph_exclude.js +1 -1
  72. package/dist/src/local/codegraph_prepare.js +1 -1
  73. package/dist/src/local/git_ops.d.ts +1 -1
  74. package/dist/src/local/git_ops.js +1 -1
  75. package/dist/src/local/git_parse.d.ts +2 -2
  76. package/dist/src/local/git_parse.js +2 -2
  77. package/dist/src/local/git_revision.d.ts +1 -1
  78. package/dist/src/local/git_revision.js +1 -1
  79. package/dist/src/local/git_untracked.d.ts +1 -1
  80. package/dist/src/local/git_untracked.js +1 -1
  81. package/dist/src/local/review_local.js +17 -14
  82. package/dist/src/local/review_lock.d.ts +1 -1
  83. package/dist/src/local/review_lock.js +1 -1
  84. package/dist/src/pr/checkout.js +2 -2
  85. package/dist/src/pr/findings.js +1 -1
  86. package/dist/src/pr/findings_json.d.ts +2 -2
  87. package/dist/src/pr/findings_json.js +1 -1
  88. package/dist/src/pr/github_revision.js +1 -1
  89. package/dist/src/pr/review_copy.d.ts +2 -2
  90. package/dist/src/pr/review_copy.js +2 -2
  91. package/dist/src/pr/reviewer.d.ts +6 -0
  92. package/dist/src/pr/reviewer.js +49 -39
  93. package/dist/src/remote/client.d.ts +4 -0
  94. package/dist/src/remote/client.js +84 -23
  95. package/dist/src/remote/fixtures.js +1 -1
  96. package/dist/src/remote/http.d.ts +10 -1
  97. package/dist/src/remote/http.js +15 -3
  98. package/dist/src/remote/schema.d.ts +3 -3
  99. package/dist/src/remote/schema.js +3 -3
  100. package/dist/src/remote/server/body.d.ts +1 -1
  101. package/dist/src/remote/server/body.js +1 -1
  102. package/dist/src/remote/server/byok_keys.d.ts +15 -0
  103. package/dist/src/remote/server/byok_keys.js +40 -0
  104. package/dist/src/remote/server/guides.js +2 -2
  105. package/dist/src/remote/server/routes.js +8 -1
  106. package/dist/src/remote/validate.js +15 -1
  107. package/dist/src/review/blocking.d.ts +2 -2
  108. package/dist/src/review/blocking.js +2 -2
  109. package/dist/src/review/carry_over.d.ts +2 -2
  110. package/dist/src/review/carry_over.js +2 -2
  111. package/dist/src/review/guides.js +1 -1
  112. package/dist/src/server/api/remote_tokens.js +2 -0
  113. package/dist/src/server/api/repos.js +51 -0
  114. package/dist/src/server/api/settings.js +10 -0
  115. package/dist/src/server/errors.d.ts +1 -1
  116. package/dist/src/server/http.js +1 -1
  117. package/dist/src/server/pages/activity.d.ts +2 -0
  118. package/dist/src/server/pages/activity.js +18 -4
  119. package/dist/src/server/pages/add_repo.js +61 -1
  120. package/dist/src/server/pages/analytics.js +18 -8
  121. package/dist/src/server/pages/cost.d.ts +21 -0
  122. package/dist/src/server/pages/cost.js +72 -0
  123. package/dist/src/server/pages/pr.js +5 -4
  124. package/dist/src/server/pages/repo.js +3 -2
  125. package/dist/src/server/pages/repo_prs.js +3 -2
  126. package/dist/src/server/pages/repo_remote.js +3 -2
  127. package/dist/src/server/pages/repo_settings.js +326 -12
  128. package/dist/src/server/pages/router.js +3 -2
  129. package/dist/src/server/pages/settings.js +50 -6
  130. package/dist/src/server/pages/styles.d.ts +1 -1
  131. package/dist/src/server/pages/styles.js +1 -1
  132. package/dist/src/services/credentials.js +8 -11
  133. package/dist/src/services/dashboard.d.ts +22 -9
  134. package/dist/src/services/dashboard.js +16 -2
  135. package/dist/src/services/probe.js +1 -1
  136. package/dist/src/services/remote_review.js +142 -35
  137. package/dist/src/services/remote_review_abort.d.ts +5 -1
  138. package/dist/src/services/remote_review_abort.js +9 -1
  139. package/dist/src/services/review.d.ts +1 -1
  140. package/dist/src/services/review.js +19 -5
  141. package/dist/src/services/review_policy.d.ts +116 -0
  142. package/dist/src/services/review_policy.js +489 -0
  143. package/dist/src/services/setup.d.ts +4 -3
  144. package/dist/src/services/setup.js +18 -16
  145. package/dist/src/services/setup_checklist.js +2 -2
  146. package/dist/src/services/webhook.d.ts +23 -0
  147. package/dist/src/services/webhook.js +141 -34
  148. package/dist/src/store/app_db.d.ts +5 -0
  149. package/dist/src/store/app_db.js +69 -13
  150. package/dist/src/store/backup.d.ts +31 -0
  151. package/dist/src/store/backup.js +158 -0
  152. package/dist/src/store/migrations.d.ts +1 -1
  153. package/dist/src/store/migrations.js +21 -1
  154. package/dist/src/store/remote_review_inputs.d.ts +3 -0
  155. package/dist/src/store/remote_review_inputs.js +3 -3
  156. package/dist/src/store/remote_tokens.d.ts +1 -1
  157. package/dist/src/store/remote_tokens.js +1 -1
  158. package/dist/src/store/repos.d.ts +2 -2
  159. package/dist/src/store/repos.js +1 -1
  160. package/dist/src/store/reviews.d.ts +21 -13
  161. package/dist/src/store/reviews.js +60 -15
  162. package/dist/src/store/rows.d.ts +10 -0
  163. package/dist/src/tools/codegraph.d.ts +4 -4
  164. package/dist/src/tools/codegraph.js +5 -5
  165. package/dist/src/tools/codegraph_exec.d.ts +1 -1
  166. package/dist/src/tools/codegraph_exec.js +1 -1
  167. package/dist/src/types.d.ts +12 -6
  168. package/dist/src/util/cost.d.ts +52 -0
  169. package/dist/src/util/cost.js +70 -0
  170. package/dist/src/util/log.d.ts +1 -1
  171. package/dist/src/util/log.js +2 -2
  172. package/dist/src/util/redact.js +2 -0
  173. package/dist/src/util/run_summary.d.ts +7 -2
  174. package/dist/src/util/run_summary.js +28 -2
  175. package/dist/src/util/time.d.ts +1 -1
  176. package/dist/src/util/time.js +1 -1
  177. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { html, layout, skSlot, text } from "./layout.js";
2
2
  import { resolveAppPrivateKey } from "../../config.js";
3
3
  import { VERSION } from "../../version.js";
4
+ import { TEMPLATE_INFO } from "../../services/review_policy.js";
4
5
  import { defaultAppName, manifestBlockedReason, } from "../../github/app_manifest.js";
5
6
  const SOURCE_URL = "https://github.com/GroophyLifefor/co-maintainer";
6
7
  export function renderSettings(username, config, webhookUrl, baseUrl) {
@@ -45,7 +46,10 @@ export function renderSettings(username, config, webhookUrl, baseUrl) {
45
46
  <select id="ai">
46
47
  <option value="none"${!config.ai || config.ai === "none" ? " selected" : ""}>None</option>
47
48
  <option value="openrouter"${config.ai === "openrouter" ? " selected" : ""}>OpenRouter</option>
48
- <option value="hetzner"${config.ai === "hetzner" ? " selected" : ""}>Hetzner</option>
49
+ <option value="openai"${config.ai === "openai" ? " selected" : ""}>OpenAI</option>
50
+ <option value="anthropic"${config.ai === "anthropic" ? " selected" : ""}>Anthropic</option>
51
+ <option value="opencode-zen"${config.ai === "opencode-zen" ? " selected" : ""}>OpenCode Zen</option>
52
+ <option value="opencode-go"${config.ai === "opencode-go" ? " selected" : ""}>OpenCode Go</option>
49
53
  </select></div>
50
54
  <div class="field"><label>API key</label>
51
55
  <input id="token" type="password" placeholder="Leave blank to keep the current key">
@@ -72,7 +76,8 @@ export function renderSettings(username, config, webhookUrl, baseUrl) {
72
76
  <option value="pat"${config.auth === "pat" ? " selected" : ""}>Personal access token</option>
73
77
  </select></div>
74
78
  <div class="field"><label>Personal access token</label>
75
- <input id="pat" type="password" placeholder="Leave blank to keep the current token"></div>
79
+ <input id="pat" type="password" placeholder="Leave blank to keep the current token">
80
+ <div class="hint">Read-only is enough. <a href="https://co-maintainer.com/docs/authentication.html" target="_blank" rel="noopener">Which permissions to grant</a></div></div>
76
81
  </div>
77
82
  <div class="ft"><button class="primary" id="save-gh">Save</button></div>
78
83
  </div>
@@ -81,7 +86,7 @@ export function renderSettings(username, config, webhookUrl, baseUrl) {
81
86
  <div class="hd"><h2>GitHub App</h2>
82
87
  <span class="st ${appOk ? "ok" : "warn"}" style="margin-left:auto">${appOk ? "Configured" : "Not set"}</span></div>
83
88
  <div class="bd">
84
- <p class="muted" style="margin:0 0 16px">Needed to post reviews on pull requests.</p>
89
+ <p class="muted" style="margin:0 0 16px">Needed to post reviews on pull requests. <a href="https://co-maintainer.com/docs/github-app.html#permissions" target="_blank" rel="noopener">Permissions it needs</a></p>
85
90
  <div style="margin:0 0 16px">
86
91
  <div class="two" style="max-width:none;margin:0 0 12px">
87
92
  <div class="field"><label>App name</label>
@@ -124,6 +129,21 @@ Leave blank to keep the current key"></textarea></div>
124
129
  <div class="field"><label>Max lines per pull request</label>
125
130
  <input id="def-lines" value="${text(config.defaults?.maxPullRequestChangeLines ?? "")}"></div>
126
131
  </div>
132
+ <div class="field wide" style="margin-top:18px">
133
+ <label for="def-policy">Who gets a review</label>
134
+ <select id="def-policy">
135
+ <option value=""${config.reviewPolicy === undefined ? " selected" : ""}>Everyone except drafts and bots (the simple switches)</option>
136
+ ${TEMPLATE_INFO.map((item) => `<option value="${text(item.name)}"${config.reviewPolicy === item.name ? " selected" : ""}>${text(item.label)}</option>`).join("")}
137
+ ${typeof config.reviewPolicy === "object"
138
+ ? `<option value="__custom" selected>Custom rules from a file</option>`
139
+ : ""}
140
+ </select>
141
+ <div class="hint" id="def-policy-desc">${text(typeof config.reviewPolicy === "string"
142
+ ? (TEMPLATE_INFO.find((item) => item.name === config.reviewPolicy)
143
+ ?.description ?? "")
144
+ : "")}</div>
145
+ <div class="hint">New repositories start with this. Each repository can change it in its own settings. Custom rules are set with <span class="mono">co-maintainer set --review-policy-file=policy.json</span>.</div>
146
+ </div>
127
147
  </div>
128
148
  <div class="ft"><button class="primary" id="save-def">Save</button></div>
129
149
  </div>
@@ -156,9 +176,17 @@ Leave blank to keep the current key"></textarea></div>
156
176
  <div class="field"><label>New token name</label>
157
177
  <input id="remote-token-name" placeholder="e.g. laptop"></div>
158
178
  </div>
179
+ <div class="field wide" style="margin-top:16px"><label>Client key policy (BYOK)</label>
180
+ <select id="remote-byok-policy">
181
+ <option value="off"${(config.remoteByokPolicy ?? "off") === "off" ? " selected" : ""}>off: reject a client key</option>
182
+ <option value="allow"${config.remoteByokPolicy === "allow" ? " selected" : ""}>allow: accept a client key, fall back to the server key</option>
183
+ <option value="require"${config.remoteByokPolicy === "require" ? " selected" : ""}>require: a client key is mandatory</option>
184
+ </select>
185
+ <div class="hint">Whether <code>review --remote</code> clients may send their own AI key. The <code>CM_REMOTE_BYOK_POLICY</code> env var overrides this.</div></div>
159
186
  </div>
160
187
  <div class="ft">
161
188
  <button class="primary" id="create-remote-token">Create token</button>
189
+ <button class="btn" id="save-remote-byok">Save policy</button>
162
190
  </div>
163
191
  </div>
164
192
  <div class="card" id="access" data-async>
@@ -318,6 +346,12 @@ document.getElementById("save-server").addEventListener("click", function() {
318
346
  remoteToolOutputMaxChars: toolChars.value
319
347
  });
320
348
  });
349
+ document.getElementById("save-remote-byok").addEventListener("click", function() {
350
+ var card = this.closest("[data-async]");
351
+ save(this, {
352
+ remoteByokPolicy: document.getElementById("remote-byok-policy").value
353
+ });
354
+ });
321
355
  function escHtml(s) {
322
356
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
323
357
  }
@@ -332,7 +366,7 @@ async function loadRemoteTokens() {
332
366
  el.innerHTML = "<table><thead><tr><th>Name</th><th>Status</th><th class=\\"num\\">Reviews (30d)</th><th class=\\"num\\">Cost (30d)</th><th></th></tr></thead><tbody>" +
333
367
  rows.map(function(r) {
334
368
  return "<tr><td>" + escHtml(r.name) + "</td><td>" + (r.active ? "Active" : "Inactive") + "</td>" +
335
- "<td class=\\"num\\">" + r.reviews30d + "</td><td class=\\"num\\">" + r.cost30d + "</td>" +
369
+ "<td class=\\"num\\">" + r.reviews30d + "</td><td class=\\"num\\">" + escHtml(r.cost30dLabel) + "</td>" +
336
370
  "<td><button type=\\"button\\" class=\\"btn sm\\" data-token-id=\\"" + escHtml(r.id) + "\\" data-active=\\"" + r.active + "\\">" +
337
371
  (r.active ? "Deactivate" : "Activate") + "</button> " +
338
372
  "<button type=\\"button\\" class=\\"btn sm\\" data-delete-token=\\"" + escHtml(r.id) + "\\">Delete</button></td></tr>";
@@ -412,6 +446,13 @@ document.getElementById("create-remote-token").addEventListener("click", functio
412
446
  await loadRemoteTokens();
413
447
  });
414
448
  });
449
+ var POLICY_INFO = ${JSON.stringify(TEMPLATE_INFO)};
450
+ document.getElementById("def-policy").addEventListener("change", function() {
451
+ var value = this.value;
452
+ var text = "";
453
+ POLICY_INFO.forEach(function(item) { if (item.name === value) text = item.description; });
454
+ document.getElementById("def-policy-desc").textContent = text;
455
+ });
415
456
  document.getElementById("save-def").addEventListener("click", function() {
416
457
  var card = this.closest("[data-async]");
417
458
  var positiveInt = function(id, label) {
@@ -438,13 +479,16 @@ document.getElementById("save-def").addEventListener("click", function() {
438
479
  fail(card, lines.message, function () {});
439
480
  return;
440
481
  }
441
- save(this, {
482
+ var body = {
442
483
  defaults: {
443
484
  maxPrMonths: months.value,
444
485
  maxCommits: commits.value,
445
486
  maxPullRequestChangeLines: lines.value
446
487
  }
447
- });
488
+ };
489
+ var policy = document.getElementById("def-policy").value;
490
+ if (policy !== "__custom") body.reviewPolicy = policy === "" ? null : policy;
491
+ save(this, body);
448
492
  });
449
493
  </script>`,
450
494
  }));
@@ -1,2 +1,2 @@
1
1
  /** Generated from src/server/pages/styles.css by scripts/embed_assets.mjs — do not edit. */
2
- export declare const styles = ":root {\n --bg: #f6f6f7;\n --card: #fff;\n --border: #e2e2e4;\n --border2: #ededef;\n --text: #1f2124;\n --muted: #6a6f75;\n --dim: #8b9096;\n --accent: #80c580;\n --accent-d: #6cae6c;\n --link: #0b5cd5;\n --green: #1a9d5a;\n --amber: #c98a00;\n --red: #cf3c3c;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n* {\n box-sizing: border-box;\n}\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--text);\n font:\n 14px/1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n}\na {\n color: var(--link);\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\ncode,\n.mono {\n font-family: var(--mono);\n font-size: 0.9em;\n}\n\n/* top bar */\n.top {\n background: #fff;\n border-bottom: 1px solid var(--border);\n}\n.top .in {\n max-width: 1100px;\n margin: 0 auto;\n padding: 0 24px;\n height: 56px;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.logo {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n font-size: 15px;\n color: var(--text);\n}\n.logo:hover {\n text-decoration: none;\n}\n.logo img {\n height: 32px;\n width: 32px;\n display: block;\n border-radius: 5px;\n image-rendering: pixelated;\n object-fit: cover;\n}\n.brand {\n width: 180px;\n height: auto;\n display: block;\n margin: 0 0 14px;\n border-radius: 8px;\n image-rendering: pixelated;\n}\n.top .right {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n font-size: 13px;\n color: var(--muted);\n}\n\n/* layout */\n.wrap {\n max-width: 1100px;\n margin: 0 auto;\n padding: 28px 24px 72px;\n}\n.wrap.side {\n display: grid;\n grid-template-columns: 196px 1fr;\n gap: 32px;\n}\naside nav {\n display: flex;\n flex-direction: column;\n gap: 1px;\n position: sticky;\n top: 24px;\n}\naside nav a {\n padding: 7px 12px;\n border-radius: 4px;\n color: var(--text);\n font-size: 14px;\n}\naside nav a:hover {\n background: #ededef;\n text-decoration: none;\n}\naside nav a.on {\n background: #fff;\n box-shadow: inset 2px 0 0 var(--accent);\n border: 1px solid var(--border);\n font-weight: 500;\n}\naside .lbl {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--dim);\n padding: 0 12px;\n margin: 0 0 8px;\n}\n\nh1 {\n font-size: 22px;\n font-weight: 600;\n margin: 0;\n}\nh2 {\n font-size: 15px;\n font-weight: 600;\n margin: 0;\n}\np.lead {\n color: var(--muted);\n margin: 6px 0 0;\n font-size: 14px;\n}\n.pagehead {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-bottom: 22px;\n}\n.pagehead .actions {\n margin-left: auto;\n display: flex;\n gap: 8px;\n}\n\n.card {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: 6px;\n margin-bottom: 18px;\n}\n.card .hd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.card .bd {\n padding: 18px;\n}\n.card .bd.flush {\n padding: 0;\n}\n.card .ft {\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n background: #fbfbfc;\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 13px;\n color: var(--muted);\n border-radius: 0 0 6px 6px;\n}\n\n/* table */\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth {\n text-align: left;\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n padding: 10px 18px;\n border-bottom: 1px solid var(--border);\n background: #fbfbfc;\n}\ntd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border2);\n font-size: 14px;\n}\ntr:last-child td {\n border-bottom: none;\n}\ntbody tr:hover {\n background: #fafafb;\n}\n.num {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* status text, not badges */\n.st {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n}\n.st::before {\n content: \"\";\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--dim);\n}\n.st.ok::before {\n background: var(--green);\n}\n.st.warn::before {\n background: var(--amber);\n}\n.st.err::before {\n background: var(--red);\n}\n.st.run::before {\n background: var(--link);\n}\n\n/* buttons */\nbutton,\n.btn {\n font: inherit;\n font-size: 14px;\n padding: 6px 14px;\n border-radius: 4px;\n border: 1px solid var(--border);\n background: #fff;\n color: var(--text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\nbutton:hover,\n.btn:hover {\n background: #f4f4f5;\n text-decoration: none;\n}\n.btn.primary,\nbutton.primary {\n background: var(--accent);\n border-color: var(--accent);\n color: #fff;\n font-weight: 500;\n}\n.btn.primary:hover,\nbutton.primary:hover {\n background: var(--accent-d);\n border-color: var(--accent-d);\n}\n.btn.sm {\n padding: 4px 10px;\n font-size: 13px;\n}\n.btn.link {\n border: none;\n background: none;\n color: var(--link);\n padding: 0;\n}\n.btn.link:hover {\n background: none;\n text-decoration: underline;\n}\nbutton[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* toggle */\n.tg {\n position: relative;\n width: 38px;\n height: 22px;\n border-radius: 22px;\n background: #cfd2d6;\n border: none;\n padding: 0;\n cursor: pointer;\n transition: background 0.12s ease;\n}\n.tg::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n transition: left 0.12s ease;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n.tg.on {\n background: var(--accent);\n}\n.tg.on::after {\n left: 18px;\n}\n.tg:hover {\n filter: brightness(0.97);\n}\n\n/* forms */\nlabel {\n display: block;\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 6px;\n}\ninput,\nselect,\ntextarea {\n width: 100%;\n background: #fff;\n border: 1px solid #cfd2d6;\n border-radius: 4px;\n color: var(--text);\n padding: 8px 10px;\n font: inherit;\n font-size: 14px;\n}\ninput:focus,\nselect:focus,\ntextarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 2px rgba(128, 197, 128, 0.35);\n}\ntextarea {\n resize: vertical;\n line-height: 1.35;\n}\n.pem {\n min-height: 150px;\n}\n.field {\n margin-bottom: 18px;\n max-width: 420px;\n}\n.field.wide {\n max-width: none;\n}\n.hint {\n font-size: 13px;\n color: var(--muted);\n margin-top: 6px;\n font-weight: 400;\n}\n.two {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 0 24px;\n}\n\n/* bits */\n.muted {\n color: var(--muted);\n}\n.dim {\n color: var(--dim);\n}\n.big {\n font-size: 26px;\n font-weight: 600;\n letter-spacing: -0.01em;\n}\n.kfig {\n display: flex;\n gap: 44px;\n}\n.kfig .k {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 2px;\n}\n.notice {\n background: #f3faf3;\n border: 1px solid #cfe6cf;\n border-radius: 6px;\n padding: 14px 18px;\n display: flex;\n align-items: center;\n gap: 14px;\n margin-bottom: 18px;\n}\n.notice.info {\n background: #f2f7ff;\n border-color: #cfe0fb;\n}\n.notice.bad {\n background: #fdf3f3;\n border-color: #f3d0d0;\n}\n.notice .txt {\n flex: 1;\n}\n.notice b {\n font-weight: 600;\n}\n.notice .dismiss-worth {\n margin-left: auto;\n border: 0;\n background: transparent;\n color: var(--muted);\n padding: 0 4px;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n}\n.notice .dismiss-worth:hover {\n background: transparent;\n color: var(--text);\n}\n.markdown > :first-child {\n margin-top: 0;\n}\n.markdown > :last-child {\n margin-bottom: 0;\n}\n.markdown p,\n.markdown h1,\n.markdown h2,\n.markdown h3,\n.markdown ul,\n.markdown ol,\n.markdown pre {\n margin: 0 0 12px;\n}\n.markdown h1,\n.markdown h2,\n.markdown h3 {\n font-size: 15px;\n}\n.markdown ul,\n.markdown ol {\n padding-left: 22px;\n}\n.markdown pre {\n overflow-x: auto;\n padding: 10px 12px;\n border-radius: 4px;\n background: #f1f2f3;\n font-family: var(--mono);\n font-size: 13px;\n white-space: pre;\n}\n.markdown code {\n padding: 1px 4px;\n border-radius: 3px;\n background: #f1f2f3;\n}\n.markdown pre code {\n padding: 0;\n background: transparent;\n}\n.log {\n background: #1f2124;\n color: #e6e6e6;\n border-radius: 6px;\n padding: 14px 16px;\n font-family: var(--mono);\n font-size: 12.5px;\n line-height: 1.7;\n max-height: 320px;\n overflow: auto;\n white-space: pre-wrap;\n}\n.log .t {\n color: #8b9096;\n}\n.crumbs {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 14px;\n}\n.pager {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n font-size: 13px;\n color: var(--muted);\n}\n.pager .sp {\n margin-left: auto;\n}\n.empty {\n padding: 56px 24px;\n text-align: center;\n color: var(--muted);\n}\n.empty h2 {\n margin-bottom: 6px;\n}\n.steps {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.steps li {\n display: flex;\n gap: 14px;\n padding: 16px 0;\n border-bottom: 1px solid var(--border2);\n}\n.steps li:last-child {\n border-bottom: none;\n}\n.steps .n {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: #ededef;\n color: var(--muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n flex: none;\n}\n.steps li.done .n {\n background: var(--green);\n color: #fff;\n}\n.steps li.now .n {\n background: var(--accent);\n color: #fff;\n}\n.steps .body {\n flex: 1;\n}\n.steps .body b {\n display: block;\n font-weight: 600;\n margin-bottom: 2px;\n}\n\nbutton,\n.btn,\n.tg,\n.tg::after,\ntbody tr {\n transition:\n background 0.12s ease,\n border-color 0.12s ease,\n filter 0.12s ease,\n left 0.12s ease,\n color 0.12s ease;\n}\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n\n#toasts {\n position: fixed;\n right: 20px;\n bottom: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n z-index: 20;\n max-width: 360px;\n}\n.toast {\n background: #1f2124;\n color: #fff;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n}\n.fail {\n margin: 12px 18px;\n padding: 12px 14px;\n background: #fdf3f3;\n border: 1px solid #f3d0d0;\n border-radius: 6px;\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 13px;\n}\n.fail p {\n margin: 0;\n flex: 1;\n}\n\n.secret {\n margin: 0 0 16px;\n padding: 12px 14px;\n background: #fffbea;\n border: 1px solid #ecd9a0;\n border-radius: 6px;\n font-size: 13px;\n}\n.secret b {\n display: block;\n margin-bottom: 8px;\n}\n.secret-row {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n.secret-row code {\n flex: 1;\n min-width: 220px;\n word-break: break-all;\n}\n\n.sk-slot {\n display: none;\n padding: 18px;\n}\n.sk,\n.sk-row {\n height: 12px;\n background: #ededef;\n border-radius: 4px;\n margin-bottom: 10px;\n}\n.sk-row {\n height: 36px;\n}\n[data-async].busy > :not(.sk-slot):not([data-fail]) {\n visibility: hidden;\n}\n[data-async].busy > .sk-slot {\n display: block !important;\n visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n.bars {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 56px;\n}\n.bars i {\n flex: 1 1 0;\n min-width: 2px;\n align-self: stretch;\n background-image: linear-gradient(\n to top,\n var(--bar, var(--accent)) var(--h),\n transparent var(--h)\n );\n}\n.bars i:hover {\n --bar: var(--accent-d);\n background-color: var(--border2);\n}\n.trend {\n font-size: 12px;\n margin-top: 3px;\n}\n.trend.up {\n color: var(--green);\n}\n.trend.down {\n color: var(--red);\n}\n.kfig.wrap4 {\n flex-wrap: wrap;\n gap: 28px 44px;\n}\n";
2
+ export declare const styles = ":root {\n --bg: #f6f6f7;\n --card: #fff;\n --border: #e2e2e4;\n --border2: #ededef;\n --text: #1f2124;\n --muted: #6a6f75;\n --dim: #8b9096;\n --accent: #80c580;\n --accent-d: #6cae6c;\n --link: #0b5cd5;\n --green: #1a9d5a;\n --amber: #c98a00;\n --red: #cf3c3c;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n* {\n box-sizing: border-box;\n}\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--text);\n font:\n 14px/1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n}\na {\n color: var(--link);\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\ncode,\n.mono {\n font-family: var(--mono);\n font-size: 0.9em;\n}\n\n/* top bar */\n.top {\n background: #fff;\n border-bottom: 1px solid var(--border);\n}\n.top .in {\n max-width: 1100px;\n margin: 0 auto;\n padding: 0 24px;\n height: 56px;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.logo {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n font-size: 15px;\n color: var(--text);\n}\n.logo:hover {\n text-decoration: none;\n}\n.logo img {\n height: 32px;\n width: 32px;\n display: block;\n border-radius: 5px;\n image-rendering: pixelated;\n object-fit: cover;\n}\n.brand {\n width: 180px;\n height: auto;\n display: block;\n margin: 0 0 14px;\n border-radius: 8px;\n image-rendering: pixelated;\n}\n.top .right {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n font-size: 13px;\n color: var(--muted);\n}\n\n/* layout */\n.wrap {\n max-width: 1100px;\n margin: 0 auto;\n padding: 28px 24px 72px;\n}\n.wrap.side {\n display: grid;\n grid-template-columns: 196px 1fr;\n gap: 32px;\n}\naside nav {\n display: flex;\n flex-direction: column;\n gap: 1px;\n position: sticky;\n top: 24px;\n}\naside nav a {\n padding: 7px 12px;\n border-radius: 4px;\n color: var(--text);\n font-size: 14px;\n}\naside nav a:hover {\n background: #ededef;\n text-decoration: none;\n}\naside nav a.on {\n background: #fff;\n box-shadow: inset 2px 0 0 var(--accent);\n border: 1px solid var(--border);\n font-weight: 500;\n}\naside .lbl {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--dim);\n padding: 0 12px;\n margin: 0 0 8px;\n}\n\nh1 {\n font-size: 22px;\n font-weight: 600;\n margin: 0;\n}\nh2 {\n font-size: 15px;\n font-weight: 600;\n margin: 0;\n}\np.lead {\n color: var(--muted);\n margin: 6px 0 0;\n font-size: 14px;\n}\n.pagehead {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-bottom: 22px;\n}\n.pagehead .actions {\n margin-left: auto;\n display: flex;\n gap: 8px;\n}\n\n.card {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: 6px;\n margin-bottom: 18px;\n}\n.card .hd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.card .bd {\n padding: 18px;\n}\n.card .bd.flush {\n padding: 0;\n}\n.card .ft {\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n background: #fbfbfc;\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 13px;\n color: var(--muted);\n border-radius: 0 0 6px 6px;\n}\n\n/* table */\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth {\n text-align: left;\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n padding: 10px 18px;\n border-bottom: 1px solid var(--border);\n background: #fbfbfc;\n}\ntd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border2);\n font-size: 14px;\n}\ntr:last-child td {\n border-bottom: none;\n}\ntbody tr:hover {\n background: #fafafb;\n}\n.num {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* status text, not badges */\n.st {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n}\n.st::before {\n content: \"\";\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--dim);\n}\n.st.ok::before {\n background: var(--green);\n}\n.st.warn::before {\n background: var(--amber);\n}\n.st.err::before {\n background: var(--red);\n}\n.st.run::before {\n background: var(--link);\n}\n\n/* buttons */\nbutton,\n.btn {\n font: inherit;\n font-size: 14px;\n padding: 6px 14px;\n border-radius: 4px;\n border: 1px solid var(--border);\n background: #fff;\n color: var(--text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\nbutton:hover,\n.btn:hover {\n background: #f4f4f5;\n text-decoration: none;\n}\n.btn.primary,\nbutton.primary {\n background: var(--accent);\n border-color: var(--accent);\n color: #fff;\n font-weight: 500;\n}\n.btn.primary:hover,\nbutton.primary:hover {\n background: var(--accent-d);\n border-color: var(--accent-d);\n}\n.btn.sm {\n padding: 4px 10px;\n font-size: 13px;\n}\n.btn.link {\n border: none;\n background: none;\n color: var(--link);\n padding: 0;\n}\n.btn.link:hover {\n background: none;\n text-decoration: underline;\n}\nbutton[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* toggle */\n.tg {\n position: relative;\n width: 38px;\n height: 22px;\n border-radius: 22px;\n background: #cfd2d6;\n border: none;\n padding: 0;\n cursor: pointer;\n transition: background 0.12s ease;\n}\n.tg::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n transition: left 0.12s ease;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n.tg.on {\n background: var(--accent);\n}\n.tg.on::after {\n left: 18px;\n}\n.tg:hover {\n filter: brightness(0.97);\n}\n\n/* forms */\nlabel {\n display: block;\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 6px;\n}\ninput,\nselect,\ntextarea {\n width: 100%;\n background: #fff;\n border: 1px solid #cfd2d6;\n border-radius: 4px;\n color: var(--text);\n padding: 8px 10px;\n font: inherit;\n font-size: 14px;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n width: auto;\n padding: 0;\n margin: 0 6px 0 0;\n vertical-align: middle;\n}\ninput:focus,\nselect:focus,\ntextarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 2px rgba(128, 197, 128, 0.35);\n}\ntextarea {\n resize: vertical;\n line-height: 1.35;\n}\n.pem {\n min-height: 150px;\n}\n.field {\n margin-bottom: 18px;\n max-width: 420px;\n}\n.field.wide {\n max-width: none;\n}\n.hint {\n font-size: 13px;\n color: var(--muted);\n margin-top: 6px;\n font-weight: 400;\n}\n.two {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 0 24px;\n}\n\n/* bits */\n.muted {\n color: var(--muted);\n}\n.dim {\n color: var(--dim);\n}\n.big {\n font-size: 26px;\n font-weight: 600;\n letter-spacing: -0.01em;\n}\n.kfig {\n display: flex;\n gap: 44px;\n}\n.kfig .k {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 2px;\n}\n.notice {\n background: #f3faf3;\n border: 1px solid #cfe6cf;\n border-radius: 6px;\n padding: 14px 18px;\n display: flex;\n align-items: center;\n gap: 14px;\n margin-bottom: 18px;\n}\n.notice.info {\n background: #f2f7ff;\n border-color: #cfe0fb;\n}\n.notice.bad {\n background: #fdf3f3;\n border-color: #f3d0d0;\n}\n.notice .txt {\n flex: 1;\n}\n.notice b {\n font-weight: 600;\n}\n.notice .dismiss-worth {\n margin-left: auto;\n border: 0;\n background: transparent;\n color: var(--muted);\n padding: 0 4px;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n}\n.notice .dismiss-worth:hover {\n background: transparent;\n color: var(--text);\n}\n.markdown > :first-child {\n margin-top: 0;\n}\n.markdown > :last-child {\n margin-bottom: 0;\n}\n.markdown p,\n.markdown h1,\n.markdown h2,\n.markdown h3,\n.markdown ul,\n.markdown ol,\n.markdown pre {\n margin: 0 0 12px;\n}\n.markdown h1,\n.markdown h2,\n.markdown h3 {\n font-size: 15px;\n}\n.markdown ul,\n.markdown ol {\n padding-left: 22px;\n}\n.markdown pre {\n overflow-x: auto;\n padding: 10px 12px;\n border-radius: 4px;\n background: #f1f2f3;\n font-family: var(--mono);\n font-size: 13px;\n white-space: pre;\n}\n.markdown code {\n padding: 1px 4px;\n border-radius: 3px;\n background: #f1f2f3;\n}\n.markdown pre code {\n padding: 0;\n background: transparent;\n}\n.log {\n background: #1f2124;\n color: #e6e6e6;\n border-radius: 6px;\n padding: 14px 16px;\n font-family: var(--mono);\n font-size: 12.5px;\n line-height: 1.7;\n max-height: 320px;\n overflow: auto;\n white-space: pre-wrap;\n}\n.log .t {\n color: #8b9096;\n}\n.crumbs {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 14px;\n}\n.pager {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n font-size: 13px;\n color: var(--muted);\n}\n.pager .sp {\n margin-left: auto;\n}\n.empty {\n padding: 56px 24px;\n text-align: center;\n color: var(--muted);\n}\n.empty h2 {\n margin-bottom: 6px;\n}\n.steps {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.steps li {\n display: flex;\n gap: 14px;\n padding: 16px 0;\n border-bottom: 1px solid var(--border2);\n}\n.steps li:last-child {\n border-bottom: none;\n}\n.steps .n {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: #ededef;\n color: var(--muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n flex: none;\n}\n.steps li.done .n {\n background: var(--green);\n color: #fff;\n}\n.steps li.now .n {\n background: var(--accent);\n color: #fff;\n}\n.steps .body {\n flex: 1;\n}\n.steps .body b {\n display: block;\n font-weight: 600;\n margin-bottom: 2px;\n}\n\nbutton,\n.btn,\n.tg,\n.tg::after,\ntbody tr {\n transition:\n background 0.12s ease,\n border-color 0.12s ease,\n filter 0.12s ease,\n left 0.12s ease,\n color 0.12s ease;\n}\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n\n#toasts {\n position: fixed;\n right: 20px;\n bottom: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n z-index: 20;\n max-width: 360px;\n}\n.toast {\n background: #1f2124;\n color: #fff;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n}\n.fail {\n margin: 12px 18px;\n padding: 12px 14px;\n background: #fdf3f3;\n border: 1px solid #f3d0d0;\n border-radius: 6px;\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 13px;\n}\n.fail p {\n margin: 0;\n flex: 1;\n}\n\n.secret {\n margin: 0 0 16px;\n padding: 12px 14px;\n background: #fffbea;\n border: 1px solid #ecd9a0;\n border-radius: 6px;\n font-size: 13px;\n}\n.secret b {\n display: block;\n margin-bottom: 8px;\n}\n.secret-row {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n.secret-row code {\n flex: 1;\n min-width: 220px;\n word-break: break-all;\n}\n\n.sk-slot {\n display: none;\n padding: 18px;\n}\n.sk,\n.sk-row {\n height: 12px;\n background: #ededef;\n border-radius: 4px;\n margin-bottom: 10px;\n}\n.sk-row {\n height: 36px;\n}\n[data-async].busy > :not(.sk-slot):not([data-fail]) {\n visibility: hidden;\n}\n[data-async].busy > .sk-slot {\n display: block !important;\n visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n.bars {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 56px;\n}\n.bars i {\n flex: 1 1 0;\n min-width: 2px;\n align-self: stretch;\n background-image: linear-gradient(\n to top,\n var(--bar, var(--accent)) var(--h),\n transparent var(--h)\n );\n}\n.bars i:hover {\n --bar: var(--accent-d);\n background-color: var(--border2);\n}\n.trend {\n font-size: 12px;\n margin-top: 3px;\n}\n.cost-unknown {\n border-bottom: 1px dotted var(--muted);\n cursor: help;\n}\n.trend.up {\n color: var(--green);\n}\n.trend.down {\n color: var(--red);\n}\n.kfig.wrap4 {\n flex-wrap: wrap;\n gap: 28px 44px;\n}\n\n/* Review policy editor: one rule, and the tick boxes inside it, kept compact. */\n.rule {\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 12px 14px;\n margin-bottom: 10px;\n background: var(--card);\n}\n.ticks {\n display: flex;\n flex-wrap: wrap;\n gap: 2px 18px;\n border: 0;\n padding: 0;\n margin: 8px 0;\n}\n.ticks legend {\n padding: 0;\n margin-bottom: 4px;\n font-size: 13px;\n color: var(--muted);\n}\n.ticks label {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin: 2px 0;\n}\n";
@@ -1,2 +1,2 @@
1
1
  /** Generated from src/server/pages/styles.css by scripts/embed_assets.mjs — do not edit. */
2
- export const styles = ":root {\n --bg: #f6f6f7;\n --card: #fff;\n --border: #e2e2e4;\n --border2: #ededef;\n --text: #1f2124;\n --muted: #6a6f75;\n --dim: #8b9096;\n --accent: #80c580;\n --accent-d: #6cae6c;\n --link: #0b5cd5;\n --green: #1a9d5a;\n --amber: #c98a00;\n --red: #cf3c3c;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n* {\n box-sizing: border-box;\n}\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--text);\n font:\n 14px/1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n}\na {\n color: var(--link);\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\ncode,\n.mono {\n font-family: var(--mono);\n font-size: 0.9em;\n}\n\n/* top bar */\n.top {\n background: #fff;\n border-bottom: 1px solid var(--border);\n}\n.top .in {\n max-width: 1100px;\n margin: 0 auto;\n padding: 0 24px;\n height: 56px;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.logo {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n font-size: 15px;\n color: var(--text);\n}\n.logo:hover {\n text-decoration: none;\n}\n.logo img {\n height: 32px;\n width: 32px;\n display: block;\n border-radius: 5px;\n image-rendering: pixelated;\n object-fit: cover;\n}\n.brand {\n width: 180px;\n height: auto;\n display: block;\n margin: 0 0 14px;\n border-radius: 8px;\n image-rendering: pixelated;\n}\n.top .right {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n font-size: 13px;\n color: var(--muted);\n}\n\n/* layout */\n.wrap {\n max-width: 1100px;\n margin: 0 auto;\n padding: 28px 24px 72px;\n}\n.wrap.side {\n display: grid;\n grid-template-columns: 196px 1fr;\n gap: 32px;\n}\naside nav {\n display: flex;\n flex-direction: column;\n gap: 1px;\n position: sticky;\n top: 24px;\n}\naside nav a {\n padding: 7px 12px;\n border-radius: 4px;\n color: var(--text);\n font-size: 14px;\n}\naside nav a:hover {\n background: #ededef;\n text-decoration: none;\n}\naside nav a.on {\n background: #fff;\n box-shadow: inset 2px 0 0 var(--accent);\n border: 1px solid var(--border);\n font-weight: 500;\n}\naside .lbl {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--dim);\n padding: 0 12px;\n margin: 0 0 8px;\n}\n\nh1 {\n font-size: 22px;\n font-weight: 600;\n margin: 0;\n}\nh2 {\n font-size: 15px;\n font-weight: 600;\n margin: 0;\n}\np.lead {\n color: var(--muted);\n margin: 6px 0 0;\n font-size: 14px;\n}\n.pagehead {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-bottom: 22px;\n}\n.pagehead .actions {\n margin-left: auto;\n display: flex;\n gap: 8px;\n}\n\n.card {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: 6px;\n margin-bottom: 18px;\n}\n.card .hd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.card .bd {\n padding: 18px;\n}\n.card .bd.flush {\n padding: 0;\n}\n.card .ft {\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n background: #fbfbfc;\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 13px;\n color: var(--muted);\n border-radius: 0 0 6px 6px;\n}\n\n/* table */\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth {\n text-align: left;\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n padding: 10px 18px;\n border-bottom: 1px solid var(--border);\n background: #fbfbfc;\n}\ntd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border2);\n font-size: 14px;\n}\ntr:last-child td {\n border-bottom: none;\n}\ntbody tr:hover {\n background: #fafafb;\n}\n.num {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* status text, not badges */\n.st {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n}\n.st::before {\n content: \"\";\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--dim);\n}\n.st.ok::before {\n background: var(--green);\n}\n.st.warn::before {\n background: var(--amber);\n}\n.st.err::before {\n background: var(--red);\n}\n.st.run::before {\n background: var(--link);\n}\n\n/* buttons */\nbutton,\n.btn {\n font: inherit;\n font-size: 14px;\n padding: 6px 14px;\n border-radius: 4px;\n border: 1px solid var(--border);\n background: #fff;\n color: var(--text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\nbutton:hover,\n.btn:hover {\n background: #f4f4f5;\n text-decoration: none;\n}\n.btn.primary,\nbutton.primary {\n background: var(--accent);\n border-color: var(--accent);\n color: #fff;\n font-weight: 500;\n}\n.btn.primary:hover,\nbutton.primary:hover {\n background: var(--accent-d);\n border-color: var(--accent-d);\n}\n.btn.sm {\n padding: 4px 10px;\n font-size: 13px;\n}\n.btn.link {\n border: none;\n background: none;\n color: var(--link);\n padding: 0;\n}\n.btn.link:hover {\n background: none;\n text-decoration: underline;\n}\nbutton[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* toggle */\n.tg {\n position: relative;\n width: 38px;\n height: 22px;\n border-radius: 22px;\n background: #cfd2d6;\n border: none;\n padding: 0;\n cursor: pointer;\n transition: background 0.12s ease;\n}\n.tg::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n transition: left 0.12s ease;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n.tg.on {\n background: var(--accent);\n}\n.tg.on::after {\n left: 18px;\n}\n.tg:hover {\n filter: brightness(0.97);\n}\n\n/* forms */\nlabel {\n display: block;\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 6px;\n}\ninput,\nselect,\ntextarea {\n width: 100%;\n background: #fff;\n border: 1px solid #cfd2d6;\n border-radius: 4px;\n color: var(--text);\n padding: 8px 10px;\n font: inherit;\n font-size: 14px;\n}\ninput:focus,\nselect:focus,\ntextarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 2px rgba(128, 197, 128, 0.35);\n}\ntextarea {\n resize: vertical;\n line-height: 1.35;\n}\n.pem {\n min-height: 150px;\n}\n.field {\n margin-bottom: 18px;\n max-width: 420px;\n}\n.field.wide {\n max-width: none;\n}\n.hint {\n font-size: 13px;\n color: var(--muted);\n margin-top: 6px;\n font-weight: 400;\n}\n.two {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 0 24px;\n}\n\n/* bits */\n.muted {\n color: var(--muted);\n}\n.dim {\n color: var(--dim);\n}\n.big {\n font-size: 26px;\n font-weight: 600;\n letter-spacing: -0.01em;\n}\n.kfig {\n display: flex;\n gap: 44px;\n}\n.kfig .k {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 2px;\n}\n.notice {\n background: #f3faf3;\n border: 1px solid #cfe6cf;\n border-radius: 6px;\n padding: 14px 18px;\n display: flex;\n align-items: center;\n gap: 14px;\n margin-bottom: 18px;\n}\n.notice.info {\n background: #f2f7ff;\n border-color: #cfe0fb;\n}\n.notice.bad {\n background: #fdf3f3;\n border-color: #f3d0d0;\n}\n.notice .txt {\n flex: 1;\n}\n.notice b {\n font-weight: 600;\n}\n.notice .dismiss-worth {\n margin-left: auto;\n border: 0;\n background: transparent;\n color: var(--muted);\n padding: 0 4px;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n}\n.notice .dismiss-worth:hover {\n background: transparent;\n color: var(--text);\n}\n.markdown > :first-child {\n margin-top: 0;\n}\n.markdown > :last-child {\n margin-bottom: 0;\n}\n.markdown p,\n.markdown h1,\n.markdown h2,\n.markdown h3,\n.markdown ul,\n.markdown ol,\n.markdown pre {\n margin: 0 0 12px;\n}\n.markdown h1,\n.markdown h2,\n.markdown h3 {\n font-size: 15px;\n}\n.markdown ul,\n.markdown ol {\n padding-left: 22px;\n}\n.markdown pre {\n overflow-x: auto;\n padding: 10px 12px;\n border-radius: 4px;\n background: #f1f2f3;\n font-family: var(--mono);\n font-size: 13px;\n white-space: pre;\n}\n.markdown code {\n padding: 1px 4px;\n border-radius: 3px;\n background: #f1f2f3;\n}\n.markdown pre code {\n padding: 0;\n background: transparent;\n}\n.log {\n background: #1f2124;\n color: #e6e6e6;\n border-radius: 6px;\n padding: 14px 16px;\n font-family: var(--mono);\n font-size: 12.5px;\n line-height: 1.7;\n max-height: 320px;\n overflow: auto;\n white-space: pre-wrap;\n}\n.log .t {\n color: #8b9096;\n}\n.crumbs {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 14px;\n}\n.pager {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n font-size: 13px;\n color: var(--muted);\n}\n.pager .sp {\n margin-left: auto;\n}\n.empty {\n padding: 56px 24px;\n text-align: center;\n color: var(--muted);\n}\n.empty h2 {\n margin-bottom: 6px;\n}\n.steps {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.steps li {\n display: flex;\n gap: 14px;\n padding: 16px 0;\n border-bottom: 1px solid var(--border2);\n}\n.steps li:last-child {\n border-bottom: none;\n}\n.steps .n {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: #ededef;\n color: var(--muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n flex: none;\n}\n.steps li.done .n {\n background: var(--green);\n color: #fff;\n}\n.steps li.now .n {\n background: var(--accent);\n color: #fff;\n}\n.steps .body {\n flex: 1;\n}\n.steps .body b {\n display: block;\n font-weight: 600;\n margin-bottom: 2px;\n}\n\nbutton,\n.btn,\n.tg,\n.tg::after,\ntbody tr {\n transition:\n background 0.12s ease,\n border-color 0.12s ease,\n filter 0.12s ease,\n left 0.12s ease,\n color 0.12s ease;\n}\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n\n#toasts {\n position: fixed;\n right: 20px;\n bottom: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n z-index: 20;\n max-width: 360px;\n}\n.toast {\n background: #1f2124;\n color: #fff;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n}\n.fail {\n margin: 12px 18px;\n padding: 12px 14px;\n background: #fdf3f3;\n border: 1px solid #f3d0d0;\n border-radius: 6px;\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 13px;\n}\n.fail p {\n margin: 0;\n flex: 1;\n}\n\n.secret {\n margin: 0 0 16px;\n padding: 12px 14px;\n background: #fffbea;\n border: 1px solid #ecd9a0;\n border-radius: 6px;\n font-size: 13px;\n}\n.secret b {\n display: block;\n margin-bottom: 8px;\n}\n.secret-row {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n.secret-row code {\n flex: 1;\n min-width: 220px;\n word-break: break-all;\n}\n\n.sk-slot {\n display: none;\n padding: 18px;\n}\n.sk,\n.sk-row {\n height: 12px;\n background: #ededef;\n border-radius: 4px;\n margin-bottom: 10px;\n}\n.sk-row {\n height: 36px;\n}\n[data-async].busy > :not(.sk-slot):not([data-fail]) {\n visibility: hidden;\n}\n[data-async].busy > .sk-slot {\n display: block !important;\n visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n.bars {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 56px;\n}\n.bars i {\n flex: 1 1 0;\n min-width: 2px;\n align-self: stretch;\n background-image: linear-gradient(\n to top,\n var(--bar, var(--accent)) var(--h),\n transparent var(--h)\n );\n}\n.bars i:hover {\n --bar: var(--accent-d);\n background-color: var(--border2);\n}\n.trend {\n font-size: 12px;\n margin-top: 3px;\n}\n.trend.up {\n color: var(--green);\n}\n.trend.down {\n color: var(--red);\n}\n.kfig.wrap4 {\n flex-wrap: wrap;\n gap: 28px 44px;\n}\n";
2
+ export const styles = ":root {\n --bg: #f6f6f7;\n --card: #fff;\n --border: #e2e2e4;\n --border2: #ededef;\n --text: #1f2124;\n --muted: #6a6f75;\n --dim: #8b9096;\n --accent: #80c580;\n --accent-d: #6cae6c;\n --link: #0b5cd5;\n --green: #1a9d5a;\n --amber: #c98a00;\n --red: #cf3c3c;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n* {\n box-sizing: border-box;\n}\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--text);\n font:\n 14px/1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n}\na {\n color: var(--link);\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\ncode,\n.mono {\n font-family: var(--mono);\n font-size: 0.9em;\n}\n\n/* top bar */\n.top {\n background: #fff;\n border-bottom: 1px solid var(--border);\n}\n.top .in {\n max-width: 1100px;\n margin: 0 auto;\n padding: 0 24px;\n height: 56px;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.logo {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n font-size: 15px;\n color: var(--text);\n}\n.logo:hover {\n text-decoration: none;\n}\n.logo img {\n height: 32px;\n width: 32px;\n display: block;\n border-radius: 5px;\n image-rendering: pixelated;\n object-fit: cover;\n}\n.brand {\n width: 180px;\n height: auto;\n display: block;\n margin: 0 0 14px;\n border-radius: 8px;\n image-rendering: pixelated;\n}\n.top .right {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n font-size: 13px;\n color: var(--muted);\n}\n\n/* layout */\n.wrap {\n max-width: 1100px;\n margin: 0 auto;\n padding: 28px 24px 72px;\n}\n.wrap.side {\n display: grid;\n grid-template-columns: 196px 1fr;\n gap: 32px;\n}\naside nav {\n display: flex;\n flex-direction: column;\n gap: 1px;\n position: sticky;\n top: 24px;\n}\naside nav a {\n padding: 7px 12px;\n border-radius: 4px;\n color: var(--text);\n font-size: 14px;\n}\naside nav a:hover {\n background: #ededef;\n text-decoration: none;\n}\naside nav a.on {\n background: #fff;\n box-shadow: inset 2px 0 0 var(--accent);\n border: 1px solid var(--border);\n font-weight: 500;\n}\naside .lbl {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--dim);\n padding: 0 12px;\n margin: 0 0 8px;\n}\n\nh1 {\n font-size: 22px;\n font-weight: 600;\n margin: 0;\n}\nh2 {\n font-size: 15px;\n font-weight: 600;\n margin: 0;\n}\np.lead {\n color: var(--muted);\n margin: 6px 0 0;\n font-size: 14px;\n}\n.pagehead {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-bottom: 22px;\n}\n.pagehead .actions {\n margin-left: auto;\n display: flex;\n gap: 8px;\n}\n\n.card {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: 6px;\n margin-bottom: 18px;\n}\n.card .hd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.card .bd {\n padding: 18px;\n}\n.card .bd.flush {\n padding: 0;\n}\n.card .ft {\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n background: #fbfbfc;\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 13px;\n color: var(--muted);\n border-radius: 0 0 6px 6px;\n}\n\n/* table */\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth {\n text-align: left;\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n padding: 10px 18px;\n border-bottom: 1px solid var(--border);\n background: #fbfbfc;\n}\ntd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border2);\n font-size: 14px;\n}\ntr:last-child td {\n border-bottom: none;\n}\ntbody tr:hover {\n background: #fafafb;\n}\n.num {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* status text, not badges */\n.st {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n}\n.st::before {\n content: \"\";\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--dim);\n}\n.st.ok::before {\n background: var(--green);\n}\n.st.warn::before {\n background: var(--amber);\n}\n.st.err::before {\n background: var(--red);\n}\n.st.run::before {\n background: var(--link);\n}\n\n/* buttons */\nbutton,\n.btn {\n font: inherit;\n font-size: 14px;\n padding: 6px 14px;\n border-radius: 4px;\n border: 1px solid var(--border);\n background: #fff;\n color: var(--text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\nbutton:hover,\n.btn:hover {\n background: #f4f4f5;\n text-decoration: none;\n}\n.btn.primary,\nbutton.primary {\n background: var(--accent);\n border-color: var(--accent);\n color: #fff;\n font-weight: 500;\n}\n.btn.primary:hover,\nbutton.primary:hover {\n background: var(--accent-d);\n border-color: var(--accent-d);\n}\n.btn.sm {\n padding: 4px 10px;\n font-size: 13px;\n}\n.btn.link {\n border: none;\n background: none;\n color: var(--link);\n padding: 0;\n}\n.btn.link:hover {\n background: none;\n text-decoration: underline;\n}\nbutton[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* toggle */\n.tg {\n position: relative;\n width: 38px;\n height: 22px;\n border-radius: 22px;\n background: #cfd2d6;\n border: none;\n padding: 0;\n cursor: pointer;\n transition: background 0.12s ease;\n}\n.tg::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n transition: left 0.12s ease;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n.tg.on {\n background: var(--accent);\n}\n.tg.on::after {\n left: 18px;\n}\n.tg:hover {\n filter: brightness(0.97);\n}\n\n/* forms */\nlabel {\n display: block;\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 6px;\n}\ninput,\nselect,\ntextarea {\n width: 100%;\n background: #fff;\n border: 1px solid #cfd2d6;\n border-radius: 4px;\n color: var(--text);\n padding: 8px 10px;\n font: inherit;\n font-size: 14px;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n width: auto;\n padding: 0;\n margin: 0 6px 0 0;\n vertical-align: middle;\n}\ninput:focus,\nselect:focus,\ntextarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 2px rgba(128, 197, 128, 0.35);\n}\ntextarea {\n resize: vertical;\n line-height: 1.35;\n}\n.pem {\n min-height: 150px;\n}\n.field {\n margin-bottom: 18px;\n max-width: 420px;\n}\n.field.wide {\n max-width: none;\n}\n.hint {\n font-size: 13px;\n color: var(--muted);\n margin-top: 6px;\n font-weight: 400;\n}\n.two {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 0 24px;\n}\n\n/* bits */\n.muted {\n color: var(--muted);\n}\n.dim {\n color: var(--dim);\n}\n.big {\n font-size: 26px;\n font-weight: 600;\n letter-spacing: -0.01em;\n}\n.kfig {\n display: flex;\n gap: 44px;\n}\n.kfig .k {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 2px;\n}\n.notice {\n background: #f3faf3;\n border: 1px solid #cfe6cf;\n border-radius: 6px;\n padding: 14px 18px;\n display: flex;\n align-items: center;\n gap: 14px;\n margin-bottom: 18px;\n}\n.notice.info {\n background: #f2f7ff;\n border-color: #cfe0fb;\n}\n.notice.bad {\n background: #fdf3f3;\n border-color: #f3d0d0;\n}\n.notice .txt {\n flex: 1;\n}\n.notice b {\n font-weight: 600;\n}\n.notice .dismiss-worth {\n margin-left: auto;\n border: 0;\n background: transparent;\n color: var(--muted);\n padding: 0 4px;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n}\n.notice .dismiss-worth:hover {\n background: transparent;\n color: var(--text);\n}\n.markdown > :first-child {\n margin-top: 0;\n}\n.markdown > :last-child {\n margin-bottom: 0;\n}\n.markdown p,\n.markdown h1,\n.markdown h2,\n.markdown h3,\n.markdown ul,\n.markdown ol,\n.markdown pre {\n margin: 0 0 12px;\n}\n.markdown h1,\n.markdown h2,\n.markdown h3 {\n font-size: 15px;\n}\n.markdown ul,\n.markdown ol {\n padding-left: 22px;\n}\n.markdown pre {\n overflow-x: auto;\n padding: 10px 12px;\n border-radius: 4px;\n background: #f1f2f3;\n font-family: var(--mono);\n font-size: 13px;\n white-space: pre;\n}\n.markdown code {\n padding: 1px 4px;\n border-radius: 3px;\n background: #f1f2f3;\n}\n.markdown pre code {\n padding: 0;\n background: transparent;\n}\n.log {\n background: #1f2124;\n color: #e6e6e6;\n border-radius: 6px;\n padding: 14px 16px;\n font-family: var(--mono);\n font-size: 12.5px;\n line-height: 1.7;\n max-height: 320px;\n overflow: auto;\n white-space: pre-wrap;\n}\n.log .t {\n color: #8b9096;\n}\n.crumbs {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 14px;\n}\n.pager {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n font-size: 13px;\n color: var(--muted);\n}\n.pager .sp {\n margin-left: auto;\n}\n.empty {\n padding: 56px 24px;\n text-align: center;\n color: var(--muted);\n}\n.empty h2 {\n margin-bottom: 6px;\n}\n.steps {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.steps li {\n display: flex;\n gap: 14px;\n padding: 16px 0;\n border-bottom: 1px solid var(--border2);\n}\n.steps li:last-child {\n border-bottom: none;\n}\n.steps .n {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: #ededef;\n color: var(--muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n flex: none;\n}\n.steps li.done .n {\n background: var(--green);\n color: #fff;\n}\n.steps li.now .n {\n background: var(--accent);\n color: #fff;\n}\n.steps .body {\n flex: 1;\n}\n.steps .body b {\n display: block;\n font-weight: 600;\n margin-bottom: 2px;\n}\n\nbutton,\n.btn,\n.tg,\n.tg::after,\ntbody tr {\n transition:\n background 0.12s ease,\n border-color 0.12s ease,\n filter 0.12s ease,\n left 0.12s ease,\n color 0.12s ease;\n}\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n\n#toasts {\n position: fixed;\n right: 20px;\n bottom: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n z-index: 20;\n max-width: 360px;\n}\n.toast {\n background: #1f2124;\n color: #fff;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n}\n.fail {\n margin: 12px 18px;\n padding: 12px 14px;\n background: #fdf3f3;\n border: 1px solid #f3d0d0;\n border-radius: 6px;\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 13px;\n}\n.fail p {\n margin: 0;\n flex: 1;\n}\n\n.secret {\n margin: 0 0 16px;\n padding: 12px 14px;\n background: #fffbea;\n border: 1px solid #ecd9a0;\n border-radius: 6px;\n font-size: 13px;\n}\n.secret b {\n display: block;\n margin-bottom: 8px;\n}\n.secret-row {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n.secret-row code {\n flex: 1;\n min-width: 220px;\n word-break: break-all;\n}\n\n.sk-slot {\n display: none;\n padding: 18px;\n}\n.sk,\n.sk-row {\n height: 12px;\n background: #ededef;\n border-radius: 4px;\n margin-bottom: 10px;\n}\n.sk-row {\n height: 36px;\n}\n[data-async].busy > :not(.sk-slot):not([data-fail]) {\n visibility: hidden;\n}\n[data-async].busy > .sk-slot {\n display: block !important;\n visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n.bars {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 56px;\n}\n.bars i {\n flex: 1 1 0;\n min-width: 2px;\n align-self: stretch;\n background-image: linear-gradient(\n to top,\n var(--bar, var(--accent)) var(--h),\n transparent var(--h)\n );\n}\n.bars i:hover {\n --bar: var(--accent-d);\n background-color: var(--border2);\n}\n.trend {\n font-size: 12px;\n margin-top: 3px;\n}\n.cost-unknown {\n border-bottom: 1px dotted var(--muted);\n cursor: help;\n}\n.trend.up {\n color: var(--green);\n}\n.trend.down {\n color: var(--red);\n}\n.kfig.wrap4 {\n flex-wrap: wrap;\n gap: 28px 44px;\n}\n\n/* Review policy editor: one rule, and the tick boxes inside it, kept compact. */\n.rule {\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 12px 14px;\n margin-bottom: 10px;\n background: var(--card);\n}\n.ticks {\n display: flex;\n flex-wrap: wrap;\n gap: 2px 18px;\n border: 0;\n padding: 0;\n margin: 8px 0;\n}\n.ticks legend {\n padding: 0;\n margin-bottom: 4px;\n font-size: 13px;\n color: var(--muted);\n}\n.ticks label {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin: 2px 0;\n}\n";
@@ -1,16 +1,13 @@
1
1
  /** Probe GitHub credentials before they are saved. A token that cannot
2
- * authenticate, or an App missing the permissions in PLAN.md Section 6,
2
+ * authenticate, or an App missing the permissions it needs,
3
3
  * is rejected instead of stored and failing later on a review. */
4
4
  import { githubFetch, GitHubHttpError } from "../github/client.js";
5
5
  import { AppJwtClient } from "../github/app.js";
6
6
  import { GhClient } from "../github/gh.js";
7
- const APP_NEED = {
8
- metadata: "read",
9
- contents: "read",
10
- issues: "write",
11
- pull_requests: "write",
12
- checks: "write",
13
- };
7
+ import { APP_PERMISSIONS } from "../github/permissions.js";
8
+ const APP_NEED = APP_PERMISSIONS;
9
+ const AUTH_DOCS = "https://co-maintainer.com/docs/authentication.html";
10
+ const APP_DOCS = "https://co-maintainer.com/docs/github-app.html#permissions";
14
11
  export function missingAppPermissions(permissions) {
15
12
  const got = permissions ?? {};
16
13
  const missing = [];
@@ -45,7 +42,7 @@ export async function testAppAccess(opts) {
45
42
  const account = installation.account?.login ?? `installation ${installation.id}`;
46
43
  return {
47
44
  ok: false,
48
- message: `The App on ${account} is missing ${missing.join(" and ")}.`,
45
+ message: `The App on ${account} is missing ${missing.join(" and ")}. Permissions it needs: ${APP_DOCS}`,
49
46
  };
50
47
  }
51
48
  return { ok: true, installations: installations.length };
@@ -74,7 +71,7 @@ async function testPat(token) {
74
71
  if (!canRead) {
75
72
  return {
76
73
  ok: false,
77
- message: "This token cannot read repositories. Grant the repo scope.",
74
+ message: `This token cannot read repositories. Grant the repo scope. ${AUTH_DOCS}`,
78
75
  };
79
76
  }
80
77
  return { ok: true, login: user.login };
@@ -83,7 +80,7 @@ async function testPat(token) {
83
80
  if (repos.status === 403 || repos.status === 401) {
84
81
  return {
85
82
  ok: false,
86
- message: "This token cannot list repositories. Grant repository access.",
83
+ message: `This token cannot list repositories. Grant it access to the repositories with Contents, Pull requests, Issues and Metadata read access. ${AUTH_DOCS}`,
87
84
  };
88
85
  }
89
86
  if (!repos.ok) {
@@ -15,25 +15,35 @@ export declare function statsForRange(range: string): {
15
15
  findings: number | undefined;
16
16
  cost: number | undefined;
17
17
  };
18
- byRepo: (import("../store/reviews.ts").ReviewStats & {
18
+ byRepo: (import("../store/reviews.ts").CostTotals & {
19
+ reviews: number;
20
+ pullRequests: number;
21
+ findings: number;
22
+ tokensIn: number;
23
+ tokensOut: number;
24
+ failed: number;
25
+ avgDurationMs: number;
26
+ } & {
19
27
  repo: string;
20
28
  })[];
21
- byDay: {
22
- day: string;
29
+ byDay: import("../store/reviews.ts").DayStats[];
30
+ byModel: (import("../store/reviews.ts").CostTotals & {
23
31
  reviews: number;
32
+ pullRequests: number;
24
33
  findings: number;
25
- cost: number;
26
- }[];
27
- byModel: (import("../store/reviews.ts").ReviewStats & {
34
+ tokensIn: number;
35
+ tokensOut: number;
36
+ failed: number;
37
+ avgDurationMs: number;
38
+ } & {
28
39
  model: string;
29
40
  })[];
30
- byRemoteToken: {
41
+ byRemoteToken: (import("../store/reviews.ts").CostTotals & {
31
42
  tokenId: string;
32
43
  tokenName: string;
33
44
  reviews: number;
34
- cost: number;
35
45
  findings: number;
36
- }[];
46
+ })[];
37
47
  bySeverity: import("../store/findings.ts").SeverityStats[];
38
48
  };
39
49
  export type RepoListItem = {
@@ -101,6 +111,9 @@ export declare function prDetail(fullName: string, prNumber: number): {
101
111
  tokens_in: number;
102
112
  tokens_out: number;
103
113
  cost: number | null;
114
+ cost_status: "known" | "unknown" | null;
115
+ cost_note: string | null;
116
+ billed_to: "server" | "byok" | null;
104
117
  duration_ms: number | null;
105
118
  posted_review_id: string | null;
106
119
  status: string;
@@ -37,7 +37,17 @@ function fillDays(days, rows) {
37
37
  const out = [];
38
38
  for (let back = days - 1; back >= 0; back--) {
39
39
  const day = daysAgoIso(back).slice(0, 10);
40
- out.push(byDay.get(day) ?? { day, reviews: 0, findings: 0, cost: 0 });
40
+ out.push(byDay.get(day) ?? {
41
+ day,
42
+ reviews: 0,
43
+ findings: 0,
44
+ cost: 0,
45
+ knownCount: 0,
46
+ unknownCount: 0,
47
+ byokUsd: 0,
48
+ byokKnownCount: 0,
49
+ byokUnknownCount: 0,
50
+ });
41
51
  }
42
52
  return out;
43
53
  }
@@ -54,7 +64,11 @@ export function statsForRange(range) {
54
64
  change: {
55
65
  pullRequests: percentChange(previous.pullRequests, totals.pullRequests),
56
66
  findings: percentChange(previous.findings, totals.findings),
57
- cost: percentChange(previous.cost, totals.cost),
67
+ // A trend line over a partly unknown total would compare an amount to
68
+ // one that is not the same thing, so it is left out rather than misled.
69
+ cost: totals.unknownCount > 0 || previous.unknownCount > 0
70
+ ? undefined
71
+ : percentChange(previous.cost, totals.cost),
58
72
  },
59
73
  byRepo: reviewStatsByRepo(since),
60
74
  byDay: fillDays(days, reviewStatsByDay(since)),
@@ -1,5 +1,5 @@
1
1
  /** The reading half of `co-maintainer probe`, shared by the CLI and the
2
- * dashboard's Add repository preview (CORE-73).
2
+ * dashboard's Add repository preview.
3
3
  *
4
4
  * It only reads: no guide is written, nothing is cached here. The CLI adds
5
5
  * the estimate, the printing and `--run`; `/api/repos/preview` adds the