Public: anyone can read this; the crew can change it.
Errors on the crew page are §5's field error, not .error-note #5091
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5069-errors-on-the-crew-page-are-5-s-field-er → main 1 commit fork
2 files
changed +43
−16
src/web/pages/crew.ts +25 −15
| @@ -100,27 +100,37 @@ function shell(ctx: Ctx, title: string, body: Html, status = 200): Response { | ||
| 100 | 100 | export function crewReadiness(ctx: Ctx): Html { |
| 101 | 101 | const crew = crewFor(ctx.app); |
| 102 | 102 | if (crew === undefined) |
| 103 | return html`<p class="error-note mb"> | |
| 104 | ${icon("alert")} No crew runner is up on this forge, so assigned issues | |
| 105 | will wait. Start the forge with apps on (the default) and Claude Code on | |
| 106 | <span class="mono">PATH</span> or at | |
| 107 | <span class="mono">FORGE_CLAUDE</span>. | |
| 108 | </p>`; | |
| 103 | return card( | |
| 104 | html`<p> | |
| 105 | ${icon("alert")} No crew runner is up on this forge, so assigned issues | |
| 106 | will wait. Start the forge with apps on (the default) and Claude Code | |
| 107 | on <span class="mono">PATH</span> or at | |
| 108 | <span class="mono">FORGE_CLAUDE</span>. | |
| 109 | </p>`, | |
| 110 | { tone: "danger", extra: "mb" }, | |
| 111 | ); | |
| 109 | 112 | if (!crew.ready) |
| 110 | return html`<p class="error-note mb"> | |
| 111 | ${icon("alert")} The crew cannot run: ${crew.detail}. Agents can be | |
| 112 | created, but nothing picks up their work until this is fixed. | |
| 113 | </p>`; | |
| 113 | return card( | |
| 114 | html`<p> | |
| 115 | ${icon("alert")} The crew cannot run: ${crew.detail}. Agents can be | |
| 116 | created, but nothing picks up their work until this is fixed. | |
| 117 | </p>`, | |
| 118 | { tone: "danger", extra: "mb" }, | |
| 119 | ); | |
| 114 | 120 | return html`<p class="muted small mb"> |
| 115 | 121 | ${icon("check")} ${crew.detail} is ready to run the crew. |
| 116 | 122 | </p> |
| 117 | 123 | ${when( |
| 118 | 124 | !crew.sandbox.enforced, |
| 119 | () => html`<p class="error-note mb" id="crew-unsandboxed"> | |
| 120 | ${icon("alert")} Unsandboxed: agents' shells run unrestricted here | |
| 121 | (${crew.sandbox.reason}). They can read this forge's data and reach any | |
| 122 | host. | |
| 123 | </p>`, | |
| 125 | () => | |
| 126 | card( | |
| 127 | html`<p> | |
| 128 | ${icon("alert")} Unsandboxed: agents' shells run unrestricted here | |
| 129 | (${crew.sandbox.reason}). They can read this forge's data and reach | |
| 130 | any host. | |
| 131 | </p>`, | |
| 132 | { tone: "danger", extra: "mb", id: "crew-unsandboxed" }, | |
| 133 | ), | |
| 124 | 134 | )}`; |
| 125 | 135 | } |
| 126 | 136 |
test/web-apps.test.ts +18 −1
| @@ -735,7 +735,13 @@ describe("apps, runs and the crew on the web", () => { | ||
| 735 | 735 | form: { username: "bot", budgetUsd: "-4" }, |
| 736 | 736 | }); |
| 737 | 737 | expect(bad.status).toBe(422); |
| 738 | expect(words(await bad.text())).toContain("A budget is a number"); | |
| 738 | const badPage = words(await bad.text()); | |
| 739 | expect(badPage).toContain("A budget is a number"); | |
| 740 | // The error sits beside its field (§5), not in an .error-note. | |
| 741 | expect(badPage).toMatch( | |
| 742 | /<div class="field" data-invalid>\s*<label for="f-budgetUsd">.*?<span class="error" id="f-budgetUsd-err">A budget is a number/, | |
| 743 | ); | |
| 744 | expect(badPage).not.toContain("error-note"); | |
| 739 | 745 | expect(getAgentByName(app.db, "bot")).toBeUndefined(); |
| 740 | 746 | |
| 741 | 747 | const made = await web(alice, "/settings/crew", { |
| @@ -764,6 +770,17 @@ describe("apps, runs and the crew on the web", () => { | ||
| 764 | 770 | expect(updated.status).toBe(303); |
| 765 | 771 | expect(getAgentByName(app.db, "bot")?.model).toBe("sonnet"); |
| 766 | 772 | expect(getAgentByName(app.db, "bot")?.budgetUsd).toBe(3); |
| 773 | // A refused edit shows the error beside its field. | |
| 774 | const refused = await web(alice, "/settings/crew/bot", { | |
| 775 | form: { model: "sonnet", brief: "Be brief.", budgetUsd: "-4" }, | |
| 776 | }); | |
| 777 | expect(refused.status).toBe(422); | |
| 778 | const refusedPage = words(await refused.text()); | |
| 779 | expect(refusedPage).toMatch( | |
| 780 | /<div class="field" data-invalid>\s*<label for="f-budgetUsd">.*?<span class="error" id="f-budgetUsd-err">A budget is a number/, | |
| 781 | ); | |
| 782 | expect(refusedPage).not.toContain("error-note"); | |
| 783 | expect(getAgentByName(app.db, "bot")?.budgetUsd).toBe(3); | |
| 767 | 784 | // A non-admin cannot touch any of it, whatever the form says. |
| 768 | 785 | const forged = await web(bob, "/settings/crew/bot", { |
| 769 | 786 | form: { model: "opus" }, |
new above
offline