mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
7 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc0bda73a6
|
feat(web): add server-managed Environments CRUD settings UI (#462)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
## What Adds a CRUD interface for **server-managed Environments** at `/settings/environments`, driven by the `/api/v1/environments` REST API (list / create / retrieve / replace / delete), and reshapes how built-in environments are provisioned and protected. The page lives in the **Workflows** settings nav section (also introduced in this branch), positioned before Variables. ## Why The Environments REST API shipped (#453) but had no UI — environments could only be managed via the API/CLI. This gives operators a web UI alongside Variables and Secrets, and along the way tightens the model: environments are seeded at install time (not silently re-created on every boot), and the `default` fallback is an ordinary, deletable environment. ## Web UI **Pages & component** - `settings-environments.tsx` — list view: provider badge, image/resource summary, row actions (Edit/Delete). **"New environment" is a dropdown** of the enabled sandbox providers; the chosen provider is fixed for the environment's lifetime. - `settings-environments-new.tsx` / `settings-environments-edit.tsx` — create/edit flows; create reads the provider from a query param. - `environment-form.tsx` — shared form, reorganized: - **General** panel (merged identity + image): id, and an **image-source selector** (Image reference *vs* inline Dockerfile) that shows, requires, and sends only the selected, mutually-exclusive source. - **Resources**: CPU / memory / disk as **range sliders** (CPU 1–8, memory 1–16 GB, disk 1–20 GB), each always writing a concrete value. - **Environment variables** key/value editor. - **Advanced** progressive-disclosure section holding **Network** (a single "Block all network access" toggle — allow-all vs block) and **Lifecycle** (preserve / stop-on-terminal / auto-stop). Opens by default when any advanced value is non-default. - The in-form **provider control and the Labels editor were removed** — labels remain API-managed and are round-tripped untouched so UI edits never clear them. **Data layer**: `environmentsApi` client, `queryKeys.environments`, `useEnvironments` / `useEnvironment` SWR hooks. **Nav & routing**: "Environments" item in the Workflows section before Variables; routes registered in `router.tsx`. ## Backend: seed at install, deletable `default` - **Seeding moved to install time.** The server no longer seeds built-ins on startup; `EnvironmentStore::load_or_seed` → `load` (load-only). A new public `seed_environments(dir)` (idempotent, preserves operator edits) is called by both the web installer and the CLI installer. An uninstalled instance therefore has no managed environments, and a run selecting an absent environment fails explicitly (`unknown environment: default`) rather than resurrecting a built-in. - **`default` is no longer protected.** The delete guard and the `Protected` error variant are gone; deleting `default` succeeds (204) and removes the run fallback on purpose — forcing an explicit choice. `local` is unchanged (reserved, in-memory). - **`volumes` removed** from environment settings across the OpenAPI spec, generated Rust + TS clients, config layers, sandbox/server/workflow plumbing, docs, and tests. ## API contract details honored - Edit sends the environment `revision` as `If-Match`; 409 conflicts surface a "changed since you opened it" message. - The REST API accepts inline Dockerfiles only — the form never sends a Dockerfile path. ## Verification - Rust: `cargo build` (touched crates) ✅, `cargo nextest -p fabro-environment` 21/21 ✅, server env unit + `tests/it` integration 2/2 + 15/15 ✅, `clippy` (nightly, touched crates, all targets) clean ✅, `fmt --check` clean ✅. Full `--workspace` suite not run here — worth a CI pass. - Web: `bun run typecheck` ✅, `bun run build` ✅, `environment-form.test.ts` 5/5 ✅. Web suite: 512 pass / 1 unrelated pre-existing `RunDetail` failure. - **Not visually verified in-browser** — the local app is login-gated and automated loads redirect to `/login`; rendering of the form, the New-environment dropdown, and `default` delete should be confirmed in a logged-in session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Co-authored-by: Fabro <noreply@fabro.sh> Co-authored-by: Release Repro <release-repro@example.com> |
||
|
|
e18772888e
|
Model run sandbox lifecycle explicitly (#431)
## Summary Fixes sandbox state reporting by separating a requested sandbox plan from an initialized sandbox instance. Runs now project sandbox lifecycle as `planned`, `initializing`, `ready`, or `failed`, and live sandbox operations only proceed once a real instance exists. ## Changes - Introduces `RunSandboxPlan`, `RunSandboxInstance`, and lifecycle-backed `RunSandbox` domain types, with serde validation that prevents `ready` sandboxes without an instance. - Updates store projection behavior so sandbox events transition through planned, initializing, ready, and failed states while preserving requested provider/image/snapshot separately from runtime metadata. - Tightens server sandbox handlers so details/files/services/terminal/VNC helpers require an initialized instance and return a clear 404 when the sandbox was never created. - Updates the OpenAPI contract and regenerated clients so `Run.sandbox` exposes lifecycle state while `SandboxDetails.sandbox` contains only initialized instance metadata. - Updates the web UI to render lifecycle state directly from run summaries, hide the Sandbox tab for pure planned sandboxes, and disable sandbox controls until the instance is ready. - Cleans up duplicated lifecycle display/type logic and duplicate server-side sandbox instance loading found during review. | Lifecycle state | Meaning | Live controls | | --- | --- | --- | | `planned` | Sandbox was requested but no provider instance exists | Hidden/disabled | | `initializing` | Provider setup has started | State view only | | `ready` | Runtime instance exists | Enabled | | `failed` | Provider setup failed with error details | State view only | ## Testing - `cargo check --workspace` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun test app/routes/run-detail.test.ts app/routes/run-sandbox.test.tsx app/components/run-summary-panel.test.tsx` - `cargo nextest run -p fabro-types --test sandbox_model_serde` - `cargo nextest run -p fabro-store run_created_projects_planned_sandbox_lifecycle sandbox_lifecycle_events_update_projected_sandbox_state run_failed_before_sandbox_events_leaves_sandbox_planned` - `cargo nextest run -p fabro-server planned_sandbox_returns_404_from_details_endpoint planned_sandbox_rejects_live_operations failed_sandbox_rejects_live_operations local_sandbox_returns_provider_neutral_details` - `cargo nextest run -p fabro-api --test run_sandbox_round_trip` - `cargo nextest run -p fabro-api --test sandbox_details_round_trip` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
62f0b3e7d1
|
refactor(web): improve React Doctor score (#405)
## Summary Improves the web UI's React Doctor audit score by separating reusable helpers from React component modules, tightening effect/state ownership, and extracting real component boundaries in the install wizard, stage activity view, run-files diff browser, RunDetail route, and Runs workspace. The branch removes the previously deferred RunDetail and Runs giant-component diagnostics without changing RunDetail UX, route contracts, action ordering, or Runs workspace behavior. | Metric | Main baseline | Initial PR | Current PR | |--------|---------------|------------|------------| | React Doctor score | 63 | 71 | 99 | | React Doctor errors | 123 | 0 | 0 | | React Doctor warnings | 241 | 163 | 3 | | React Doctor diagnostics | 364 | 163 | 3 | ## Changes - Moves exported helper logic out of component files so Fast Refresh/component-export rules no longer dominate the audit. - Adds a targeted React Doctor config exception for React Router route modules, where non-component exports like route metadata are intentional. - Refactors low-risk state/effect patterns: keyed interview question state, reducer-backed editable run title state, event-owned preview opening, route-keyed insights editor initialization, refresh timer ownership, and selection/derived list cleanup. - Reworks `InstallApp` around an install reducer, a controller hook for install lifecycle state, and focused wizard step components for LLM, server, object-store, sandbox, and GitHub setup. - Moves `RunStages` selected-stage activity into a keyed boundary for panel/debug detail state while preserving stage activity filters across navigation. - Extracts the `RunFiles` loaded diff-browser view from route/query coordination so the route owns data/URL state and the loaded view owns rendering. - Splits `RunDetail` into route-local header, actions, tab shell, docked controls, model, and lifecycle-toast modules; the actions menu now uses grouped descriptors instead of a large boolean/callback prop matrix. - Extracts Runs workspace preference ownership into `useRunsWorkspacePreferences` and moves toolbar rendering into `RunsToolbar`, leaving the route focused on data, DnD state, filtering, and view selection. - Guards `InsightsEditor` query execution with a latest-run id and timeout cleanup so stale or unmounted mock query runs cannot overwrite newer results. - Adds regression coverage for archived-run deletion from RunDetail and stale-result handling in InsightsEditor. - Improves semantic/accessibility coverage with labeled controls, native meter/section semantics, decorative status dots, and clearer unavailable copy. - Removes dead UI code and applies local suppressions only where the rule is a documented false positive or an intentional imperative integration boundary. ## Remaining React Doctor warnings Current score is 99 with 0 errors and 3 warnings. The remaining warnings are intentionally left for separate judgment rather than mechanical churn: - `prefer-useReducer` (3): `AutomationsNew`, `InsightsEditor`, and `CreateSecretForm` need reducers only if they encode real coupled transitions, not simple field setters. ## Verification - `cd apps/fabro-web && bun test app/routes/run-detail.test.ts` -> `22 pass`, `0 fail` - `cd apps/fabro-web && bun test app/routes/insights-editor.test.tsx app/routes/runs.preferences.test.tsx` -> `7 pass`, `0 fail` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun test --isolate` -> `490 pass`, `0 fail` - `cd apps/fabro-web && bunx react-doctor@latest --full --json > /tmp/fabro-react-doctor-runs-insights.json` -> score `99`, `0` errors, `3` warnings - Earlier branch verification also included `cd apps/fabro-web && bun run build` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context not reported, default reasoning) via [Codex](https://openai.com/codex) |
||
|
|
7d8506cb7c
|
feat: add manual retry for failed and dead runs (#362)
## Summary
Adds a **Retry** action that creates a fresh run from a failed or dead
run's captured durable definition. The new run gets a new ID, records
`retried_from: <source_run_id>`, and is immediately queued. The source
run is left entirely unchanged.
### Plan Summary
- `retried_from: Option<RunId>` added to `RunCreatedProps`,
`RunProjection`, and `Run` (API surface); defaults to `null` for
backward compat with legacy events.
- `POST /api/v1/runs/{id}/retry` → `201` with the new run; eligible
source states are `failed` (except `reason=cancelled`) and `dead`.
- New `retry_run` workflow operation (modeled after `fork`) creates a
new run store, appends `run.created` with `retried_from` set, then
`run.submitted`. No checkpoints, sandbox state, PR links, questions, or
conclusions are copied.
- `start_run` handler refactored into a reusable `queue_run_start(state,
id, resume)` helper so retry can queue the new run through the same
path.
- Web: `canRetry` predicate, `useRetryRun` mutation, **Retry** menu item
in `ActionsMenu`, "Retried from" link in `RunSummaryPanelView`, and
navigation to the new run on success (deduplicated via
`lastProcessed.retry`).
- OpenAPI spec updated; generated Rust and TypeScript client types
regenerated.
## Key design decisions
**No runtime state is copied.** Only the durable definition fields
(`graph`, `settings`, `labels`, `git`, `manifest_blob`,
`definition_blob`, `fork_source_ref`, `parent_id`, `title`) are
forwarded to the new `RunCreated` event. Checkpoints, sandbox, billing,
PR links, and pending controls are left in the source run.
**`queue_run_start` extraction.** `start_run` was restructured to
extract a `queue_run_start(state, id, resume) -> Result<(), ApiError>`
helper, so the retry handler can reuse the exact same queueing path
without duplicating logic.
**`lastProcessed.retry` deduplication.** The UI effect that fires on
`retryMutation.data` checks `state.lastProcessed.retry === result`
before navigating, so React StrictMode double-invocation or re-renders
won't push duplicate navigations.
**Demo mode guard.** `canRetry` is gated by `!demoMode` at the call site
so the button is hidden in demo mode and never navigates to a missing
demo run.
### Fabro Details
<details>
<summary>Ran 9 stages in 65m 2s for $51.70</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 53s | – | 0 |
| preflight_lint | 2m 8s | – | 0 |
| implement | 36m 51s | $36.06 | 0 |
| simplify_opus | 16m 22s | $14.46 | 0 |
| simplify_gpt | 3m 10s | $1.18 | 0 |
| verify | 3m 39s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **65m 2s** | **$51.70** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
f2efaf70fb
|
feat(web): show sandbox status dot on the run overview panel
The Sandbox cell now renders a colored status dot before the resource summary, with a tooltip explaining the state on hover. The dot reuses the data already fetched for CPU/memory, so no new API call. Falls back to the state label when resources are unavailable. Lifts the per-state display map into a shared lib/sandbox-state module so the overview panel and the dedicated sandbox page stay consistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d17599898e
|
feat(web): show creator avatar on run "Created by" cell (#319)
## Summary The run Overview tab's "Created by" cell rendered every user as a colored circle with the first letter of their login. Reviewers and run owners expected the same GitHub avatar shown on `/profile` and in the top-right nav. The cell only had `login` to work with — the `PrincipalUser` schema carried no avatar URL. This threads an optional `avatar_url` through `UserPrincipal` end-to-end: schema, server auth, and frontend. The avatar is captured at action time from the request's auth context and persisted with the run's `created_by` principal — a point-in-time snapshot, the same pattern as audit logs and chat apps. ## What changed - **`fabro-types`** — `UserPrincipal` gains `avatar_url: Option<String>` with `#[serde(default, skip_serializing_if)]`, plus a `Principal::user_with_avatar` constructor. The existing `Principal::user` constructor is unchanged (sets `None`), so test fixtures and CLI/replay call sites need no edits. - **OpenAPI** — `PrincipalUser` gains an optional nullable `avatar_url`; Rust (progenitor) and TypeScript clients regenerated. - **`fabro-server`** — `auth_context_from_session` (cookie auth) and `classify_user_token` (JWT auth) populate the principal's avatar from the session/JWT, treating an empty string as `None`. - **`fabro-web`** — the `run-summary-panel` "Created by" cell renders an `<img>` when `avatar_url` is present, falling back to the initial circle otherwise. ## Compatibility The field is optional with serde defaults, so old persisted runs and `RunEvent.actor` payloads deserialize unchanged — they show the initial-circle fallback. No migration or backfill. ## Known gap CLI-initiated runs (`fabro run ...`) still show the initial circle: the CLI auth flow hardcodes an empty `avatar_url` in the JWT subject (`cli_flow.rs:508`). Wiring the avatar through CLI login (`~/.fabro/auth.json`, JWT claims, refresh-token chain) is a deliberate follow-up. Web-initiated runs get the avatar today. ## Test plan - `cargo nextest run --workspace` — 5,832 tests pass, including new `principal.rs` and `principal_round_trip.rs` cases covering avatar serialization and legacy-JSON (no-field) deserialization. - `cd apps/fabro-web && bun test run-summary-panel` — 13 tests pass, including a new case asserting the `<img>` renders with the avatar src. - `bun run typecheck`, `cargo +nightly-2026-04-14 fmt --check --all`, and `clippy --workspace --all-targets -- -D warnings` all clean. - Manual: restart `fabro server`, create a run from the web UI, confirm the real avatar renders on the Overview tab; confirm an older run falls back to the initial circle. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
581ab41d28
|
feat(web): add Overview summary panel and promote PR to header pill (#299)
## Summary Promotes the most useful per-run state into the Overview tab so users no longer have to tab-hop to read the basics of a run. - **Adds a horizontal summary panel** above the workflow graph (right column only — does not span the stages sidebar) with five columns: **Created by · Changes · Sandbox · Cost · Artifacts**. Quiet-uppercase labels (`text-[10px] uppercase tracking-[0.08em] text-fg-muted`) over regular-weight values. Skeleton loaders while queries are in flight; em dash in muted color for missing/zero data. - **Promotes the PR chip** out of the meta strip into a `SECONDARY_BUTTON_CLASS`-style pill next to the Actions menu, visible on every tab. Pill renders only when a PR exists. - Lifts `formatBytesAsMemory`, `formatCpuCores`, `formatUsdMicros` to `lib/format.ts` so the panel can reuse them. - New `RunSummaryPanel` is split into a smart wrapper (owns the SWR hooks) + a presentational `RunSummaryPanelView` (prop-driven) for clean test seams. - All 7 `Principal` kinds (user / agent / system / slack / webhook / worker / anonymous) map to glyph + label; user kind uses login-initial avatar. ## Screenshots Captured against a real local Fabro server (`fabro server start`) on demo runs — these only exercise the Created-by column (the other cells display em dashes because the demo runs have no PR / diff / billing / artifacts data). The em-dash states **are** the intended empty-state design. ### Overview tab — full page  ### Header + tabs + summary panel close-up  ### Summary panel detail  > The PR pill (mint icon + `#number` next to Actions) is unverified visually because no demo run on this server has an associated PR — but the rendering path is the same `SECONDARY_BUTTON_CLASS` markup as the Actions button and is conditioned on `run.pullRequestUrl && run.number != null`. See the [HTML prototype](https://github.com/fabro-sh/fabro/blob/feat/run-overview-summary-panel/.context/run-overview-options.html) for the locked design. ## Test plan - [x] `cd apps/fabro-web && bun run typecheck` clean (only pre-existing assistant-ui errors) - [x] `bun test` — +12 new passes, no new failures (387 pass / 5 fail / 2 errors vs baseline 375 / 6 / 3) - [x] Manual: load `/runs/<id>` against a real server, confirm panel + em dashes render correctly - [ ] Manual on a run **with** a PR: verify the pill appears next to Actions and opens the PR in a new tab - [ ] Manual on a run **with** rich data (diff / billing / sandbox resources / artifacts): verify each column populates correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |