diff options
| author | srdusr <[email protected]> | 2026-05-18 23:30:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-05-18 23:30:00 +0200 |
| commit | 4fa511ac5f2aa422456c49610a2cf29dfee46dfd (patch) | |
| tree | c033651e2fcd3a3f1131998f86a5118bcfc56034 /internal | |
| parent | 1707cf827592bc3de7ea9c2805b7adea49b3cbc5 (diff) | |
| download | mitmux-4fa511ac5f2aa422456c49610a2cf29dfee46dfd.tar.gz mitmux-4fa511ac5f2aa422456c49610a2cf29dfee46dfd.zip | |
History deletion: delete one entry (x) or clear everything (X)
Store had full CRUD for match-and-replace rules but no way to delete or
prune history at all - it only ever grew, with no way to remove an
accidental capture or start a new engagement clean short of manually
deleting the DB file outside the tool entirely.
internal/store: DeleteEntry(id) removes one history row and its
history_fts search index row in a transaction. ClearHistory() empties
both tables entirely; rules are untouched. internal/ipc: new
"delete_entry" and "clear_history" request types, Client.DeleteEntry/
ClearHistory methods.
TUI: 'x' deletes the selected history entry, 'X' clears the whole
database. Both gated behind a y/n confirmation - a small reusable
confirmPrompt/confirmYes model state, checked first in the history
list's key handling, so any key other than y/Y safely cancels rather
than falling through to whatever that key normally does elsewhere (this
also means ctrl+c during a pending confirmation cancels the prompt
rather than quitting - a deliberate fail-safe, not an oversight: quick
to dismiss, and a second ctrl+c then quits normally).
'X' is explicitly NOT scoped to an active search filter - it always
clears the true total (read from daemon status, not len(m.entries),
which would understate the count under a filter and make the
confirmation prompt itself misleading about what's about to happen).
Verified live in tmux against a running daemon with real captured
entries: 'x' shows "delete #N? y/n", 'n' cancels with the entry
untouched, 'y' deletes it and the list/count both refresh correctly;
'X' shows "clear all N history entries (not just this view)? y/n" with
the true count, 'y' empties the database (confirmed via direct SQLite
query: both history and history_fts at 0 rows afterward) and the TUI
correctly shows "history (0)" / "0 requests"; 'x'/'X' on an empty list
correctly no-op without crashing.
go build/vet/gofmt/test/mod tidy all clean.
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ipc/ipc.go | 41 | ||||
| -rw-r--r-- | internal/ipc/server.go | 14 | ||||
| -rw-r--r-- | internal/store/store.go | 33 |
3 files changed, 85 insertions, 3 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 5dac21f..c2be080 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -18,7 +18,7 @@ import ( // Request is sent by a client to the daemon. type Request struct { - Type string `json:"type"` // "list", "get", "subscribe", "repeat", "intrude", "rules_list", "rules_save", "rules_delete", or "rules_toggle" + Type string `json:"type"` // "list", "get", "subscribe", "repeat", "intrude", "rules_list", "rules_save", "rules_delete", "rules_toggle", "delete_entry", or "clear_history" Limit int `json:"limit,omitempty"` BeforeID int64 `json:"before_id,omitempty"` ID int64 `json:"id,omitempty"` @@ -55,13 +55,14 @@ type Request struct { RuleID int64 `json:"rule_id,omitempty"` RuleEnabled bool `json:"rule_enabled,omitempty"` - // For "set_flagged": ID identifies the history entry. + // For "set_flagged" and "delete_entry": ID identifies the history + // entry. "clear_history" needs no fields at all. Flagged bool `json:"flagged,omitempty"` } // Response is sent by the daemon to a client. type Response struct { - Type string `json:"type"` // "list", "get", "new", "repeat", "rules", "intrude_result", "intrude_done", "status", "flagged", or "error" + Type string `json:"type"` // "list", "get", "new", "repeat", "rules", "intrude_result", "intrude_done", "status", "flagged", "deleted", "cleared", or "error" Entries []store.Summary `json:"entries,omitempty"` // for "list" Detail *EntryDetail `json:"detail,omitempty"` // for "get" and "repeat" New *store.Summary `json:"new,omitempty"` // for "new" (subscribe push) @@ -158,6 +159,40 @@ func (c *Client) SetFlagged(id int64, flagged bool) error { return nil } +// DeleteEntry removes a single history entry. +func (c *Client) DeleteEntry(id int64) error { + c.mu.Lock() + defer c.mu.Unlock() + if err := c.enc.Encode(Request{Type: "delete_entry", ID: id}); err != nil { + return err + } + var resp Response + if err := c.dec.Decode(&resp); err != nil { + return err + } + if resp.Type == "error" { + return errors.New(resp.Error) + } + return nil +} + +// ClearHistory removes every history entry. Rules are untouched. +func (c *Client) ClearHistory() error { + c.mu.Lock() + defer c.mu.Unlock() + if err := c.enc.Encode(Request{Type: "clear_history"}); err != nil { + return err + } + var resp Response + if err := c.dec.Decode(&resp); err != nil { + return err + } + if resp.Type == "error" { + return errors.New(resp.Error) + } + return nil +} + // Status returns basic daemon info for a status bar. func (c *Client) Status() (*StatusMsg, error) { c.mu.Lock() diff --git a/internal/ipc/server.go b/internal/ipc/server.go index 2879b24..7f77ceb 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -215,6 +215,20 @@ func (s *Server) handleConn(conn net.Conn) { } enc.Encode(Response{Type: "flagged"}) + case "delete_entry": + if err := s.db.DeleteEntry(req.ID); err != nil { + enc.Encode(Response{Type: "error", Error: err.Error()}) + continue + } + enc.Encode(Response{Type: "deleted"}) + + case "clear_history": + if err := s.db.ClearHistory(); err != nil { + enc.Encode(Response{Type: "error", Error: err.Error()}) + continue + } + enc.Encode(Response{Type: "cleared"}) + case "rules_list": rs, err := s.db.ListRules() if err != nil { diff --git a/internal/store/store.go b/internal/store/store.go index 19b71b1..c0cdea5 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -560,6 +560,39 @@ func (s *Store) SetRuleEnabled(id int64, enabled bool) error { return nil } +// DeleteEntry removes a single history entry and its search index row. +func (s *Store) DeleteEntry(id int64) error { + tx, err := s.db.Begin() + if err != nil { + return fmt.Errorf("begin delete entry: %w", err) + } + defer tx.Rollback() + if _, err := tx.Exec(`DELETE FROM history WHERE id = ?`, id); err != nil { + return fmt.Errorf("delete history entry %d: %w", id, err) + } + if _, err := tx.Exec(`DELETE FROM history_fts WHERE rowid = ?`, id); err != nil { + return fmt.Errorf("delete search index for entry %d: %w", id, err) + } + return tx.Commit() +} + +// ClearHistory removes every history entry and resets the search index. +// Rules are untouched - this only clears captured traffic. +func (s *Store) ClearHistory() error { + tx, err := s.db.Begin() + if err != nil { + return fmt.Errorf("begin clear history: %w", err) + } + defer tx.Rollback() + if _, err := tx.Exec(`DELETE FROM history`); err != nil { + return fmt.Errorf("clear history: %w", err) + } + if _, err := tx.Exec(`DELETE FROM history_fts`); err != nil { + return fmt.Errorf("clear search index: %w", err) + } + return tx.Commit() +} + // DeleteRule removes a rule. func (s *Store) DeleteRule(id int64) error { if _, err := s.db.Exec(`DELETE FROM rules WHERE id = ?`, id); err != nil { |