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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/6335-the-crew-settings-page-and-its-forms-lea → main 3 commits fork
3 files
changed +100
−88
src/http/server.ts +7 −5
| @@ -372,11 +372,6 @@ import { | ||
| 372 | 372 | appSettingsUpdate, |
| 373 | 373 | autonomySettingsPage, |
| 374 | 374 | autonomySettingsUpdate, |
| 375 | crewAdd, | |
| 376 | crewSubscriptionsSave, | |
| 377 | crewRemove, | |
| 378 | crewRoleUpdate, | |
| 379 | crewSettingsPage, | |
| 380 | 375 | settingsPage, |
| 381 | 376 | settingsUpdate, |
| 382 | 377 | waveCut, |
| @@ -386,6 +381,13 @@ import { | ||
| 386 | 381 | } from "../web/pages/settings.ts"; |
| 387 | 382 | import { dangerPage, repoDelete } from "../web/pages/danger.ts"; |
| 388 | 383 | 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"; | |
| 389 | 391 | import { |
| 390 | 392 | promptSourceAdd, |
| 391 | 393 | 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 { | ||
| 38 | 38 | type AppEnvName, |
| 39 | 39 | type AppEnvScope, |
| 40 | 40 | } from "../../core/app_env.ts"; |
| 41 | import { addToCrew, listCrew, removeFromCrew } from "../../actions/crew.ts"; | |
| 42 | import { getAgentByName, isCrewRole, setCrewRole } from "../../core/agents.ts"; | |
| 43 | 41 | import { isAutonomy, isDecisionsBy } from "../../core/repos.ts"; |
| 44 | 42 | import { updateRepoSettings } from "../../actions/repos.ts"; |
| 45 | 43 | import { appsRuntime } from "../../apps/runtime.ts"; |
| @@ -99,7 +97,6 @@ import { proposalTokensSection } from "./proposal-tokens.ts"; | ||
| 99 | 97 | import { secretsSection } from "./secrets.ts"; |
| 100 | 98 | import { checkMemoryAt } from "../../apps/check-memory.ts"; |
| 101 | 99 | import { followUpstreamCard } from "./upstream-follow.ts"; |
| 102 | import { saveGrants } from "./subscriptions.ts"; | |
| 103 | 100 | |
| 104 | 101 | type Section = |
| 105 | 102 | | "general" |
| @@ -1426,86 +1423,6 @@ function redeploying(ctx: Ctx, how: string): string { | ||
| 1426 | 1423 | : ` Production and every preview are starting again ${how}.`; |
| 1427 | 1424 | } |
| 1428 | 1425 | |
| 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 | ||
| 1509 | 1426 | // ---- autonomy ---- |
| 1510 | 1427 | |
| 1511 | 1428 | /** GET /:owner/:name/settings/autonomy */ |
new above
offline