Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f7a3e60
feat(web): unify files, sources, and deletion review
rodboev Oct 30, 2026
709cba3
fix(web): preserve archive workflows across redesigned views
rodboev Oct 30, 2026
4acb8ce
fix(web): preserve source drafts and deletion outcomes
rodboev Oct 30, 2026
f8fe169
fix(web): keep source settings in their existing editor
rodboev Oct 30, 2026
8a0046b
fix(web): keep deletion progress and source failures visible
rodboev Oct 30, 2026
01b22ef
fix(web): clear file filters and retain deletion history during refresh
rodboev Oct 30, 2026
5ce3148
fix(web): load contacts independently and retry deletion retention
rodboev Oct 30, 2026
c6d3bea
test(web): focus archive management coverage on owned behavior
rodboev Oct 30, 2026
f4d8303
fix(web): keep sample addresses synthetic
rodboev Oct 30, 2026
e03ac0f
test(web): make archive regressions detect the failures they name
rodboev Oct 30, 2026
5690c07
test(web): use complete source status fixtures
rodboev Oct 30, 2026
e17ac4d
fix(web): keep contacts progress visible through mail status failures
rodboev Oct 30, 2026
dbb710d
fix(web): keep source sync progress independent
rodboev Oct 30, 2026
5f5f672
fix(web): expose source recovery and file setup actions
rodboev Oct 30, 2026
5107e05
fix(web): show executing scheduled syncs as running
rodboev Oct 30, 2026
7a6caed
docs(web): clarify grouped Files filters
rodboev Oct 30, 2026
f47b3f4
fix(web): open repair settings for the selected contacts account
rodboev Oct 30, 2026
a84886e
fix(web): keep staged batch details open across refreshes
rodboev Oct 30, 2026
f2778c6
fix(web): reconcile refreshed deletion results across views
rodboev Oct 30, 2026
2f82d9f
fix(web): preserve batch opening across status refreshes
rodboev Oct 30, 2026
d3952cc
fix(web): follow replacement batch details from the queue
rodboev Oct 30, 2026
4e07472
test(web): follow current embedded Files controls
rodboev Oct 30, 2026
34640ad
test(web): follow current archive review controls
rodboev Oct 30, 2026
1ba0e42
fix(web): keep archive controls reachable after the redesign
rodboev Oct 30, 2026
832adc1
test(web): keep spacing journeys on current archive pages
rodboev Oct 30, 2026
18aee11
fix(web): keep sync recovery inside the source panel
rodboev Oct 30, 2026
6fc92fd
test(web): exercise task recovery through real API states
rodboev Oct 30, 2026
6289b14
test(web): retain the restored Directory header gap
rodboev Oct 30, 2026
b29f547
fix(web): keep staged deletion history reachable without detail reads
rodboev Oct 31, 2026
4b2cee8
fix(web): load deletion accounts only when opening a batch
rodboev Oct 31, 2026
a72e030
fix(web): keep archive management actions and status accurate
rodboev Oct 31, 2026
4c4e39f
test(web): cover contacts status recovery after sync
rodboev Oct 31, 2026
578782e
fix(web): refresh visual readiness and deletion panel status
rodboev Oct 31, 2026
dd92b31
fix(web): preserve source diagnostics after restacking
rodboev Oct 31, 2026
0341d89
test(web): focus archive coverage on each component's behavior
rodboev Oct 31, 2026
c382064
fix(web): retry visual search readiness after initialization
rodboev Oct 31, 2026
b74c8de
fix(web): avoid claiming staged deletions are empty after a load error
rodboev Oct 31, 2026
7710be7
fix(web): let users retry failed deletion detail requests
rodboev Oct 31, 2026
4d49490
fix(web): wait for deletion details before reporting a missing account
rodboev Oct 31, 2026
10bf923
fix(web): refresh visual readiness and expose source status errors
rodboev Oct 31, 2026
34c3f1c
fix(web): show status errors after an accepted sync
rodboev Oct 31, 2026
63a833a
fix(web): offer a usable command for the first source sync
rodboev Oct 31, 2026
ef36ee3
test(web): remove duplicate coverage and repair workspace checks
rodboev Oct 31, 2026
54e089d
fix(web): expose paused status polling in the source panel
rodboev Oct 31, 2026
dd820c8
test(web): align archive fixtures with reachable states
rodboev Oct 31, 2026
2ff5ec3
fix(api): report scheduled contact syncs in source summaries
rodboev Oct 31, 2026
618414a
fix(web): restore Directory's shared filter command
rodboev Oct 31, 2026
5002e82
fix(web): preserve cancelled batches after stale list responses
rodboev Nov 1, 2026
0eff420
fix(web): retain Settings refreshes through the Files layer
mariusvniekerk Nov 1, 2026
2b0640b
fix(web): preserve completed contacts sync results
rodboev Nov 1, 2026
59ca0d5
fix(web): keep contacts results after concurrent refreshes
rodboev Nov 1, 2026
c28b040
Merge remote-tracking branch 'origin/pr/redesign-4-messages' into pr/…
rodboev Nov 1, 2026
3f2a0ef
fix(web): let saved-view edits finish while deletions load
rodboev Nov 2, 2026
aa2a896
test(web): preserve navigation coverage and sharpen assertions
rodboev Nov 2, 2026
ba82c51
test(web): type optional mailbox values in source-label cases
mariusvniekerk Nov 2, 2026
d6b4f92
fix(web): identify staged deletion batches by account
rodboev Nov 2, 2026
ea50e26
test: make file and deletion checks catch distinct regressions
rodboev Nov 2, 2026
e49ee4a
Merge commit '854dc9256' into pr/redesign-5-files
rodboev Nov 2, 2026
06ad28a
Merge commit '83f1d1cde' into pr/redesign-5-files
rodboev Nov 2, 2026
fdd58dc
fix(web): keep scheduled contacts status current in Sources
rodboev Nov 2, 2026
e5deb2b
fix(web): preserve contacts status during sync reconciliation
rodboev Nov 2, 2026
229e735
fix(web): restore contacts sync after a Sources refresh
rodboev Nov 2, 2026
2ee5618
test(web): give the idle contacts fixture its scheduler flag
rodboev Nov 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3620,6 +3620,8 @@ components:
DeletionManifestSummary:
additionalProperties: true
properties:
account:
type: string
created_at:
format: date-time
type: string
Expand Down Expand Up @@ -16956,7 +16958,7 @@ components:
type: apiKey
info:
title: msgvault API
version: 3.36.0
version: 3.37.0
openapi: 3.1.0
paths:
/api/ping:
Expand Down
5 changes: 4 additions & 1 deletion docs/api-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ recurrence limits, notification behavior, and reconciliation instructions.
The API publishes its generated OpenAPI contract at `/openapi.json`.
`msgvault openapi` prints the checked-in contract without starting a daemon or
opening an archive. OpenAPI `info.version` is the **API schema version**;
it is separate from the binary release version. The current schema is **3.36.0**.
it is separate from the binary release version. The current schema is **3.37.0**.

Schema 3.37.0 adds the optional `account` to staged deletion list summaries.

Schema 3.36.0 adds exact sender and To participant identities to conversation windows.

Expand Down Expand Up @@ -3016,6 +3018,7 @@ filters by one of `pending`, `in_progress`, `completed`, `failed`, or
"created_at": "2026-07-06T15:30:00Z",
"created_by": "api",
"description": "old newsletters",
"account": "user@example.com",
"message_count": 1234
}
]
Expand Down
9 changes: 2 additions & 7 deletions docs/usage/deletion.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,13 @@ mail accounts are the ones added with `add-o365 --graph`. Creating a manifest ne

## Staging in the Web UI

In Messages, select individual rows or all rows matching the current
canonical filter, then press `d` or `D` to open the Deletions workspace. The UI
runs a server-side preflight before it enables staging and requires a separate
confirmation to create the manifest. The Deletions workspace also lists,
inspects, and cancels staged manifests. It never executes remote deletion;
that final step remains the explicit CLI command described below.
In Messages, select individual rows or all rows matching the current filter, then choose **Review deletion…** or press `d` or `D`. A review dialog shows the server preflight counts, exclusions, and expiration. **Dry run** reports what would be staged. **Stage N messages** creates the batch and shows its ID and counts; **Done** returns to Messages. The Staged deletions workspace lists batches by date, message count, and status. Select a row to see its account, description, and failed message IDs. If the account can't be read, the panel shows "Account unavailable". **Cancel batch** asks for confirmation and then cancels the batch immediately. The sidebar shows Staged deletions while a listed batch is pending, running, or failed. **Go to Staged deletions** in the command palette always opens the history. Remote deletion remains the explicit CLI command described below.

Preflight validates the selection itself (that it still matches, that the
cache/search revision hasn't moved) but does not check which accounts the
matched messages belong to. Each deletion manifest executes against exactly
one mailbox, so if you stage "all matching" across an unfiltered or
multi-account view, confirming the stage fails with a `multi_account_selection`
multi-account view, staging fails with a `multi_account_selection`
error even though preflight succeeded. Filter to one source/account
(the `A` key in the TUI, or the equivalent source filter in the Web UI) before
staging an all-matching selection that could span more than one account.
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/kata-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ window, so neighbouring passages overlap. The text in the
preview is exactly what the issue quotes. To quote less, select part of the
passage and choose **Quote selection**; **Whole passage** goes back.

To quote a file, open it and choose **Create Kata issue from file**, search for
To quote a file, open it and choose **Add to task…**, search for
a passage, and pick one; the dialog then pages and narrows it like a message.
This needs text that document indexing has already extracted.

Expand Down
71 changes: 19 additions & 52 deletions docs/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ you do not need a separate web application process.
| Which identity matches or profile facts need my decision? | Reviews |
| Can I return to this search later? | Saved views under Messages and Files |
| Did sync, enrichment, or indexing finish? | Status dot, Past runs, and Sources |
| What is staged for deletion? | Deletions |
| What is staged for deletion? | Staged deletions |
| How do I change the daemon's configuration? | Settings |

The sidebar lists **Messages**, **People**, **Files**, and **Sources**. Saved views nest under Messages or Files. The footer holds **Settings**, **Help**, and a status dot. Directory, Reviews, and Deletions remain available through the command palette. Choose **Collapse sidebar** to shrink it to an icon rail; the browser remembers the choice. Below 900 pixels wide, **Open navigation** opens a drawer.
The sidebar lists **Messages**, **People**, **Files**, and **Sources**. Saved views nest under Messages or Files. The footer holds **Settings**, **Help**, and a status dot. Directory and Reviews remain available through the command palette. **Staged deletions** appears while a listed batch is pending, running, or failed. Use **Go to Staged deletions** in the command palette to open its history when the sidebar entry is hidden. Choose **Collapse sidebar** to shrink it to an icon rail; the browser remembers the choice. Below 900 pixels wide, **Open navigation** opens a drawer.

The top bar holds global search and **Display**. Display sets theme and density for this tab. On the ungrouped Messages table, it also shows **Columns**. **Reader position** appears when the results are at least 960 pixels wide. **Use Settings defaults** clears temporary theme and density choices. [Settings](#settings-and-restart-behavior) holds the saved defaults.

Expand Down Expand Up @@ -121,8 +121,8 @@ toolbar. Select the chip's remove button, such as **Remove search** or **Remove
Person filter**, to drop it. **Save view…** in the page header saves the current
context; see [Saved views](#saved-views).

Selecting rows opens a selection bar. **Review for deletion…** opens the
[Deletions](#deletions) workspace. The button is disabled when the daemon
Selecting rows opens a selection bar. **Review deletion…** opens a review dialog over Messages; see
[Deletions](#deletions). The button is disabled when the daemon
reports that staging is unavailable for the selection, and the bar states the
reason beside it, for example "None of the selected items can be deleted from
their source." Export and **Open selection in source** explain their
Expand Down Expand Up @@ -221,7 +221,7 @@ true` for daemon startup to build a stale cache. With `analytics.engine =
## Files and containing context

Files is the single file view. It is a searchable table of attachment date,
filename, type, size, person or domain, source, containing item, and content
filename, type, size, person or domain, source, Message, and content
availability. Archived images and PDFs open in application-controlled viewers.
Metadata-only, missing, unsupported, and previewable content remain distinct.
From a file, navigate to its containing item and then its email or chat
Expand All @@ -230,10 +230,10 @@ conversation.
Files uses the same toolbar as Messages, with **Show as** set to Files. Choose Table
or Timeline there to return to Messages. **Sort** offers Newest first, Oldest
first, Filename A–Z, Filename Z–A, Largest first, and Smallest first. The
Filename field and the **Type** menu (Images, PDFs, Audio, Video, Text,
Documents, Archives, Other) narrow the table. Grouping replaces the table with
groups and hides Sort, Filename, Type, and Visual search; active Filename and
Type filters stay visible as chips.
**Filter** menu includes Filename and Type (Images, PDFs, Audio, Video, Text,
Documents, Archives, Other). Grouping replaces the table with groups and hides
Sort and Visual search; Filename and Type remain editable in Filter, and active
filters stay visible as chips.
**Save view…** saves the search, filters, and grouping, but not the Filename
filter, Type filter, or file sort.

Expand Down Expand Up @@ -360,27 +360,16 @@ All three read the same records and see the same revisions.

## Sources and sync status

Sources is a status workspace. Each row shows a source's name, readable type and
identifier, schedule, status, last successful sync, and an action. The schedule
reads as a sentence, with the cron expression in its tooltip.
Sources lists Source, Status, Last good sync, and Sync now. Status shows Running, Succeeded, Partial, Failed, Paused, Queued, Cancelled, or Never synced. Contacts accounts appear in their own group. Sources stays available while contacts status loads, and contacts-only runs continue polling.

The **Status** chip is **Syncing**, **Completed**, **Completed with errors**,
**Failed**, or **Never synced**. A running sync also shows its processed, added,
and error counts. Otherwise the time of the latest result appears under the
chip, and the last successful sync has its own column. A latest result older
than 24 hours adds "This result may be out of date." A failed status request
remains an error rather than becoming an empty source list.
Select a source to open its panel. Schedule, Next sync, and attachment rules show the current values. **Edit in Settings › Sources** opens the existing editor for settings shared by that source type. The panel also shows Runs, item errors, and **Sync now**. A latest run's error appears before a scheduler error. **Sync history** opens Past runs filtered to source sync.

A source with a sync error message, item errors, or a scheduler error has
**Show details for {name}**, which opens the run-level errors, item-level
errors, and scheduler error. **Sync history** in the page header opens
**Past runs** filtered to source sync.
A contacts panel shows its repair reason and **Open Settings › Contacts** for sign-in recovery. That editor retains the selected OAuth app and provides commands for both shells. Mail sync instructions in the source panel give `msgvault sync --source-id N`.

**Sync now** is available only when that source reports the capability. When it
is not, the Action column says why in words: "Imported file — nothing to sync",
is not, the Sync now column says why in words: "Imported file — nothing to sync",
"Sync in progress", "Scheduler unavailable", "Sync not set up", or "Sync
unavailable". The daemon's reason code stays in the text's tooltip.
Meeting-import sources show "On-demand API source" instead, because meetings
unavailable". Meeting-import sources show "On demand" instead, because meetings
arrive through the API rather than a sync. A `202
Accepted` response means the daemon accepted the request, not that work has
finished. While the page is visible, the UI polls source status with bounded
Expand Down Expand Up @@ -421,33 +410,11 @@ Old Operations links with a run, status, or history filter open Past runs over S

## Deletions

Messages supports explicit row selection and select-all-matching for the
current query and filters. **Review for deletion…** in the selection bar, or `d`
and `D`, opens the Deletions workspace. The daemon first preflights the
selection and reports any unavailable action before the UI offers a separate
staging confirmation. The workspace lists, inspects, and cancels manifests; it
cannot execute deletion against a provider. Use the explicit
`msgvault delete-staged` CLI workflow for that final operation.

Without a selection, the workspace says "Nothing selected for deletion" and
points back to Messages. With one, the **Review selection** panel shows the
item count and estimated size, how many items can be staged and how many will be
skipped, and when the review expires, as a relative time such as "in 8 minutes"
with the exact time in a tooltip. When the daemon reports that staging is
unavailable, the panel gives the reason as a sentence and disables **Stage
deletion…**. **Dry run** reports matched, staged, and skipped counts without
staging anything. **Stage deletion…** opens a confirmation, and only its red
**Confirm stage deletion** button creates the manifest. Staging covers Gmail and
Microsoft Graph mail only and does not execute deletion. Arriving from
Messages runs the review at once and, when staging is available, opens the
confirmation.

The manifests table lists ID, Description, Items, Status, and Created. Status is
Pending, In progress, Completed, Failed, or Cancelled. **Inspect** opens a
manifest's detail beside the table, or below it at 900 pixels wide or narrower;
**Close manifest detail** closes it. **Cancel** appears for Pending and In
progress manifests and asks for **Confirm cancel manifest**. With no manifests,
the workspace says "No staged deletions".
Messages supports individual row selection and select-all-matching for the current query and filters. **Review deletion…** in the selection bar, or `d` and `D`, opens a review dialog over Messages. The daemon preflights the selection before enabling staging.

The dialog shows the selected count and estimated size, eligible and skipped counts, exclusions, and review expiration. Semantic search reviews warn that they cover active messages only. **Dry run** reports matched, staged, and skipped counts without creating a batch. **Stage N messages** creates the batch and leaves a success summary with its ID. **Done** returns to Messages. Staging supports Gmail and Microsoft Graph mail; use `msgvault delete-staged` to execute deletion.

The Staged deletions workspace lists batches by creation date, message count, and status. Select a row to see its account and inspect its description, summary, execution counts, and failed message IDs. If the account can't be read, the panel shows "Account unavailable". **Cancel batch** appears for staged or running batches and asks for confirmation before cancelling. **Show cancelled** includes cancelled batches in page history. With no batches, the workspace says "Nothing staged". The sidebar entry appears while a listed batch is pending, running, or failed. **Go to Staged deletions** in the command palette always opens the history.

## Keyboard controls

Expand Down
52 changes: 29 additions & 23 deletions internal/api/carddav.go
Original file line number Diff line number Diff line change
Expand Up @@ -1095,34 +1095,40 @@ func (s *Server) handleCardDAVStatus(w http.ResponseWriter, r *http.Request) {
}
return
}
if s.scheduler != nil && s.scheduler.IsRunning() {
jobs := map[string]bool{}
if present {
jobs[CardDAVJobNameForConnection(name)] = true
} else {
jobs[CardDAVJobName] = true
for _, connection := range s.cardDAV.connectionNames() {
jobs[CardDAVJobNameForConnection(connection)] = true
}
}
for _, job := range s.scheduler.JobStatus() {
if !jobs[job.Name] {
continue
}
status.Scheduled = true
status.SchedulerRunning = status.SchedulerRunning || job.Running
if job.Queued && !job.QueuedAt.IsZero() && (status.QueuedAt == nil || job.QueuedAt.Before(*status.QueuedAt)) {
status.QueuedAt = new(job.QueuedAt)
}
if !job.NextRun.IsZero() && (status.NextScheduledAt == nil || job.NextRun.Before(*status.NextScheduledAt)) {
next := job.NextRun
status.NextScheduledAt = &next
}
statuses := map[string]*CardDAVStatusResponse{}
if present {
statuses[CardDAVJobNameForConnection(name)] = &status
} else {
statuses[CardDAVJobName] = &status
for _, connection := range s.cardDAV.connectionNames() {
statuses[CardDAVJobNameForConnection(connection)] = &status
}
}
s.applyCardDAVSchedules(statuses)
writeJSON(w, http.StatusOK, status)
}

func (s *Server) applyCardDAVSchedules(statuses map[string]*CardDAVStatusResponse) {
if s.scheduler == nil || !s.scheduler.IsRunning() {
return
}
for _, job := range s.scheduler.JobStatus() {
status := statuses[job.Name]
if status == nil {
continue
}
status.Scheduled = true
status.SchedulerRunning = status.SchedulerRunning || job.Running
if job.Queued && !job.QueuedAt.IsZero() && (status.QueuedAt == nil || job.QueuedAt.Before(*status.QueuedAt)) {
status.QueuedAt = new(job.QueuedAt)
}
if !job.NextRun.IsZero() && (status.NextScheduledAt == nil || job.NextRun.Before(*status.NextScheduledAt)) {
next := job.NextRun
status.NextScheduledAt = &next
}
}
}

func (s *Server) handleCardDAVRuns(w http.ResponseWriter, r *http.Request) {
if s.cardDAV == nil {
writeError(w, http.StatusServiceUnavailable, "carddav_unavailable", "CardDAV run history is unavailable")
Expand Down
14 changes: 13 additions & 1 deletion internal/api/carddav_connections.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,5 +410,17 @@ func (s *Server) handleCardDAVConnections(w http.ResponseWriter, r *http.Request
s.writeCardDAVAccountError(w, err, "CardDAV connection lookup failed")
return
}
writeJSON(w, http.StatusOK, result)
writeJSON(w, http.StatusOK, s.scheduledCardDAVConnections(result))
}

func (s *Server) scheduledCardDAVConnections(result CardDAVConnectionsResponse) CardDAVConnectionsResponse {
statuses := map[string]*CardDAVStatusResponse{}
for i := range result.Connections {
connection := &result.Connections[i]
if !connection.Orphaned {
statuses[CardDAVJobNameForConnection(connection.Connection)] = &connection.Status
}
}
s.applyCardDAVSchedules(statuses)
return result
}
26 changes: 26 additions & 0 deletions internal/api/carddav_multiple_connections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,32 @@ func TestCardDAVMultipleConnectionsReadSelectorsAndNamedOnlyStatus(t *testing.T)
assertions.Equal("work", books.Books[0].Connection)
}

func TestCardDAVMultipleConnectionsReportsIdleSchedule(t *testing.T) {
assertions := assert.New(t)
require := require.New(t)
controller, baseURL := multipleCardDAVController(t)
_, err := controller.Save(t.Context(), CardDAVAccountRequest{Connection: "work", BaseURL: baseURL,
Username: "work", Password: "work-synthetic-secret", Enabled: new(true), Schedule: "0 1 * * *"})
require.NoError(err)
next := time.Date(2026, 11, 1, 1, 0, 0, 0, time.UTC)
sched := newMockScheduler()
sched.jobStatuses = []JobStatus{
{Name: "unrelated", NextRun: next.Add(-time.Hour)},
{Name: "carddav:work", NextRun: next},
}
response := getCardDAVRead(t, cardDAVReadServer(t, controller.cfg, controller, sched), "/api/v1/carddav/connections")
require.Equal(http.StatusOK, response.Code, response.Body.String())
var summaries CardDAVConnectionsResponse
require.NoError(json.NewDecoder(response.Body).Decode(&summaries))
require.Len(summaries.Connections, 1)
assertions.Equal("work", summaries.Connections[0].Connection)
status := summaries.Connections[0].Status
assertions.Nil(status.Active)
assertions.True(status.Scheduled)
require.NotNil(status.NextScheduledAt)
assertions.Equal(next, *status.NextScheduledAt)
}

func TestCardDAVMultipleConnectionsRouteGlobalTarget(t *testing.T) {
assertions := assert.New(t)
require := require.New(t)
Expand Down
2 changes: 2 additions & 0 deletions internal/api/deletions.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type DeletionManifestSummary struct {
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"`
Description string `json:"description"`
Account string `json:"account,omitempty"`
MessageCount int `json:"message_count"`
}

Expand Down Expand Up @@ -515,6 +516,7 @@ func (s *Server) handleListDeletions(w http.ResponseWriter, r *http.Request) {
CreatedAt: m.CreatedAt,
CreatedBy: m.CreatedBy,
Description: m.Description,
Account: m.Filters.Account,
MessageCount: len(m.GmailIDs),
})
}
Expand Down
4 changes: 4 additions & 0 deletions internal/api/deletions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,10 +649,12 @@ func TestListDeletions(t *testing.T) {
deletion.StatusPending: {{
ID: "batch-1", Status: deletion.StatusPending, CreatedAt: now,
CreatedBy: "api", Description: "old mail", GmailIDs: []string{"gm-1", "gm-2"},
Filters: deletion.Filters{Account: "work@example.com"},
}},
deletion.StatusCancelled: {{
ID: "batch-2", Status: deletion.StatusCancelled, CreatedAt: now.Add(time.Hour),
CreatedBy: "tui", Description: "cancelled batch", GmailIDs: []string{"gm-3"},
Filters: deletion.Filters{Account: "personal@example.com"},
}},
}}
srv := newDeletionTestServer(t, st, &querytest.MockEngine{})
Expand All @@ -665,6 +667,8 @@ func TestListDeletions(t *testing.T) {
require.NoError(json.NewDecoder(w.Body).Decode(&resp), "decode")
require.Len(resp.Manifests, 2)
assert.Equal("batch-2", resp.Manifests[0].ID, "newest first")
assert.Equal("personal@example.com", resp.Manifests[0].Account)
assert.Equal("work@example.com", resp.Manifests[1].Account)
assert.Equal(2, resp.Manifests[1].MessageCount)

// Filtered by status.
Expand Down
Loading
Loading