Skip to content

feat(build): implement build synchronization and Docker context management#37

Merged
enegalan merged 3 commits into
mainfrom
26-feat-builds
Jul 4, 2026
Merged

feat(build): implement build synchronization and Docker context management#37
enegalan merged 3 commits into
mainfrom
26-feat-builds

Conversation

@enegalan

@enegalan enegalan commented Jul 4, 2026

Copy link
Copy Markdown
Owner
  • Added StartBuildSync and StartDockerContextManager methods to manage build history synchronization and Docker context activation.
  • Introduced new API endpoints for handling builds, including filtering by tag and retrieving build logs and sources.
  • Enhanced build artifact handling with new functions for listing and inspecting build history.
  • Updated configuration to support Docker context management, ensuring seamless integration with Docker CLI.
  • Added tests for new functionality in build history and Docker context management.

Summary by CodeRabbit

  • New Features

    • Build history is now saved locally and restored after restart.
    • Builds can be viewed in a detailed screen with source, logs, history, and timing breakdowns.
    • Build creation now supports selecting a platform.
    • Settings include a toggle to use Calf for Docker CLI management, with current status shown in the UI.
  • Bug Fixes

    • Build lists and details now load more reliably, including persisted history and log/source data.
    • Build logs and step data are shown consistently even when some fields are missing.

…ement

- Added `StartBuildSync` and `StartDockerContextManager` methods to manage build history synchronization and Docker context activation.
- Introduced new API endpoints for handling builds, including filtering by tag and retrieving build logs and sources.
- Enhanced build artifact handling with new functions for listing and inspecting build history.
- Updated configuration to support Docker context management, ensuring seamless integration with Docker CLI.
- Added tests for new functionality in build history and Docker context management.
@enegalan enegalan linked an issue Jul 4, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@enegalan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb0f5b8c-fe82-4888-974e-dd78232c4150

📥 Commits

Reviewing files that changed from the base of the PR and between b382a99 and 2e8d743.

📒 Files selected for processing (20)
  • backend/internal/api/build_sync.go
  • backend/internal/api/builds.go
  • backend/internal/api/docker_cli.go
  • backend/internal/api/handlers.go
  • backend/internal/api/migrate.go
  • backend/internal/api/server.go
  • backend/internal/buildhistory/artifacts.go
  • backend/internal/buildhistory/artifacts_test.go
  • backend/internal/buildhistory/history.go
  • backend/internal/buildhistory/history_test.go
  • backend/internal/buildhistory/inspect.go
  • backend/internal/buildhistory/inspect_test.go
  • backend/internal/buildstore/store.go
  • backend/internal/dockercli/context.go
  • backend/internal/runtime/build_parser.go
  • backend/internal/runtime/build_parser_test.go
  • backend/test/buildstore/buildstore_test.go
  • ui/lib/app_shell.dart
  • ui/lib/screens/build_detail_screen.dart
  • ui/test/widget_test.dart
📝 Walkthrough

Walkthrough

This PR adds persisted build history with a background sync from Docker Buildx history, structured build metadata (steps, dependencies, artifacts, tags, timing), an expanded builds API (list/create/detail/source/logs), Docker context (CLI) management, and corresponding Flutter UI: a build detail view, updated builds list, and a settings toggle.

Changes

Backend: Build Lifecycle, History Sync, and Docker Context

Layer / File(s) Summary
Runtime build contract and platform-aware RunBuild
backend/internal/runtime/build.go, runtime.go, native.go, lima.go, nerdctl.go, mock.go
New Build/BuildResult/BuildStep/BuildArtifact/BuildTag/BuildTiming types; Runtime.RunBuild gains a platform parameter and returns (BuildResult, error) across all implementations.
Build log parsing and metadata enrichment
build_parser.go, build_parser_test.go, build_enrich.go, build_enrich_test.go
Parses raw build logs into steps/timing, derives Dockerfile dependencies, image digests, tags, artifacts, git metadata, and normalizes Dockerfile paths via EnrichSyncedBuild.
Buildx history CLI wrappers
backend/internal/buildhistory/*
New package to list/inspect/fetch logs and collect artifacts from docker buildx history, with unit tests.
On-disk build persistence
backend/internal/buildstore/store.go, backend/test/buildstore/*
Loads/saves build history JSON under a config directory with a max-entry cap.
Builds API endpoints
backend/internal/api/builds.go, server.go
Adds tag filtering, async creation (202), detail/source/logs routes, source path resolution, and persistence wiring.
Background build history sync
backend/internal/api/build_sync.go, main.go, ROADMAP.md
Periodically merges Buildx history rows into stored builds via StartBuildSync.
Docker context management
backend/internal/dockercli/*, docker_cli.go, config.go, config.yaml, handlers.go, migrate.go
Creates/activates a calf Docker context, adds DockerContextManaged config, and wires activation into config updates and migration completion.
Backend integration tests
backend/test/api/*
HTTP tests for build creation, reload persistence, and detail/source/logs endpoints.

UI: Build Detail View and Docker Context Settings

Layer / File(s) Summary
API client models and endpoints
ui/lib/api/client.dart
New build data models, tag-filterable fetchBuilds, detail/source/logs client calls, 202 handling, and Docker context config fields.
Build detail screen
ui/lib/screens/build_detail_screen.dart
New BuildDetailView with Info/Source/Logs/History tabs, timing pie charts, and history line charts.
Builds list navigation
ui/lib/screens/resources_screen.dart
Opens/closes build detail, adds status dots and duration formatting.
Settings Docker context toggle
ui/lib/app_shell.dart
Adds switch to enable/disable Calf-managed Docker CLI context.
Widget test doubles
ui/test/widget_test.dart
Updates fakes for new fetchBuilds/fetchBuildDetail/fetchBuildSource/fetchBuildLogs signatures.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server as api.Server
  participant Runtime
  participant Store as buildstore

  Client->>Server: POST /v1/builds
  Server->>Server: newBuild(running, platform)
  Server-->>Client: 202 Accepted
  Server->>Runtime: runBuildJob (goroutine)
  Runtime-->>Server: BuildResult
  Server->>Store: persistBuildsLocked()
  Client->>Server: GET /v1/builds/{id}
  Server->>Store: loadBuilds()
  Server-->>Client: enriched Build JSON
Loading
sequenceDiagram
  participant Ticker as StartBuildSync
  participant BuildHistory as buildhistory.List
  participant Server as api.Server
  participant Store as buildstore

  Ticker->>BuildHistory: List(ctx, socket)
  BuildHistory-->>Ticker: []Row
  Ticker->>Server: updateSyncedBuilds / buildFromHistoryRow
  Server->>Store: persistBuildsLocked()
Loading

Possibly related PRs

  • enegalan/calf#19: Both PRs modify the builds API handler in backend/internal/api/builds.go, with this PR expanding the same list/create endpoint into a full build lifecycle API.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: build synchronization and Docker context management.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 26-feat-builds

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

- Updated the trailing widget condition in the `_SectionHeader` class to use a more concise syntax.
- Modified test cases to include a new property `cached` in the `BuildStep` for consistency in build logs handling.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/lib/app_shell.dart (1)

297-326: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

applyConfig() silently resets Docker context management to enabled.

This constructs a fresh Config(...) without dockerContextManaged, so it falls back to the constructor's default of true (client.dart line 1450) and gets serialized via toJson()'s docker_context_managed key. If a user disables "Use Calf for Docker CLI" and later adjusts CPU/Memory/Swap here, clicking "Apply" will silently re-enable Docker context management, overwriting the user's explicit choice.

setDockerContextManaged already avoids this by using current.copyWith(...); applyConfig should do the same.

🐛 Proposed fix
     try {
       final updated = await widget.apiClient.updateConfig(
-        Config(
-          pollIntervalMs: current.pollIntervalMs,
-          cpus: _draftCpus.toInt(),
-          memoryGB: _draftMemory.toInt(),
-          memorySwapGB: _draftSwap.toInt(),
-          hostCPUs: current.hostCPUs,
-          hostMemoryGB: current.hostMemoryGB,
-        ),
+        current.copyWith(
+          cpus: _draftCpus.toInt(),
+          memoryGB: _draftMemory.toInt(),
+          memorySwapGB: _draftSwap.toInt(),
+        ),
       );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/lib/app_shell.dart` around lines 297 - 326, `applyConfig()` is rebuilding
a new Config with only CPU, memory, swap, and host values, which drops the
existing dockerContextManaged setting and can reset it to the constructor
default. Update `applyConfig` in app_shell.dart to preserve the current config
state by using `current.copyWith(...)` (as `setDockerContextManaged` already
does) or otherwise carrying over `dockerContextManaged` from `current` when
calling `widget.apiClient.updateConfig`, so the user’s Docker CLI management
preference is not overwritten.
🧹 Nitpick comments (9)
backend/internal/api/build_sync.go (1)

110-126: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Length-only equality for Dependencies/Results/Tags.

buildsEqual compares these slices by length only, while enrichHistoryBuild re-runs EnrichSyncedBuild/BuildArtifacts unconditionally every cycle (line 164, 166-168). If content changes without a count change, the update is silently dropped (loop continues in updateSyncedBuilds) and never persisted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/api/build_sync.go` around lines 110 - 126, The buildsEqual
helper is only comparing Dependencies, Results, and Tags by slice length, so
content changes with the same count are missed and updateSyncedBuilds will skip
persistence. Update buildsEqual to compare the actual contents of those fields
(along with the existing comparisons in buildsEqual) so changes from
enrichHistoryBuild/EnrichSyncedBuild and BuildArtifacts are detected even when
slice lengths do not change.
backend/internal/buildstore/store.go (2)

55-76: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Non-atomic write risks a corrupted builds.json.

Save() writes directly to the target path. If the process is killed or crashes mid-write (e.g., during shutdown, OOM), builds.json can be left truncated/corrupted, and the next Load() will fail, silently discarding all build history (per loadBuilds()'s warn-and-continue behavior in backend/internal/api/server.go).

♻️ Proposed fix: write to a temp file then rename
 func Save(builds []runtime.Build, seq int) error {
 	path, err := Path()
 	if err != nil {
 		return err
 	}
 
 	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
 		return err
 	}
 
 	file := File{
 		Builds: trimBuilds(builds),
 		Seq:    seq,
 	}
 
 	data, err := json.MarshalIndent(file, "", "  ")
 	if err != nil {
 		return err
 	}
 
-	return os.WriteFile(path, data, 0o644)
+	tmp := path + ".tmp"
+	if err := os.WriteFile(tmp, data, 0o644); err != nil {
+		return err
+	}
+	return os.Rename(tmp, path)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/buildstore/store.go` around lines 55 - 76, Save in
buildstore/store.go is writing builds.json directly, which can leave the file
corrupted if interrupted. Update Save to write the marshaled File data to a
temporary file in the same directory, then atomically rename it into place using
Path() so the final write is all-or-nothing. Keep the existing error handling
around Path, os.MkdirAll, json.MarshalIndent, and os.WriteFile, and make sure
the temp-file flow is contained within Save.

78-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Undocumented ordering invariant.

trimBuilds keeps the first maxBuilds entries, which is only correct because callers always prepend new builds (newest-first). Worth a short comment noting this invariant so a future refactor to append-based insertion doesn't silently start dropping the newest builds instead of the oldest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/buildstore/store.go` around lines 78 - 84, The trimBuilds
helper in store.go relies on an implicit newest-first ordering invariant, since
it returns the first maxBuilds entries and only behaves correctly when callers
prepend new builds. Add a short comment near trimBuilds explaining that builds
must be kept in newest-first order so trimming removes the oldest entries.
Reference trimBuilds and any caller that inserts into the builds slice so future
changes to append-based insertion do not break this assumption.
backend/test/buildstore/buildstore_test.go (1)

12-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for the 200-build cap.

The core retention behavior (trimBuilds) isn't exercised here. Consider adding a case that saves >200 builds and asserts only the first 200 (newest) are retained.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/test/buildstore/buildstore_test.go` around lines 12 - 49, The current
buildstore test only verifies save/load for a single build and does not cover
the retention logic in trimBuilds. Extend TestSaveAndLoadBuilds (or add a new
test in buildstore_test.go) to save more than 200 runtime.Build entries through
buildstore.Save, then load them back with buildstore.Load and assert that only
200 builds remain and that the retained entries are the newest ones in the
expected order.
backend/internal/buildhistory/history.go (2)

173-208: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

ParseDurationMs doesn't handle an hour component.

Tokens ending in "h" (e.g., "1h5m10s") are never matched by either the "m" or "s" suffix checks, so any hour portion is silently dropped/misparsed. Impact is currently mitigated by the timestamp-diff fallback in BuildDurationMs, and available evidence suggests the primary List() path may not actually populate Duration from the JSON format used here, but this remains a latent gap if that assumption changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/buildhistory/history.go` around lines 173 - 208,
ParseDurationMs in history.go only handles minutes and seconds, so hour values
are skipped when parsing combined durations like 1h5m10s. Update ParseDurationMs
to recognize and accumulate an hour component alongside the existing
minute/second parsing, and make sure the tokenization logic still works for
mixed duration strings used by BuildDurationMs and List().

210-219: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider bounding runDocker with its own timeout.

This helper backs all buildhistory CLI calls (used by periodic background sync per build_sync.go). It relies entirely on the caller's ctx for a deadline; a hang from an unresponsive docker daemon would block the calling goroutine indefinitely with no defense at this layer.

🛡️ Suggested defensive timeout
 func runDocker(ctx context.Context, socket string, args ...string) ([]byte, error) {
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
 	command := exec.CommandContext(ctx, "docker", args...)
 	command.Env = append(os.Environ(), "DOCKER_HOST=unix://"+socket)
 	output, err := command.CombinedOutput()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/buildhistory/history.go` around lines 210 - 219, The
runDocker helper currently depends only on the caller’s ctx, so a hung docker
invocation can block buildhistory work indefinitely. Add a defensive timeout
inside runDocker by deriving a child context with context.WithTimeout before
calling exec.CommandContext, then cancel it properly after CombinedOutput. Keep
the existing error wrapping and apply this change in runDocker so all
buildhistory CLI paths inherit the safeguard.
backend/internal/runtime/build_parser_test.go (1)

10-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a test with a continuation line before DONE.

None of the fixtures place a progress/continuation line (e.g. #N some progress text) between a step's header and its DONE, so the header-overwrite issue flagged in build_parser.go wouldn't be caught here. Adding a case like a RUN step with an interim output line before DONE would guard against regressions once that fix lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/runtime/build_parser_test.go` around lines 10 - 61, Add a
parser test in TestParseBuildOutputSteps that includes a build step with an
intermediate progress/continuation line between the step header and its DONE
line, so ParseBuildOutput is exercised against the header-overwrite case. Extend
the existing output fixture to include a step like RUN with a non-DONE line
before completion, then assert the step is still captured correctly with the
right name, caching state, and duration. Use the existing
TestParseBuildOutputSteps and ParseBuildOutput symbols to locate the change.
ui/test/widget_test.dart (1)

375-400: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New build methods on _ErrorCalfClient return success data instead of simulating a daemon error, unlike its other overrides.

Every other method on this class throws ApiException(..., statusCode: 503) to simulate "daemon unavailable" for error-path tests. The newly added fetchBuildDetail/fetchBuildSource/fetchBuildLogs instead return fixed success payloads, so any future widget test using _ErrorCalfClient to exercise the build detail error states (_detailError/_sourceError/_logsError) won't actually see an error.

♻️ Proposed fix for consistency
   `@override`
-  Future<BuildDetail> fetchBuildDetail(String id) async => BuildDetail(
-        id: id,
-        tag: 'demo',
-        context: '.',
-        status: 'success',
-        createdAt: '2026-01-01T00:00:00Z',
-      );
-
-  `@override`
-  Future<BuildSource> fetchBuildSource(String id) async => const BuildSource(
-        path: 'Dockerfile',
-        filename: 'Dockerfile',
-        content: 'FROM alpine',
-        platform: 'arm64',
-      );
-
-  `@override`
-  Future<BuildLogs> fetchBuildLogs(String id) async => const BuildLogs(
-        rawLog: '`#1` DONE 0.1s',
-        steps: [
-          BuildStep(index: 1, total: 1, name: 'load build definition', durationMs: 100),
-        ],
-      );
+  Future<BuildDetail> fetchBuildDetail(String id) async {
+    await Future<void>.delayed(Duration.zero);
+    throw ApiException('daemon unavailable', statusCode: 503);
+  }
+
+  `@override`
+  Future<BuildSource> fetchBuildSource(String id) async {
+    await Future<void>.delayed(Duration.zero);
+    throw ApiException('daemon unavailable', statusCode: 503);
+  }
+
+  `@override`
+  Future<BuildLogs> fetchBuildLogs(String id) async {
+    await Future<void>.delayed(Duration.zero);
+    throw ApiException('daemon unavailable', statusCode: 503);
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/test/widget_test.dart` around lines 375 - 400, The new build-related
overrides on _ErrorCalfClient are returning successful payloads instead of
matching the daemon-unavailable behavior used by the rest of the class. Update
fetchBuildDetail, fetchBuildSource, and fetchBuildLogs to throw the same
ApiException with statusCode 503 that the other _ErrorCalfClient methods use, so
widget tests can actually exercise the _detailError, _sourceError, and
_logsError paths consistently.
ui/lib/screens/build_detail_screen.dart (1)

1296-1335: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate status/duration formatting logic with resources_screen.dart.

_statusColor/_formatDuration here are near-identical to _buildStatusColor/_formatBuildDuration added in ui/lib/screens/resources_screen.dart. Consider extracting a shared build_formatting.dart (or similar) helper to avoid divergence.

♻️ Proposed shared helper sketch
// ui/lib/utils/build_format.dart
Color buildStatusColor(String status, ShadThemeData theme) { ... }
String formatBuildDuration(int durationMs, {String zeroLabel = '0.0s'}) { ... }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/lib/screens/build_detail_screen.dart` around lines 1296 - 1335, The status
and duration formatting logic in build_detail_screen is duplicated with the
newer helpers in resources_screen, so extract the shared behavior into a common
utility and have both screens call it. Move the shared logic from _statusLabel,
_statusColor, and _formatDuration alongside the matching _buildStatusColor and
_formatBuildDuration usage into a reusable helper such as build_format.dart,
then update build_detail_screen to use the shared functions to keep formatting
consistent and avoid divergence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/internal/api/build_sync.go`:
- Around line 61-171: `s.buildsMu` is held while `syncBuildHistory` performs
slow Buildx/CLI-backed enrichment, which can block all build access if a
subprocess hangs. Update `syncBuildHistory` and its helpers
(`updateSyncedBuilds`, `buildFromHistoryRow`, `enrichHistoryBuild`) so external
calls like `buildhistory.Inspect`, `buildhistory.Logs`,
`runtime.EnrichSyncedBuild`, and `buildhistory.BuildArtifacts` run with a
bounded timeout and/or outside the mutex, then re-lock only to merge the updated
`s.builds` results.
- Around line 74-78: The build import path in build_sync.go is reversing the
newest-first order returned by buildhistory.MergeRows by prepending each item
individually inside the imported loop. Update the logic around
buildhistory.MergeRows and s.buildFromHistoryRow so the entire imported batch is
added in one shot, or iterate imported in reverse, and keep trimBuilds working
on the resulting newest-first slice so newer builds are not dropped before older
ones.

In `@backend/internal/api/builds.go`:
- Around line 215-270: The background build path in runBuildJob currently uses
context.Background() when calling s.runtime.RunBuild, so a hung build can run
forever with no cancellation. Update runBuildJob to create a deadline-bound
context for the build execution, pass that context into RunBuild, and ensure the
timeout/cancellation is handled when setting build.Status and build.Error. Use
the existing runBuildJob and s.runtime.RunBuild symbols to locate the change.
- Around line 331-371: enrichHistoryBuildIfNeeded can overwrite a newer snapshot
because it enriches a build read outside buildsMu and then writes the whole
struct back later; adjust the flow so the latest build state is reloaded under
s.buildsMu before persisting, or merge only the missing enrichment fields into
the current slice entry instead of replacing the full runtime.Build. Keep the
fix anchored in enrichHistoryBuildIfNeeded, buildsEqual, and the s.builds update
loop so StartBuildSync updates are preserved.

In `@backend/internal/api/docker_cli.go`:
- Around line 29-37: `Server.cfg` is accessed concurrently without
synchronization, causing a data race between background Docker context checks
and request/migration code. Add a `cfgMu sync.RWMutex` to `Server`, use a read
lock in `ensureDockerContext`, `dockerCLIStatus`, and `configResponse`, and use
a write lock anywhere `s.cfg` is mutated or read during updates in the
`handleConfig` PUT path and the `migrate.go` config save/post-migration flow.

In `@backend/internal/api/handlers.go`:
- Around line 95-110: The PUT handler in handlers.go can hang indefinitely
because activateDockerContext is called synchronously with only r.Context() and
no explicit deadline. Update the request path around activateDockerContext to
use a bounded timeout context (for example via context.WithTimeout) before
invoking the docker CLI work, and make sure the timeout is applied consistently
through config PUT so stalled docker context creation/update/use cannot block
the response forever. Also verify the timeout propagates through
activateDockerContext and any helper it calls, especially contextExists in
context.go.

In `@backend/internal/api/migrate.go`:
- Around line 91-95: The post-migration Docker context activation in
migrateCompleted/activateDockerContext is using context.Background(), which
makes the docker CLI call unbounded and can hang indefinitely. Replace it with
an appropriate cancellable context from the surrounding flow (or derive one with
timeout/deadline consistent with the periodic manager and config PUT handler),
and thread that context through activateDockerContext so the post-migration path
can abort cleanly if docker stalls.

In `@backend/internal/buildhistory/artifacts.go`:
- Around line 171-187: The traceArtifact helper is using buildx history trace,
which returns Jaeger UI/status output instead of actual trace bytes. Update
traceArtifact to either call a real trace export path that produces
OpenTelemetry data before hashing it with digestForBytes, or remove the artifact
entirely if no export is available. Keep the logic in traceArtifact and its
runDocker usage aligned with the real byte-producing source so the BuildArtifact
name, digest, and size reflect exported trace data.

In `@backend/internal/buildhistory/inspect.go`:
- Around line 20-56: The parsing in parseInspectDetail is splitting the buildx
history inspect output on whitespace, which breaks Context values containing
spaces and can bypass the empty check. Update inspect to request structured
output from runDocker for buildx history inspect (for example JSON via the
format flag) and parse that payload directly in parseInspectDetail instead of
using strings.Fields. Add a test case covering a Context path with spaces to
verify the value is preserved end-to-end.

In `@backend/internal/dockercli/context.go`:
- Around line 66-74: The `contextExists` helper currently uses `exec.Command`
without any `context.Context`, so Docker inspection can hang indefinitely and
block callers like `StatusFor` and `EnsureContext`. Update `contextExists` to
accept a context and use a context-aware command execution path, then thread a
bounded timeout context from the `StatusFor` and `EnsureContext` call sites so
Docker CLI checks can be canceled or time-limited. Keep the change localized
around `contextExists`, `StatusFor`, and `EnsureContext` to preserve existing
behavior while preventing unbounded blocking.

In `@backend/internal/runtime/build_parser.go`:
- Around line 10-15: The build parser is treating continuation/progress lines as
new headers, which overwrites the current step name and can misclassify timing
in classifyTiming. Update the parsing logic in build_parser.go around
buildStepHeaderRe and the step-handling branch so that a line only becomes a
header when it is the first match for a step; if a step is already open, route
bracket-less progress/output lines into the log-append path instead of resetting
step.Name. Use buildStepHeaderRe, classifyTiming, and the existing open-step
handling to keep genuine headers like exporting lines working while preserving
the original step display name and timing bucket.

In `@ui/lib/screens/build_detail_screen.dart`:
- Around line 592-665: The timing charts in _TimingCharts are double-counting
values and producing misleading pie percentages. Update the “Accumulated time”
card to use its own aggregated slices instead of reusing realTime, and revise
cacheSlices so the segments represent a single partition of totalSteps rather
than including both total and its parts. Also fix the “Parallel execution”
slices so Active and Idle are mutually exclusive and do not sum to totalMs
twice; use the existing BuildTiming fields in _timingSlices and
_TimingChartCardState to keep tooltip percentages accurate.
- Around line 517-531: The Build results table is passing `item.size` as the
last cell, so `_DataTable`’s copy action uses the wrong value. Update the
`BuildDetailScreen` rows passed into `_DataTable` so the digest is the final
entry, matching the copy behavior used by the other tables. Keep the `columns`
and row order aligned in the table-building logic around `results.map(...)`.
- Around line 545-590: The _PlatformFilter button menu is anchored to a fixed
overlay origin instead of the trigger widget. Update the showMenu call inside
_PlatformFilter.build/onPressed to compute the popup position from the button’s
actual RenderBox/overlay location, so the menu opens aligned under the “Filter
by platform” CalfButton rather than at the screen corner.

---

Outside diff comments:
In `@ui/lib/app_shell.dart`:
- Around line 297-326: `applyConfig()` is rebuilding a new Config with only CPU,
memory, swap, and host values, which drops the existing dockerContextManaged
setting and can reset it to the constructor default. Update `applyConfig` in
app_shell.dart to preserve the current config state by using
`current.copyWith(...)` (as `setDockerContextManaged` already does) or otherwise
carrying over `dockerContextManaged` from `current` when calling
`widget.apiClient.updateConfig`, so the user’s Docker CLI management preference
is not overwritten.

---

Nitpick comments:
In `@backend/internal/api/build_sync.go`:
- Around line 110-126: The buildsEqual helper is only comparing Dependencies,
Results, and Tags by slice length, so content changes with the same count are
missed and updateSyncedBuilds will skip persistence. Update buildsEqual to
compare the actual contents of those fields (along with the existing comparisons
in buildsEqual) so changes from enrichHistoryBuild/EnrichSyncedBuild and
BuildArtifacts are detected even when slice lengths do not change.

In `@backend/internal/buildhistory/history.go`:
- Around line 173-208: ParseDurationMs in history.go only handles minutes and
seconds, so hour values are skipped when parsing combined durations like
1h5m10s. Update ParseDurationMs to recognize and accumulate an hour component
alongside the existing minute/second parsing, and make sure the tokenization
logic still works for mixed duration strings used by BuildDurationMs and List().
- Around line 210-219: The runDocker helper currently depends only on the
caller’s ctx, so a hung docker invocation can block buildhistory work
indefinitely. Add a defensive timeout inside runDocker by deriving a child
context with context.WithTimeout before calling exec.CommandContext, then cancel
it properly after CombinedOutput. Keep the existing error wrapping and apply
this change in runDocker so all buildhistory CLI paths inherit the safeguard.

In `@backend/internal/buildstore/store.go`:
- Around line 55-76: Save in buildstore/store.go is writing builds.json
directly, which can leave the file corrupted if interrupted. Update Save to
write the marshaled File data to a temporary file in the same directory, then
atomically rename it into place using Path() so the final write is
all-or-nothing. Keep the existing error handling around Path, os.MkdirAll,
json.MarshalIndent, and os.WriteFile, and make sure the temp-file flow is
contained within Save.
- Around line 78-84: The trimBuilds helper in store.go relies on an implicit
newest-first ordering invariant, since it returns the first maxBuilds entries
and only behaves correctly when callers prepend new builds. Add a short comment
near trimBuilds explaining that builds must be kept in newest-first order so
trimming removes the oldest entries. Reference trimBuilds and any caller that
inserts into the builds slice so future changes to append-based insertion do not
break this assumption.

In `@backend/internal/runtime/build_parser_test.go`:
- Around line 10-61: Add a parser test in TestParseBuildOutputSteps that
includes a build step with an intermediate progress/continuation line between
the step header and its DONE line, so ParseBuildOutput is exercised against the
header-overwrite case. Extend the existing output fixture to include a step like
RUN with a non-DONE line before completion, then assert the step is still
captured correctly with the right name, caching state, and duration. Use the
existing TestParseBuildOutputSteps and ParseBuildOutput symbols to locate the
change.

In `@backend/test/buildstore/buildstore_test.go`:
- Around line 12-49: The current buildstore test only verifies save/load for a
single build and does not cover the retention logic in trimBuilds. Extend
TestSaveAndLoadBuilds (or add a new test in buildstore_test.go) to save more
than 200 runtime.Build entries through buildstore.Save, then load them back with
buildstore.Load and assert that only 200 builds remain and that the retained
entries are the newest ones in the expected order.

In `@ui/lib/screens/build_detail_screen.dart`:
- Around line 1296-1335: The status and duration formatting logic in
build_detail_screen is duplicated with the newer helpers in resources_screen, so
extract the shared behavior into a common utility and have both screens call it.
Move the shared logic from _statusLabel, _statusColor, and _formatDuration
alongside the matching _buildStatusColor and _formatBuildDuration usage into a
reusable helper such as build_format.dart, then update build_detail_screen to
use the shared functions to keep formatting consistent and avoid divergence.

In `@ui/test/widget_test.dart`:
- Around line 375-400: The new build-related overrides on _ErrorCalfClient are
returning successful payloads instead of matching the daemon-unavailable
behavior used by the rest of the class. Update fetchBuildDetail,
fetchBuildSource, and fetchBuildLogs to throw the same ApiException with
statusCode 503 that the other _ErrorCalfClient methods use, so widget tests can
actually exercise the _detailError, _sourceError, and _logsError paths
consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03d8000b-ee33-4c43-8cab-fae41ac7ca0c

📥 Commits

Reviewing files that changed from the base of the PR and between 0a4b338 and b382a99.

📒 Files selected for processing (41)
  • ROADMAP.md
  • backend/api.test
  • backend/buildstore.test
  • backend/cmd/calf/main.go
  • backend/internal/api/build_sync.go
  • backend/internal/api/builds.go
  • backend/internal/api/docker_cli.go
  • backend/internal/api/handlers.go
  • backend/internal/api/migrate.go
  • backend/internal/api/server.go
  • backend/internal/buildhistory/artifacts.go
  • backend/internal/buildhistory/artifacts_test.go
  • backend/internal/buildhistory/history.go
  • backend/internal/buildhistory/history_test.go
  • backend/internal/buildhistory/inspect.go
  • backend/internal/buildhistory/inspect_test.go
  • backend/internal/buildhistory/logs.go
  • backend/internal/buildstore/store.go
  • backend/internal/config/config.go
  • backend/internal/config/config.yaml
  • backend/internal/dockercli/context.go
  • backend/internal/dockercli/context_test.go
  • backend/internal/runtime/build.go
  • backend/internal/runtime/build_enrich.go
  • backend/internal/runtime/build_enrich_test.go
  • backend/internal/runtime/build_parser.go
  • backend/internal/runtime/build_parser_test.go
  • backend/internal/runtime/lima.go
  • backend/internal/runtime/mock.go
  • backend/internal/runtime/native.go
  • backend/internal/runtime/nerdctl.go
  • backend/internal/runtime/runtime.go
  • backend/runtime.test
  • backend/test/api/api_test.go
  • backend/test/api/builds_test.go
  • backend/test/buildstore/buildstore_test.go
  • ui/lib/api/client.dart
  • ui/lib/app_shell.dart
  • ui/lib/screens/build_detail_screen.dart
  • ui/lib/screens/resources_screen.dart
  • ui/test/widget_test.dart

Comment thread backend/internal/api/build_sync.go Outdated
Comment thread backend/internal/api/build_sync.go Outdated
Comment thread backend/internal/api/builds.go
Comment thread backend/internal/api/builds.go
Comment on lines +29 to +37
func (s *Server) ensureDockerContext(ctx context.Context) {
if !s.cfg.DockerContextManaged {
return
}

socket := s.runtime.DockerSocket()
if socket == "" {
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Data race on s.cfg across goroutines.

ensureDockerContext reads s.cfg.DockerContextManaged from the periodic background goroutine (StartDockerContextManager ticker), while handlers.go's handleConfig PUT branch and migrate.go's post-migration block mutate/read the same Server.cfg fields from separate per-request/migration goroutines. Server has no mutex protecting cfg (only buildsMu/migrateMu exist for other fields), so this is an unsynchronized concurrent read/write — a data race per Go's memory model.

🔒 Suggested approach

Add a cfgMu sync.RWMutex to Server, take a read lock in ensureDockerContext/dockerCLIStatus/configResponse, and a write lock wherever s.cfg fields are mutated (handlers.go PUT handler, migrate.go's SaveConfig callback and post-migration activation check), mirroring the existing buildsMu/migrateMu pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/api/docker_cli.go` around lines 29 - 37, `Server.cfg` is
accessed concurrently without synchronization, causing a data race between
background Docker context checks and request/migration code. Add a `cfgMu
sync.RWMutex` to `Server`, use a read lock in `ensureDockerContext`,
`dockerCLIStatus`, and `configResponse`, and use a write lock anywhere `s.cfg`
is mutated or read during updates in the `handleConfig` PUT path and the
`migrate.go` config save/post-migration flow.

Comment thread backend/internal/dockercli/context.go Outdated
Comment thread backend/internal/runtime/build_parser.go
Comment on lines +517 to +531
_DataTable(
theme: theme,
columns: const ['Artifact', 'Platform', 'Digest', 'Size'],
rows: results
.map(
(item) => [
item.name,
item.platform,
item.digest,
item.size,
],
)
.toList(),
onCopy: onCopy,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Copy button on the "Build results" table copies Size, not Digest, unlike the other tables.

_DataTable assumes row.last is the copyable value (works for Dependencies/Tags where the last column is Digest), but here the last column is item.size, so tapping copy on a result row copies the size string instead of the digest.

🐛 Proposed fix — reorder columns so digest is last
         _DataTable(
           theme: theme,
-          columns: const ['Artifact', 'Platform', 'Digest', 'Size'],
+          columns: const ['Artifact', 'Platform', 'Size', 'Digest'],
           rows: results
               .map(
                 (item) => [
                   item.name,
                   item.platform,
-                  item.digest,
                   item.size,
+                  item.digest,
                 ],
               )
               .toList(),
           onCopy: onCopy,
         ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_DataTable(
theme: theme,
columns: const ['Artifact', 'Platform', 'Digest', 'Size'],
rows: results
.map(
(item) => [
item.name,
item.platform,
item.digest,
item.size,
],
)
.toList(),
onCopy: onCopy,
),
_DataTable(
theme: theme,
columns: const ['Artifact', 'Platform', 'Size', 'Digest'],
rows: results
.map(
(item) => [
item.name,
item.platform,
item.size,
item.digest,
],
)
.toList(),
onCopy: onCopy,
),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/lib/screens/build_detail_screen.dart` around lines 517 - 531, The Build
results table is passing `item.size` as the last cell, so `_DataTable`’s copy
action uses the wrong value. Update the `BuildDetailScreen` rows passed into
`_DataTable` so the digest is the final entry, matching the copy behavior used
by the other tables. Keep the `columns` and row order aligned in the
table-building logic around `results.map(...)`.

Comment thread ui/lib/screens/build_detail_screen.dart
Comment on lines +592 to +665
class _TimingCharts extends StatelessWidget {
const _TimingCharts({
required this.theme,
required this.timing,
required this.totalMs,
required this.cachedSteps,
required this.totalSteps,
});

final ShadThemeData theme;
final BuildTiming timing;
final int totalMs;
final int cachedSteps;
final int totalSteps;

@override
Widget build(BuildContext context) {
final realTime = _timingSlices(timing);
final cacheSlices = [
_TimingSlice('Executions', totalSteps.toDouble(), const Color(0xFF3B82F6)),
_TimingSlice('Cached steps', cachedSteps.toDouble(), const Color(0xFF22C55E)),
_TimingSlice('Other steps', (totalSteps - cachedSteps).toDouble().clamp(0, double.infinity), theme.colorScheme.mutedForeground),
];

return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.4,
children: [
_TimingChartCard(
theme: theme,
title: 'Real time (${_formatDuration(totalMs)})',
slices: realTime,
formatValue: (value) => _formatDuration(value.toInt()),
),
_TimingChartCard(
theme: theme,
title: 'Accumulated time (${_formatDuration(totalMs)})',
slices: realTime,
formatValue: (value) => _formatDuration(value.toInt()),
),
_TimingChartCard(
theme: theme,
title: 'Cache usage ($cachedSteps/$totalSteps)',
slices: cacheSlices,
formatValue: (value) => '${value.toInt()}',
),
_TimingChartCard(
theme: theme,
title: 'Parallel execution',
slices: [
_TimingSlice('Active', totalMs > 0 ? totalMs.toDouble() : 1, const Color(0xFF3B82F6)),
_TimingSlice('Idle', timing.idleMs.toDouble(), theme.colorScheme.mutedForeground),
],
formatValue: (value) => _formatDuration(value.toInt()),
),
],
);
}

List<_TimingSlice> _timingSlices(BuildTiming timing) {
return [
_TimingSlice('Local file transfers', timing.localTransfersMs.toDouble(), const Color(0xFF166534)),
_TimingSlice('Image pulls', timing.imagePullsMs.toDouble(), const Color(0xFF4ADE80)),
_TimingSlice('Executions', timing.executionsMs.toDouble(), const Color(0xFF3B82F6)),
_TimingSlice('File operations', timing.fileOperationsMs.toDouble(), const Color(0xFFEF4444)),
_TimingSlice('Result exports', timing.resultExportsMs.toDouble(), const Color(0xFFA855F7)),
_TimingSlice('Idle', timing.idleMs.toDouble(), theme.colorScheme.mutedForeground),
].where((slice) => slice.value > 0).toList();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Timing pie charts double-count totals, producing misleading percentages.

Two issues in _TimingCharts:

  1. "Real time" and "Accumulated time" cards (Lines 624-635) both render slices: realTime — they show identical data, so "Accumulated time" adds no information.
  2. cacheSlices (Lines 610-614) includes 'Executions' (= totalSteps) alongside 'Cached steps' (= cachedSteps) and 'Other steps' (= totalSteps - cachedSteps). Since Cached + Other == Executions == totalSteps, the slices sum to 2 × totalSteps, so the pie (and the touch tooltip percentage in _TimingChartCardState) will show roughly half the correct cache ratio.
  3. Similarly, the "Parallel execution" chart (Lines 642-650) sets 'Active' = totalMs and 'Idle' = timing.idleMs, but idle time is presumably already part of totalMs, so this again double-counts and skews the percentages.
🐛 Proposed fix for the cache-usage double counting
-    final cacheSlices = [
-      _TimingSlice('Executions', totalSteps.toDouble(), const Color(0xFF3B82F6)),
-      _TimingSlice('Cached steps', cachedSteps.toDouble(), const Color(0xFF22C55E)),
-      _TimingSlice('Other steps', (totalSteps - cachedSteps).toDouble().clamp(0, double.infinity), theme.colorScheme.mutedForeground),
-    ];
+    final cacheSlices = [
+      _TimingSlice('Cached steps', cachedSteps.toDouble(), const Color(0xFF22C55E)),
+      _TimingSlice('Other steps', (totalSteps - cachedSteps).toDouble().clamp(0, double.infinity), theme.colorScheme.mutedForeground),
+    ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class _TimingCharts extends StatelessWidget {
const _TimingCharts({
required this.theme,
required this.timing,
required this.totalMs,
required this.cachedSteps,
required this.totalSteps,
});
final ShadThemeData theme;
final BuildTiming timing;
final int totalMs;
final int cachedSteps;
final int totalSteps;
@override
Widget build(BuildContext context) {
final realTime = _timingSlices(timing);
final cacheSlices = [
_TimingSlice('Executions', totalSteps.toDouble(), const Color(0xFF3B82F6)),
_TimingSlice('Cached steps', cachedSteps.toDouble(), const Color(0xFF22C55E)),
_TimingSlice('Other steps', (totalSteps - cachedSteps).toDouble().clamp(0, double.infinity), theme.colorScheme.mutedForeground),
];
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.4,
children: [
_TimingChartCard(
theme: theme,
title: 'Real time (${_formatDuration(totalMs)})',
slices: realTime,
formatValue: (value) => _formatDuration(value.toInt()),
),
_TimingChartCard(
theme: theme,
title: 'Accumulated time (${_formatDuration(totalMs)})',
slices: realTime,
formatValue: (value) => _formatDuration(value.toInt()),
),
_TimingChartCard(
theme: theme,
title: 'Cache usage ($cachedSteps/$totalSteps)',
slices: cacheSlices,
formatValue: (value) => '${value.toInt()}',
),
_TimingChartCard(
theme: theme,
title: 'Parallel execution',
slices: [
_TimingSlice('Active', totalMs > 0 ? totalMs.toDouble() : 1, const Color(0xFF3B82F6)),
_TimingSlice('Idle', timing.idleMs.toDouble(), theme.colorScheme.mutedForeground),
],
formatValue: (value) => _formatDuration(value.toInt()),
),
],
);
}
List<_TimingSlice> _timingSlices(BuildTiming timing) {
return [
_TimingSlice('Local file transfers', timing.localTransfersMs.toDouble(), const Color(0xFF166534)),
_TimingSlice('Image pulls', timing.imagePullsMs.toDouble(), const Color(0xFF4ADE80)),
_TimingSlice('Executions', timing.executionsMs.toDouble(), const Color(0xFF3B82F6)),
_TimingSlice('File operations', timing.fileOperationsMs.toDouble(), const Color(0xFFEF4444)),
_TimingSlice('Result exports', timing.resultExportsMs.toDouble(), const Color(0xFFA855F7)),
_TimingSlice('Idle', timing.idleMs.toDouble(), theme.colorScheme.mutedForeground),
].where((slice) => slice.value > 0).toList();
}
}
class _TimingCharts extends StatelessWidget {
const _TimingCharts({
required this.theme,
required this.timing,
required this.totalMs,
required this.cachedSteps,
required this.totalSteps,
});
final ShadThemeData theme;
final BuildTiming timing;
final int totalMs;
final int cachedSteps;
final int totalSteps;
`@override`
Widget build(BuildContext context) {
final realTime = _timingSlices(timing);
final cacheSlices = [
_TimingSlice('Cached steps', cachedSteps.toDouble(), const Color(0xFF22C55E)),
_TimingSlice('Other steps', (totalSteps - cachedSteps).toDouble().clamp(0, double.infinity), theme.colorScheme.mutedForeground),
];
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.4,
children: [
_TimingChartCard(
theme: theme,
title: 'Real time (${_formatDuration(totalMs)})',
slices: realTime,
formatValue: (value) => _formatDuration(value.toInt()),
),
_TimingChartCard(
theme: theme,
title: 'Accumulated time (${_formatDuration(totalMs)})',
slices: realTime,
formatValue: (value) => _formatDuration(value.toInt()),
),
_TimingChartCard(
theme: theme,
title: 'Cache usage ($cachedSteps/$totalSteps)',
slices: cacheSlices,
formatValue: (value) => '${value.toInt()}',
),
_TimingChartCard(
theme: theme,
title: 'Parallel execution',
slices: [
_TimingSlice('Active', totalMs > 0 ? totalMs.toDouble() : 1, const Color(0xFF3B82F6)),
_TimingSlice('Idle', timing.idleMs.toDouble(), theme.colorScheme.mutedForeground),
],
formatValue: (value) => _formatDuration(value.toInt()),
),
],
);
}
List<_TimingSlice> _timingSlices(BuildTiming timing) {
return [
_TimingSlice('Local file transfers', timing.localTransfersMs.toDouble(), const Color(0xFF166534)),
_TimingSlice('Image pulls', timing.imagePullsMs.toDouble(), const Color(0xFF4ADE80)),
_TimingSlice('Executions', timing.executionsMs.toDouble(), const Color(0xFF3B82F6)),
_TimingSlice('File operations', timing.fileOperationsMs.toDouble(), const Color(0xFFEF4444)),
_TimingSlice('Result exports', timing.resultExportsMs.toDouble(), const Color(0xFFA855F7)),
_TimingSlice('Idle', timing.idleMs.toDouble(), theme.colorScheme.mutedForeground),
].where((slice) => slice.value > 0).toList();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/lib/screens/build_detail_screen.dart` around lines 592 - 665, The timing
charts in _TimingCharts are double-counting values and producing misleading pie
percentages. Update the “Accumulated time” card to use its own aggregated slices
instead of reusing realTime, and revise cacheSlices so the segments represent a
single partition of totalSteps rather than including both total and its parts.
Also fix the “Parallel execution” slices so Active and Idle are mutually
exclusive and do not sum to totalMs twice; use the existing BuildTiming fields
in _timingSlices and _TimingChartCardState to keep tooltip percentages accurate.

- Introduced timeouts for build synchronization and Docker context operations to improve reliability.
- Refactored build history synchronization logic to utilize enriched build data and ensure accurate updates.
- Updated context management to handle Docker context activation with timeouts, enhancing responsiveness.
- Improved error handling in build job execution to provide clearer feedback on timeouts and cancellations.
- Added tests for new timeout behaviors and build synchronization enhancements.
@enegalan enegalan merged commit e3935bd into main Jul 4, 2026
3 checks passed
@enegalan enegalan deleted the 26-feat-builds branch July 4, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Builds

1 participant