echo v5.3.0 reverted to v4 behavior where every group with middleware
implicitly registers a 404 route at its prefix. Our route setup creates
multiple groups with the same prefix (rate-limit subgroups of /api/v1),
which now panics with a duplicate-route error at startup.
Mail sending is asynchronous: SendMail only enqueues the message and a
background daemon goroutine performs the actual SMTP delivery. CLI
commands exited as soon as their run function returned, killing the
daemon before the handshake completed, so `user reset-password`,
`user delete` and `user create` reported success without ever
sending their mail.
Add mail.StopMailDaemon which closes the queue and blocks until the
daemon has delivered all remaining messages (with a timeout so a broken
SMTP server can't hang the CLI), and call it from a PersistentPostRun
hook on the user command. The web server's non-blocking behavior is
unchanged.
iOS Reminders (and other RFC 6578 clients) never learned about tasks
deleted on the Vikunja side: caldav-go answers unknown REPORT types
with 412, which makes iOS silently stop syncing, and deleted tasks left
no protocol-level trace a client could observe.
Intercept sync-collection REPORTs before caldav-go sees them and answer
per RFC 6578: tasks changed since the sync token as 200 entries and
deleted tasks as 404 entries. Deletion records come from task
soft-deletes, which are kept for 30 days before being purged.
Token edge cases: delta comparisons are inclusive because tokens have
second granularity (re-reported items keep their etag, so clients skip
the download), and tokens older than the soft-delete retention answer
403 + valid-sync-token to force a full resync instead of a delta that
would silently miss purged deletions.
Relations, comments, attachments, assignees (via the shared listener)
and label add/remove now advance the project's updated timestamp, and
label changes also advance the task's, so CalDAV ctags and
sync-collection deltas reflect every change that is visible in a VTODO.
The ranking-gate comment still claimed title and description weigh
equally, which the 1.5x boost made wrong; scoring semantics now live only
at the boost call site. Test subtests share searchTaskPositions instead
of hand-building position maps four times.
With constant-score relevance, 1.5 ranks a title match above a description
match while two matched words (2.0) still beat a single boosted title word,
preserving "more matched words wins". ParadeDB-only; the substring fallback
has no scoring.
The fuzzy query shape makes pdb.score a constant-score sum (1.0 per
exact/prefix word-field match, 0.5 per one-edit match), not BM25, and no
field boosting is configured. Correct the comments claiming otherwise.
Covers per-token matching across title and description, word-order
independence, description-only matches, and fuzzy(1)-against-token-prefix
matching, including its surprising looseness ("two" matching "world" via
the one-edit prefix "wo"). Skipped on databases without ParadeDB, whose
whole-string substring fallback intentionally matches less.
sort_by=relevance renders as pdb.score(tasks.id) DESC at its requested
position, so clients can express orderings like done,relevance (undone
first, most relevant within each group). The implicit no-sort ranking
reuses the same mechanism via an injected sort param.
When the database or query shape cannot score, relevance params are
dropped and the remaining sorts plus the id tiebreaker apply, so the
field is safe to send unconditionally; getOrderByDBStatement also only
emits pdb.score when ParadeDB is actually available.
The quick-actions search sends sort_by=done,relevance (typed via
TaskFilterParams) and keeps surfacing undone tasks first.