co-maintainer 0.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +2 -0
- package/dist/package.json +54 -0
- package/dist/src/ai/batch.d.ts +16 -0
- package/dist/src/ai/batch.js +117 -0
- package/dist/src/ai/fake.d.ts +7 -0
- package/dist/src/ai/fake.js +40 -0
- package/dist/src/ai/hetzner.d.ts +16 -0
- package/dist/src/ai/hetzner.js +69 -0
- package/dist/src/ai/mermaid.d.ts +27 -0
- package/dist/src/ai/mermaid.js +457 -0
- package/dist/src/ai/mermaid_loop.d.ts +12 -0
- package/dist/src/ai/mermaid_loop.js +100 -0
- package/dist/src/ai/openrouter.d.ts +9 -0
- package/dist/src/ai/openrouter.js +29 -0
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +63 -0
- package/dist/src/cli/args.d.ts +5 -0
- package/dist/src/cli/args.js +252 -0
- package/dist/src/cli/commands/probe.d.ts +2 -0
- package/dist/src/cli/commands/probe.js +122 -0
- package/dist/src/cli/commands/review.d.ts +3 -0
- package/dist/src/cli/commands/review.js +84 -0
- package/dist/src/cli/commands/serve.d.ts +18 -0
- package/dist/src/cli/commands/serve.js +183 -0
- package/dist/src/cli/commands/set.d.ts +7 -0
- package/dist/src/cli/commands/set.js +171 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +27 -0
- package/dist/src/cli/prompt.d.ts +4 -0
- package/dist/src/cli/prompt.js +26 -0
- package/dist/src/cli/review_args.d.ts +28 -0
- package/dist/src/cli/review_args.js +84 -0
- package/dist/src/cli/review_output.d.ts +2 -0
- package/dist/src/cli/review_output.js +10 -0
- package/dist/src/cli/review_result.d.ts +81 -0
- package/dist/src/cli/review_result.js +281 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +138 -0
- package/dist/src/git/default_branch.d.ts +5 -0
- package/dist/src/git/default_branch.js +22 -0
- package/dist/src/github/app.d.ts +43 -0
- package/dist/src/github/app.js +120 -0
- package/dist/src/github/client.d.ts +17 -0
- package/dist/src/github/client.js +66 -0
- package/dist/src/github/collect.d.ts +7 -0
- package/dist/src/github/collect.js +368 -0
- package/dist/src/github/gh.d.ts +8 -0
- package/dist/src/github/gh.js +63 -0
- package/dist/src/github/jwt.d.ts +12 -0
- package/dist/src/github/jwt.js +84 -0
- package/dist/src/github/oauth.d.ts +9 -0
- package/dist/src/github/oauth.js +51 -0
- package/dist/src/github/pat.d.ts +10 -0
- package/dist/src/github/pat.js +60 -0
- package/dist/src/knowledge/facts.d.ts +3 -0
- package/dist/src/knowledge/facts.js +325 -0
- package/dist/src/knowledge/guide.d.ts +6 -0
- package/dist/src/knowledge/guide.js +82 -0
- package/dist/src/knowledge/probe.d.ts +12 -0
- package/dist/src/knowledge/probe.js +144 -0
- package/dist/src/knowledge/sections.d.ts +2 -0
- package/dist/src/knowledge/sections.js +13 -0
- package/dist/src/knowledge/skill.d.ts +10 -0
- package/dist/src/knowledge/skill.js +106 -0
- package/dist/src/knowledge/synthesis.d.ts +7 -0
- package/dist/src/knowledge/synthesis.js +315 -0
- package/dist/src/knowledge/types.d.ts +49 -0
- package/dist/src/knowledge/types.js +1 -0
- package/dist/src/knowledge/validate.d.ts +6 -0
- package/dist/src/knowledge/validate.js +134 -0
- package/dist/src/local/carry_over_store.d.ts +18 -0
- package/dist/src/local/carry_over_store.js +40 -0
- package/dist/src/local/codegraph_exclude.d.ts +3 -0
- package/dist/src/local/codegraph_exclude.js +26 -0
- package/dist/src/local/codegraph_prepare.d.ts +17 -0
- package/dist/src/local/codegraph_prepare.js +51 -0
- package/dist/src/local/git_ops.d.ts +12 -0
- package/dist/src/local/git_ops.js +98 -0
- package/dist/src/local/git_parse.d.ts +27 -0
- package/dist/src/local/git_parse.js +131 -0
- package/dist/src/local/git_revision.d.ts +15 -0
- package/dist/src/local/git_revision.js +141 -0
- package/dist/src/local/git_untracked.d.ts +21 -0
- package/dist/src/local/git_untracked.js +79 -0
- package/dist/src/local/review_local.d.ts +4 -0
- package/dist/src/local/review_local.js +273 -0
- package/dist/src/local/review_lock.d.ts +7 -0
- package/dist/src/local/review_lock.js +45 -0
- package/dist/src/pr/checkout.d.ts +25 -0
- package/dist/src/pr/checkout.js +136 -0
- package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
- package/dist/src/pr/codegraph_tool_args.js +34 -0
- package/dist/src/pr/codegraph_tools.d.ts +15 -0
- package/dist/src/pr/codegraph_tools.js +408 -0
- package/dist/src/pr/diff_summary.d.ts +6 -0
- package/dist/src/pr/diff_summary.js +52 -0
- package/dist/src/pr/findings.d.ts +14 -0
- package/dist/src/pr/findings.js +94 -0
- package/dist/src/pr/github_revision.d.ts +4 -0
- package/dist/src/pr/github_revision.js +39 -0
- package/dist/src/pr/hunks.d.ts +18 -0
- package/dist/src/pr/hunks.js +73 -0
- package/dist/src/pr/reviewer.d.ts +40 -0
- package/dist/src/pr/reviewer.js +582 -0
- package/dist/src/pr/rounds.d.ts +17 -0
- package/dist/src/pr/rounds.js +43 -0
- package/dist/src/pr/scope.d.ts +35 -0
- package/dist/src/pr/scope.js +84 -0
- package/dist/src/pr/snapshot.d.ts +11 -0
- package/dist/src/pr/snapshot.js +1 -0
- package/dist/src/pr/suggestion.d.ts +17 -0
- package/dist/src/pr/suggestion.js +70 -0
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +199 -0
- package/dist/src/remote/client_tools.d.ts +13 -0
- package/dist/src/remote/client_tools.js +30 -0
- package/dist/src/remote/fixtures.d.ts +1 -0
- package/dist/src/remote/fixtures.js +30 -0
- package/dist/src/remote/revision_from_submit.d.ts +2 -0
- package/dist/src/remote/revision_from_submit.js +26 -0
- package/dist/src/remote/schema.d.ts +14 -0
- package/dist/src/remote/schema.js +30 -0
- package/dist/src/remote/server/body.d.ts +18 -0
- package/dist/src/remote/server/body.js +73 -0
- package/dist/src/remote/server/routes.d.ts +1 -0
- package/dist/src/remote/server/routes.js +150 -0
- package/dist/src/remote/server/sessions.d.ts +16 -0
- package/dist/src/remote/server/sessions.js +48 -0
- package/dist/src/remote/server/sync.d.ts +3 -0
- package/dist/src/remote/server/sync.js +90 -0
- package/dist/src/remote/tool_bridge.d.ts +15 -0
- package/dist/src/remote/tool_bridge.js +84 -0
- package/dist/src/remote/validate.d.ts +8 -0
- package/dist/src/remote/validate.js +248 -0
- package/dist/src/review/carry_over.d.ts +63 -0
- package/dist/src/review/carry_over.js +372 -0
- package/dist/src/review/engine.d.ts +13 -0
- package/dist/src/review/engine.js +4 -0
- package/dist/src/review/guides.d.ts +12 -0
- package/dist/src/review/guides.js +44 -0
- package/dist/src/review/revision.d.ts +26 -0
- package/dist/src/review/revision.js +57 -0
- package/dist/src/server/api/activity.d.ts +1 -0
- package/dist/src/server/api/activity.js +12 -0
- package/dist/src/server/api/analytics.d.ts +1 -0
- package/dist/src/server/api/analytics.js +9 -0
- package/dist/src/server/api/installations.d.ts +4 -0
- package/dist/src/server/api/installations.js +24 -0
- package/dist/src/server/api/jobs.d.ts +1 -0
- package/dist/src/server/api/jobs.js +86 -0
- package/dist/src/server/api/json_body.d.ts +1 -0
- package/dist/src/server/api/json_body.js +14 -0
- package/dist/src/server/api/remote_tokens.d.ts +1 -0
- package/dist/src/server/api/remote_tokens.js +71 -0
- package/dist/src/server/api/repos.d.ts +4 -0
- package/dist/src/server/api/repos.js +136 -0
- package/dist/src/server/api/settings.d.ts +1 -0
- package/dist/src/server/api/settings.js +210 -0
- package/dist/src/server/app.d.ts +27 -0
- package/dist/src/server/app.js +147 -0
- package/dist/src/server/assets.d.ts +3 -0
- package/dist/src/server/assets.js +21 -0
- package/dist/src/server/auth.d.ts +44 -0
- package/dist/src/server/auth.js +188 -0
- package/dist/src/server/errors.d.ts +8 -0
- package/dist/src/server/errors.js +10 -0
- package/dist/src/server/http.d.ts +8 -0
- package/dist/src/server/http.js +117 -0
- package/dist/src/server/logo.d.ts +1 -0
- package/dist/src/server/logo.js +2 -0
- package/dist/src/server/pages/activity.d.ts +4 -0
- package/dist/src/server/pages/activity.js +200 -0
- package/dist/src/server/pages/add_repo.d.ts +9 -0
- package/dist/src/server/pages/add_repo.js +56 -0
- package/dist/src/server/pages/analytics.d.ts +2 -0
- package/dist/src/server/pages/analytics.js +193 -0
- package/dist/src/server/pages/client.d.ts +2 -0
- package/dist/src/server/pages/client.js +2 -0
- package/dist/src/server/pages/home.d.ts +16 -0
- package/dist/src/server/pages/home.js +87 -0
- package/dist/src/server/pages/knowledge.d.ts +2 -0
- package/dist/src/server/pages/knowledge.js +64 -0
- package/dist/src/server/pages/layout.d.ts +32 -0
- package/dist/src/server/pages/layout.js +230 -0
- package/dist/src/server/pages/pr.d.ts +2 -0
- package/dist/src/server/pages/pr.js +109 -0
- package/dist/src/server/pages/repo.d.ts +2 -0
- package/dist/src/server/pages/repo.js +92 -0
- package/dist/src/server/pages/repo_prs.d.ts +2 -0
- package/dist/src/server/pages/repo_prs.js +75 -0
- package/dist/src/server/pages/repo_remote.d.ts +2 -0
- package/dist/src/server/pages/repo_remote.js +38 -0
- package/dist/src/server/pages/repo_settings.d.ts +3 -0
- package/dist/src/server/pages/repo_settings.js +127 -0
- package/dist/src/server/pages/router.d.ts +17 -0
- package/dist/src/server/pages/router.js +312 -0
- package/dist/src/server/pages/settings.d.ts +2 -0
- package/dist/src/server/pages/settings.js +337 -0
- package/dist/src/server/pages/setup.d.ts +5 -0
- package/dist/src/server/pages/setup.js +38 -0
- package/dist/src/server/pages/styles.d.ts +2 -0
- package/dist/src/server/pages/styles.js +2 -0
- package/dist/src/server/webhook/index.d.ts +1 -0
- package/dist/src/server/webhook/index.js +64 -0
- package/dist/src/server/webhook/signature.d.ts +7 -0
- package/dist/src/server/webhook/signature.js +41 -0
- package/dist/src/services/credentials.d.ts +19 -0
- package/dist/src/services/credentials.js +146 -0
- package/dist/src/services/dashboard.d.ts +165 -0
- package/dist/src/services/dashboard.js +200 -0
- package/dist/src/services/drift.d.ts +13 -0
- package/dist/src/services/drift.js +76 -0
- package/dist/src/services/jobs.d.ts +34 -0
- package/dist/src/services/jobs.js +222 -0
- package/dist/src/services/remote_review.d.ts +10 -0
- package/dist/src/services/remote_review.js +274 -0
- package/dist/src/services/remote_review_abort.d.ts +3 -0
- package/dist/src/services/remote_review_abort.js +11 -0
- package/dist/src/services/remote_token_jobs.d.ts +1 -0
- package/dist/src/services/remote_token_jobs.js +20 -0
- package/dist/src/services/remote_tokens.d.ts +13 -0
- package/dist/src/services/remote_tokens.js +57 -0
- package/dist/src/services/replies.d.ts +7 -0
- package/dist/src/services/replies.js +248 -0
- package/dist/src/services/review.d.ts +35 -0
- package/dist/src/services/review.js +671 -0
- package/dist/src/services/setup.d.ts +29 -0
- package/dist/src/services/setup.js +364 -0
- package/dist/src/services/webhook.d.ts +22 -0
- package/dist/src/services/webhook.js +401 -0
- package/dist/src/store/app_db.d.ts +10 -0
- package/dist/src/store/app_db.js +85 -0
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +63 -0
- package/dist/src/store/deliveries.d.ts +12 -0
- package/dist/src/store/deliveries.js +23 -0
- package/dist/src/store/drift.d.ts +4 -0
- package/dist/src/store/drift.js +22 -0
- package/dist/src/store/findings.d.ts +31 -0
- package/dist/src/store/findings.js +64 -0
- package/dist/src/store/installations.d.ts +9 -0
- package/dist/src/store/installations.js +35 -0
- package/dist/src/store/job_logs.d.ts +5 -0
- package/dist/src/store/job_logs.js +17 -0
- package/dist/src/store/jobs.d.ts +27 -0
- package/dist/src/store/jobs.js +86 -0
- package/dist/src/store/migrations.d.ts +5 -0
- package/dist/src/store/migrations.js +258 -0
- package/dist/src/store/remote_review_inputs.d.ts +11 -0
- package/dist/src/store/remote_review_inputs.js +25 -0
- package/dist/src/store/remote_tokens.d.ts +10 -0
- package/dist/src/store/remote_tokens.js +45 -0
- package/dist/src/store/replies.d.ts +24 -0
- package/dist/src/store/replies.js +79 -0
- package/dist/src/store/repos.d.ts +11 -0
- package/dist/src/store/repos.js +59 -0
- package/dist/src/store/reviews.d.ts +86 -0
- package/dist/src/store/reviews.js +220 -0
- package/dist/src/store/rows.d.ts +179 -0
- package/dist/src/store/rows.js +4 -0
- package/dist/src/store/sessions.d.ts +8 -0
- package/dist/src/store/sessions.js +34 -0
- package/dist/src/store/skill_state.d.ts +3 -0
- package/dist/src/store/skill_state.js +24 -0
- package/dist/src/store/sqlite.d.ts +39 -0
- package/dist/src/store/sqlite.js +77 -0
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/subjects.js +78 -0
- package/dist/src/tools/codegraph.d.ts +58 -0
- package/dist/src/tools/codegraph.js +165 -0
- package/dist/src/tools/codegraph_exec.d.ts +13 -0
- package/dist/src/tools/codegraph_exec.js +66 -0
- package/dist/src/types.d.ts +89 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util/atomic.d.ts +4 -0
- package/dist/src/util/atomic.js +78 -0
- package/dist/src/util/keyed_lock.d.ts +3 -0
- package/dist/src/util/keyed_lock.js +33 -0
- package/dist/src/util/log.d.ts +11 -0
- package/dist/src/util/log.js +42 -0
- package/dist/src/util/redact.d.ts +5 -0
- package/dist/src/util/redact.js +30 -0
- package/dist/src/util/runtime.d.ts +90 -0
- package/dist/src/util/runtime.js +206 -0
- package/dist/src/util/time.d.ts +9 -0
- package/dist/src/util/time.js +36 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +5 -0
- package/package.json +54 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { safeCopy } from "../../util/redact.js";
|
|
2
|
+
import { relativeTime } from "../../util/time.js";
|
|
3
|
+
export function escapeHtml(value) {
|
|
4
|
+
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
'"': """,
|
|
9
|
+
"'": "'",
|
|
10
|
+
})[char] ?? char);
|
|
11
|
+
}
|
|
12
|
+
export function text(value) {
|
|
13
|
+
return escapeHtml(safeCopy(String(value ?? "")));
|
|
14
|
+
}
|
|
15
|
+
function markdownInline(value) {
|
|
16
|
+
const slots = [];
|
|
17
|
+
const slot = (html) => {
|
|
18
|
+
const index = slots.push(html) - 1;
|
|
19
|
+
return `\uE000${index}\uE001`;
|
|
20
|
+
};
|
|
21
|
+
let result = escapeHtml(value);
|
|
22
|
+
result = result.replace(/`([^`\n]+)`/g, (_, code) => slot(`<code>${code}</code>`));
|
|
23
|
+
result = result.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_, label, href) => slot(`<a href="${href}" rel="noreferrer">${label}</a>`));
|
|
24
|
+
result = result.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>");
|
|
25
|
+
result = result.replace(/__([^_\n]+)__/g, "<strong>$1</strong>");
|
|
26
|
+
result = result.replace(/\*([^*\n]+)\*/g, "<em>$1</em>");
|
|
27
|
+
result = result.replace(/_([^_\n]+)_/g, "<em>$1</em>");
|
|
28
|
+
result = result.replace(/\uE000(\d+)\uE001/g, (_, index) => slots[Number(index)]);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
export function markdown(value) {
|
|
32
|
+
const lines = safeCopy(String(value ?? ""))
|
|
33
|
+
.replace(/\r\n?/g, "\n")
|
|
34
|
+
.split("\n");
|
|
35
|
+
const blocks = [];
|
|
36
|
+
let paragraph = [];
|
|
37
|
+
let list = [];
|
|
38
|
+
let listKind;
|
|
39
|
+
let code;
|
|
40
|
+
const flushParagraph = () => {
|
|
41
|
+
if (paragraph.length) {
|
|
42
|
+
blocks.push(`<p>${markdownInline(paragraph.join("\n"))}</p>`);
|
|
43
|
+
paragraph = [];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const flushList = () => {
|
|
47
|
+
if (!list.length || !listKind)
|
|
48
|
+
return;
|
|
49
|
+
blocks.push(`<${listKind}>${list.join("")}</${listKind}>`);
|
|
50
|
+
list = [];
|
|
51
|
+
listKind = undefined;
|
|
52
|
+
};
|
|
53
|
+
const flushText = () => {
|
|
54
|
+
flushParagraph();
|
|
55
|
+
flushList();
|
|
56
|
+
};
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
const fence = line.match(/^```(?:[A-Za-z0-9_-]+)?\s*$/);
|
|
59
|
+
if (fence) {
|
|
60
|
+
flushText();
|
|
61
|
+
if (code) {
|
|
62
|
+
blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}</code></pre>`);
|
|
63
|
+
code = undefined;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
code = [];
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (code) {
|
|
71
|
+
code.push(line);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!line.trim()) {
|
|
75
|
+
flushText();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const heading = line.match(/^(#{1,3})\s+(.+)$/);
|
|
79
|
+
if (heading) {
|
|
80
|
+
flushText();
|
|
81
|
+
const level = heading[1].length;
|
|
82
|
+
blocks.push(`<h${level}>${markdownInline(heading[2])}</h${level}>`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const bullet = line.match(/^\s*[-*+]\s+(.+)$/);
|
|
86
|
+
const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
|
|
87
|
+
if (bullet || ordered) {
|
|
88
|
+
const kind = bullet ? "ul" : "ol";
|
|
89
|
+
if (listKind && listKind !== kind)
|
|
90
|
+
flushList();
|
|
91
|
+
flushParagraph();
|
|
92
|
+
listKind = kind;
|
|
93
|
+
list.push(`<li>${markdownInline((bullet ?? ordered)[1])}</li>`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
flushList();
|
|
97
|
+
paragraph.push(line.trim());
|
|
98
|
+
}
|
|
99
|
+
if (code) {
|
|
100
|
+
blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}</code></pre>`);
|
|
101
|
+
}
|
|
102
|
+
flushText();
|
|
103
|
+
return `<div class="markdown">${blocks.join("")}</div>`;
|
|
104
|
+
}
|
|
105
|
+
export function money(value) {
|
|
106
|
+
const n = Number(value || 0);
|
|
107
|
+
if (!Number.isFinite(n) || n === 0)
|
|
108
|
+
return "$0.00";
|
|
109
|
+
if (Math.abs(n) >= 0.01)
|
|
110
|
+
return `$${n.toFixed(2)}`;
|
|
111
|
+
return `$${n.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
112
|
+
}
|
|
113
|
+
const compactNumber = new Intl.NumberFormat("en", {
|
|
114
|
+
notation: "compact",
|
|
115
|
+
maximumFractionDigits: 1,
|
|
116
|
+
});
|
|
117
|
+
export function count(value) {
|
|
118
|
+
const n = Number(value || 0);
|
|
119
|
+
if (!Number.isFinite(n))
|
|
120
|
+
return "0";
|
|
121
|
+
return n < 1000 ? String(Math.round(n)) : compactNumber.format(n);
|
|
122
|
+
}
|
|
123
|
+
export function duration(ms) {
|
|
124
|
+
const n = Number(ms || 0);
|
|
125
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
126
|
+
return "n/a";
|
|
127
|
+
if (n < 1000)
|
|
128
|
+
return `${Math.round(n)}ms`;
|
|
129
|
+
const sec = Math.round(n / 1000);
|
|
130
|
+
if (sec < 60)
|
|
131
|
+
return `${sec}s`;
|
|
132
|
+
return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
133
|
+
}
|
|
134
|
+
/** Rendered only when the earlier window had something to compare against,
|
|
135
|
+
* so an empty first month stays quiet rather than claiming a jump. */
|
|
136
|
+
export function delta(change, days, lowerIsBetter = false) {
|
|
137
|
+
if (change === undefined || change === 0)
|
|
138
|
+
return "";
|
|
139
|
+
const better = lowerIsBetter ? change < 0 : change > 0;
|
|
140
|
+
const sign = change > 0 ? "+" : "";
|
|
141
|
+
return `<div class="trend ${better ? "up" : "down"}">${sign}${change}% vs previous ${days} days</div>`;
|
|
142
|
+
}
|
|
143
|
+
/** A flex row of divs beats pulling in a chart library for one series, and
|
|
144
|
+
* it stays readable when a range has a single day in it. Each column is full
|
|
145
|
+
* height with the bar painted as a gradient stop, because a bar sized by its
|
|
146
|
+
* own height leaves nothing to hover over on a quiet day. */
|
|
147
|
+
export function bars(items) {
|
|
148
|
+
if (items.length === 0)
|
|
149
|
+
return "";
|
|
150
|
+
const max = Math.max(...items.map((item) => item.value));
|
|
151
|
+
return `<div class="bars">${items
|
|
152
|
+
.map((item) => `<i title="${escapeHtml(item.label)}" style="--h:${max > 0 ? Math.max(2, (item.value / max) * 100).toFixed(1) : "2"}%"></i>`)
|
|
153
|
+
.join("")}</div>`;
|
|
154
|
+
}
|
|
155
|
+
/** GitHub truncates long responses, so a count that reached its ceiling is
|
|
156
|
+
* a floor rather than a total and has to read as one. */
|
|
157
|
+
export function capped(value, cap) {
|
|
158
|
+
return value >= cap ? `${cap}+` : String(value);
|
|
159
|
+
}
|
|
160
|
+
export function when(iso) {
|
|
161
|
+
if (!iso)
|
|
162
|
+
return "never";
|
|
163
|
+
const abs = new Date(iso);
|
|
164
|
+
const title = Number.isNaN(abs.getTime())
|
|
165
|
+
? iso
|
|
166
|
+
: abs
|
|
167
|
+
.toISOString()
|
|
168
|
+
.replace("T", " ")
|
|
169
|
+
.replace(/\.\d{3}Z$/, " UTC");
|
|
170
|
+
return `<time datetime="${escapeHtml(iso)}" title="${escapeHtml(title)}">${text(relativeTime(iso))}</time>`;
|
|
171
|
+
}
|
|
172
|
+
export function skSlot(kind = "block") {
|
|
173
|
+
if (kind === "table") {
|
|
174
|
+
return `<div class="sk-slot" hidden><div class="sk-row"></div><div class="sk-row"></div><div class="sk-row"></div></div>`;
|
|
175
|
+
}
|
|
176
|
+
return `<div class="sk-slot" hidden><div class="sk"></div><div class="sk"></div></div>`;
|
|
177
|
+
}
|
|
178
|
+
export function layout(opts) {
|
|
179
|
+
const right = opts.username
|
|
180
|
+
? `<div class="right">
|
|
181
|
+
<a href="/activity"${opts.active === "activity"
|
|
182
|
+
? ` style="color:var(--text);font-weight:500"`
|
|
183
|
+
: ""}>Activity</a>
|
|
184
|
+
<a href="/analytics"${opts.active === "analytics"
|
|
185
|
+
? ` style="color:var(--text);font-weight:500"`
|
|
186
|
+
: ""}>Usage</a>
|
|
187
|
+
<a href="/settings"${opts.active === "settings"
|
|
188
|
+
? ` style="color:var(--text);font-weight:500"`
|
|
189
|
+
: ""}>Settings</a>
|
|
190
|
+
<span>${text(opts.username)}</span>
|
|
191
|
+
</div>`
|
|
192
|
+
: "";
|
|
193
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
194
|
+
<title>${text(opts.title)}</title><link rel="stylesheet" href="/styles.css"><link rel="icon" href="/logo.png" type="image/png"></head><body>
|
|
195
|
+
<div class="top"><div class="in">
|
|
196
|
+
<a class="logo" href="/"><img src="/logo.png" alt="">co-maintainer</a>
|
|
197
|
+
${right}
|
|
198
|
+
</div></div>
|
|
199
|
+
${opts.body}
|
|
200
|
+
<div id="toasts"></div>
|
|
201
|
+
<script src="/client.js"></script>
|
|
202
|
+
</body></html>`;
|
|
203
|
+
}
|
|
204
|
+
export function html(body, status = 200) {
|
|
205
|
+
return new Response(body, {
|
|
206
|
+
status,
|
|
207
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
export function repoNav(fullName, on) {
|
|
211
|
+
const short = text(fullName.split("/")[1] ?? fullName);
|
|
212
|
+
const href = `/repos/${fullName}`;
|
|
213
|
+
const item = (id, label, path) => `<a href="${path}"${on === id ? ` class="on"` : ""}>${label}</a>`;
|
|
214
|
+
return `<aside>
|
|
215
|
+
<p class="lbl">${short}</p>
|
|
216
|
+
<nav>
|
|
217
|
+
${item("overview", "Overview", href)}
|
|
218
|
+
${item("pulls", "Pull requests", `${href}/pulls`)}
|
|
219
|
+
${item("remote", "Remote", `${href}/remote`)}
|
|
220
|
+
${item("knowledge", "Knowledge", `${href}/knowledge`)}
|
|
221
|
+
${item("settings", "Settings", `${href}/settings`)}
|
|
222
|
+
</nav>
|
|
223
|
+
</aside>`;
|
|
224
|
+
}
|
|
225
|
+
export function empty(title, lead) {
|
|
226
|
+
return `<div class="empty"><h2>${text(title)}</h2><p>${text(lead)}</p></div>`;
|
|
227
|
+
}
|
|
228
|
+
export function statusClass(kind, label) {
|
|
229
|
+
return `<span class="st ${kind}">${text(label)}</span>`;
|
|
230
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { empty, html, layout, markdown, money, repoNav, skSlot, text, when, } from "./layout.js";
|
|
2
|
+
export function renderPr(username, fullName, data) {
|
|
3
|
+
const n = data.prNumber;
|
|
4
|
+
const totalCost = data.reviews.reduce((sum, review) => sum + Number(review.cost ?? 0), 0);
|
|
5
|
+
const stateLabel = (state) => {
|
|
6
|
+
if (state === "closed")
|
|
7
|
+
return "Closed";
|
|
8
|
+
if (state === "open")
|
|
9
|
+
return "Still open";
|
|
10
|
+
return "New";
|
|
11
|
+
};
|
|
12
|
+
const findingList = (findings) => {
|
|
13
|
+
if (findings.length === 0) {
|
|
14
|
+
return empty("No findings", "This review did not report anything to fix.");
|
|
15
|
+
}
|
|
16
|
+
const order = ["new", "open", "closed"];
|
|
17
|
+
const groups = order
|
|
18
|
+
.map((state) => ({
|
|
19
|
+
state,
|
|
20
|
+
items: findings.filter((f) => (f.state ?? "new") === state),
|
|
21
|
+
}))
|
|
22
|
+
.filter((group) => group.items.length > 0);
|
|
23
|
+
return groups
|
|
24
|
+
.map((group) => `<div style="margin-bottom:20px">
|
|
25
|
+
<div class="muted" style="font-size:13px;font-weight:600;margin-bottom:10px;text-transform:uppercase;letter-spacing:.04em">${text(stateLabel(group.state))}</div>
|
|
26
|
+
${group.items
|
|
27
|
+
.map((finding, index) => `<div${index < group.items.length - 1
|
|
28
|
+
? ` style="padding-bottom:18px;border-bottom:1px solid var(--border2);margin-bottom:18px"`
|
|
29
|
+
: ""}>
|
|
30
|
+
<div style="margin-bottom:6px"><b>${text(finding.title)}</b>${finding.first_seen_review_id
|
|
31
|
+
? ` <span class="muted" style="font-size:13px">Seen in an earlier round</span>`
|
|
32
|
+
: ""}</div>
|
|
33
|
+
<div class="mono muted" style="font-size:13px;margin-bottom:8px">${text(finding.path ? `${finding.path}:${finding.line_from ?? ""}` : "")}</div>
|
|
34
|
+
${markdown(finding.body_md)}
|
|
35
|
+
</div>`)
|
|
36
|
+
.join("")}
|
|
37
|
+
</div>`)
|
|
38
|
+
.join("");
|
|
39
|
+
};
|
|
40
|
+
const findingsBlock = data.reviews.length === 0
|
|
41
|
+
? empty("No findings", "Run a review to post findings on this pull request.")
|
|
42
|
+
: data.reviews
|
|
43
|
+
.map((review, index) => `<details${index === 0 ? " open" : ""} style="padding:14px 0${index < data.reviews.length - 1
|
|
44
|
+
? ";border-bottom:1px solid var(--border2)"
|
|
45
|
+
: ""}">
|
|
46
|
+
<summary style="cursor:pointer">
|
|
47
|
+
<b>Round ${text(review.round)}</b>
|
|
48
|
+
<span class="muted"> · ${text((review.head_sha ?? "").slice(0, 7))} · ${review.findings.length} finding${review.findings.length === 1 ? "" : "s"}</span>
|
|
49
|
+
</summary>
|
|
50
|
+
<div style="padding:16px 4px 4px">${findingList(review.findings)}</div>
|
|
51
|
+
</details>`)
|
|
52
|
+
.join("");
|
|
53
|
+
const reviewsTable = data.reviews.length === 0
|
|
54
|
+
? empty("Not reviewed yet", "Run a review to post findings on this pull request.")
|
|
55
|
+
: `<table>
|
|
56
|
+
<thead><tr><th>Round</th><th>When</th><th>Commit</th><th>Looked at</th>
|
|
57
|
+
<th class="num">Findings</th><th class="num">Cost</th></tr></thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
${data.reviews
|
|
60
|
+
.map((review) => `<tr><td>${text(review.round)}</td>
|
|
61
|
+
<td>${when(review.created_at)}</td>
|
|
62
|
+
<td class="mono">${text((review.head_sha ?? "").slice(0, 7))}</td>
|
|
63
|
+
<td class="muted">${text(review.scope === "incremental"
|
|
64
|
+
? "Changes since the last review"
|
|
65
|
+
: "Whole pull request")}</td>
|
|
66
|
+
<td class="num">${review.findings_count}</td>
|
|
67
|
+
<td class="num">${money(Number(review.cost ?? 0))}</td></tr>`)
|
|
68
|
+
.join("")}
|
|
69
|
+
</tbody>
|
|
70
|
+
</table>`;
|
|
71
|
+
return html(layout({
|
|
72
|
+
title: `#${n} · co-maintainer`,
|
|
73
|
+
username,
|
|
74
|
+
body: `<div class="wrap side">
|
|
75
|
+
${repoNav(fullName, "pulls")}
|
|
76
|
+
<div data-async>
|
|
77
|
+
${skSlot()}
|
|
78
|
+
<div class="crumbs"><a href="/">Repositories</a> /
|
|
79
|
+
<a href="/repos/${text(fullName)}">${text(fullName)}</a> /
|
|
80
|
+
<a href="/repos/${text(fullName)}/pulls">Pull requests</a> / #${n}</div>
|
|
81
|
+
<div class="pagehead">
|
|
82
|
+
<div>
|
|
83
|
+
<h1>#${n}</h1>
|
|
84
|
+
<p class="lead">${data.reviews.length} review${data.reviews.length === 1 ? "" : "s"} on record</p>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="actions">
|
|
87
|
+
<button id="again">Review again</button>
|
|
88
|
+
<a class="btn" href="https://github.com/${text(fullName)}/pull/${n}">Open on GitHub</a>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="card">
|
|
92
|
+
<div class="hd"><h2>Findings</h2>
|
|
93
|
+
<span class="muted" style="margin-left:auto;font-size:13px">${data.reviews.reduce((sum, review) => sum + review.findings.length, 0)} findings across ${data.reviews.length} review${data.reviews.length === 1 ? "" : "s"}</span></div>
|
|
94
|
+
<div class="bd">${findingsBlock}</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="card">
|
|
97
|
+
<div class="hd"><h2>Reviews</h2>
|
|
98
|
+
<span class="muted" style="margin-left:auto;font-size:13px">${money(totalCost)} total</span></div>
|
|
99
|
+
<div class="bd flush">${reviewsTable}</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<script>
|
|
104
|
+
document.getElementById("again").addEventListener("click", function() {
|
|
105
|
+
postAndGo("/api/repos/" + ${JSON.stringify(fullName)} + "/pulls/${n}/review", {}, "/activity", this);
|
|
106
|
+
});
|
|
107
|
+
</script>`,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { capped, empty, html, layout, money, repoNav, skSlot, text, when, } from "./layout.js";
|
|
2
|
+
import { COMPARE_FILE_CAP, MAX_COMMITS_COUNTED } from "../../services/drift.js";
|
|
3
|
+
export function renderRepo(username, data) {
|
|
4
|
+
const name = data.repo.full_name;
|
|
5
|
+
const stats = data.stats;
|
|
6
|
+
const drift = data.drift;
|
|
7
|
+
const notice = drift &&
|
|
8
|
+
(drift.prs_since > 0 || drift.prs_updated > 0 || drift.commits_since > 0)
|
|
9
|
+
? `<div class="notice" data-async>
|
|
10
|
+
${skSlot()}
|
|
11
|
+
<div class="txt"><b>Update recommended.</b> Since the guide was built:
|
|
12
|
+
${drift.prs_since} new pull requests, ${drift.prs_updated} changed pull requests, ${capped(drift.commits_since, MAX_COMMITS_COUNTED)} commits, ${capped(drift.files_changed, COMPARE_FILE_CAP)} files changed.</div>
|
|
13
|
+
<button class="btn primary" id="remake">Update now</button>
|
|
14
|
+
</div>`
|
|
15
|
+
: "";
|
|
16
|
+
const pulls = data.recentPulls.length === 0
|
|
17
|
+
? empty("No reviews yet", "Reviews appear here after a pull request is opened.")
|
|
18
|
+
: `<table>
|
|
19
|
+
<thead><tr><th>Pull request</th><th>Reviewed</th><th class="num">Findings</th></tr></thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
${data.recentPulls
|
|
22
|
+
.map((review) => `<tr><td><a href="/repos/${text(name)}/pulls/${review.pr_number}">#${review.pr_number}</a></td>
|
|
23
|
+
<td class="muted">${when(review.created_at)}</td>
|
|
24
|
+
<td class="num">${review.findings_count}</td></tr>`)
|
|
25
|
+
.join("")}
|
|
26
|
+
</tbody>
|
|
27
|
+
</table>`;
|
|
28
|
+
const auto = data.repo.auto_review === 1;
|
|
29
|
+
return html(layout({
|
|
30
|
+
title: `${name} · co-maintainer`,
|
|
31
|
+
username,
|
|
32
|
+
body: `<div class="wrap side">
|
|
33
|
+
${repoNav(name, "overview")}
|
|
34
|
+
<div>
|
|
35
|
+
<div class="crumbs"><a href="/">Repositories</a> / ${text(name)}</div>
|
|
36
|
+
<div class="pagehead">
|
|
37
|
+
<div>
|
|
38
|
+
<h1>${text(name)}</h1>
|
|
39
|
+
<p class="lead">${auto
|
|
40
|
+
? "Reviewing pull requests automatically"
|
|
41
|
+
: "Automatic review is off"} · knowledge ${data.repo.knowledge_built_at
|
|
42
|
+
? when(data.repo.knowledge_built_at)
|
|
43
|
+
: "not built yet"}</p>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="actions">
|
|
46
|
+
<a class="btn" href="https://github.com/${text(name)}">Open on GitHub</a>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
${notice}
|
|
50
|
+
<div class="card">
|
|
51
|
+
<div class="hd"><h2>Last 30 days</h2></div>
|
|
52
|
+
<div class="bd">
|
|
53
|
+
<div class="kfig">
|
|
54
|
+
<div><div class="k">Pull requests reviewed</div><div class="big">${stats.pullRequests}</div></div>
|
|
55
|
+
<div><div class="k">Findings</div><div class="big">${stats.findings}</div></div>
|
|
56
|
+
<div><div class="k">Cost</div><div class="big">${money(stats.cost)}</div></div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="card">
|
|
61
|
+
<div class="hd"><h2>Recent pull requests</h2>
|
|
62
|
+
<a class="btn sm" style="margin-left:auto" href="/repos/${text(name)}/pulls">View all</a></div>
|
|
63
|
+
<div class="bd flush">${pulls}</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="card" data-async>
|
|
66
|
+
${skSlot()}
|
|
67
|
+
<div class="hd"><h2>Automatic review</h2></div>
|
|
68
|
+
<div class="bd">
|
|
69
|
+
<div style="display:flex;align-items:center;gap:14px">
|
|
70
|
+
<button class="tg${auto ? " on" : ""}" id="auto"></button>
|
|
71
|
+
<div>
|
|
72
|
+
<div><b>${auto ? "On" : "Off"}</b>. ${auto
|
|
73
|
+
? "Every new push to an open pull request gets reviewed."
|
|
74
|
+
: "New pushes are not reviewed until you turn this on."}</div>
|
|
75
|
+
<div class="hint" style="margin:2px 0 0">Drafts and bot pull requests follow the skip settings.
|
|
76
|
+
<a href="/repos/${text(name)}/settings">Change</a></div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<script>
|
|
84
|
+
var repo = ${JSON.stringify(name)};
|
|
85
|
+
bindToggle(document.getElementById("auto"), "/api/repos/" + repo, "autoReview");
|
|
86
|
+
var remake = document.getElementById("remake");
|
|
87
|
+
if (remake) remake.addEventListener("click", function() {
|
|
88
|
+
postAndGo("/api/repos/" + repo + "/remake", {}, "/activity", remake);
|
|
89
|
+
});
|
|
90
|
+
</script>`,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { empty, html, layout, money, repoNav, skSlot, text, when, } from "./layout.js";
|
|
2
|
+
export function renderRepoPulls(username, fullName, data) {
|
|
3
|
+
const start = (data.page - 1) * data.per + 1;
|
|
4
|
+
const end = Math.min(data.page * data.per, data.total);
|
|
5
|
+
const pager = data.total === 0
|
|
6
|
+
? ""
|
|
7
|
+
: `<div class="pager">
|
|
8
|
+
<span>${start}-${end} of ${data.total}</span>
|
|
9
|
+
<span class="sp"></span>
|
|
10
|
+
${data.page > 1
|
|
11
|
+
? `<a class="btn sm" href="?page=${data.page - 1}">Previous</a>`
|
|
12
|
+
: `<button class="btn sm" disabled>Previous</button>`}
|
|
13
|
+
${end < data.total
|
|
14
|
+
? `<a class="btn sm" href="?page=${data.page + 1}">Next</a>`
|
|
15
|
+
: `<button class="btn sm" disabled>Next</button>`}
|
|
16
|
+
</div>`;
|
|
17
|
+
const table = data.items.length === 0
|
|
18
|
+
? empty("No pull requests reviewed", "A review of an open pull request will show up here.")
|
|
19
|
+
: `<table>
|
|
20
|
+
<thead><tr><th>Pull request</th><th>Last reviewed</th>
|
|
21
|
+
<th class="num">Findings</th><th class="num">Cost</th></tr></thead>
|
|
22
|
+
<tbody>
|
|
23
|
+
${data.items
|
|
24
|
+
.map((item) => `<tr><td><a href="/repos/${text(fullName)}/pulls/${item.pr_number}">#${item.pr_number}</a></td>
|
|
25
|
+
<td class="muted">${when(item.last_reviewed)}</td>
|
|
26
|
+
<td class="num">${item.findings}</td>
|
|
27
|
+
<td class="num">${money(item.cost)}</td></tr>`)
|
|
28
|
+
.join("")}
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>${pager}`;
|
|
31
|
+
const [owner, repo] = fullName.split("/");
|
|
32
|
+
const reviewBase = `/api/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/`;
|
|
33
|
+
return html(layout({
|
|
34
|
+
title: `Pull requests · ${fullName}`,
|
|
35
|
+
username,
|
|
36
|
+
body: `<div class="wrap side">
|
|
37
|
+
${repoNav(fullName, "pulls")}
|
|
38
|
+
<div>
|
|
39
|
+
<div class="crumbs"><a href="/">Repositories</a> /
|
|
40
|
+
<a href="/repos/${text(fullName)}">${text(fullName)}</a> / Pull requests</div>
|
|
41
|
+
<div class="pagehead">
|
|
42
|
+
<div><h1>Pull requests</h1>
|
|
43
|
+
<p class="lead">${data.stats.pullRequests} reviewed in the last 30 days</p></div>
|
|
44
|
+
</div>
|
|
45
|
+
<div data-async>
|
|
46
|
+
${skSlot()}
|
|
47
|
+
<div class="card"><div class="bd">
|
|
48
|
+
<h2>Review a pull request</h2>
|
|
49
|
+
<p class="lead">Enter an open pull request number to start a review.</p>
|
|
50
|
+
<form id="manual-review" style="display:flex;align-items:end;gap:8px;margin-top:14px">
|
|
51
|
+
<div style="width:180px">
|
|
52
|
+
<label for="pr-number">PR number</label>
|
|
53
|
+
<input id="pr-number" name="prNumber" type="number" min="1" step="1" required>
|
|
54
|
+
</div>
|
|
55
|
+
<button class="primary" type="submit">Start review</button>
|
|
56
|
+
</form>
|
|
57
|
+
</div></div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="card"><div class="bd flush">${table}</div></div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<script>
|
|
63
|
+
document.getElementById("manual-review").addEventListener("submit", function(event) {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
var input = document.getElementById("pr-number");
|
|
66
|
+
var number = Number(input.value);
|
|
67
|
+
if (!Number.isSafeInteger(number) || number < 1) {
|
|
68
|
+
input.focus();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
postAndGo(${JSON.stringify(reviewBase)} + number + "/review", {}, "/activity", this.querySelector("button"));
|
|
72
|
+
});
|
|
73
|
+
</script>`,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { empty, html, layout, money, repoNav, text, when } from "./layout.js";
|
|
2
|
+
export function renderRepoRemote(username, fullName, data) {
|
|
3
|
+
const rows = data.items.length === 0
|
|
4
|
+
? empty("No remote reviews yet", "Developers run co-maintainer review --remote with a token from Settings.")
|
|
5
|
+
: `<table>
|
|
6
|
+
<thead><tr><th>Branch</th><th>Token</th><th>Status</th><th class="num">Findings</th>
|
|
7
|
+
<th class="num">Cost</th><th>When</th></tr></thead>
|
|
8
|
+
<tbody>
|
|
9
|
+
${data.items
|
|
10
|
+
.map((review) => `<tr>
|
|
11
|
+
<td>${text(review.branch ?? "—")}</td>
|
|
12
|
+
<td>${text(review.token_name ?? "—")}</td>
|
|
13
|
+
<td>${text(review.status)}</td>
|
|
14
|
+
<td class="num">${review.findings_count ?? 0}</td>
|
|
15
|
+
<td class="num">${money(review.cost ?? 0)}</td>
|
|
16
|
+
<td class="muted">${when(review.created_at)}</td>
|
|
17
|
+
</tr>`)
|
|
18
|
+
.join("")}
|
|
19
|
+
</tbody>
|
|
20
|
+
</table>`;
|
|
21
|
+
return html(layout({
|
|
22
|
+
title: `${fullName} · Remote · co-maintainer`,
|
|
23
|
+
username,
|
|
24
|
+
body: `<div class="wrap side">
|
|
25
|
+
${repoNav(fullName, "remote")}
|
|
26
|
+
<div>
|
|
27
|
+
<div class="crumbs"><a href="/">Repositories</a> / ${text(fullName)}</div>
|
|
28
|
+
<div class="pagehead">
|
|
29
|
+
<div>
|
|
30
|
+
<h1>Remote reviews</h1>
|
|
31
|
+
<p class="lead">CLI reviews submitted from developer machines (last 30 days).</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="card"><div class="bd flush">${rows}</div></div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>`,
|
|
37
|
+
}));
|
|
38
|
+
}
|