/all-contacts now returns [{ email, name? }], so pickers can show and match the display name; filter values stay addresses. The mailbox avatar takes its initials from from_name.
Accounts may now list ordered headers whose first valid mailbox replaces the stored `from` address at ingest. The real From header remains the fallback, and the original address is kept in `wire_from` whenever the rule actually changed it.
- store the sender display name in `from_name` and index both new fields into `from_text`/`from_text_tri`, so sender search and the dashboard use the resolved address
- append both fields to the email and attachment schemas, bump the index format to 4, and re-derive them from the raw message during reindex using the account's current rule list
- expose the rule on account create/update (advanced settings, collapsed by default) and render `Name <address>` plus a `via` hint in the web views
- archive rules keep matching the real From address; forged headers are deliberately not verified
bichon-server runs as PID 1 without an init shim, so a healthcheck process
reparented to it (the shell is killed when a check times out, curl keeps
running) is never waited for and stays a zombie.
Install tini and make it the entrypoint so orphans are reaped and SIGTERM is
forwarded. Bound curl with --max-time 4 so a stalled check exits before
Docker's 5s timeout instead of being orphaned.
These keys already existed in en.json; add them to the other 17 locales:
token filters and the metrics-only token labels, plus the message view's
show/hide details and search-entry labels.
- widen the folder tree and its labels so long names stay readable
- use a plain button for the row delete control: the Button svg sizing rule inflated the icon
- simplify the mailboxView message count key to a single pluralised string
- count the blob store and the search index separately, and honour a custom index directory
- expose storage_stats_available so callers that cannot see every account are told the figures are unavailable instead of seeing a partial total
- relabel the storage figure as email data size and explain the ratio
- move archived mail into bichon_auto_created_trash, manually or on a schedule
- MOVE (RFC 6851) is the only move path; Bichon never issues EXPUNGE, and a server without MOVE is reported as unsupported
- metadata-only verification before every move; the MOVE capability is checked per connection, case-insensitively
- fill each batch with messages that can actually move; skips no longer shrink it
- scheduled runs drain batches under a 10-minute budget
- jobs are listed/polled/cancelled over the API and reattached after a reload
- reports keep the last 30 runs and show moved/failed only
- WebUI dialogs and 18 locales
Skip candidates rejected by header-only pre-filtering and surface
filtered counts in state, API and UI. Also fix cancellation finalization, advance highest_uid when every candidate is filtered, make partial-batch accounting consistent, and merge the duplicated progress/filtered state writes.
- Multi-word queries AND by default; legacy saved searches replay with
`match_all: false` and keep their OR behaviour.
- Tag edits rebuild documents losslessly: non-stored text, trigram
siblings and (attachments) re-extracted content, from one implementation shared with the regression test.
- reindex builds the whole index root aside, validates it, then swaps by renaming the old root; nothing is deleted, unreadable documents go to a 0600 NDJSON
report. The v1.x migration can run it inline.
- Index markers self-heal when the schema proves the current format; a custom `bichon_index_dir` is honoured.
- Search box gains a syntax hint; tests that write the process-global index are serialized (Windows PermissionDenied).
The tag filter could only express "has one of these tags": the absence of a
facet value is not a value that can be picked from the tag list. Add
`has_tags` to `EmailSearchFilter`, mirroring `has_attachment`:
- `Some(true)` keeps only tagged messages, via `ExistsQuery` over the tags
fast field.
- `Some(false)` keeps only messages carrying no tag at all. A `BooleanQuery`
holding nothing but `MustNot` clauses matches nothing in Tantivy, so the
complement is built as `AllQuery` minus that existence check.
- `None`, the default, does not filter.
`has_tags = false` next to a non-empty `tags` is a contradiction between two
conditions that can never both hold, so `EmailSearchRequest::validate`
rejects the combination instead of returning an empty page the caller has to
explain.
The search page exposes the new filter as a pinned "No tags" entry in the tag
popover. It stays visible while the tag list is searched, because it is not a
tag value from the index, and it replaces (or is replaced by) the tag
selection, so the two are never sent together.
Theme names, labels, picker swatch colors and the browser meta color were
defined separately in lib/theme.ts, theme-switch.tsx and
appearance-form.tsx, and the two pickers paired light and dark variants by
array index. Adding or reordering a theme meant editing several copies in
lockstep, and a mismatch silently paired the wrong variants.
THEME_FAMILIES in lib/theme.ts is now the single table; Theme, ALL_THEMES
and the family/mode helpers derive from it, and both pickers iterate it, so
a family's variants can no longer be mispaired. The duplicated preview
components move to components/theme-preview.tsx.
Theme names and order are unchanged, so stored user preferences stay
valid; a test pins the list. Tests also check that theme.css has a block
for every theme and styles no unknown theme names, reading class names
from whole selectors.
Unknown theme values no longer produce invalid class names: the stored
theme and the login response's theme are checked with the new isTheme
and fall back to the default, and the helpers fall back to the default
family for anything that still gets through. LoginResult.theme is typed
as a plain string instead of a stale local 'dark' | 'light' union.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVBG38Df8bRPm3n8iDtD8W
Users could only pick a fixed light or dark theme, so the UI ignored the
OS/browser color scheme. Add a "Use system theme" switch to Settings ->
Appearance and the header theme popover. When enabled, the chosen color
family is kept and its light/dark variant is resolved from
prefers-color-scheme, updating live when the client setting changes.
While the toggle is on, both pickers show one entry per color family
whose preview is split diagonally (light top-left, dark bottom-right).
Picking one keeps the variant currently shown, and turning the toggle off
stores the variant on screen as the chosen theme, so the look stays the
same.
The flag is stored per browser in localStorage
(vite-ui-theme-follow-system). The theme saved to the user account is
still one of the existing concrete theme names, so there are no backend,
API or data-format changes and it is fully backward compatible.
- lib/theme.ts: pure helpers (themeFamily, themeMode, themeWithMode,
resolveTheme)
- theme-context: followSystem state, matchMedia listener, resolvedTheme
- theme pickers consume resolvedTheme for the applied theme; Settings ->
Appearance keeps its form field on the provider's theme, so a pick in
the header popover is highlighted there and is what Update saves
- folder tree: the item component moves to module level, so a theme
change (now also an OS switch) no longer remounts every tree item
- i18n: theme.use_system and settings.appearance.description.use_system
in all 18 locales
Verified with unit tests for the helpers and ThemeProvider (turning the
toggle off, OS changes, listener cleanup) and manually in the browser by
emulating light/dark color schemes.
Closes#381
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVBG38Df8bRPm3n8iDtD8W
The replay line was an unconditional eprintln to stderr, so it showed
up in every server log on startup after an unclean shutdown. Route it
through tracing at debug level; the count is only interesting when
diagnosing WAL behavior.
The `<<BICHON_DETACH_HASH:{hash}>>` prefix is unchanged but the hash it
carries switched domains in #375 (raw encoded body, domain-separated)
versus legacy records (decoded content). The literal was duplicated
across core, server export, and the Pro integrity checker, and the
server export doc still described the pre-#375 keying.
Centralize the prefix as core's DETACH_PLACEHOLDER_PREFIX with full
domain documentation, use it at every production site, and fix the
stale export comment. Test literals intentionally keep the raw string
to pin the on-disk format.
update_envelope_tags_guard_fires_for_held_account writes through the
backup write gate (account insert + tag update) but did not take the
BACKUP_STATE_TESTS lock, so while a backup test held its capture window
the insert was rejected with "writes are temporarily paused" — a
roughly one-in-three flake on the full suite. Follows the established
protocol documented on BACKUP_STATE_TESTS in backup/manager.rs.
write_temp keyed its directory on pid + file name only, so parallel
tests writing the same name (mem1.txt is used by three tests, s1.txt by
three, mem2.txt/s1b.txt by two) shared one path on disk. One test's
fs::write truncating the file between another test's stat and read
surfaced as a spurious "changed size during capture" upload failure
roughly one full-suite run in three on Windows.
Add a per-call sequence number to the directory name; no test relies on
two write_temp calls resolving to the same path.
The attachment-detaching logic (range clamping, raw/decoded hashing,
placeholder splicing, metadata) existed as two hand-maintained copies:
detach_and_store_attachments in core (live ingest) and
detach_attachments_standalone in admin (offline store migration). They
had already drifted on the invalid-range branch (empty raw-domain
record vs. no record, Some vs. None raw_content_hash), which is the
kind of divergence that produced the #375 bugs.
Extract the pure part into core::envelope::extractor::strip_attachment_
bodies (stripped EML, blob records, attachment metadata, and optional
text-extraction candidates via ExtractionFilter). The live path wraps it
with account rules, background extraction, and the blob queue; the
offline migration delegates to it with no filter, adopting core's
invalid-range behavior for consistency.
A re-ingest refreshes envelope metadata to raw storage keys while the
blob store's `exists` dedup keeps the previously stored stripped EML,
whose placeholders still carry the legacy decoded hash. The raw-hash
placeholder scan then finds no occurrence and the reconstructed EML
keeps literal `<<BICHON_DETACH_HASH:...>>` placeholders.
reattach_parts now retries with the legacy decoded placeholder when the
primary scan finds nothing, loading the legacy blob under the decoded
key (or the identical raw-key blob as an alternate when the legacy one
was garbage-collected after the original envelope was deleted).
Also removes the blob-store roundtrip test, which depended on the
process-global BLOB_MANAGER and only passed in isolation; reattach
coverage is provided by the reattach_parts tests with injected loads.