Skip to content

feat(chats): delete a conversation from the chat 3-dot menu - #101

Open
sathyaprakash000 wants to merge 2 commits into
mainfrom
claude/chat-history-deletion-9630f9
Open

feat(chats): delete a conversation from the chat 3-dot menu#101
sathyaprakash000 wants to merge 2 commits into
mainfrom
claude/chat-history-deletion-9630f9

Conversation

@sathyaprakash000

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a way to delete a chat from inside the conversation itself. Clearing a chat previously meant going into the database by hand.

The chat window's 3-dot menu (alongside Contact info / Refresh / Export chat) gains two admin-only actions:

  • Delete chat history — removes every chat_history row, its message_reactions, and the conversation_reads marker for one (wa_number, contact_number) pair, and unlinks the conversation's stored media. The saved contact (name, tags, custom fields, assignment) is kept, so the chat simply reopens empty and future inbound messages land normally.
  • Delete contact & chat — the same sweep, plus the contacts row.

Both go through the repo's existing DeleteConfirmModal. On success ChatsPage unselects the chat and refetches the contact list.

Backed by a new admin-gated DELETE /api/chat-history?waNumber=&contactNumber=[&withContact=1].

Notes for a reviewer:

  • Ordering is deliberate. The row deletes run in one transaction; media paths are SELECTed inside it but unlinked only after the commit. A rollback can undo a DELETE, but nothing can undo an unlink — deleting files first would leave unreachable-media rows in a chat that still exists, which is strictly worse than the reverse failure (an orphan blob on disk). File cleanup is best-effort and never fails the request.
  • Path containment. Unlinking goes through a new resolveInMediaDir guard, mirroring the containment the media read route already applies, so a bad media_storage_path in the DB can never reach outside MEDIA_DIR.
  • Media is safe to hard-delete. persistOutboundMedia writes a per-message copy even when the source is a shared Media Library object, so removing a conversation's files cannot orphan the library.
  • Role gate, not assertContactAccess. This is a semantic choice, not just a stricter check: "is this your conversation?" is the right question for reading and replying, but the wrong one for erasing — the person most attached to a chat is exactly the one you don't want quietly deleting it. The frontend menu items are hidden for non-admins to mirror the server gate.
  • Scope. deals and automation_executions are deliberately left intact — they're CRM history, not chat history.
  • Every deletion is recorded via auditLog (chat.delete_history / chat.delete_with_contact) with the affected counts.

Related issue

None — raised directly as a product request.

Type of change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor
  • 📝 Documentation
  • 🔧 Chore / tooling

How was this tested?

  • npx vitest run in frontend/51 passed (3 files).
  • node --test test/ in backend/20 passed.
  • npx vite build — clean.
  • node --check on the modified route.
  • Confirmed against db/migrations/ that all five tables touched (chat_history, message_reactions, conversation_reads, contacts, user_audit_log) already exist, so no migration is needed. The same set is precedented by the existing /contacts/change-number route, which updates exactly these tables.

Not run: the Playwright half of npm test (test:e2e), which needs a running app; and I could not smoke-test the SQL against a live database — the running forgechat-db container on this host belongs to a different Forgechat app (Prisma public.* schema), not this repo's coexistence schema. Worth a manual pass on a real conversation with media before merge.

Checklist

  • Branch is up to date with upstream/main.
  • The change is focused on a single concern.
  • Frontend tests pass (npm test in frontend/) and tests were added where relevant.
  • Any schema change is a new numbered migration in db/migrations/. (N/A — no schema change.)
  • Commits follow Conventional Commits and are signed off (git commit -s, DCO).
  • Docs updated, including a ## [Unreleased] entry in CHANGELOG.md if behaviour changed.
  • No secrets, .env files, or generated artifacts are committed.

🤖 Generated with Claude Code

forgemindbusiness and others added 2 commits August 23, 2026 07:15
Clearing a chat previously meant going into the database by hand. The chat
window's 3-dot menu now offers two admin-only actions:

  * Delete chat history — removes every chat_history row, its reactions and
    the read marker for one (wa_number, contact_number) pair, and unlinks the
    conversation's stored media. The saved contact (name, tags, custom fields,
    assignment) is kept, so the chat simply reopens empty.
  * Delete contact & chat — the same sweep, plus the contacts row.

Backed by a new admin-gated DELETE /api/chat-history. The row deletes run in
one transaction; media paths are read inside it but unlinked only after the
commit, since a rollback can undo a DELETE but nothing can undo an unlink.
Unlinking goes through a resolveInMediaDir guard so a bad DB-stored path can
never reach outside MEDIA_DIR — the same containment the media read route
applies. Every deletion is recorded via auditLog.

The route is gated on role rather than assertContactAccess deliberately:
"is this your conversation?" is the right question for reading and replying,
but the wrong one for erasing. Deals and automation_executions are left
intact — they are CRM history, not chat history.

No schema change; all five tables touched already exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: KingArthur000 <forgemind.business@gmail.com>
Two follow-ups to the initial chat-deletion feature, mirroring what now runs
in the internal repo.

1. The sweep was incomplete — text survived a "delete". Now also removed:

   * agent_runs — final_reply is the text the AI agent actually sent this
     contact, and agent_run_steps (the full prompt / tool-call / output
     transcript) cascades off the run. Keyed on wa_account_id, so the account
     is resolved from the wa_number first.
   * webhook_events — the raw Meta payloads, which carry verbatim message
     text and media ids. Leaving these behind meant a deleted chat was still
     fully readable in the webhook log. Matched by jsonpath on the four places
     a contact's number appears in Meta's envelope (messages.from,
     statuses.recipient_id, contacts.wa_id, message_echoes.to) so a different
     contact's events are never caught.

2. Deletion is reachable from the chat list, not just inside an open chat.
   Each contact row gets a hover kebab with the same two actions. The row was
   a <button>, which cannot legally contain another button, so it is now a div
   with role="button" plus Enter/Space handling — same behaviour, valid markup.

Also drops the admin-only gate: anyone who can open a conversation may erase
it, enforced server-side by the same assertContactAccess check used by the
rest of the chat routes. Every deletion is still written to the audit log.

Drive-by: removed a stray comma after the contacts .map() in ContactList,
which rendered as a literal "," under the list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: KingArthur000 <forgemind.business@gmail.com>
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.

2 participants