Skip to content

fix: keep deployment log stream connected on redeploy and harden project creation - #68

Merged
dviejokfs merged 3 commits into
mainfrom
fix/logs-reconnect-and-project-dedup
Apr 29, 2026
Merged

dviejokfs merged 3 commits into
mainfrom
fix/logs-reconnect-and-project-dedup

Conversation

@dviejokfs

Copy link
Copy Markdown
Contributor

Summary

Fixes two user-reported bugs in one branch, plus tightens backend project-creation consistency and error mapping.

1. Deployment logs WebSocket dies on redeploy

web/src/components/deployments/DeploymentStages.tsx

The log stream gave up the moment it received any normal (1000) closure — exactly what happens when a redeploy churns the underlying log source — leaving the user staring at a frozen log pane. It also wiped state on every reconnect, so the partial logs they had were lost too.

  • Always reconnect until unmount; exponential backoff capped at 10s
  • Preserve prior log state instead of clearing it
  • Dedupe by absolute file line so the backend's last-1000-lines replay after reconnect doesn't show up twice

2. Single click → 15 duplicate projects

web/src/components/project/{ProjectConfigurator,ManualProjectConfigurator}.tsx

Submit was disabled via React state (isSubmitting), but state updates aren't synchronous — fast double-clicks slipped multiple submissions through before the disabled prop landed. Adds a useRef synchronous re-entry guard at the top of handleSubmit in both configurators.

3. Backend project-create consistency

When create_project failed any step after the project row was inserted (default env, env vars, storage links), the project row was left dangling. The handler returned 500 and the user ended up with a half-initialized project visible in their list. The real failure was buried inside ProjectError::Other(format!(...)) so the HTTP status was always 500 even when the cause was a 4xx.

  • Manual rollback via CASCADE delete: post-insert work moved to finalize_project_creation, on error Entity::delete_by_id on the project row cascades to environments, env vars, links. No long-running transaction → no cross-service table locks.
  • New typed ProjectError variants: SlugConflict, EnvironmentCreationFailed, EnvVarCreationFailed, StorageLinkFailed — all with structured fields per CLAUDE.md error rules.
  • SlugAlreadyExists now correctly maps to 409 (was 400). Concurrent slug races now surface as SlugConflict → 409 instead of a generic 500.
  • is_unique_violation helper classifies via SQLSTATE 23505 + RecordNotInserted + textual fallbacks.

Test plan

Backend (crates/temps-projects, 41 passed including 5 new):

  • test_is_unique_violation_detects_record_not_inserted — pure unit, classifier coverage
  • test_create_project_succeeds_and_creates_default_environment — happy path + auto-env
  • test_create_project_with_duplicate_name_gets_suffixed_slug — duplicate name → name-<6char>
  • test_create_project_slug_conflict_returns_typed_error — real PG unique-violation classified, Problem is HTTP 409
  • test_create_project_rolls_back_on_invalid_storage_service — validation path leaves zero phantom projects

Frontend:

  • Manually verify logs page reconnects after triggering a redeploy on a project with a long-running deploy
  • Manually rage-click "Create Project" — only one project should be created (check /projects list)
  • Slow network throttling: confirm second submission attempt is blocked synchronously

Verification commands:

```bash
cargo check --lib # 0 errors
cargo test --lib -p temps-projects # 41 passed
cd web && bunx tsc --noEmit # no new TS errors vs main
```

…t duplicate project creates

The deployment logs WebSocket previously gave up the moment it received any
normal (1000) closure, which is exactly what happens when a redeploy churns
the underlying log source — leaving the user staring at a frozen log pane.
It also wiped state on every reconnect, so the partial logs they had were
lost too.

The reconnect path now:
- always retries (exponential backoff, capped at 10s) until the component
  unmounts, since the backend tail is an infinite stream
- preserves prior log state instead of clearing it
- de-dupes by absolute file line number so the last-1000-lines replay the
  backend sends after reconnect doesn't show up twice

Project creation could result in N duplicate projects from a single user
intent because the submit-disabled flag was driven by React state, which
isn't synchronous — a quick double click slipped two submissions through
before the disabled prop landed. Adds a `useRef` re-entry guard in both
`ProjectConfigurator` and `ManualProjectConfigurator` so the second call
is dropped synchronously.
…and map slug races to 409

When create_project failed any step after the project row was inserted —
default-environment creation, env-var insertion, or storage-link wiring —
the project row was left dangling. The handler returned 500 but the user
ended up with a half-initialized project visible in their list, and the
real failure was buried inside ProjectError::Other(format!(...)) so the
HTTP status was always 500 even when the underlying cause was a 4xx.

Wrapping the multi-step create in a single Sea-ORM transaction would
require pushing `&impl ConnectionTrait` through `EnvironmentService`,
`EnvVarService`, and `ExternalServiceManager` — and would hold table locks
across cross-service calls. We do manual rollback instead: split the
post-insert work into `finalize_project_creation`, and on any error
issue a single `Entity::delete_by_id` on the project row. CASCADE FKs
clean up environments, env vars, and links. No long-lived locks; failures
are observable through the typed error.

Adds typed variants — `EnvironmentCreationFailed`, `EnvVarCreationFailed`,
`StorageLinkFailed`, `SlugConflict` — replacing the `Other(String)` flatten.
`SlugAlreadyExists` and the new `SlugConflict` both map to 409 (was 400).
Slug detection now classifies via SQLSTATE `23505` plus textual fallbacks
inside an `is_unique_violation` helper, so a concurrent create racing on
the same slug surfaces as `SlugConflict` -> HTTP 409 instead of a generic
500 the frontend can't act on.

Adds 5 tests: pure-unit classifier coverage, happy-path create with
auto-environment, duplicate-name -> suffixed-slug, slug conflict -> 409,
and validation rejection without leaving a phantom project. Integration
tests skip gracefully without Docker per CLAUDE.md guidance.
@dviejokfs
dviejokfs merged commit d6d1fcf into main Apr 29, 2026
11 checks passed
@dviejokfs
dviejokfs deleted the fix/logs-reconnect-and-project-dedup branch July 1, 2026 00:08
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.

1 participant