fix: SSE reconnect + status sync + logs + Advanced + webhook + Swarm - #9
Conversation
The native EventSource retry stops on non-2xx responses (typically a 401 expired cookie), so the SSE banner stayed stuck on 'Stream error' until the user manually reloaded. We now intercept readyState=CLOSED in onerror, schedule our own reconnect with exponential backoff (capped at 30s), and refresh the access cookie before each attempt. Banner now exposes a 3-state status (connecting | open | reconnecting | offline) instead of a boolean, so the UI shows 'Live reconnecting…' during the recovery window. Signed-off-by: kevin <kev.aubree@gmail.com>
When a container is recreated (blue/green app deploy, host restart, watchtower swap) the stored apps.container_id no longer matches the live agent ID/name, so selectAppSnapshot returned null and the UI showed 'Stopped' on healthy apps. Two-sided fix: - API reconciler persists the new canonical reference whenever the label-based lookup finds a different container than the stored one. - Front falls back to the highest-priority snapshot for the app_id when the strict expectedRef match fails — covers the brief window between container recreation and the next /apps poll. Signed-off-by: kevin <kev.aubree@gmail.com>
Two improvements to stop the 'Failed to load logs (404)' banner on the
deployments tab:
- API: when build.log_path is null (legacy row, killed worker), try the
convention path ${PLOYDOK_BUILD_DIR}/<appId>/<buildId>.log before giving
up. Return 200 with '(no logs captured)' body when truly empty so the UI
doesn't surface a misleading 404.
- Front: defer the WS→REST fallback by 1s. Builds that just finished can
race the DB write of log_path; the delay lets the worker persist before
we hit the archive endpoint.
Signed-off-by: kevin <kev.aubree@gmail.com>
The orange warning alert had no dark: variants and rendered as a white-on- white block in dark mode. Added dark variants, reworded the message to explain the merge-into-route-handler behaviour and the server-side validation safety net, and added a collapsible 'Examples' block with three copy-paste snippets (HSTS header, redirect, URI rewrite) so first-time users have something to start from beyond the upstream Caddy docs link. Signed-off-by: kevin <kev.aubree@gmail.com>
Adds --runtime=swarm (default) that deploys api/web as Swarm services with
replicas: 2 + update_config.order: start-first + healthcheck-gated cutover.
A new systemd timer (ploydok-update.timer, 5-min cadence) re-runs
`docker stack deploy --resolve-image always` so a freshly published
:edge image rolls out without downtime. Watchtower is gone — Swarm's
update_config replaces it with proper health gating and rollback.
- installer/install.sh: --runtime flag, ensure_swarm() that auto-inits a
single-node Swarm if needed, db_migrate handles both runtimes.
- docker-stack.yml: new template with deploy: blocks, host-mode ports on
Caddy (so ACME's same-source-IP rule still holds), buildkit switched to
the non-rootless image since Swarm ignores security_opt.
- ploydok.service: ExecStart now `docker stack deploy` (idempotent).
- ploydok-update.{service,timer}: cron equivalent of watchtower for Swarm.
- docker-compose.yml: watchtower removed (legacy compose mode now needs
ploydok-cli upgrade for updates).
- getting-started.md: Swarm vs Compose section + bascule procedure.
Signed-off-by: kevin <kev.aubree@gmail.com>
The custom bytea type relied on Drizzle's default driver path, but postgres.js (Bun) sees the Buffer as a plain Object during prepared statement bind and crashes with 'string argument must be of type string or instance of Buffer or ArrayBuffer'. The whole webhook handler aborted mid-flight: webhook signature was verified, push event was matched, then the audit insert into webhook_deliveries killed the request — so the deploy job never reached BullMQ. Fix: add toDriver() on the bytea customType that returns a Uint8Array view over the same memory, which postgres.js binds correctly. Signed-off-by: kevin <kev.aubree@gmail.com>
The CI dry-run installer test asserts /opt/ploydok/docker-compose.yml exists, which the swarm-default writes to docker-stack.yml instead. Defaulting to swarm also silently swarm-init's any host that runs the installer, which is a behaviour change too big for this PR. Make --runtime=swarm explicit opt-in for now; compose stays the default. Signed-off-by: kevin <kev.aubree@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a421ece478
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # is restarted reconciles the stack to the current docker-stack.yml without | ||
| # tearing existing services down. The first invocation provisions the stack; | ||
| # subsequent ones (e.g. after a reboot) are essentially no-ops. | ||
| ExecStart=/usr/bin/docker stack deploy --resolve-image always --with-registry-auth --prune -c ${PLOYDOK_INSTALL_DIR}/docker-stack.yml ploydok |
There was a problem hiding this comment.
Restore compose systemd unit when runtime is compose
The --runtime=compose path still writes ploydok.service, but this template now always executes docker stack deploy against docker-stack.yml. In compose mode, ensure_swarm is skipped and only docker-compose.yml is rendered, so systemctl restart ploydok.service will fail on hosts not initialized as Swarm (and may also fail due to missing docker-stack.yml). This regresses the advertised fallback runtime and makes --runtime=compose non-functional.
Useful? React with 👍 / 👎.
| - target: 3335 | ||
| published: 3335 | ||
| host_ip: 127.0.0.1 | ||
| protocol: tcp | ||
| mode: host |
There was a problem hiding this comment.
Remove host-published fixed API port with 2 replicas
This service publishes API with mode: host and a fixed published: 3335 while also setting deploy.replicas: 2. Docker Swarm documents that mode=host + fixed published port allows only one task per node, so on the default single-node Swarm install one replica cannot be scheduled and the service will remain degraded (breaking the intended zero-downtime two-replica design).
Useful? React with 👍 / 👎.
Summary
Closes the cluster of bugs observed in prod plus introduces a Docker Swarm runtime for zero-downtime control-plane updates.
Test plan