Commit Graph

267 Commits

Author SHA1 Message Date
kallegrens
82ee780161 feat(caldav): implement RFC 6578 sync-collection REPORT to sync task deletions
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.
2026-07-07 11:57:04 +00:00
Tink
d83e90541c feat(tasks): soft-delete tasks with permanent deletion after 30 days (#3119) 2026-07-07 11:38:33 +02:00
kolaente
167556ce2b feat(search): add explicit relevance sort field
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.
2026-07-06 15:45:55 +00:00
kolaente
e24ec07293 feat(search): rank ParadeDB search results by relevance
Searching with ParadeDB returns fuzzy/OR matches in id order, so a task
matching all query words can sink below tasks matching only one. When a
search term is present and the client sends no explicit sort, order the
results by pdb.score(tasks.id), keeping the stable id tiebreak.

Numeric #id searches and the Favorites view keep the default order since
pdb.score rejects those query shapes. On the all-projects scope the
favorites arm is dropped when every favorited task already lies inside
the project scope, so global search stays ranked for users with
favorites; out-of-scope favorites fall back to the unranked order.

The frontend omits sort_by while searching in project views unless the
user explicitly picked a sort, so the backend ranking engages.
2026-07-06 15:45:55 +00:00
nithin varma
afdc471c24 fix(task): recurring tasks return to original bucket when no default is set (#2806) 2026-07-03 23:18:00 +02:00
Tink
f11c90ca6b fix: create saved filter positions before the fetch query (#3084) 2026-07-02 19:43:15 +02:00
Tink
767ce3bc7e fix(tasks): reset description checklist when a recurring task recurs (#2941) 2026-06-19 16:54:20 +02:00
kolaente
5236e0c306 fix(notifications): use full user so notifications show display name
Notifications and emails showed the acting user's auto-generated
username instead of their display Name.

The doer attached to notification events was built straight from the
JWT via user.GetFromAuth, which only carries id + username (Name is
never set in GetUserFromClaims). Notifications render n.Doer.GetName(),
which falls back to the username when Name is empty, so every "assigned
you", "mentioned you", task-deleted, project-created and team-member
notification rendered the username.

Resolve the full user from the database at the event-producing dispatch
sites. doerFromAuth now re-fetches the user (with Name) and is reused by
all the notification doers; account-status errors are swallowed so flows
acting on behalf of disabled accounts (e.g. user deletion deleting that
user's tasks) keep working while still carrying the display name.

Fixes #2720
2026-06-18 20:57:05 +00:00
kolaente
7b7c850dd8 refactor(tasks): drop in-memory task dedup, rely on unique index
The duplicate task rows getTasksForProjects deduplicated came from the
LEFT JOIN multiplying when duplicate task_positions rows existed. The new
unique index on (task_id, project_view_id) removes the root cause at the
SQL layer (the migration also runs before serving), so the join can no
longer multiply. Revert getTasksForProjects and getRawTasksForProjects to
their pre-dedup shape.
2026-06-17 21:16:41 +00:00
kolaente
a61e594952 fix(tasks): prevent duplicate task_positions rows and stale identifiers
A task could end up with more than one task_positions row for the same
(task_id, project_view_id): rapid/concurrent creation raced the
check-then-insert paths, and the create path could insert a position that
a triggered RecalculateTaskPositions had already persisted for the new
task. The table had no unique constraint, so the duplicates were stored
silently (#2844).

In the table view this made the LEFT JOIN on task_positions emit the task
twice; getTasksForProjects enriched only the map entry, so the duplicate
slice row kept an empty identifier and rendered as "#N" instead of
"PREFIX-N" (#2725).

- Add a unique index on task_positions(task_id, project_view_id) via a
  dedup migration (mirrors the task_buckets fix in 20250624092830) plus the
  unique(task_view) struct tag so fresh installs get it too.
- Harden the create path: only queue a position insert when one does not
  already exist for the task+view, and dedupe within the batch.
- Dedupe the task slice returned by getTasksForProjects by id, returning
  the enriched entry, so duplicate position rows can never surface a task
  twice or with a missing identifier.

Fixes #2844
Fixes #2725
2026-06-17 21:16:41 +00:00
kolaente
cf456fb223 fix(kanban): count tasks in bucket, not filter total, for saved-filter bucket limits
On a saved-filter (or view-filter) kanban view, checkBucketLimit counted
the total number of tasks matching the filter instead of the number of
tasks actually in the target bucket. Adding the first task to an empty
limited bucket was therefore wrongly rejected with code 10004
"exceeded the limit", even though the bucket was at 0/limit. The same
setup on a regular project bucket worked because that branch counts
task_buckets rows scoped to the bucket.

Scope the count to the bucket by adding `bucket_id = <id>` to the
TaskCollection filter. ReadAll combines this with the saved-filter /
view filter, so the count reflects exactly the tasks that are in this
bucket and match the filter. This keeps the #355 behaviour (stale
task_buckets rows whose tasks no longer match the filter are excluded)
while fixing the unscoped over-count.

Fixes #2672
2026-06-17 18:20:25 +00:00
kolaente
4316554b27 docs(api/v2): tag task fields for the v2 schema 2026-06-10 10:12:09 +00:00
kolaente
e197b1912f feat(time-tracking): count tracked time entries per task 2026-06-08 13:54:09 +00:00
kolaente
c6c57d9d15 refactor(models): remove *Arr helper fields now handled by normalizer 2026-05-31 12:56:57 +00:00
kolaente
9206f98d64 feat(tasks): enforce unique (project_id, index) via migration 2026-04-11 20:44:28 +00:00
kolaente
8f9b50bdcb feat(tasks): add GetTaskByProjectAndIndex resolver 2026-04-11 20:44:28 +00:00
kolaente
6df0d6c8f5 feat(tasks): cap repeat_after at 10 years to harden repeating-task handler
Add MaxTaskRepeatAfterSeconds (10 years in seconds) and reject any
create/update that tries to store a value outside [0, cap] with a new
ErrInvalidTaskRepeatInterval (error code 4029). Defense-in-depth
alongside the arithmetic fix in addRepeatIntervalToTime: keeps stored
values well away from int64 overflow and bounds the range of inputs
a future refactor could trip over.
2026-04-09 16:07:48 +00:00
kolaente
9dc3d7eb4f fix(tasks): replace O(n) loop in repeating-task handler with arithmetic
addRepeatIntervalToTime used to advance t by whole intervals via an
unbounded loop. A repeating task with an ancient due_date and a
one-second interval required billions of iterations per task update,
turning completion of such a task into a trivial denial-of-service
(GHSA-r4fg-73rc-hhh7). Compute the number of intervals directly, with
guards for zero/negative durations, saturated time.Sub, and int64
overflow.

Covered by TestAddRepeatIntervalToTime, including the 1900-01-01 PoC
case.
2026-04-09 16:07:48 +00:00
kolaente
f1e12c6f64 fix(caldav): enforce task read authorization on GetTasksByUIDs
Previously GetTasksByUIDs returned any task matching the UID regardless
of the caller's access, letting any authenticated CalDAV user read any
task by guessing or knowing a UID. Filter by accessible project IDs at
the SQL level using the existing accessibleProjectIDsSubquery helper.

Fixes GHSA-48ch-p4gq-x46x.
2026-04-09 16:07:32 +00:00
kolaente
1e784aa194 fix(tasks): route repeating tasks to default bucket when marked done (#2573) 2026-04-09 10:45:34 +00:00
kolaente
007379cff1 refactor(tasks): add moveTaskToDefaultBuckets helper (#2573) 2026-04-09 10:45:34 +00:00
kolaente
0c3d01099f fix: detect and resolve position conflicts during task creation
Fixes #2495, fixes #2400

When tasks are created, setTaskInBucketInViews() assigns positions
without checking for conflicts. If two tasks get the same position value,
their order scrambles on reload. This adds a conflict check after
bulk-inserting positions, reusing the existing resolution logic from the
update path.
2026-04-03 17:26:55 +00:00
kolaente
833f2aec00 refactor: use accessibleProjectIDsSubquery in addBucketsToTasks 2026-03-23 16:26:37 +00:00
kolaente
67a47787fa fix: filter related tasks by project access to prevent cross-project info disclosure 2026-03-23 16:26:37 +00:00
Tink
d11f097eee fix(tasks): support both expand and expand[] query parameter formats (#2415)
The `expand` query parameter only supported the `expand[]=foo` array
format, but the swagger docs described it as a plain string parameter.
This adds support for both formats (`expand=foo` and `expand[]=foo`),
matching the existing pattern used by `sort_by` and `order_by`
parameters.

Closes #2408

---------

Co-authored-by: kolaente <k@knt.li>
2026-03-19 09:18:11 +00:00
kolaente
3eb289262f fix(events): defer task event dispatch until after transaction commit
Convert events.Dispatch to events.DispatchOnCommit in Task.Create,
updateSingleTask, Task.Delete, and triggerTaskUpdatedEventForTaskID.
Events are now dispatched by the handler after s.Commit(), ensuring
webhook listeners see committed data.

Fixes #2315
2026-03-03 12:46:34 +01:00
kolaente
a5b1a90c42 refactor: remove typesense support
Typesense was an optional external search backend. This commit fully
removes the integration, leaving the database searcher as the only
search implementation.

Changes:
- Delete pkg/models/typesense.go (core integration)
- Delete pkg/cmd/index.go (CLI command for indexing)
- Simplify task search to always use database searcher
- Remove Typesense event listeners for task sync
- Remove TypesenseSync model registration
- Remove Typesense config keys and defaults
- Remove Typesense doctor health check
- Remove Typesense initialization from startup
- Clean up benchmark test
- Add migration to drop typesense_sync table
- Remove golangci-lint suppression for typesense.go
- Remove typesense-go dependency
2026-02-25 12:15:28 +01:00
kolaente
cbfd0e63ed fix: pass pointer to xorm Update to avoid hash panic in transaction mode
In transaction mode, xorm stores the bean argument as a map key in
afterUpdateBeans. Since Task contains slices and maps (unhashable
types), passing a Task value causes "hash of unhashable type" panic.
Passing a pointer (&ot) fixes this since pointers are always hashable.
2026-02-25 11:03:02 +01:00
renovate[bot]
9a61453e86 fix(deps): update module github.com/labstack/echo/v4 to v5 (#2131)
Closes https://github.com/go-vikunja/vikunja/pull/2133

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kolaente <k@knt.li>
2026-01-24 20:38:32 +01:00
kolaente
82558dba87 fix(filters): ensure saved filter views never have position=0 (#1996)
Fixes #724 - Tasks in saved filter views get `position: 0` when they first appear in the filter, causing drag-and-drop sorting to not persist correctly.

**Changes:**
- Remove harmful `Position: 0` inserts from cron job and
`SavedFilter.Update` - `RecalculateTaskPositions` already creates
positions with proper values, so the intermediate inserts created a race
window
- Add on-demand position creation when fetching tasks for saved filter
views - safety net for newly matching tasks before the cron runs
- Add 5 new tests covering the fix and regression scenarios

🐰 Positions once zero, now bloom with care,
Sorted with grace, no more despair,
When filters call and tasks appear,
Numbers spring up, crystal clear!
Issue 724 hops away—
Sorting's fixed to stay, hooray! 🎉
2025-12-16 22:13:40 +00:00
kolaente
0b3decd869 fix: ensure API consistency for /tasks and empty array responses (#1988)
- Renames the `/tasks/all` endpoint to `/tasks` for consistency with
other collection endpoints like `/projects` and `/labels`
- Returns `[]` instead of `null` for empty pagination results across all
list endpoints
- Updates the frontend service to use the new endpoint path
- Updates API token tests to use the new endpoint path

Fixes #1984
2025-12-15 15:34:13 +00:00
Mithilesh Gupta
7dddc5dfa2 feat: task unread tracking (#1857)
---------

Co-authored-by: Mithilesh Gupta <guptamithilesh@protonmail.com>
Co-authored-by: kolaente <k@knt.li>
2025-11-27 15:14:42 +01:00
kolaente
9b78584734 fix(events): only trigger task.updated once when marking task done
Resolves https://github.com/go-vikunja/vikunja/issues/1724
2025-11-16 11:01:15 +01:00
Biagio00
5b42724205 fix(kanban): repeating tasks dates won't update when moved in done bucket (#1638) 2025-11-14 16:57:53 +00:00
Mithilesh Gupta
01a84dd2d5 feat: add comment count to tasks (#1771) 2025-11-11 23:00:05 +01:00
kolaente
d33e742961 chore: make condition simpler 2025-10-07 10:56:03 +02:00
kolaente
e5e0413b70 fix(task): ensure done_at can never be set by user (#1461) 2025-09-11 07:45:42 +00:00
kolaente
db123674a7 feat: share logic for bulk update (#1456)
This change refactors the bulk task update logic so that it updates all fields a single task update would update as well.

Could be improved in the future so that it is more efficient, instead of calling the update function repeatedly. Right now, this reduces the complexity by a lot and it should be fast enough for most cases using this.

Resolves #1452
2025-09-10 16:40:59 +00:00
kolaente
a81a3ee0e5 feat!: rename right to permission (#1277) 2025-08-13 11:05:05 +02:00
kolaente
a8025a9e36 fix: guard invalid user lookups (#1034) 2025-06-26 21:58:47 +00:00
kolaente
53264d350e fix(kanban): make bucket query fixed per-view (#1007) 2025-06-25 11:38:24 +00:00
Dominik Pschenitschni
296577a875 fix: correct license header references (#882)
See originals:
- https://www.gnu.org/licenses/agpl-3.0.txt
- https://www.gnu.org/licenses/gpl-3.0.txt
2025-06-10 12:18:38 +02:00
Dominik Pschenitschni
508cdf027c fix: param type (#895) 2025-06-10 12:06:41 +02:00
Vladimir
c3fffefcf4 fix(db): refactor filtering with subqueries (#701)
Resolves #285
2025-05-12 10:52:48 +02:00
renovate[bot]
4a75f7d4da chore(deps): update golangci/golangci-lint-action action to v7 (#462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kolaente <k@knt.li>
2025-04-02 09:28:56 +02:00
kolaente
0585045260 fix(kanban): Mark tasks done when creating them in the done bucket
Resolves https://community.vikunja.io/t/bugs-around-project-duplication-with-kanban-buckets/3433/6
2025-03-21 17:32:50 +01:00
kolaente
617070f9f7 fix(kanban): use full updated kanban bucket when moving task 2025-03-19 16:06:20 +01:00
kolaente
134c2a1a80 fix(kanban): do not allow creating tasks in full bucket 2025-03-19 15:18:55 +01:00
kolaente
5c6be117fe fix(kanban): increase dates when moving a task into the done bucket
Resolves https://community.vikunja.io/t/unable-to-drag-a-repeating-task-to-done-bucket/3321/2
Resolves https://github.com/go-vikunja/vikunja/issues/402
2025-03-16 17:19:51 +01:00
kolaente
4e90c6bb78 chore(deps): update golangci-lint 2025-03-02 13:00:18 +01:00