Skip to content

feat: show each rollout's Swarm tasks and downtime as it happens - #431

Open
PiquelChips wants to merge 9 commits into
feat/timeline-4-uifrom
feat/timeline-5-tasks
Open

PiquelChips wants to merge 9 commits into
feat/timeline-4-uifrom
feat/timeline-5-tasks

Conversation

@PiquelChips

@PiquelChips PiquelChips commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

While a deployment rolled a service out, the timeline only said "Wait for service web · running". You could not see which tasks Swarm was replacing, whether Docker was still watching a new one for failure, or whether the service was down: a stop-first rollout (any service that mounts a volume read-write) stops its old task before the new one serves, and nothing showed that gap.

Now the daemon records the service's Swarm tasks each time one changes state while it waits, and both surfaces show the rollout as it happens:

  • Dashboard: the wait step's detail draws a lane per task, old and new, colored by state (preparing, starting while its health check runs, running, stopped). While Docker watches a new task, its lane is striped and a ring counts the monitor window down. A red striped bar on the step's row marks each time no task served, and the detail says for how long ("No web task is serving: down for 2.1 s so far", then "Downtime 3.2 s"). It only says there was no downtime when the data shows it: an old task seen still serving once a new one did.
  • piquelctl: the running step's row reads 1/2 new tasks running · monitor 4.2 s left, and down 3.2 s when the service was down. Without a terminal, a line prints each time one more new task runs, and the step's last line carries the downtime. --json emits step events on the same changes.

Service steps are also named by their manifest name ("Wait for service web") instead of their long Docker name.

Checked against a dev instance with a 2-replica, health-checked start-first service and a 1-replica stop-first service that mounts a volume. That run turned up a bug in the first commit: task and service specifications never compare equal on a real engine, because Docker reports defaults in one and not the other, so every task looked old. Tasks are now told apart by whether Swarm created them since the service's latest update started.

Known limit: the daemon polls tasks at least once a second, so a handover shorter than one poll shows neither a gap nor an overlap. In that case the dashboard says nothing about downtime rather than guessing.

Performance: lanes redraw only when their step changes. The shared one-second clock moves one CSS variable that open states grow to, and the ring sweeps through a CSS animation. The CLI fetches nothing extra and redraws on its existing 100 ms tick.

Proved on a dev instance with real Swarm rollouts (start-first and stop-first, task lanes, monitor countdown, downtime) and core, CLI and Playwright tests.

Reviewed with review-loop: 3 rounds with 3× Claude Opus 5.5 (GPT 6.1 Sol was out of credits; a Sol pass is still owed). Verdict: approved

Note

Written by Claude Opus 5.5 in Claude Code and reviewed by Claude Opus 5.5 (via T3 Code)

@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 22023ede-47a6-42ac-b95b-419a80e778a0
Commits

Reviewing files that changed from the base of the PR and between c7f36a6 and afd4963.

Files ignored due to path filters: 43

  • docs/images/accounts/accounts-page.png is excluded by !**/*.png
  • docs/images/accounts/grant-editor.png is excluded by !**/*.png
  • docs/images/accounts/new-api-token.png is excluded by !**/*.png
  • docs/images/accounts/passkeys.png is excluded by !**/*.png
  • docs/images/accounts/pending-links.png is excluded by !**/*.png
  • docs/images/accounts/sessions-and-tokens.png is excluded by !**/*.png
  • docs/images/applications/application-secrets.png is excluded by !**/*.png
  • docs/images/applications/build-output.png is excluded by !**/*.png
  • docs/images/applications/command-events.png is excluded by !**/*.png
  • docs/images/applications/environment-links.png is excluded by !**/*.png
  • docs/images/applications/environment-secrets.png is excluded by !**/*.png
  • docs/images/applications/environment-source.png is excluded by !**/*.png
  • docs/images/applications/environment-sync.png is excluded by !**/*.png
  • docs/images/applications/environments.png is excluded by !**/*.png
  • docs/images/applications/job-run.png is excluded by !**/*.png
  • docs/images/applications/jobs-tab.png is excluded by !**/*.png
  • docs/images/applications/links-tab.png is excluded by !**/*.png
  • docs/images/applications/preview-create.png is excluded by !**/*.png
  • docs/images/applications/preview-local.png is excluded by !**/*.png
  • docs/images/applications/previews.png is excluded by !**/*.png
  • docs/images/applications/repository-connect.png is excluded by !**/*.png
  • docs/images/applications/repository-webhook.png is excluded by !**/*.png
  • docs/images/applications/restore.png is excluded by !**/*.png
  • docs/images/applications/rollback.png is excluded by !**/*.png
  • docs/images/applications/service-git-source.png is excluded by !**/*.png
  • docs/images/applications/service-health-check.png is excluded by !**/*.png
  • docs/images/applications/terminal.png is excluded by !**/*.png
  • docs/images/applications/volume-mounts.png is excluded by !**/*.png
  • docs/images/getting-started/add-service.png is excluded by !**/*.png
  • docs/images/getting-started/create-account.png is excluded by !**/*.png
  • docs/images/getting-started/environment.png is excluded by !**/*.png
  • docs/images/getting-started/health-check.png is excluded by !**/*.png
  • docs/images/getting-started/new-application.png is excluded by !**/*.png
  • docs/images/getting-started/overview.png is excluded by !**/*.png
  • docs/images/networking/environment-visibility.png is excluded by !**/*.png
  • docs/images/networking/routes-tab.png is excluded by !**/*.png
  • docs/images/networking/webhook-card.png is excluded by !**/*.png
  • docs/images/operations/audit.png is excluded by !**/*.png
  • docs/images/operations/deployment-timeline.png is excluded by !**/*.png
  • docs/images/operations/destination.png is excluded by !**/*.png
  • docs/images/operations/errors.png is excluded by !**/*.png
  • docs/images/operations/notifications.png is excluded by !**/*.png
  • docs/images/operations/telemetry-grant.png is excluded by !**/*.png
Files selected for processing: 28

  • apps/piquelctl/src/output/timeline.rs
  • apps/piquelctl/tests/cli.rs
  • apps/piqueld-ui/src/browser/management/timeline.rs
  • apps/piqueld-ui/src/timeline.rs
  • apps/piqueld-ui/styles/components.css
  • apps/piqueld/src/api/service/views.rs
  • apps/piqueld/src/reconcile/actions.rs
  • apps/piqueld/tests/fake_docker/rollouts.rs
  • apps/piqueld/tests/fake_docker/timeline.rs
  • crates/piqueld-client/src/lib.rs
  • crates/piqueld-core/src/event.rs
  • crates/piqueld-core/src/planner.rs
  • crates/piqueld-core/src/resource.rs
  • crates/piqueld-core/src/timeline.rs
  • crates/piqueld-core/tests/resource_planning.rs
  • crates/piqueld-docker/src/exec.rs
  • crates/piqueld-docker/src/fake.rs
  • crates/piqueld-docker/src/observation.rs
  • crates/piqueld-docker/src/resources.rs
  • crates/piqueld-store/src/journal.rs
  • docs/dev/architecture/observability.md
  • docs/openapi-v1.json
  • docs/user/dashboard.md
  • docs/user/reference/api.md
  • docs/user/reference/cli.md
  • docs/user/reference/manifest.md
  • tests/playwright/screenshots/docs.ts
  • tests/playwright/tests/deployments.spec.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@PiquelChips
PiquelChips force-pushed the feat/timeline-5-tasks branch 3 times, most recently from 83afc70 to 966cf7b Compare November 10, 2026 20:02
@PiquelChips
PiquelChips force-pushed the feat/timeline-5-tasks branch from 966cf7b to 93c5ae5 Compare November 10, 2026 20:32
@PiquelChips
PiquelChips marked this pull request as ready for review November 10, 2026 20:33
@PiquelChips
PiquelChips force-pushed the feat/timeline-5-tasks branch from 93c5ae5 to 711f8d8 Compare November 10, 2026 21:29
@PiquelChips
PiquelChips force-pushed the feat/timeline-5-tasks branch from 711f8d8 to eacdd0c Compare November 10, 2026 21:52
@PiquelChips
PiquelChips force-pushed the feat/timeline-5-tasks branch from eacdd0c to 8fdabd0 Compare November 10, 2026 22:13
@PiquelChips
PiquelChips added this pull request to stack #449 November 12, 2026 13:48
@PiquelChips
PiquelChips force-pushed the feat/timeline-5-tasks branch 2 times, most recently from 189954b to eefc8d4 Compare November 13, 2026 15:00
While a deployment waits for a service to roll out, it now records the
service's Swarm tasks each time one changes state, as `action_progressed`
events on the wait's action. The timeline turns them into one lane per task,
old and new, with the states it moved through, and `Step::gaps` reports when
no task served requests, the downtime of a stop-first rollout.

Observed tasks gain their ID, slot, and whether they run the service's
current specification, which is how Swarm decides what to replace. The fake
engine's slow services now start their tasks and run them once converged.
Updating and waiting for a service were journaled under its Docker name,
`piqueld-env-…-servi-6991c03b8095`, which made timelines hard to read and
named a service's update and its wait differently. Wait actions now carry
the service's logical name, and both steps record it, as plan output
already showed.
The dashboard and piquelctl both need the same few facts about a rollout
while it happens, so the timeline's steps derive them once: how many new
tasks serve (`Step::new_tasks`), the monitor window open now
(`Step::monitor`), how long no task served (`Step::downtime`), and whether
an old task was seen serving once a new one did (`Step::overlapped`), the
only honest ground for saying there was no downtime.
Comparing a task's specification with its service's never matched on a
real engine: Docker reports defaults such as `StopGracePeriod` and
`Runtime` in the service's and not in its tasks', so every task looked old.
A task now runs the current specification when Swarm created it since the
service's latest update started, or always for a service never updated.
While a deployment waits for a service, its step's row shows how many new
tasks run, how long Docker still watches the newest one, and how long no
task served: `1/2 new tasks running · monitor 4.2 s left · down 3.2 s`.
Without a terminal, a line prints each time one more new task runs, and the
step's last line says how long the service was down; `--json` emits a
`step` event on the same changes.
A wait for a service now shows its Swarm tasks in the step detail: a lane
per task, old and new, colored by state, with its monitor window striped
and counted down by a ring while Docker watches a new task. When no task
served, a red striped bar marks it on the step's row and the detail says
for how long; when an old task was seen serving until a new one did, it
says there was no downtime.

Lanes redraw only when their step changes: the shared clock moves one CSS
variable that open states reach, and the ring sweeps through CSS.
Docker can list a service's tasks in another order between two polls,
which recorded a rollout progress event, and woke every open stream, for
no change. The tasks are now compared in slot and ID order.
Rust 1.100's clippy flags `map(..).unwrap_or_default()`, which the
downtime bars used.
@PiquelChips
PiquelChips force-pushed the feat/timeline-5-tasks branch from eefc8d4 to afd4963 Compare November 13, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs Triage

Development

Successfully merging this pull request may close these issues.

1 participant