Public: anyone can read this; the crew can change it.

The crew settings page and its forms leave settings.ts for src/web/pages/crew-settings.ts #6457

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/6335-the-crew-settings-page-and-its-forms-lea → main 3 commits fork

  • Merged. mason/6335-the-crew-settings-page-and-its-forms-lea was merged into main as 25ab42d.

Review

Sign in to review this change.
3 files changed +100 −88
src/http/server.ts +7 −5
@@ -372,11 +372,6 @@ import {
372372 appSettingsUpdate,
373373 autonomySettingsPage,
374374 autonomySettingsUpdate,
375 crewAdd,
376 crewSubscriptionsSave,
377 crewRemove,
378 crewRoleUpdate,
379 crewSettingsPage,
380375 settingsPage,
381376 settingsUpdate,
382377 waveCut,
@@ -386,6 +381,13 @@ import {
386381 } from "../web/pages/settings.ts";
387382 import { dangerPage, repoDelete } from "../web/pages/danger.ts";
388383 import { hooksPage, webhookCreate, webhookDelete } from "../web/pages/hooks.ts";
384 import {
385 crewAdd,
386 crewRemove,
387 crewRoleUpdate,
388 crewSettingsPage,
389 crewSubscriptionsSave,
390 } from "../web/pages/crew-settings.ts";
389391 import {
390392 promptSourceAdd,
391393 promptSourceDelete,
src/web/pages/crew-settings.ts added +93
@@ -0,0 +1,93 @@
1 /**
2 * Settings › Crew, admin-only: the forms that put an agent on this
3 * repository's crew, take it off, set its role, and save what the crew
4 * subscribes to. The crew card itself is drawn on the settings page; every
5 * form here redirects back to its `#crew` anchor with a flash.
6 */
7
8 import { addToCrew, listCrew, removeFromCrew } from "../../actions/crew.ts";
9 import { getAgentByName, isCrewRole, setCrewRole } from "../../core/agents.ts";
10 import type { Ctx } from "../../http/context.ts";
11 import { loadRepo, repoBase, repoNotFound } from "../chrome.ts";
12 import { field } from "../form.ts";
13 import { flashTo, redirect } from "../layout.ts";
14 import { adminForm } from "./settings.ts";
15 import { saveGrants } from "./subscriptions.ts";
16
17 // GET /:owner/:name/settings/crew
18 export function crewSettingsPage(ctx: Ctx): Response {
19 const repo = loadRepo(ctx);
20 if (!repo) return repoNotFound(ctx);
21 return redirect(`${repoBase(repo)}/settings#crew`);
22 }
23
24 // POST /:owner/:name/settings/crew/subscriptions
25 export async function crewSubscriptionsSave(ctx: Ctx): Promise<Response> {
26 const gate = await adminForm(ctx);
27 if (gate instanceof Response) return gate;
28 const { repo, form } = gate;
29 if (ctx.auth.user?.isAdmin !== true) return repoNotFound(ctx);
30 const crew = listCrew(ctx.app, ctx.auth.user, repo);
31 saveGrants(ctx, form, repo.id, crew.ok ? crew.value : []);
32 return flashTo(ctx, `${repoBase(repo)}/settings#crew`, {
33 message: "Saved. The next run here works on what is ticked.",
34 });
35 }
36
37 // POST /:owner/:name/settings/crew
38 export async function crewAdd(ctx: Ctx): Promise<Response> {
39 const gate = await adminForm(ctx);
40 if (gate instanceof Response) return gate;
41 const { repo, form } = gate;
42 const added = addToCrew(ctx.app, ctx.auth.user, repo, field(form, "agent"));
43 return flashTo(
44 ctx,
45 `${repoBase(repo)}/settings#crew`,
46 added.ok
47 ? {
48 message: `${added.value.user.username} is on the crew. Assign it an issue and it starts.`,
49 }
50 : {
51 tone: "danger",
52 message:
53 added.error === "not_an_agent"
54 ? "Only an agent can be on a crew; a person is given access through an organization's team."
55 : "No such agent on this forge.",
56 },
57 );
58 }
59
60 // POST /:owner/:name/settings/crew/:username/remove
61 export async function crewRemove(ctx: Ctx): Promise<Response> {
62 const gate = await adminForm(ctx);
63 if (gate instanceof Response) return gate;
64 const { repo } = gate;
65 const username = ctx.params.username ?? "";
66 const removed = removeFromCrew(ctx.app, ctx.auth.user, repo, username);
67 return flashTo(
68 ctx,
69 `${repoBase(repo)}/settings#crew`,
70 removed.ok
71 ? { message: `${username} is off the crew.` }
72 : { tone: "danger", message: "No such agent on this forge." },
73 );
74 }
75
76 // POST /:owner/:name/settings/crew/:username/role
77 export async function crewRoleUpdate(ctx: Ctx): Promise<Response> {
78 const gate = await adminForm(ctx);
79 if (gate instanceof Response) return gate;
80 const { repo, form } = gate;
81 const base = repoBase(repo);
82 const agent = getAgentByName(ctx.app.db, ctx.params.username ?? "");
83 const role = field(form, "role");
84 if (!agent || !isCrewRole(role))
85 return flashTo(ctx, `${base}/settings#crew`, {
86 tone: "danger",
87 message: "That role could not be set.",
88 });
89 setCrewRole(ctx.app.db, repo.id, agent.user.id, role);
90 return flashTo(ctx, `${base}/settings#crew`, {
91 message: `${agent.user.username} is a ${role} here.`,
92 });
93 }
src/web/pages/settings.ts −83
@@ -38,8 +38,6 @@ import {
3838 type AppEnvName,
3939 type AppEnvScope,
4040 } from "../../core/app_env.ts";
41 import { addToCrew, listCrew, removeFromCrew } from "../../actions/crew.ts";
42 import { getAgentByName, isCrewRole, setCrewRole } from "../../core/agents.ts";
4341 import { isAutonomy, isDecisionsBy } from "../../core/repos.ts";
4442 import { updateRepoSettings } from "../../actions/repos.ts";
4543 import { appsRuntime } from "../../apps/runtime.ts";
@@ -99,7 +97,6 @@ import { proposalTokensSection } from "./proposal-tokens.ts";
9997 import { secretsSection } from "./secrets.ts";
10098 import { checkMemoryAt } from "../../apps/check-memory.ts";
10199 import { followUpstreamCard } from "./upstream-follow.ts";
102 import { saveGrants } from "./subscriptions.ts";
103100
104101 type Section =
105102 | "general"
@@ -1426,86 +1423,6 @@ function redeploying(ctx: Ctx, how: string): string {
14261423 : ` Production and every preview are starting again ${how}.`;
14271424 }
14281425
1429 // ---- crew ----
1430
1431 // GET /:owner/:name/settings/crew
1432 export function crewSettingsPage(ctx: Ctx): Response {
1433 const repo = loadRepo(ctx);
1434 if (!repo) return repoNotFound(ctx);
1435 return redirect(`${repoBase(repo)}/settings#crew`);
1436 }
1437
1438 // POST /:owner/:name/settings/crew/subscriptions
1439 export async function crewSubscriptionsSave(ctx: Ctx): Promise<Response> {
1440 const gate = await adminForm(ctx);
1441 if (gate instanceof Response) return gate;
1442 const { repo, form } = gate;
1443 if (ctx.auth.user?.isAdmin !== true) return repoNotFound(ctx);
1444 const crew = listCrew(ctx.app, ctx.auth.user, repo);
1445 saveGrants(ctx, form, repo.id, crew.ok ? crew.value : []);
1446 return flashTo(ctx, `${repoBase(repo)}/settings#crew`, {
1447 message: "Saved. The next run here works on what is ticked.",
1448 });
1449 }
1450
1451 // POST /:owner/:name/settings/crew
1452 export async function crewAdd(ctx: Ctx): Promise<Response> {
1453 const gate = await adminForm(ctx);
1454 if (gate instanceof Response) return gate;
1455 const { repo, form } = gate;
1456 const added = addToCrew(ctx.app, ctx.auth.user, repo, field(form, "agent"));
1457 return flashTo(
1458 ctx,
1459 `${repoBase(repo)}/settings#crew`,
1460 added.ok
1461 ? {
1462 message: `${added.value.user.username} is on the crew. Assign it an issue and it starts.`,
1463 }
1464 : {
1465 tone: "danger",
1466 message:
1467 added.error === "not_an_agent"
1468 ? "Only an agent can be on a crew; a person is given access through an organization's team."
1469 : "No such agent on this forge.",
1470 },
1471 );
1472 }
1473
1474 // POST /:owner/:name/settings/crew/:username/remove
1475 export async function crewRemove(ctx: Ctx): Promise<Response> {
1476 const gate = await adminForm(ctx);
1477 if (gate instanceof Response) return gate;
1478 const { repo } = gate;
1479 const username = ctx.params.username ?? "";
1480 const removed = removeFromCrew(ctx.app, ctx.auth.user, repo, username);
1481 return flashTo(
1482 ctx,
1483 `${repoBase(repo)}/settings#crew`,
1484 removed.ok
1485 ? { message: `${username} is off the crew.` }
1486 : { tone: "danger", message: "No such agent on this forge." },
1487 );
1488 }
1489
1490 // POST /:owner/:name/settings/crew/:username/role
1491 export async function crewRoleUpdate(ctx: Ctx): Promise<Response> {
1492 const gate = await adminForm(ctx);
1493 if (gate instanceof Response) return gate;
1494 const { repo, form } = gate;
1495 const base = repoBase(repo);
1496 const agent = getAgentByName(ctx.app.db, ctx.params.username ?? "");
1497 const role = field(form, "role");
1498 if (!agent || !isCrewRole(role))
1499 return flashTo(ctx, `${base}/settings#crew`, {
1500 tone: "danger",
1501 message: "That role could not be set.",
1502 });
1503 setCrewRole(ctx.app.db, repo.id, agent.user.id, role);
1504 return flashTo(ctx, `${base}/settings#crew`, {
1505 message: `${agent.user.username} is a ${role} here.`,
1506 });
1507 }
1508
15091426 // ---- autonomy ----
15101427
15111428 /** GET /:owner/:name/settings/autonomy */

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs