co-maintainer 0.4.13 → 0.5.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 (140) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +57 -38
  3. package/dist/main.js +2 -3
  4. package/dist/package.json +21 -15
  5. package/dist/src/ai/batch.d.ts +24 -1
  6. package/dist/src/ai/batch.js +75 -23
  7. package/dist/src/ai/estimate.d.ts +40 -0
  8. package/dist/src/ai/estimate.js +113 -0
  9. package/dist/src/ai/fake.d.ts +1 -1
  10. package/dist/src/ai/fake.js +1 -1
  11. package/dist/src/ai/hetzner.js +1 -1
  12. package/dist/src/ai/openrouter.d.ts +13 -0
  13. package/dist/src/ai/openrouter.js +66 -12
  14. package/dist/src/ai/pricing.d.ts +14 -0
  15. package/dist/src/ai/pricing.js +77 -0
  16. package/dist/src/ai/provider.js +8 -0
  17. package/dist/src/ai/verify.d.ts +25 -0
  18. package/dist/src/ai/verify.js +78 -0
  19. package/dist/src/cli/args.js +25 -29
  20. package/dist/src/cli/commands/config.d.ts +44 -0
  21. package/dist/src/cli/commands/config.js +220 -0
  22. package/dist/src/cli/commands/probe.js +70 -103
  23. package/dist/src/cli/commands/registry.d.ts +57 -0
  24. package/dist/src/cli/commands/registry.js +713 -0
  25. package/dist/src/cli/commands/review.js +66 -43
  26. package/dist/src/cli/commands/serve.d.ts +4 -2
  27. package/dist/src/cli/commands/serve.js +14 -10
  28. package/dist/src/cli/commands/set.d.ts +2 -1
  29. package/dist/src/cli/commands/set.js +63 -17
  30. package/dist/src/cli/commands/view.d.ts +1 -0
  31. package/dist/src/cli/commands/view.js +180 -0
  32. package/dist/src/cli/error.d.ts +39 -0
  33. package/dist/src/cli/error.js +64 -0
  34. package/dist/src/cli/main.d.ts +7 -0
  35. package/dist/src/cli/main.js +82 -1
  36. package/dist/src/cli/prompt.js +21 -7
  37. package/dist/src/cli/review_args.d.ts +4 -0
  38. package/dist/src/cli/review_args.js +22 -9
  39. package/dist/src/cli/review_output.d.ts +2 -2
  40. package/dist/src/cli/review_output.js +7 -6
  41. package/dist/src/cli/review_result.d.ts +59 -8
  42. package/dist/src/cli/review_result.js +145 -103
  43. package/dist/src/config.d.ts +15 -0
  44. package/dist/src/config.js +31 -0
  45. package/dist/src/github/app.d.ts +13 -0
  46. package/dist/src/github/app.js +23 -0
  47. package/dist/src/github/app_manifest.d.ts +50 -0
  48. package/dist/src/github/app_manifest.js +138 -0
  49. package/dist/src/github/client.js +7 -1
  50. package/dist/src/github/collect.js +1 -1
  51. package/dist/src/github/gh.js +75 -18
  52. package/dist/src/knowledge/facts.js +8 -3
  53. package/dist/src/knowledge/guide.d.ts +8 -0
  54. package/dist/src/knowledge/guide.js +32 -8
  55. package/dist/src/knowledge/probe.js +13 -3
  56. package/dist/src/knowledge/sections.d.ts +9 -0
  57. package/dist/src/knowledge/sections.js +16 -0
  58. package/dist/src/knowledge/skill.d.ts +5 -0
  59. package/dist/src/knowledge/skill.js +68 -21
  60. package/dist/src/knowledge/synthesis.d.ts +17 -1
  61. package/dist/src/knowledge/synthesis.js +70 -15
  62. package/dist/src/knowledge/types.d.ts +5 -0
  63. package/dist/src/local/codegraph_prepare.d.ts +3 -2
  64. package/dist/src/local/codegraph_prepare.js +4 -2
  65. package/dist/src/local/git_ops.d.ts +5 -4
  66. package/dist/src/local/git_ops.js +8 -9
  67. package/dist/src/local/review_local.js +36 -14
  68. package/dist/src/pr/checkout.js +14 -3
  69. package/dist/src/pr/codegraph_tools.js +3 -3
  70. package/dist/src/pr/diff_summary.js +3 -3
  71. package/dist/src/pr/findings.js +16 -5
  72. package/dist/src/pr/findings_json.d.ts +26 -0
  73. package/dist/src/pr/findings_json.js +187 -0
  74. package/dist/src/pr/review_copy.d.ts +20 -0
  75. package/dist/src/pr/review_copy.js +40 -0
  76. package/dist/src/pr/reviewer.d.ts +2 -1
  77. package/dist/src/pr/reviewer.js +84 -91
  78. package/dist/src/remote/client.js +42 -42
  79. package/dist/src/remote/http.d.ts +5 -0
  80. package/dist/src/remote/http.js +49 -0
  81. package/dist/src/remote/server/guides.d.ts +22 -0
  82. package/dist/src/remote/server/guides.js +81 -0
  83. package/dist/src/remote/server/routes.js +24 -0
  84. package/dist/src/review/blocking.d.ts +35 -0
  85. package/dist/src/review/blocking.js +45 -0
  86. package/dist/src/review/carry_over.d.ts +12 -1
  87. package/dist/src/review/carry_over.js +35 -9
  88. package/dist/src/review/engine.d.ts +1 -0
  89. package/dist/src/review/guides.js +28 -1
  90. package/dist/src/server/api/installations.js +1 -1
  91. package/dist/src/server/api/repos.js +86 -3
  92. package/dist/src/server/api/settings.js +3 -3
  93. package/dist/src/server/app.d.ts +3 -0
  94. package/dist/src/server/app.js +12 -5
  95. package/dist/src/server/pages/activity.d.ts +1 -0
  96. package/dist/src/server/pages/activity.js +2 -2
  97. package/dist/src/server/pages/add_repo.js +73 -5
  98. package/dist/src/server/pages/client.d.ts +1 -1
  99. package/dist/src/server/pages/client.js +1 -1
  100. package/dist/src/server/pages/home.d.ts +6 -1
  101. package/dist/src/server/pages/home.js +25 -2
  102. package/dist/src/server/pages/knowledge.js +1 -1
  103. package/dist/src/server/pages/layout.js +2 -2
  104. package/dist/src/server/pages/repo.d.ts +1 -1
  105. package/dist/src/server/pages/repo.js +21 -2
  106. package/dist/src/server/pages/repo_prs.d.ts +5 -1
  107. package/dist/src/server/pages/repo_prs.js +36 -1
  108. package/dist/src/server/pages/repo_remote.js +2 -2
  109. package/dist/src/server/pages/repo_settings.js +2 -2
  110. package/dist/src/server/pages/router.d.ts +6 -0
  111. package/dist/src/server/pages/router.js +129 -9
  112. package/dist/src/server/pages/settings.d.ts +1 -1
  113. package/dist/src/server/pages/settings.js +87 -9
  114. package/dist/src/server/pages/styles.d.ts +1 -1
  115. package/dist/src/server/pages/styles.js +1 -1
  116. package/dist/src/services/probe.d.ts +28 -0
  117. package/dist/src/services/probe.js +133 -0
  118. package/dist/src/services/remake_cron.d.ts +1 -1
  119. package/dist/src/services/remake_cron.js +3 -3
  120. package/dist/src/services/remote_review.js +14 -4
  121. package/dist/src/services/review.d.ts +11 -0
  122. package/dist/src/services/review.js +39 -11
  123. package/dist/src/services/setup.js +37 -19
  124. package/dist/src/services/setup_checklist.d.ts +10 -0
  125. package/dist/src/services/setup_checklist.js +87 -0
  126. package/dist/src/store/app_db.js +5 -2
  127. package/dist/src/store/cache_db.d.ts +4 -0
  128. package/dist/src/store/cache_db.js +27 -4
  129. package/dist/src/store/deliveries.d.ts +7 -0
  130. package/dist/src/store/deliveries.js +15 -0
  131. package/dist/src/tools/codegraph.d.ts +30 -0
  132. package/dist/src/tools/codegraph.js +49 -12
  133. package/dist/src/types.d.ts +10 -0
  134. package/dist/src/util/log.d.ts +3 -0
  135. package/dist/src/util/log.js +12 -0
  136. package/dist/src/util/run_summary.d.ts +33 -0
  137. package/dist/src/util/run_summary.js +47 -0
  138. package/dist/src/util/webhook_reachability.d.ts +11 -0
  139. package/dist/src/util/webhook_reachability.js +64 -0
  140. package/package.json +21 -15
@@ -90,9 +90,9 @@ export function renderRepoSettings(username, repo, config) {
90
90
  </div>
91
91
  <div class="card" data-async>
92
92
  ${skSlot()}
93
- <div class="hd"><h2>Scheduled remake</h2></div>
93
+ <div class="hd"><h2>Scheduled sync</h2></div>
94
94
  <div class="bd">
95
- <p class="muted" style="margin:0 0 18px">Rebuild the knowledge for this repository on a schedule. Times are UTC and a remake runs at most once an hour.</p>
95
+ <p class="muted" style="margin:0 0 18px">Rebuild the knowledge for this repository on a schedule. Times are UTC and a sync runs at most once an hour.</p>
96
96
  <div class="field wide"><label>Cron schedule</label>
97
97
  <input id="cron" placeholder="0 3 * * 1" value="${text(config.remakeCron ?? "")}">
98
98
  <div class="hint">${text(cronHint(repo, config.remakeCron))}</div></div>
@@ -13,5 +13,11 @@ export type PageDeps = {
13
13
  clientSecret: string;
14
14
  allowedUser: string;
15
15
  };
16
+ /** Behind a reverse proxy this process trusts, absolute URLs are built
17
+ * from `x-forwarded-host` and `x-forwarded-proto`. */
18
+ trustProxy?: boolean;
19
+ /** `CM_LOGIN_HINT`, shown on the sign-in page in place of the default
20
+ * "printed when serve started" line. */
21
+ loginHint?: string;
16
22
  };
17
23
  export declare function handlePageRequest(request: Request, deps: PageDeps, ip: string): Promise<Response | undefined>;
@@ -1,9 +1,10 @@
1
1
  import { clearOauthStateCookieHeader, clearSessionCookieHeader, createSession, login, logout, oauthStateCookieHeader, randomToken, readOauthState, readSessionToken, sessionCookieHeader, verifySession, } from "../auth.js";
2
2
  import { githubAuthorizeUrl, githubLoginFromCode } from "../../github/oauth.js";
3
3
  import { handleClient, handleLogo, handleStyles } from "../assets.js";
4
- import { readConfig } from "../../config.js";
5
4
  import { activityFeed, listReposForHome, prDetail, repoKnowledge, RepoNotFound, repoOverview, repoPulls, repoRemoteReviews, runningJobs, skippedDeliveries, statsForRange, } from "../../services/dashboard.js";
6
- import { money } from "./layout.js";
5
+ import { escapeHtml, html, money } from "./layout.js";
6
+ import { readConfig, resolveAppPrivateKey, writeUserConfig, } from "../../config.js";
7
+ import { appNameProblem, beginManifestState, buildAppManifest, consumeManifestState, convertManifest, } from "../../github/app_manifest.js";
7
8
  import { renderHome, renderLogin } from "./home.js";
8
9
  import { renderSetup } from "./setup.js";
9
10
  import { renderAddRepo } from "./add_repo.js";
@@ -19,7 +20,8 @@ import { renderSettings } from "./settings.js";
19
20
  import { getRepo } from "../../store/repos.js";
20
21
  import { getJob } from "../../store/jobs.js";
21
22
  import { getLogsSince } from "../../services/jobs.js";
22
- import { listInstallationsWithRepos } from "../../github/app.js";
23
+ import { listInstallationsWithRepos, listOpenPulls } from "../../github/app.js";
24
+ import { setupChecklist } from "../../services/setup_checklist.js";
23
25
  const REPO = /^\/repos\/([^/]+)\/([^/]+)(?:\/(pulls|knowledge|settings|remote)(?:\/(\d+))?)?$/;
24
26
  /** Matches a repository page URL. Only `pulls` is keyed by a number, so a stray
25
27
  * suffix such as `/repos/o/r/remote/7` must fall through to a 404 rather than
@@ -54,6 +56,7 @@ export async function handlePageRequest(request, deps, ip) {
54
56
  error: url.searchParams.get("error") ?? undefined,
55
57
  showPassword: auth.password,
56
58
  showGithub: auth.github,
59
+ hint: deps.loginHint,
57
60
  });
58
61
  }
59
62
  if (url.pathname === "/login" && request.method === "POST") {
@@ -103,19 +106,27 @@ export async function handlePageRequest(request, deps, ip) {
103
106
  const username = session.username;
104
107
  try {
105
108
  if (url.pathname === "/" && request.method === "GET") {
106
- return renderHome(username, listReposForHome().map(toHomeRow));
109
+ const config = readConfig();
110
+ return renderHome(username, listReposForHome().map(toHomeRow), setupChecklist(config.webhookUrl || deps.webhookUrl || ""));
107
111
  }
108
112
  if (url.pathname === "/setup" && request.method === "GET") {
109
113
  const config = readConfig();
110
114
  return renderSetup(username, {
111
115
  ai: Boolean(config.ai && config.ai !== "none" && config.token),
112
116
  github: Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat)),
113
- app: Boolean(config.githubAppId && config.githubAppPrivateKey),
117
+ app: Boolean(config.githubAppId && resolveAppPrivateKey(config)),
114
118
  });
115
119
  }
116
120
  if (url.pathname === "/repos/new" && request.method === "GET") {
117
121
  return renderAddRepo(username, await repoPicker(deps.githubApp));
118
122
  }
123
+ if (url.pathname === "/github/app-manifest" && request.method === "POST") {
124
+ return await beginAppManifest(request, url, deps);
125
+ }
126
+ if (url.pathname === "/github/app-manifest/callback" &&
127
+ request.method === "GET") {
128
+ return await finishAppManifest(request, url);
129
+ }
119
130
  if (url.pathname === "/activity" && request.method === "GET") {
120
131
  const page = Number(url.searchParams.get("page") ?? 1);
121
132
  return renderActivity(username, runningJobs(), activityFeed(page, 20), skippedDeliveries());
@@ -133,7 +144,7 @@ export async function handlePageRequest(request, deps, ip) {
133
144
  }
134
145
  if (url.pathname === "/settings" && request.method === "GET") {
135
146
  const config = readConfig();
136
- return renderSettings(username, config, config.webhookUrl || deps.webhookUrl || "");
147
+ return renderSettings(username, config, config.webhookUrl || deps.webhookUrl || "", requestBaseUrl(request, url, Boolean(deps.trustProxy)));
137
148
  }
138
149
  const match = matchRepo(url.pathname);
139
150
  if (match && request.method === "GET") {
@@ -141,14 +152,16 @@ export async function handlePageRequest(request, deps, ip) {
141
152
  const row = requireRepo(fullName);
142
153
  const sub = match[3];
143
154
  const pr = match[4] ? Number(match[4]) : undefined;
144
- if (!sub)
145
- return renderRepo(username, repoOverview(fullName));
155
+ if (!sub) {
156
+ const config = readConfig();
157
+ return renderRepo(username, repoOverview(fullName), config.webhookUrl || deps.webhookUrl || "");
158
+ }
146
159
  if (sub === "pulls" && pr) {
147
160
  return renderPr(username, fullName, prDetail(fullName, pr));
148
161
  }
149
162
  if (sub === "pulls") {
150
163
  const page = Number(url.searchParams.get("page") ?? 1);
151
- return renderRepoPulls(username, fullName, repoPulls(fullName, page, 20));
164
+ return renderRepoPulls(username, fullName, repoPulls(fullName, page, 20), await openPullsFor(fullName, deps.githubApp));
152
165
  }
153
166
  if (sub === "remote") {
154
167
  return renderRepoRemote(username, fullName, repoRemoteReviews(fullName));
@@ -243,6 +256,21 @@ function isPagePath(pathname) {
243
256
  pathname === "/repos/new" ||
244
257
  pathname.startsWith("/repos/"));
245
258
  }
259
+ /** Open pull requests for the Pull requests tab, or `undefined` when the App
260
+ * is not configured or cannot see the repo. A GitHub failure degrades to
261
+ * `undefined` (the tab still works by number) rather than blanking the page. */
262
+ async function openPullsFor(fullName, githubApp) {
263
+ if (!githubApp)
264
+ return { pulls: undefined, appConfigured: false };
265
+ try {
266
+ const pulls = await listOpenPulls(githubApp.appId, githubApp.privateKeyPem, fullName);
267
+ return { pulls, appConfigured: true };
268
+ }
269
+ catch (error) {
270
+ console.error("[dashboard] Could not list open pull requests:", error instanceof Error ? (error.stack ?? error.message) : String(error));
271
+ return { pulls: undefined, appConfigured: true };
272
+ }
273
+ }
246
274
  async function sessionOf(request) {
247
275
  const token = readSessionToken(request);
248
276
  if (!token)
@@ -254,6 +282,96 @@ function safeNext(value) {
254
282
  return "/";
255
283
  return value;
256
284
  }
285
+ /** The externally visible origin. Behind a trusted proxy the browser reached
286
+ * `x-forwarded-host`/`-proto`, which is what GitHub must be told for the
287
+ * callback and webhook URLs. Without the flag, `new URL(request.url).origin`
288
+ * is the socket's own view and a spoofed header is ignored. */
289
+ function requestBaseUrl(request, url, trustProxy) {
290
+ if (!trustProxy)
291
+ return url.origin;
292
+ const last = (name) => request.headers.get(name)?.split(",").pop()?.trim();
293
+ const host = last("x-forwarded-host");
294
+ if (!host)
295
+ return url.origin;
296
+ const proto = last("x-forwarded-proto");
297
+ const scheme = proto === "https" || proto === "http"
298
+ ? proto
299
+ : url.protocol.replace(":", "");
300
+ return `${scheme}://${host}`;
301
+ }
302
+ /** Step one of the App manifest flow: build the manifest from the posted
303
+ * name and auto-submit it to GitHub. The `state` is kept server-side (ten
304
+ * minutes, single use) so a forged callback cannot write credentials. */
305
+ async function beginAppManifest(request, url, deps) {
306
+ let name = "";
307
+ try {
308
+ const form = await request.formData();
309
+ name = String(form.get("name") ?? "");
310
+ }
311
+ catch {
312
+ return new Response("Expected a form post.", { status: 400 });
313
+ }
314
+ const problem = appNameProblem(name);
315
+ if (problem)
316
+ return new Response(problem, { status: 422 });
317
+ const config = readConfig();
318
+ const baseUrl = requestBaseUrl(request, url, Boolean(deps.trustProxy));
319
+ const webhookUrl = config.webhookUrl || deps.webhookUrl || "";
320
+ const manifest = buildAppManifest({ name, baseUrl, webhookUrl });
321
+ const state = beginManifestState();
322
+ return html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
323
+ <title>Creating the GitHub App · co-maintainer</title></head>
324
+ <body>
325
+ <form id="manifest" action="https://github.com/settings/apps/new?state=${escapeHtml(state)}" method="post">
326
+ <input type="hidden" name="manifest" value="${escapeHtml(JSON.stringify(manifest).replace(/</g, "\\u003c"))}">
327
+ <noscript><button type="submit">Continue to GitHub</button></noscript>
328
+ </form>
329
+ <p>Taking you to GitHub to create the App.</p>
330
+ <script>document.getElementById("manifest").submit();</script>
331
+ </body></html>`);
332
+ }
333
+ /** Step three, after GitHub redirects back with a one-time `code`. The code
334
+ * is converted into real credentials and written to the config keys the
335
+ * manual fields already use, then the browser is sent to the install page. */
336
+ async function finishAppManifest(request, url) {
337
+ const state = url.searchParams.get("state") ?? "";
338
+ if (!consumeManifestState(state)) {
339
+ return new Response("This App creation link is no longer valid. Start again from Settings.", { status: 400 });
340
+ }
341
+ const code = url.searchParams.get("code") ?? "";
342
+ if (!code) {
343
+ return new Response("GitHub did not return a manifest code.", {
344
+ status: 400,
345
+ });
346
+ }
347
+ const config = readConfig();
348
+ let conversion;
349
+ try {
350
+ conversion = await convertManifest(code);
351
+ }
352
+ catch (error) {
353
+ console.error("[dashboard] GitHub App manifest conversion failed:", error instanceof Error ? (error.stack ?? error.message) : String(error));
354
+ return new Response("Could not create the GitHub App. Try again from Settings.", { status: 502 });
355
+ }
356
+ const patch = {
357
+ githubAppId: conversion.appId,
358
+ githubAppPrivateKey: conversion.pem,
359
+ };
360
+ if (conversion.webhookSecret) {
361
+ patch.githubWebhookSecret = conversion.webhookSecret;
362
+ }
363
+ if (conversion.clientId && !config.githubOAuthClientId) {
364
+ patch.githubOAuthClientId = conversion.clientId;
365
+ }
366
+ if (conversion.clientSecret && !config.githubOAuthClientSecret) {
367
+ patch.githubOAuthClientSecret = conversion.clientSecret;
368
+ }
369
+ await writeUserConfig(patch);
370
+ const installUrl = conversion.slug
371
+ ? `https://github.com/apps/${encodeURIComponent(conversion.slug)}/installations/new`
372
+ : "https://github.com/settings/apps";
373
+ return Response.redirect(installUrl, 303);
374
+ }
257
375
  async function handleLoginForm(request, deps, ip, auth) {
258
376
  const form = await request.formData();
259
377
  const password = String(form.get("password") ?? "");
@@ -264,6 +382,7 @@ async function handleLoginForm(request, deps, ip, auth) {
264
382
  error: "Password sign-in is disabled.",
265
383
  showPassword: auth.password,
266
384
  showGithub: auth.github,
385
+ hint: deps.loginHint,
267
386
  });
268
387
  }
269
388
  const result = await login(password, deps.passwordStore, ip);
@@ -273,6 +392,7 @@ async function handleLoginForm(request, deps, ip, auth) {
273
392
  error: "Wrong password.",
274
393
  showPassword: auth.password,
275
394
  showGithub: auth.github,
395
+ hint: deps.loginHint,
276
396
  });
277
397
  }
278
398
  return new Response(null, {
@@ -1,2 +1,2 @@
1
1
  import type { UserConfig } from "../../config.ts";
2
- export declare function renderSettings(username: string, config: UserConfig, webhookUrl: string): Response;
2
+ export declare function renderSettings(username: string, config: UserConfig, webhookUrl: string, baseUrl: string): Response;
@@ -1,10 +1,21 @@
1
1
  import { html, layout, skSlot, text } from "./layout.js";
2
+ import { resolveAppPrivateKey } from "../../config.js";
2
3
  import { VERSION } from "../../version.js";
4
+ import { defaultAppName, manifestBlockedReason, } from "../../github/app_manifest.js";
3
5
  const SOURCE_URL = "https://github.com/GroophyLifefor/co-maintainer";
4
- export function renderSettings(username, config, webhookUrl) {
6
+ export function renderSettings(username, config, webhookUrl, baseUrl) {
5
7
  const aiOk = Boolean(config.ai && config.ai !== "none" && config.token);
6
8
  const ghOk = Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat));
7
- const appOk = Boolean(config.githubAppId && config.githubAppPrivateKey);
9
+ const appOk = Boolean(config.githubAppId && resolveAppPrivateKey(config));
10
+ let host = "localhost";
11
+ try {
12
+ host = new URL(baseUrl).hostname || host;
13
+ }
14
+ catch {
15
+ // baseUrl is built by the router from a valid Request URL; keep the
16
+ // fallback rather than blanking the default App name.
17
+ }
18
+ const manifestBlock = manifestBlockedReason(webhookUrl);
8
19
  return html(layout({
9
20
  title: "Settings · co-maintainer",
10
21
  username,
@@ -25,7 +36,7 @@ export function renderSettings(username, config, webhookUrl) {
25
36
  <div>
26
37
  <div class="pagehead"><div><h1>Settings</h1>
27
38
  <p class="lead">Used by every repository unless it overrides them.</p></div></div>
28
- <div class="card" data-async>
39
+ <div class="card" id="ai" data-async>
29
40
  ${skSlot()}
30
41
  <div class="hd"><h2>Models and API key</h2>
31
42
  <span class="st ${aiOk ? "ok" : "warn"}" style="margin-left:auto">${aiOk ? "Configured" : "Not set"}</span></div>
@@ -40,12 +51,12 @@ export function renderSettings(username, config, webhookUrl) {
40
51
  <input id="token" type="password" placeholder="Leave blank to keep the current key">
41
52
  <div class="hint">${config.token ? "A key is saved" : "No key saved"}</div></div>
42
53
  <div class="two" style="max-width:none">
43
- <div class="field"><label>Main model</label>
54
+ <div class="field"><label>High model</label>
44
55
  <input id="high" value="${text(config.highModel ?? "")}">
45
- <div class="hint">Writes reviews</div></div>
46
- <div class="field"><label>Cheap model</label>
56
+ <div class="hint">Writes reviews and synthesizes the guides</div></div>
57
+ <div class="field"><label>Low model</label>
47
58
  <input id="low" value="${text(config.lowModel ?? "")}">
48
- <div class="hint">Reads history when setting up a repository</div></div>
59
+ <div class="hint">Extracts facts from history during init and sync</div></div>
49
60
  </div>
50
61
  </div>
51
62
  <div class="ft"><button class="primary" id="save-ai">Save</button></div>
@@ -71,6 +82,17 @@ export function renderSettings(username, config, webhookUrl) {
71
82
  <span class="st ${appOk ? "ok" : "warn"}" style="margin-left:auto">${appOk ? "Configured" : "Not set"}</span></div>
72
83
  <div class="bd">
73
84
  <p class="muted" style="margin:0 0 16px">Needed to post reviews on pull requests.</p>
85
+ <div style="margin:0 0 16px">
86
+ <div class="two" style="max-width:none;margin:0 0 12px">
87
+ <div class="field"><label>App name</label>
88
+ <input id="app-name" value="${text(defaultAppName(host))}">
89
+ <div class="hint">Must be unique on GitHub. Editable.</div></div>
90
+ </div>
91
+ <button class="btn" id="create-app">Create GitHub App</button>
92
+ <div class="hint" style="margin:6px 0 0">${manifestBlock
93
+ ? `${text(manifestBlock)}`
94
+ : "Opens GitHub, then fills App ID, private key, webhook secret, and OAuth below for you."}</div>
95
+ </div>
74
96
  <div class="two" style="max-width:none">
75
97
  <div class="field"><label>App ID</label>
76
98
  <input id="app-id" value="${text(config.githubAppId ?? "")}"></div>
@@ -111,7 +133,7 @@ Leave blank to keep the current key"></textarea></div>
111
133
  <div class="bd">
112
134
  <div class="field"><label>Max concurrent jobs</label>
113
135
  <input id="max-jobs" value="${text(config.maxConcurrentJobs ?? "")}" placeholder="No limit">
114
- <div class="hint">Cap how many background jobs run at once across init, remake, and review. Leave blank for no limit.</div></div>
136
+ <div class="hint">Cap how many background jobs run at once across init, sync, and review. Leave blank for no limit.</div></div>
115
137
  <div class="two" style="max-width:none;margin-top:16px">
116
138
  <div class="field"><label>Remote sync timeout (seconds)</label>
117
139
  <input id="remote-timeout" value="${text(config.remoteSyncTimeoutSeconds ?? "")}" placeholder="10"></div>
@@ -128,6 +150,7 @@ Leave blank to keep the current key"></textarea></div>
128
150
  <div class="hd"><h2>Remote review tokens</h2></div>
129
151
  <div class="bd">
130
152
  <p class="muted" style="margin:0 0 16px">Bearer tokens for <code>co-maintainer review --remote</code>. The secret is shown once when created.</p>
153
+ <div id="remote-token-secret" class="secret" hidden></div>
131
154
  <div id="remote-token-list" class="muted">Loading…</div>
132
155
  <div class="two" style="max-width:none;margin-top:16px">
133
156
  <div class="field"><label>New token name</label>
@@ -223,6 +246,25 @@ document.getElementById("save-app").addEventListener("click", function() {
223
246
  webhookUrl: document.getElementById("webhook-url").value
224
247
  });
225
248
  });
249
+ document.getElementById("create-app").addEventListener("click", function() {
250
+ var card = this.closest("[data-async]");
251
+ var name = document.getElementById("app-name").value.trim();
252
+ if (!name) {
253
+ fail(card, "Enter a name for the GitHub App.", function () {});
254
+ return;
255
+ }
256
+ var form = document.createElement("form");
257
+ form.method = "POST";
258
+ form.action = "/github/app-manifest";
259
+ form.style.display = "none";
260
+ var field = document.createElement("input");
261
+ field.type = "hidden";
262
+ field.name = "name";
263
+ field.value = name;
264
+ form.appendChild(field);
265
+ document.body.appendChild(form);
266
+ form.submit();
267
+ });
226
268
  document.getElementById("save-access").addEventListener("click", function() {
227
269
  save(this, {
228
270
  passwordAuthDisabled: !document.getElementById("password-auth").checked,
@@ -320,6 +362,42 @@ async function loadRemoteTokens() {
320
362
  }
321
363
  }
322
364
  function cardFor(btn) { return btn.closest("[data-async]"); }
365
+ function showSecret(token) {
366
+ var box = document.getElementById("remote-token-secret");
367
+ box.hidden = false;
368
+ box.replaceChildren();
369
+ var title = document.createElement("b");
370
+ title.textContent = "Copy this token now. It is not shown again.";
371
+ var row = document.createElement("div");
372
+ row.className = "secret-row";
373
+ var code = document.createElement("code");
374
+ code.textContent = token;
375
+ var copy = document.createElement("button");
376
+ copy.type = "button";
377
+ copy.className = "btn sm";
378
+ copy.textContent = "Copy";
379
+ copy.addEventListener("click", async function() {
380
+ try {
381
+ await navigator.clipboard.writeText(token);
382
+ copy.textContent = "Copied";
383
+ } catch (err) {
384
+ toast("Copy failed. Select the token and copy it by hand.");
385
+ }
386
+ });
387
+ var dismiss = document.createElement("button");
388
+ dismiss.type = "button";
389
+ dismiss.className = "btn sm";
390
+ dismiss.textContent = "I have saved it";
391
+ dismiss.addEventListener("click", function() {
392
+ box.hidden = true;
393
+ box.replaceChildren();
394
+ });
395
+ row.appendChild(code);
396
+ row.appendChild(copy);
397
+ row.appendChild(dismiss);
398
+ box.appendChild(title);
399
+ box.appendChild(row);
400
+ }
323
401
  loadRemoteTokens();
324
402
  document.getElementById("create-remote-token").addEventListener("click", function() {
325
403
  var name = document.getElementById("remote-token-name").value.trim();
@@ -329,7 +407,7 @@ document.getElementById("create-remote-token").addEventListener("click", functio
329
407
  }
330
408
  run(this, this.closest("[data-async]"), async function() {
331
409
  var created = await api("POST", "/api/remote-tokens", { name: name });
332
- prompt("Copy this token now — it will not be shown again:", created.token);
410
+ showSecret(created.token);
333
411
  document.getElementById("remote-token-name").value = "";
334
412
  await loadRemoteTokens();
335
413
  });
@@ -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.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: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";
@@ -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.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: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";
@@ -0,0 +1,28 @@
1
+ import type { ProbeResult } from "../knowledge/probe.ts";
2
+ import type { GitHubClient, Json } from "../types.ts";
3
+ export type ProbePlan = {
4
+ analysis: ProbeResult;
5
+ /** The `init` flags the analysis implies, in command order. */
6
+ recommendation: string[];
7
+ /** The whole `co-maintainer init owner/repo …` line. */
8
+ command: string;
9
+ /** The analysis report plus the recommendation and reasons, for storage. */
10
+ report: Json;
11
+ };
12
+ export declare function probePlan(client: GitHubClient, repo: string, opts: {
13
+ ghConcurrent: number;
14
+ logTime?: boolean;
15
+ log?: (message: string) => void;
16
+ }): Promise<ProbePlan>;
17
+ /** The `RepoConfig` patch that matches a recommendation, so a confirmed
18
+ * preview stores the same limits the printed command carries. */
19
+ export declare function recommendationPatch(recommendation: string[]): {
20
+ includeCodebase: boolean;
21
+ includePullRequests: boolean;
22
+ includePullRequestChanges: boolean;
23
+ includeCommitHistory: boolean;
24
+ includeHowRepoWorks: boolean;
25
+ maxPrMonths?: number;
26
+ maxCommits?: number;
27
+ maxPullRequestChangeLines?: number;
28
+ };