Skip to content

server: add DELETE /api/projects route; UI delete-project action#522

Merged
bpowers merged 1 commit into
mainfrom
delete-projects
May 12, 2026
Merged

server: add DELETE /api/projects route; UI delete-project action#522
bpowers merged 1 commit into
mainfrom
delete-projects

Conversation

@bpowers
Copy link
Copy Markdown
Owner

@bpowers bpowers commented May 12, 2026

What

Adds the ability to delete a hosted project. Closes #49.

Hosted projects could be created and edited but never removed, which makes the web app awkward for repeated use -- e.g. a class that builds many throwaway models over a semester (#49).

How

End to end:

  • server -- new DELETE /api/projects/:username/:projectName route, implemented as createDeleteProjectHandler in route-handlers.ts (following the existing createProjectRouteHandler pattern: a factory over a narrow DB interface, unit-tested with mocks). It deletes the project document and, best-effort, the cached preview PNG. Ownership is checked against the URL username before any DB lookup (so a logged-in user can't probe whether someone else's private project exists via 404-vs-401) and again against the stored record's ownerId as defense in depth. authz already 401s unauthenticated non-GET /projects/... requests, and the handler re-checks defensively.
  • diagram -- a low-emphasis destructive "Delete project" button in the model-properties drawer (the hamburger-menu panel with the sim specs + "Download model"), behind a modal confirmation (DeleteProjectButton). A rejected delete keeps the dialog open with the error message so the user can retry; a resolved one means the host has navigated away. Editor gains an optional onDeleteProject prop, forwarded to the drawer only when the editor is not read-only. Button gains a color="error" variant (uses the existing --color-error token) and an SVG DeleteIcon.
  • HostedWebEditor (src/app's editor wrapper) wires onDeleteProject to handleDelete, which calls the new endpoint and on success full-navigates back to the project list (so it refetches without the deleted project). It's only passed when the viewer owns the project; the local file-backed viewer (simlin-serve) and embeds leave it undefined and never see the affordance.

Decisions

  • Hard delete, no soft-delete/undo. A deleted project's SSR route returns the existing 404, same as any nonexistent project.
  • Orphaned File documents are left behind. Every save already orphans the prior File doc (no GC, prev_id is dead schema); cleaning that up generally is out of scope and tracked separately as server: hosted-project saves leak orphaned File docs in Firestore (no version chain, no GC) #521.
  • The delete affordance lives in the model-detail drawer for now; a delete control on the Home project grid would be a nicer fit for the "clean up lots of old projects" workflow and is a natural fast-follow -- it would reuse this same server route.

Tests

TDD throughout:

  • src/server/tests/delete-project.test.ts -- owner deletes (project + preview removed, 200); 404 for missing project; 401 unauthenticated; 401 (without a DB lookup) for non-URL-owner; 401 for stored-owner mismatch; preview-delete failure is non-fatal.
  • src/diagram/tests/delete-project-button.test.tsx -- renders/opens/cancels the dialog; confirm calls onDelete; a rejected onDelete keeps the dialog open and shows the error; action buttons disabled while in flight.
  • src/diagram/tests/model-properties-drawer.test.tsx -- delete action present only when onDelete supplied; confirming invokes it.
  • src/diagram/tests/editor-drawer-delete.test.ts -- getDrawer() forwards onDeleteProject only when editable; omits it when read-only / unset / embedded.
  • src/diagram/tests/hosted-web-editor-delete.test.ts -- DELETEs the right URL (relative and base-prefixed), navigates home on success, throws the server error and doesn't navigate on failure, no-op in read-only mode.
  • src/diagram/tests/button.test.tsx -- color="error" class application for all three variants.

Full pre-commit gate (Rust fmt/clippy/test, TS lint/build/tsc/test, pysimlin) is green.

🤖 Generated with Claude Code

Hosted projects could be created and edited but never removed (issue #49),
which makes the web app awkward for repeated use -- e.g. a class that builds
many throwaway models over a semester. Add a delete capability end to end:

- server: a DELETE route (route-handlers.ts `createDeleteProjectHandler`)
  that removes the project document and, best-effort, its cached preview PNG.
  Ownership is checked against the URL username before any DB lookup -- so a
  logged-in user can't probe other users' private projects via 404-vs-401 --
  and again against the stored record's ownerId as defense in depth. The
  superseded `File` documents are intentionally left orphaned: every save
  already orphans the prior one (tracked separately as #521).
- diagram: a low-emphasis destructive "Delete project" button in the
  model-properties drawer (next to "Download model"), behind a modal
  confirmation. It lives in a small `DeleteProjectButton`; a rejected delete
  keeps the dialog open with the error message so the user can retry. `Editor`
  gains an optional `onDeleteProject` prop, forwarded to the drawer only when
  the editor is not read-only. `Button` gains a `color="error"` variant.
- HostedWebEditor wires `onDeleteProject` to a `handleDelete` that calls the
  new endpoint and, on success, full-navigates back to the project list so it
  refetches without the deleted project. It is only passed when the viewer
  owns the project; the local file-backed viewer and embeds leave it undefined.

Hard delete, no undo: a deleted project's SSR route returns the existing 404,
the same as any nonexistent project.
@claude
Copy link
Copy Markdown

claude Bot commented May 12, 2026

Reviewed the server DELETE /api/projects/:username/:projectName handler, the DeleteProjectButton / ModelPropertiesDrawer / Editor wiring, HostedWebEditor.handleDelete, the Button color="error" variant, and the new tests.

No blocking issues found. A few notes (none require action):

  • The new DELETE endpoint relies on cookie session auth without a CSRF token, but that's consistent with the existing state-changing routes (POST /projects, POST /projects/:username/:projectName, PATCH /user), and the global cors({ methods: ['GET'] }) config makes a cross-origin DELETE fail its preflight — so this isn't a new exposure.
  • HostedWebEditor.handleDelete doesn't URL-encode username/projectName in the request path, but neither do loadProject/handleSave — consistent with the surrounding code.
  • The confirmation dialog shows project.name (the value already used as the drawer <h2>), which is the engine/XMILE project name rather than the hosted-project slug; in practice these match for hosted projects, and it matches what the drawer already displays.

Overall correctness: correct. Existing code and tests should not break, and the patch is free of blocking bugs.

@bpowers bpowers merged commit 465fe98 into main May 12, 2026
13 checks passed
@bpowers bpowers deleted the delete-projects branch May 12, 2026 03:16
@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.45%. Comparing base (29d4d70) to head (f8df76e).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #522   +/-   ##
=======================================
  Coverage   82.44%   82.45%           
=======================================
  Files         242      242           
  Lines       63290    63290           
=======================================
+ Hits        52182    52184    +2     
+ Misses      11108    11106    -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

delete models

1 participant