toolroll 0.9.86 → 0.9.88
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/CHANGELOG.md +86 -0
- package/dist/acp-check.d.ts +9 -0
- package/dist/acp-check.js +40 -28
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -70
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +329 -183
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/design/kit.css +722 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +31 -1
- package/dist/discord-settings.js +8 -4
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.js +7 -3
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +141 -30
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +38 -4
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +23 -0
- package/dist/release.js +46 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +3 -0
- package/dist/scope.js +6 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +16 -16
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +24 -12
- package/dist/server/flows.js +64 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +70 -0
- package/dist/server/kit.d.ts +419 -0
- package/dist/server/kit.js +369 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +22 -190
- package/dist/server/render-pages.js +123 -575
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +35 -5
- package/dist/server/render-tasks.js +114 -85
- package/dist/server/route-table.js +20 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +85 -2
- package/dist/server/tasks.d.ts +8 -0
- package/dist/server/tasks.js +95 -40
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +112 -3
- package/dist/store.js +292 -20
- package/dist/surface.js +5 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.js +11 -7
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +7 -4
- package/package.json +1 -1
|
@@ -7,17 +7,37 @@ export type ChatSettingsOptions = {
|
|
|
7
7
|
now?: Date;
|
|
8
8
|
who?: string;
|
|
9
9
|
};
|
|
10
|
-
/** What differs between the Slack and
|
|
10
|
+
/** What differs between the Slack, Discord and Teams settings pages; everything else is one page. */
|
|
11
11
|
export type ChatSettingsPage = {
|
|
12
|
-
provider: "slack" | "discord";
|
|
12
|
+
provider: "slack" | "discord" | "teams";
|
|
13
13
|
label: string;
|
|
14
14
|
credentials: ChatIdentity | null;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
/** The
|
|
20
|
-
|
|
15
|
+
/** One sentence on what connecting gives, shown before it is connected. */
|
|
16
|
+
intro: string;
|
|
17
|
+
/** How to create the app, as list items, shown before it is connected. */
|
|
18
|
+
steps: Html;
|
|
19
|
+
/** The connect form's own fields (the password is the kit's). */
|
|
20
|
+
fields: Html;
|
|
21
|
+
/** One line under the connect fields. */
|
|
22
|
+
fieldsNote: string;
|
|
23
|
+
/** Where the pairing message is sent ("in a direct message to your Toolroll Slack app"). */
|
|
24
|
+
pairWhere: string;
|
|
25
|
+
/** What to try once paired. */
|
|
26
|
+
tryLine: Html;
|
|
27
|
+
/** Lines shown with the connection (the Teams messaging endpoint). */
|
|
28
|
+
connection?: Html;
|
|
29
|
+
/** The status words while connected and while waiting. */
|
|
30
|
+
liveWords?: readonly [on: string, off: string];
|
|
31
|
+
/** The "Access and setup" paragraphs, if any. */
|
|
32
|
+
access?: Html;
|
|
21
33
|
};
|
|
22
|
-
/**
|
|
34
|
+
/** A secret the connect form posts (an app or bot token, a client secret): masked, never given a value, kept out of
|
|
35
|
+
* password managers. */
|
|
36
|
+
export declare function chatSecretRow(options: {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
label: string;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
}): Html;
|
|
42
|
+
/** Settings → Slack, Discord or Teams: connect the app once, then each teammate pairs their own account. */
|
|
23
43
|
export declare function chatSettingsHtml(store: Store, dir: string, options: ChatSettingsOptions, page: ChatSettingsPage): Html;
|
|
@@ -1,41 +1,72 @@
|
|
|
1
|
+
import { settingsSubPage } from "./settings-groups-ui.js";
|
|
1
2
|
import { loadPrimary } from "./webhooks.js";
|
|
2
|
-
import { html,
|
|
3
|
+
import { html, joinHtml } from "./html.js";
|
|
3
4
|
import { ChatState } from "./chat-delivery-state.js";
|
|
4
|
-
|
|
5
|
+
import { acts, btn, chip, codeLine, disclose, formRow, infoLine, input, note, notice, settingsSection } from "./server/kit.js";
|
|
6
|
+
import { kitSectionForm, SECRET_OPT_OUTS } from "./settings-kit.js";
|
|
7
|
+
/** A secret the connect form posts (an app or bot token, a client secret): masked, never given a value, kept out of
|
|
8
|
+
* password managers. */
|
|
9
|
+
export function chatSecretRow(options) {
|
|
10
|
+
return formRow(options.label, null, input({ id: options.id, name: options.name, type: "password",
|
|
11
|
+
attrs: { autocomplete: "off", spellcheck: "false", "data-so-secret": true, ...SECRET_OPT_OUTS, required: true, placeholder: options.placeholder, "aria-label": options.label } }));
|
|
12
|
+
}
|
|
13
|
+
/** Settings → Slack, Discord or Teams: connect the app once, then each teammate pairs their own account. */
|
|
5
14
|
export function chatSettingsHtml(store, dir, options, page) {
|
|
6
15
|
const { provider, label, credentials } = page, state = new ChatState(store, provider), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
|
|
7
16
|
const runtime = credentials ? state.runtime(credentials.installation) : null;
|
|
8
|
-
const now = options.now ?? new Date(), live = runtime &&
|
|
17
|
+
const now = options.now ?? new Date(), live = runtime !== null &&
|
|
9
18
|
runtime.leaseUntil !== null &&
|
|
10
19
|
runtime.leaseUntil > now.toISOString() &&
|
|
11
20
|
runtime.connected;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
21
|
+
const route = (action) => `/settings/${provider}/${action}`;
|
|
22
|
+
const problem = options.problem ? notice(options.problem) : "";
|
|
23
|
+
const access = page.access === undefined ? "" : disclose("Access and setup", page.access);
|
|
24
|
+
const sections = [];
|
|
15
25
|
if (!credentials) {
|
|
16
|
-
|
|
26
|
+
sections.push(settingsSection({ id: "setup", title: `1. Create the ${label} app`, body: page.steps }), settingsSection({ id: "connect", title: `2. Connect ${label}`,
|
|
27
|
+
body: kitSectionForm({ proof: `${provider}-open`, action: route("connect"), id: "connect-form", save: { label: `Connect ${label}` },
|
|
28
|
+
confirm: `Connect ${label}`, body: joinHtml([page.fields, note(page.fieldsNote)]) }) }), settingsSection({ id: "pair", title: "3. Pair your account", body: note(`Once ${label} is connected, pair your own account here.`) }));
|
|
29
|
+
return settingsSubPage("alerts", label, joinHtml([problem, ...sections, access]), { lead: page.intro });
|
|
30
|
+
}
|
|
31
|
+
const [onWords, offWords] = page.liveWords ?? ["Connected", "Waiting for connection"];
|
|
32
|
+
sections.push(settingsSection({ id: "connection", title: credentials.workspace, state: html ` ${chip(live ? onWords : offWords, live ? "done" : "")}`,
|
|
33
|
+
body: joinHtml([
|
|
34
|
+
page.connection ?? "",
|
|
35
|
+
runtime?.problem ? notice(String(runtime.problem)) : "",
|
|
36
|
+
!live && !runtime?.problem ? note(`Keep Toolroll's builder running to receive ${label} messages.`) : "",
|
|
37
|
+
disclose("Connection settings", kitSectionForm({ proof: `${provider}-close`, action: route("disconnect"), id: "disconnect-form",
|
|
38
|
+
save: { label: `Disconnect ${label}`, variant: "danger" }, confirm: `Disconnect ${label}`, body: html `` })),
|
|
39
|
+
]) }));
|
|
40
|
+
const teammates = others ? `${others} teammate${others === 1 ? " is" : "s are"}` : "";
|
|
41
|
+
if (options.code) {
|
|
42
|
+
// The code shows once, on this page only: selectable text, never in a link or an address.
|
|
43
|
+
sections.push(settingsSection({ id: "pair", title: "Pair your account", state: " · Code ready",
|
|
44
|
+
lead: `Send this ${page.pairWhere}. It expires in 10 minutes.`,
|
|
45
|
+
body: joinHtml([codeLine(`pair ${options.code}`), note("Once you've sent it, this page says your account is paired."),
|
|
46
|
+
acts(btn("Check connection", { sm: true, href: `/settings/${provider}` }))]) }));
|
|
47
|
+
}
|
|
48
|
+
else if (binding && state.live(binding)) {
|
|
49
|
+
const { pending, dropped: failed } = state.partCounts(binding.id);
|
|
50
|
+
sections.push(settingsSection({ id: "pair", title: "Your account", state: " · Paired",
|
|
51
|
+
lead: `Your ${label} account is paired.${teammates ? ` ${teammates} paired too.` : ""}`,
|
|
52
|
+
body: joinHtml([
|
|
53
|
+
pending ? infoLine(`${pending} ${pending === 1 ? "reply is" : "replies are"} waiting to send.`) : "",
|
|
54
|
+
failed ? notice(`${failed} ${failed === 1 ? "reply" : "replies"} could not be delivered. Open the saved chat to recover them.`) : "",
|
|
55
|
+
note(page.tryLine),
|
|
56
|
+
acts(btn("Open saved chat", { sm: true, href: "/chat" })),
|
|
57
|
+
loadPrimary(process.env, dir) === provider
|
|
58
|
+
? formRow("Task updates", null, html `<span>Sent here</span>`, "inline")
|
|
59
|
+
: kitSectionForm({ proof: `${provider}-alerts`, action: route("alerts"), id: "alerts-form", save: { label: "Send task updates here", variant: "outline" },
|
|
60
|
+
body: note(`Task updates aren't sent to ${label} now.`) }),
|
|
61
|
+
kitSectionForm({ proof: `${provider}-close`, action: route("unpair"), id: "unpair-form", save: { label: "Unpair my account", variant: "danger" },
|
|
62
|
+
confirm: `Unpair your ${label} account`, body: note("Unpairing ends every open button in your chat. Teammates are unaffected.") }),
|
|
63
|
+
]) }));
|
|
17
64
|
}
|
|
18
65
|
else {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
parts.push(html `<p>Keep the Toolroll worker running to receive ${label} messages.</p>`);
|
|
24
|
-
if (options.code) {
|
|
25
|
-
parts.push(html `<h2>Pair your account</h2><p>Send this in a direct message to your Toolroll ${label} app. It expires in 10 minutes.</p>`, html `<label>Pairing message<input type="text" readonly value="pair ${options.code}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>`, html `<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/${provider}">Check connection</a>`);
|
|
26
|
-
}
|
|
27
|
-
else if (binding && state.live(binding)) {
|
|
28
|
-
const { pending, dropped: failed } = state.partCounts(binding.id);
|
|
29
|
-
parts.push(html `<p>Your ${label} account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>`, html `<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === provider
|
|
30
|
-
? html `<p>Task updates are sent here.</p>`
|
|
31
|
-
: post("alerts", html `<button type="submit">Send task updates here</button>`));
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired ${label} account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : ""}</p>${password}<button type="submit">Create pairing code</button>`));
|
|
35
|
-
}
|
|
36
|
-
if (binding && state.live(binding) && !options.code)
|
|
37
|
-
parts.push(post("unpair", html `${password}<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>`));
|
|
38
|
-
parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect ${label}</button>`)}</details>`);
|
|
66
|
+
sections.push(settingsSection({ id: "pair", title: "Pair your account", state: " · Not paired",
|
|
67
|
+
lead: `Your paired ${label} account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${teammates ? ` ${teammates} already paired.` : ""}`,
|
|
68
|
+
body: kitSectionForm({ proof: `${provider}-open`, action: route("pair"), id: "pair-form", native: true, save: { label: "Create pairing code" },
|
|
69
|
+
confirm: `Pair your ${label} account`, body: note("You get a one-time message to send to the app.") }) }));
|
|
39
70
|
}
|
|
40
|
-
return
|
|
71
|
+
return settingsSubPage("alerts", label, joinHtml([problem, ...sections, access]));
|
|
41
72
|
}
|
|
@@ -28,4 +28,5 @@ export type CheckSettingsView = {
|
|
|
28
28
|
problem: string | null;
|
|
29
29
|
};
|
|
30
30
|
export declare function checkSettingsHtml(view: CheckSettingsView): Html;
|
|
31
|
-
|
|
31
|
+
/** The follow-up list's look (result-follow-ups.ts); the checks page itself is kit markup. */
|
|
32
|
+
export declare const CHECK_SETTINGS_CSS = "\n .follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }\n .follow-ups .follow-up-act { margin: 0; }\n .follow-ups button { min-height: 44px; }\n @media (max-width: 480px) { .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: grid; } .follow-ups .follow-up-act button { width: 100%; } }\n";
|
package/dist/check-levels-ui.js
CHANGED
|
@@ -2,72 +2,81 @@
|
|
|
2
2
|
* words. One choice (Quick, Full, Off) and the quick command beside the full
|
|
3
3
|
* one. Every change takes the password and lands in the ledger; approving a
|
|
4
4
|
* command restates exactly what will run, unattended, before the yes. */
|
|
5
|
-
import { html,
|
|
5
|
+
import { html, joinHtml } from "./html.js";
|
|
6
6
|
import { CHECK_LEVELS, CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS } from "./check-levels.js";
|
|
7
7
|
import { BATCH_HINT } from "./batch-policy.js";
|
|
8
|
+
import { codeLine, disclose, formRow, infoLine, input, note, notice, radios, settingsSection, textarea } from "./server/kit.js";
|
|
9
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
10
|
+
const list = (items) => html `<ul class="k-note">${items.map(one => html `<li>${one}</li>`)}</ul>`;
|
|
8
11
|
export function checkSettingsHtml(view) {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
view.said === null ? "" :
|
|
13
|
-
view.
|
|
14
|
-
];
|
|
15
|
-
const
|
|
16
|
-
const command = (one) => html `<p class="mono check-command">${one.command}</p><p class="meta">Up to ${Math.round(one.timeoutMs / 1000)}s · approved by ${one.approvedBy}</p>`;
|
|
12
|
+
const form = (id, act, body, save, extra = {}) => kitSectionForm({ proof: "checks", action: "/settings/checks", id, hidden: { repo: view.repo, act, ...extra }, save, body });
|
|
13
|
+
const head = joinHtml([
|
|
14
|
+
view.problem === null ? "" : notice(view.problem),
|
|
15
|
+
view.said === null ? "" : infoLine(view.said),
|
|
16
|
+
note(html `${view.name} · <span class="k-mono">${view.repo}</span>`),
|
|
17
|
+
]);
|
|
18
|
+
const command = (one) => joinHtml([codeLine(one.command), note(`Up to ${Math.round(one.timeoutMs / 1000)}s · approved by ${one.approvedBy}`)]);
|
|
17
19
|
// Quick with no quick command runs the full check; say so where the choice is made.
|
|
18
20
|
const fallsBack = view.level === "quick" && view.quick === null;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const levelNotes = joinHtml([
|
|
22
|
+
fallsBack ? note("No quick command yet, so builds run the full check.") : "",
|
|
23
|
+
view.level !== "full" && view.full !== null ? note("The full check still runs when a pull request opens, and Merge waits for it.") : "",
|
|
24
|
+
]);
|
|
25
|
+
const level = settingsSection({ id: "check-level", title: "After each build", state: ` · ${CHECK_LEVEL_WORDS[view.level]}`, lead: CHECK_LEVEL_HINTS[view.level],
|
|
26
|
+
body: !view.canChange ? levelNotes : joinHtml([levelNotes, kitSectionForm({ proof: "checks", action: "/settings/checks", id: "check-level-form", gate: true,
|
|
27
|
+
hidden: { repo: view.repo, act: "level" }, confirm: "Change checks",
|
|
28
|
+
body: joinHtml([
|
|
29
|
+
formRow("After each build", "A task can choose its own checks when it's filed. With Quick or Off, the full check runs when a pull request opens, and Merge waits for it unless you merge anyway.", radios({ name: "level", legend: "After each build", current: view.level, items: CHECK_LEVELS.map(one => ({ value: one, label: CHECK_LEVEL_WORDS[one], text: CHECK_LEVEL_HINTS[one] })) }), "wide"),
|
|
30
|
+
]) })]) });
|
|
31
|
+
const commandForm = (id, value, submit) => form(id, "quick", joinHtml([
|
|
32
|
+
formRow("Command", null, textarea({ id: `${id}-command`, name: "command", value, rows: 2, attrs: { maxlength: 2000, spellcheck: "false", required: true, "aria-label": "Command" } }), "wide"),
|
|
33
|
+
formRow("Time limit", "Seconds, up to 3600.", input({ id: `${id}-timeout`, name: "timeout", type: "number", value: String(view.quick === null ? 180 : Math.round(view.quick.timeoutMs / 1000)),
|
|
34
|
+
attrs: { min: 1, max: 3600, required: true, "aria-label": "Time limit in seconds" } }), "inline"),
|
|
35
|
+
list(["Runs unattended right after every Quick build, in that build's checkout.", "Plain environment: no credentials.", "A failure shows on the result; nothing is blocked."]),
|
|
36
|
+
]), { label: submit, variant: "outline" });
|
|
37
|
+
const quick = settingsSection({ id: "quick-check", title: "Quick check", state: view.quick === null ? " · None yet" : "",
|
|
38
|
+
lead: view.quick === null && view.canChange
|
|
39
|
+
? view.suggestion === null ? "Typecheck and the tests near the change, ideally under 2 minutes." : "Suggested from the project's scripts. Edit it before approving."
|
|
40
|
+
: undefined,
|
|
41
|
+
body: view.quick === null
|
|
42
|
+
? view.canChange ? commandForm("quick-form", view.suggestion ?? "", "Approve quick check") : ""
|
|
43
|
+
: joinHtml([command(view.quick), !view.canChange ? "" : joinHtml([
|
|
44
|
+
disclose("Change quick check", commandForm("quick-change-form", view.quick.command, "Approve change")),
|
|
45
|
+
disclose("Remove quick check", form("quick-clear-form", "quick-clear", note("Quick builds run the full check instead."), { label: "Remove", variant: "danger" })),
|
|
46
|
+
])]) });
|
|
47
|
+
const full = settingsSection({ id: "full-check", title: "Full check", state: view.full === null ? " · None yet" : "",
|
|
48
|
+
body: view.full === null
|
|
49
|
+
? joinHtml([note("Approve one from the command line:"), codeLine(`toolroll verify set --repo … --command "…"`)])
|
|
50
|
+
: command(view.full) });
|
|
51
|
+
return joinHtml([head, level, batchHtml(view), reviewHtml(view), quick, full]);
|
|
52
|
+
/** Batch checks: one state and one sentence; turning it on restates exactly what happens before the password. */
|
|
53
|
+
function batchHtml(view) {
|
|
54
|
+
const batch = view.batch;
|
|
55
|
+
if (batch === undefined)
|
|
56
|
+
return "";
|
|
57
|
+
const act = batch.on ? "Turn off" : "Turn on";
|
|
58
|
+
const terms = batch.on ? note("Each result runs its own full check again. Results already waiting are checked now.")
|
|
59
|
+
: list(["When results finish within 10 minutes of each other, they're merged in a temporary checkout and the full check runs once.",
|
|
60
|
+
"If it fails, the batch is split until the result that breaks it is found. Results that conflict are checked on their own.",
|
|
61
|
+
"Nothing is merged into a real branch. Each result still lands on its own."]);
|
|
62
|
+
return settingsSection({ id: "batch-checks", title: "Batch checks", state: html `<span data-batch-checks="${batch.on ? "on" : "off"}"> · ${batch.on ? "On" : "Off"}</span>`, lead: BATCH_HINT,
|
|
63
|
+
body: joinHtml([batch.on && view.level !== "full" ? note("Only Full checks are batched.") : "",
|
|
64
|
+
view.canChange ? disclose(act, form("batch-form", "batch", terms, { label: act, variant: "outline" }, { on: batch.on ? "0" : "1" })) : ""]) });
|
|
65
|
+
}
|
|
66
|
+
/** Automatic review: one state, one sentence, and its one change behind a disclosure (the password). */
|
|
67
|
+
function reviewHtml(view) {
|
|
68
|
+
const review = view.review;
|
|
69
|
+
if (review === undefined)
|
|
70
|
+
return "";
|
|
71
|
+
const state = review.on ? (review.source === "hands-off" ? "On while hands-off lasts" : "On") : "Off";
|
|
72
|
+
const act = review.on ? "Turn off" : "Turn on";
|
|
73
|
+
return settingsSection({ id: "automatic-review", title: "Automatic review", state: html `<span data-review-switch="${review.on ? "on" : "off"}"> · ${state}</span>`,
|
|
74
|
+
lead: "One read-only review after each build passes its checks. Only high findings send it back, once.",
|
|
75
|
+
body: view.canChange ? disclose(act, form("review-form", "review", html ``, { label: act, variant: "outline" }, { on: review.on ? "0" : "1" })) : "" });
|
|
22
76
|
}
|
|
23
|
-
const choices = CHECK_LEVELS.map(level => html `<label class="check-choice"><input type="radio" name="level" value="${level}"${level === view.level ? html ` checked` : ""}><span><strong>${CHECK_LEVEL_WORDS[level]}</strong><span class="meta">${CHECK_LEVEL_HINTS[level]}</span></span></label>`);
|
|
24
|
-
const levelForm = post("level", html `<fieldset><legend>After each build</legend>${choices}</fieldset>${fallsBack ? html `<p class="meta">No quick command yet, so Quick runs the full check.</p>` : ""}<p class="meta">A task can choose its own checks when it's filed. With Quick or Off, the full check runs when a pull request opens, and Merge waits for it unless you merge anyway.</p>${password}<button type="submit">Save</button>`, { class: "card check-settings" });
|
|
25
|
-
const quickTerms = html `<ul class="check-terms"><li>Runs unattended right after every Quick build, in that build's checkout.</li><li>Plain environment: no credentials.</li><li>A failure shows on the result; nothing is blocked.</li></ul>`;
|
|
26
|
-
const quickForm = (value, submit) => post("quick", html `<label>Command<textarea name="command" rows="2" maxlength="2000" spellcheck="false" class="mono" required>${value}</textarea></label><label>Time limit (seconds)<input type="number" name="timeout" min="1" max="3600" value="${view.quick === null ? 180 : Math.round(view.quick.timeoutMs / 1000)}" required></label>${quickTerms}${password}<button type="submit">${submit}</button>`, { class: "check-settings" });
|
|
27
|
-
const quick = view.quick === null
|
|
28
|
-
? html `<section class="card check-settings"><h2>Quick check</h2>${view.suggestion === null ? html `<p class="meta">Typecheck and the tests near the change, ideally under 2 minutes.</p>` : html `<p class="meta">Suggested from the project's scripts. Edit it before approving.</p>`}${quickForm(view.suggestion ?? "", "Approve quick check")}</section>`
|
|
29
|
-
: html `<section class="card check-settings"><h2>Quick check</h2>${command(view.quick)}<details class="settings-more"><summary>Change quick check</summary>${quickForm(view.quick.command, "Approve change")}</details><details class="settings-more"><summary>Remove quick check</summary>${post("quick-clear", html `<p class="meta">Quick builds run the full check instead.</p>${password}<button type="submit" class="danger">Remove</button>`, { class: "check-settings" })}</details></section>`;
|
|
30
|
-
const full = html `<details class="settings-more"><summary>Full check</summary><section class="card check-settings">${view.full === null ? html `<p class="meta">None yet. Approve one with <span class="mono">toolroll verify set --repo … --command "…"</span>.</p>` : command(view.full)}</section></details>`;
|
|
31
|
-
return html `${head}${levelForm}${batchHtml(view, password)}${reviewHtml(view, password)}${quick}${full}`;
|
|
32
|
-
}
|
|
33
|
-
/** Batch checks: one state and one sentence; turning it on restates exactly what happens, behind a disclosure with the password. */
|
|
34
|
-
function batchHtml(view, password) {
|
|
35
|
-
const batch = view.batch;
|
|
36
|
-
if (batch === undefined)
|
|
37
|
-
return "";
|
|
38
|
-
const act = batch.on ? "Turn off" : "Turn on";
|
|
39
|
-
const terms = batch.on ? html `<p class="meta">Each result runs its own full check again. Results already waiting are checked now.</p>`
|
|
40
|
-
: html `<ul class="check-terms"><li>When results finish within 10 minutes of each other, they're merged in a temporary checkout and the full check runs once.</li><li>If it fails, the batch is split until the result that breaks it is found. Results that conflict are checked on their own.</li><li>Nothing is merged into a real branch. Each result still lands on its own.</li></ul>`;
|
|
41
|
-
const change = password === null ? ""
|
|
42
|
-
: html `<details class="settings-more"><summary>${act}</summary>${postForm("/settings/checks", html `${terms}${password}<button type="submit">${act}</button>`, { attrs: { class: "check-settings" }, hidden: { repo: view.repo, act: "batch", on: batch.on ? "0" : "1" } })}</details>`;
|
|
43
|
-
return html `<section class="card check-settings" data-batch-checks="${batch.on ? "on" : "off"}"><h2>Batch checks</h2><p><strong>${batch.on ? "On" : "Off"}</strong> · ${BATCH_HINT}</p>${batch.on && view.level !== "full" ? html `<p class="meta">Only Full checks are batched.</p>` : ""}${change}</section>`;
|
|
44
|
-
}
|
|
45
|
-
/** Automatic review: one state, one sentence, and its one change behind a disclosure (the password). */
|
|
46
|
-
function reviewHtml(view, password) {
|
|
47
|
-
const review = view.review;
|
|
48
|
-
if (review === undefined)
|
|
49
|
-
return "";
|
|
50
|
-
const state = review.on ? (review.source === "hands-off" ? "On while hands-off lasts" : "On") : "Off";
|
|
51
|
-
const act = review.on ? "Turn off" : "Turn on";
|
|
52
|
-
const change = password === null ? ""
|
|
53
|
-
: html `<details class="settings-more"><summary>${act}</summary>${postForm("/settings/checks", html `${password}<button type="submit">${act}</button>`, { attrs: { class: "check-settings" }, hidden: { repo: view.repo, act: "review", on: review.on ? "0" : "1" } })}</details>`;
|
|
54
|
-
return html `<section class="card check-settings" data-review-switch="${review.on ? "on" : "off"}"><h2>Automatic review</h2><p><strong>${state}</strong> · One read-only review after each build passes its checks. Only high findings send it back, once.</p>${change}</section>`;
|
|
55
77
|
}
|
|
78
|
+
/** The follow-up list's look (result-follow-ups.ts); the checks page itself is kit markup. */
|
|
56
79
|
export const CHECK_SETTINGS_CSS = `
|
|
57
|
-
.check-settings { display: grid; gap: .6rem; max-width: 40rem; }
|
|
58
|
-
.check-settings p, .check-settings h2 { margin: 0; }
|
|
59
|
-
.check-settings h2 { font-size: 1rem; }
|
|
60
|
-
.check-settings fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: .25rem; }
|
|
61
|
-
.check-settings legend { font-weight: 600; margin-bottom: .25rem; }
|
|
62
|
-
.check-settings label { display: grid; gap: .25rem; }
|
|
63
|
-
.check-settings .check-choice { display: flex; align-items: flex-start; gap: .6rem; min-height: 44px; padding: .35rem 0; cursor: pointer; }
|
|
64
|
-
.check-settings .check-choice > span { display: grid; gap: .1rem; }
|
|
65
|
-
.check-settings .check-choice input { margin-top: .2rem; }
|
|
66
|
-
.check-settings input[type=password], .check-settings input[type=number], .check-settings textarea { min-height: 44px; box-sizing: border-box; width: 100%; }
|
|
67
|
-
.check-settings button { min-height: 44px; justify-self: start; }
|
|
68
|
-
.check-settings .check-command { overflow-wrap: anywhere; }
|
|
69
|
-
.check-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
70
|
-
@media (max-width: 480px) { .check-settings button { justify-self: stretch; } }
|
|
71
80
|
.follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
72
81
|
.follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }
|
|
73
82
|
.follow-ups .follow-up-act { margin: 0; }
|
package/dist/contracts/cli.d.ts
CHANGED
|
@@ -513,6 +513,49 @@ export declare const COMMAND_OUTPUTS: {
|
|
|
513
513
|
reason: z.ZodString;
|
|
514
514
|
message: z.ZodOptional<z.ZodString>;
|
|
515
515
|
}, z.core.$loose>], "ok">;
|
|
516
|
+
readonly "task ship": z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
517
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
518
|
+
ok: z.ZodLiteral<true>;
|
|
519
|
+
command: z.ZodLiteral<"task ship">;
|
|
520
|
+
task: z.ZodString;
|
|
521
|
+
pullRequest: z.ZodNumber;
|
|
522
|
+
githubRepo: z.ZodString;
|
|
523
|
+
head: z.ZodString;
|
|
524
|
+
observedAt: z.ZodString;
|
|
525
|
+
status: z.ZodEnum<{
|
|
526
|
+
failing: "failing";
|
|
527
|
+
none: "none";
|
|
528
|
+
passing: "passing";
|
|
529
|
+
running: "running";
|
|
530
|
+
}>;
|
|
531
|
+
checks: z.ZodArray<z.ZodObject<{
|
|
532
|
+
name: z.ZodString;
|
|
533
|
+
workflow: z.ZodNullable<z.ZodString>;
|
|
534
|
+
state: z.ZodEnum<{
|
|
535
|
+
cancelled: "cancelled";
|
|
536
|
+
failed: "failed";
|
|
537
|
+
neutral: "neutral";
|
|
538
|
+
passed: "passed";
|
|
539
|
+
queued: "queued";
|
|
540
|
+
running: "running";
|
|
541
|
+
skipped: "skipped";
|
|
542
|
+
}>;
|
|
543
|
+
startedAt: z.ZodNullable<z.ZodString>;
|
|
544
|
+
completedAt: z.ZodNullable<z.ZodString>;
|
|
545
|
+
url: z.ZodNullable<z.ZodString>;
|
|
546
|
+
kind: z.ZodEnum<{
|
|
547
|
+
run: "run";
|
|
548
|
+
status: "status";
|
|
549
|
+
}>;
|
|
550
|
+
seconds: z.ZodNullable<z.ZodNumber>;
|
|
551
|
+
}, z.core.$loose>>;
|
|
552
|
+
}, z.core.$loose>, z.ZodObject<{
|
|
553
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
554
|
+
ok: z.ZodLiteral<false>;
|
|
555
|
+
command: z.ZodLiteral<"task ship">;
|
|
556
|
+
reason: z.ZodString;
|
|
557
|
+
message: z.ZodOptional<z.ZodString>;
|
|
558
|
+
}, z.core.$loose>], "ok">;
|
|
516
559
|
readonly "task hold": z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
517
560
|
envelopeVersion: z.ZodLiteral<1>;
|
|
518
561
|
ok: z.ZodLiteral<true>;
|
package/dist/contracts/cli.js
CHANGED
|
@@ -176,6 +176,10 @@ export const COMMAND_OUTPUTS = {
|
|
|
176
176
|
]),
|
|
177
177
|
"task list": taskListOutput,
|
|
178
178
|
"task show": taskShowOutput,
|
|
179
|
+
// The task's pull request checks (store.ts ci_check, v146): each with its state and seconds (null: not started).
|
|
180
|
+
// `status` is the observation's own answer (the one merges read); the rows are each check's timing.
|
|
181
|
+
"task ship": answers("task ship", { task: str, pullRequest: int, githubRepo: str, head: str, observedAt: str, status: z.enum(["passing", "failing", "running", "none"]),
|
|
182
|
+
checks: rows({ name: str, workflow: nullable(str), state: z.enum(["queued", "running", "passed", "failed", "skipped", "cancelled", "neutral"]), startedAt: nullable(str), completedAt: nullable(str), url: nullable(str), kind: z.enum(["run", "status"]), seconds: nullable(int) }) }),
|
|
179
183
|
"task hold": answers("task hold", { id: str, reason: str, until: nullable(str) }),
|
|
180
184
|
"task unhold": answers("task unhold", { id: str }),
|
|
181
185
|
"task wait": answers("task wait", { task: str, outcome: str }),
|
|
@@ -79,7 +79,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
79
79
|
readonly kit: FormContract<"password" | "repo">;
|
|
80
80
|
readonly subagents: FormContract<"choice" | "dailyTurns" | "id" | "manager" | "model" | "name" | "note" | "op" | "password" | "repo" | "schedule" | "soul" | "state" | "template" | "text" | "tool" | `field.${string}` | `over.${string}` | `undo.${string}` | `use.${string}`>;
|
|
81
81
|
readonly galleryUse: FormContract<string>;
|
|
82
|
-
readonly flows: FormContract<"about" | "address" | "answers" | "approve" | "body" | "budget" | "choice" | "confirm" | "days" | "decision" | "definition" | "description" | "digest" | "document" | "draft" | "entry" | "file" | "include" | "key" | "label" | "language" | "name" | "note" | "owner" | "password" | "previewed" | "project" | "remove" | "repo" | "revision" | "runs" | "schedules" | "secret" | "sender" | "stage" | "team" | "template" | "timeoutMinutes" | "title" | "trigger" | "url" | "value" | "watching" | `param.${string}`>;
|
|
82
|
+
readonly flows: FormContract<"about" | "address" | "answers" | "approve" | "body" | "budget" | "choice" | "confirm" | "days" | "decision" | "definition" | "description" | "digest" | "document" | "draft" | "entry" | "file" | "include" | "key" | "label" | "language" | "name" | "note" | "owner" | "password" | "previewed" | "project" | "remove" | "repo" | "revision" | "runs" | "schedules" | "secret" | "sender" | "stage" | "team" | "template" | "timeoutMinutes" | "title" | "trigger" | "url" | "value" | "version" | "watching" | `param.${string}`>;
|
|
83
83
|
readonly spendBudget: FormContract<"action" | "password" | "stop" | "target" | "usd">;
|
|
84
84
|
readonly projectConcurrency: FormContract<"concurrency" | "repo">;
|
|
85
85
|
readonly projectLiveView: FormContract<"act" | "nonce" | "password" | "repo">;
|
|
@@ -88,7 +88,7 @@ export const CONSOLE_FORMS = {
|
|
|
88
88
|
kit: formContract("POST /kits/<kit>/<act>", ["repo", "password"]),
|
|
89
89
|
subagents: formContract("POST /settings/lead/subagents/new, /settings/lead/subagents/<id>/<part>, /settings/lead/subagents/questions/<id>/answer", ["choice", "text", "repo", "template", "name", "soul", "op", "id", "schedule", "tool", "state", "note", "model", "dailyTurns", "manager", "password"], { prefixes: ["use.", "undo.", "field.", "over."] }),
|
|
90
90
|
galleryUse: formContract("POST /flows/new/<template>", ["repo", "send-result", "name", "intent", "previewed", "password"], { dynamic: true }),
|
|
91
|
-
flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "sender", "project", "team", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching", "days", "budget", "runs", "include", "schedules", "digest"], { prefixes: ["param."] }),
|
|
91
|
+
flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "sender", "project", "team", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching", "days", "budget", "runs", "include", "schedules", "digest", "version"], { prefixes: ["param."] }),
|
|
92
92
|
spendBudget: formContract("POST /spend/budget", ["password", "target", "action", "usd", "stop"]),
|
|
93
93
|
projectConcurrency: formContract("POST /settings/project/concurrency", ["repo", "concurrency"]),
|
|
94
94
|
projectLiveView: formContract("POST /settings/project/live-view", ["repo", "act", "password", "nonce"]),
|
package/dist/credentials-ui.d.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ApiTokenRow, WebSessionRow } from "./store.js";
|
|
7
7
|
import { type Html } from "./html.js";
|
|
8
|
-
|
|
8
|
+
/** The old card look; the page is built from the kit now (src/server/kit.ts), so nothing is left to style. Kept for
|
|
9
|
+
* chrome.ts, which still concatenates it. */
|
|
10
|
+
export declare const CREDENTIALS_CSS = "";
|
|
9
11
|
/** "Chrome on macOS", from a user agent, for a person to recognise a session. */
|
|
10
12
|
export declare function deviceWords(agent: string | null): string;
|
|
11
13
|
export type CredentialsView = {
|
|
@@ -23,9 +25,9 @@ export type CredentialsView = {
|
|
|
23
25
|
section: Html;
|
|
24
26
|
};
|
|
25
27
|
};
|
|
26
|
-
export declare function credentialsHtml(view: CredentialsView,
|
|
28
|
+
export declare function credentialsHtml(view: CredentialsView, outcome: {
|
|
27
29
|
said?: string | null;
|
|
28
30
|
problem?: string | null;
|
|
29
31
|
}): Html;
|
|
30
|
-
/** The token, once
|
|
32
|
+
/** The token, once, on a page with no script: selectable text, what it may do, and where to revoke it. */
|
|
31
33
|
export declare function tokenShownHtml(name: string, token: string, access: "read" | "act", expiresAt: string): Html;
|
package/dist/credentials-ui.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { daysLeft, expiryNoticeDue, TOKEN_DAYS, tokenLive } from "./api-tokens.js";
|
|
2
2
|
import { projectName } from "./project.js";
|
|
3
|
-
import { html, postForm } from "./html.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
4
|
+
import { acts, btn, chip, disclose, formRow, infoLine, input, note, notice, select, settingsRecord, settingsSection } from "./server/kit.js";
|
|
5
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
6
|
+
import { settingsSubPage } from "./settings-groups-ui.js";
|
|
7
|
+
/** The old card look; the page is built from the kit now (src/server/kit.ts), so nothing is left to style. Kept for
|
|
8
|
+
* chrome.ts, which still concatenates it. */
|
|
9
|
+
export const CREDENTIALS_CSS = "";
|
|
10
10
|
/** "Chrome on macOS", from a user agent, for a person to recognise a session. */
|
|
11
11
|
export function deviceWords(agent) {
|
|
12
12
|
if (agent === null || agent === "")
|
|
@@ -22,38 +22,65 @@ const when = (value, now) => {
|
|
|
22
22
|
const minutes = Math.round((now - at) / 60_000);
|
|
23
23
|
return minutes < 1 ? "just now" : minutes < 60 ? `${minutes} min ago` : minutes < 60 * 24 ? `${Math.round(minutes / 60)} h ago` : new Date(at).toISOString().slice(0, 10);
|
|
24
24
|
};
|
|
25
|
-
export function credentialsHtml(view,
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
<p class="meta">${one.ssoAt === null ? "Password" : "Identity provider"} · ${one.address ?? "unknown address"} · active ${when(one.lastSeen, view.now)} · signed in ${when(one.createdAt, view.now)}</p></div>\
|
|
31
|
-
${one.here ? "" : post({ action: "end-session", session: one.idHash }, "Sign out")}</div>`)}</div>`;
|
|
25
|
+
export function credentialsHtml(view, outcome) {
|
|
26
|
+
// An act on this page (sign out, revoke): one button, nothing to prove (the proof row covers only making a token).
|
|
27
|
+
const act = (fields, label, variant = "outline") => postForm("/settings/sessions", btn(label, { sm: true, type: "submit", variant }), { hidden: { ...fields, ...(view.everyone ? { everyone: "1" } : {}) } });
|
|
28
|
+
const answer = outcome.problem ? notice(outcome.problem) : outcome.said ? infoLine(outcome.said) : "";
|
|
29
|
+
const toggle = view.canSeeEveryone ? btn(view.everyone ? "Show only mine" : "Show everyone’s", { sm: true, variant: "ghost", href: `/settings/sessions${view.everyone ? "" : "?everyone=1"}` }) : undefined;
|
|
32
30
|
const others = view.sessions.filter(one => !one.here && one.account === view.who).length;
|
|
33
|
-
const endOthers = others > 0 && !view.everyone ?
|
|
31
|
+
const endOthers = others > 0 && !view.everyone ? acts(act({ action: "end-others" }, `Sign out everywhere else (${others})`)) : "";
|
|
32
|
+
const sessions = settingsSection({ id: "signed-in", title: "Signed in", ...(toggle ? { right: toggle } : {}),
|
|
33
|
+
body: joinHtml([
|
|
34
|
+
view.sessions.length === 0 ? note("No one else is signed in.") : joinHtml(view.sessions.map(one => settingsRecord({
|
|
35
|
+
title: html `${view.everyone ? html `<b>${one.account}</b> · ` : ""}${deviceWords(one.agent)}${one.here ? html ` ${chip("This browser", "")}` : ""}`,
|
|
36
|
+
sub: `${one.ssoAt === null ? "Password" : "Identity provider"} · ${one.address ?? "unknown address"} · signed in ${when(one.createdAt, view.now)}`,
|
|
37
|
+
when: `Active ${when(one.lastSeen, view.now)}`,
|
|
38
|
+
action: one.here ? "" : act({ action: "end-session", session: one.idHash }, "Sign out"),
|
|
39
|
+
attrs: { "data-session": one.idHash.slice(0, 12) },
|
|
40
|
+
}))),
|
|
41
|
+
endOthers,
|
|
42
|
+
]) });
|
|
34
43
|
const live = view.tokens.filter(one => tokenLive(one, view.now));
|
|
35
|
-
// v111: its projects, and one state: when it stops (soon, said once
|
|
44
|
+
// v111: its projects, and one state: when it stops (soon, said once) or, once replaced, when it ends.
|
|
36
45
|
const ends = (one) => {
|
|
37
46
|
if (one.replacedBy !== null && one.overlapUntil !== null)
|
|
38
|
-
return
|
|
47
|
+
return `Replaced · stops ${one.overlapUntil.slice(11, 16)} UTC`;
|
|
39
48
|
if (expiryNoticeDue(one, view.now) === null)
|
|
40
|
-
return
|
|
49
|
+
return `Expires ${one.expiresAt.slice(0, 10)}`;
|
|
41
50
|
const days = daysLeft(one.expiresAt, view.now);
|
|
42
|
-
return
|
|
51
|
+
return `Expires ${days <= 1 ? "within a day" : `in ${days} days`}`;
|
|
43
52
|
};
|
|
44
53
|
const scope = (one) => one.projects === null ? "All projects" : one.projects.length === 0 ? "No projects" : one.projects.map(repo => projectName(repo)).join(", ");
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
const create = disclose("New API token", kitSectionForm({ proof: "sessions-token", action: "/settings/sessions", id: "token-form", native: true, hidden: { action: "create-token" },
|
|
55
|
+
save: { label: "Make the token" }, confirm: "Make an API token",
|
|
56
|
+
body: joinHtml([
|
|
57
|
+
formRow("Name", "So you can tell it apart later.", input({ id: "token-name", name: "name", attrs: { maxlength: 60, required: true, placeholder: "for example: CI", autocomplete: "off" } })),
|
|
58
|
+
formRow("It can", null, select({ id: "token-access", name: "access", label: "It can", current: "read",
|
|
59
|
+
items: [["read", "Read (tasks, results, the ledger)"], ["act", "Act as you (file and manage work; never approve)"]] })),
|
|
60
|
+
formRow("Expires in", null, select({ id: "token-days", name: "days", label: "Expires in", current: "90", items: TOKEN_DAYS.map(days => [String(days), `${days} days`]) })),
|
|
61
|
+
note("Making a token is a new way in, so it always asks for your password. The token is shown once, on the next page."),
|
|
62
|
+
]) }), live.length === 0);
|
|
63
|
+
const tokens = settingsSection({ id: "api-tokens", title: "API tokens",
|
|
64
|
+
lead: html `For scripts and CI: send <code class="k-mono">Authorization: Bearer <token></code>. A token can’t approve anything.`,
|
|
65
|
+
body: joinHtml([
|
|
66
|
+
live.length === 0 ? note("No API tokens.") : joinHtml(live.map(one => settingsRecord({
|
|
67
|
+
title: html `${view.everyone ? html `<b>${one.account}</b> · ` : ""}${one.name}`,
|
|
68
|
+
sub: `${one.access === "act" ? "Can act" : "Read only"} · ${scope(one)} · ${ends(one)}${view.limits === undefined ? "" : ` · ${view.limits.note(one)}`}`,
|
|
69
|
+
when: `Last used ${when(one.lastUsedAt, view.now)}`,
|
|
70
|
+
action: act({ action: "revoke-token", token: one.id }, "Revoke", "danger"),
|
|
71
|
+
attrs: { "data-token": one.id },
|
|
72
|
+
}))),
|
|
73
|
+
view.everyone ? "" : create,
|
|
74
|
+
]) });
|
|
75
|
+
return joinHtml([answer, sessions, tokens, view.limits?.section ?? ""]);
|
|
54
76
|
}
|
|
55
|
-
/** The token, once
|
|
77
|
+
/** The token, once, on a page with no script: selectable text, what it may do, and where to revoke it. */
|
|
56
78
|
export function tokenShownHtml(name, token, access, expiresAt) {
|
|
57
|
-
return
|
|
58
|
-
|
|
79
|
+
return settingsSubPage("access", "API token", settingsSection({ id: "token-shown", title: `${name} · shown once, so copy it now`,
|
|
80
|
+
lead: "Only a hash is kept, so this page is the only time you’ll see it.",
|
|
81
|
+
body: joinHtml([
|
|
82
|
+
html `<p class="k-secret-val">${token}</p>`,
|
|
83
|
+
note(`${access === "act" ? "It acts as you and never approves" : "It reads"} until ${expiresAt.slice(0, 10)}. Revoke it any time on Sessions and tokens.`),
|
|
84
|
+
acts(btn("Back to Sessions and tokens", { sm: true, href: "/settings/sessions" })),
|
|
85
|
+
]) }));
|
|
59
86
|
}
|