Commit Graph
1443 Commits
Author SHA1 Message Date
kolaente 0d14a05aca feat(projects): expand task counts on the project list
Pass expand=task_counts to get each project's done and undone task
counts. The v2 expand param is now repeatable so it combines with
expand=permissions.
2026-10-02 16:50:34 +02:00
kolaenteandTink 30c468b1d5 fix(bots): order the bot list by id so pages are stable
Without an ORDER BY the paged LIMIT/OFFSET query could return overlapping
or missing bots across pages.
2026-10-01 17:29:11 +02:00
kolaenteandTink 41bb5e1cfb fix(api-tokens): order token list by id before paging
Without an ORDER BY, LIMIT/OFFSET pages are not stable, so tokens could
repeat or go missing across pages.
2026-10-01 17:29:11 +02:00
kolaenteandTink 45001e8827 fix(webhooks): add stable id ordering to Webhook.ReadAll
Webhook.ReadAll paged with Limit/Offset but no ORDER BY, so the row
order (and thus page contents) was undefined across requests.
2026-10-01 17:29:11 +02:00
kolaenteandTink 2fc1d98a42 fix(teams): order team list by relevance and id before paginating
Team.ReadAll had no ORDER BY under LIMIT/OFFSET, so pages could overlap or
skip teams, and an exact search match could fall off the first page the
pickers fetch.
2026-10-01 17:29:10 +02:00
kolaenteandTink 1cd2f5a941 refactor(teams): filter team list by membership subquery instead of DISTINCT join
Prepares for ordering by relevance: Postgres rejects ORDER BY expressions
that are not in a SELECT DISTINCT list.
2026-10-01 17:29:10 +02:00
kolaenteandTink 3a36b2193e fix(admin): match admin user search case-insensitively
Postgres LIKE is case-sensitive, so the admin user picker missed users the
team picker (db.ILIKE) would have found.
2026-10-01 17:29:10 +02:00
kolaenteandTink 36a2a2e56b fix(admin): rank exact and prefix team matches before the page cap
Same cut-off as the user picker for the invite link team picker.
2026-10-01 17:29:10 +02:00
kolaenteandTink 678c387353 fix(admin): rank exact and prefix user matches before the page cap
The user picker fetches one page of 20, so an exact match with a high id
could be cut off by 20 older substring matches.
2026-10-01 17:29:10 +02:00
kolaente b781773719 fix(reminders): show due reminders in notifications without email
The reminder cron only ran, and only created the in-app notification,
when the mailer was enabled and the user had email reminders on. The
database notification is now always stored; the mail is skipped in
ReminderDueNotification.ToMail instead.

Fixes #4072
2026-10-01 15:38:27 +02:00
kolaente f5c68ac049 fix(bots): 404 unowned bots, require status on PUT, audit owner status changes
- Owners can manage bots in any status (disabled or locked); unowned,
  missing and non-bot ids all return 404 so existence never leaks.
- PUT /api/v2/user/bots/{bot} requires status, so a partial body can no
  longer re-enable a disabled bot. A bot_status validator tag limits it
  to active/disabled.
- BotUser.Status is a pointer: the v1 update route keeps the current
  status when the body omits it instead of unlocking the bot.
- Owner status changes dispatch bot.status.changed for the audit log.
- Regenerate the frontend client and adapt the bot update callers.

Refs #3999
2026-10-01 15:27:34 +02:00
kolaente c650f7ddfe feat(admin): filter and sort the admin project list 2026-10-01 03:18:23 +02:00
kolaenteandTink 49bdd5f160 fix(sessions): break last_active ties by id so pages stay stable
Sessions sharing last_active could swap between LIMIT/OFFSET pages.
2026-10-01 00:06:26 +02:00
kolaente e6baee4852 test(reminders): exclude notifications for completed tasks 2026-09-30 23:26:55 +02:00
kolaente 3700ea1ba3 test(reminders): include reminders exactly at the cron tick 2026-09-30 23:26:55 +02:00
kolaente b9446eec74 fix(filters): respect filter_include_nulls on saved filter views
The gantt view of a saved filter sends filter_include_nulls to show tasks
without dates, but the flag was dropped when merging the request filter into
the saved filter. It now applies to the request filter only, so it does not
leak into the saved filter's own conditions (e.g. pulling in unassigned tasks).

Fixes #4009
2026-09-30 21:33:09 +02:00
kolaente 0990f6f94b fix(kanban): move tasks into saved filter done bucket when done elsewhere
The saved filter view listener only inserted bucket rows for tasks not
yet in a view, so a task completed in its own project stayed in the
filter's default bucket. Move it into the view's done bucket when done,
and back to the default bucket when undone.

Fixes #4018
2026-09-30 21:30:05 +02:00
kolaenteandTink 94791debe4 fix(bots): allow owners to manage disabled bots 2026-09-30 10:42:35 +02:00
kolaenteandTink 1f8a45f6a3 test(bots): reproduce disabled bot management failures 2026-09-30 10:42:35 +02:00
kolaente eba8a33d81 feat(auth): return error code 16005 when the refresh cookie is missing
Both refresh endpoints answered a missing cookie with a code-less 401, so
clients couldn't tell it apart from a rotated-away token, which is worth one
retry. ErrNoRefreshToken gives it its own code.

Refs #4023
2026-09-29 22:54:20 +02:00
kolaente 44d137aff1 fix(labels): remove task associations when deleting a label
Label.Delete only deleted the row in `labels`. Since `label_tasks` has
no foreign key cascade and no soft-delete column, every association of
the deleted label stayed behind as an orphaned row.
2026-09-28 15:21:55 +02:00
kolaenteandTink 33142cd8e6 fix(saved-filters): delete by id instead of every loaded field
CanDelete loads the whole filter into the struct. xorm turns every non-empty
field into a delete condition, so a filters JSON that does not re-serialize
byte for byte silently matches nothing while still returning success.
2026-09-16 17:23:51 +02:00
kolaenteandTink d390a7c374 feat(mcp): expose connection settings and token presets
(cherry picked from commit a796db73475469bba18e3c20c8d7d1b818abcc5a)
(cherry picked from commit 0e47ccb001b8bf385f2688126bef8f4918917b45)
2026-09-15 11:41:34 +02:00
kolaenteandTink 88ac0edb82 refactor(api-tokens): share the bearer selection and usage audit
The MCP loopback needs the same rules the token middleware applies, so both
now call models.APITokenAuthorization and models.RecordAPITokenUse.
2026-09-15 11:41:34 +02:00
kolaenteandTink 0bb5a01845 feat(api-tokens): add the mcp:access scope and permission helpers 2026-09-15 11:41:34 +02:00
kolaenteandTink d9e581d62b refactor(api-v2): rename the projecttask path parameter to task
The v1 name leaked into the v2 OpenAPI document and everything derived from
it. Every other /tasks/... route already uses {task}. The expand-scope check
is keyed by echo path template, so its entry moves too, and a webtest now
fails if any of its keys stops naming a registered route.
2026-09-15 11:41:34 +02:00
kolaente 98ca0175a2 refactor(notifications): rename local that shadowed the mail package 2026-09-14 17:50:28 +02:00
kolaente 1e37dfb7ca fix(reminders): skip inactive assignees when collecting task users 2026-09-14 17:50:28 +02:00
kolaente 5888756cd8 test(reminders): exercise the subscriber pass in the IsAssignee test 2026-09-14 17:50:28 +02:00
kolaente 99f0dbe1c3 feat(reminders): split overdue mail into assigned and followed sections
Users subscribed to busy projects got every overdue task in one flat list.
Tasks the user is assigned to now appear under "Assigned to you", tasks
they merely created or follow under "Tasks you follow". A task appears
once; assignee wins over follower.

Resolves https://github.com/go-vikunja/vikunja/issues/3612
2026-09-14 17:50:28 +02:00
kolaente f846d87e66 feat(reminders): track whether a task user is an assignee 2026-09-14 17:50:28 +02:00
kolaente 27ccdba7a0 feat(invites): add admin-managed user invite links 2026-09-12 18:21:35 +02:00
kolaente 814f164e33 feat(tasks): allocate task indexes from the per-project counter
Indexes come from an atomic counter increment instead of MAX(index)+1, so
retired indexes are never reused. Client-supplied indexes are ignored;
import and duplication keep presets above the old high-water mark. Moving
a task records its old address as an alias; the task row is read under a
row lock so concurrent moves serialize. Task bucket inserts are chunked
since whole batches now go through one create.
2026-09-11 20:47:11 +02:00
kolaente 0fd60fc080 feat(tasks): add task index counters and historical aliases
Each project gets a monotonic high-water counter row, backfilled from the
highest existing task index, and an append-only alias table recording
retired (project, index) addresses. Project creation seeds the counter,
project deletion removes both.
2026-09-11 20:47:11 +02:00
kolaente 2e6670e08e perf(projects): resolve project access and ancestors through project_ancestors
The access query joins grants once against the closure table instead of
walking the tree with a recursive CTE, and GetAllParentProjects reads
the chain from it.
2026-09-10 22:44:26 +02:00
kolaente 73da1fe4b0 test(projects): insert raw test projects through one helper that writes their ancestors 2026-09-10 22:44:26 +02:00
kolaente 47a0d0134a feat(projects): maintain a project_ancestors closure table
One row per project and each ancestor including itself, so the tree
resolves with a join instead of a recursive walk. CreateProject inserts
the chain, UpdateProject re-links the subtree on a parent change and
Project.Delete drops the rows; the relink reads take FOR UPDATE so
concurrent reparents of overlapping subtrees serialize, and a project
without its self row fails loudly instead of silently losing access.
Descendant archiving reads from the table. RepairOrphanedProjects
rebuilds the whole table so drift and rows of deleted projects go away.
2026-09-10 22:44:26 +02:00
kolaente b4012f1d08 refactor(models)!: drop the unused GetProjectsByIDs
It duplicated GetProjectsMapByIDs' query without the memo and had no caller left besides the yaegi symbol export.
2026-09-08 20:28:52 +02:00
kolaente 07e99dca0b test(db): cover the session memo
A behind-the-back write from a second session makes a memo hit the only way
the loader tests can pass.
2026-09-08 20:28:52 +02:00
kolaente aaecfadda5 refactor(models): load tasks by id through the memoized GetTaskByIDSimple
Addresses pr-swarm finding.
2026-09-08 20:28:52 +02:00
kolaente 5baf839ded perf(db): memoize single-row task, project and user lookups per session
One GET /tasks/{id} loaded the task twice (Task.CanRead, then ReadOne), its
project twice and users by id several times, each a round trip.
GetTaskByIDSimple, GetProjectSimpleByID, GetProjectsMapByIDs,
user.GetUserByID and user.GetUsersByIDs now go through the per-session memo,
which drops itself as soon as that session writes anything.

db.Remember and db.RememberEach hold the get-or-fetch dance, mirroring
keyvalue.Remember; the two pre-existing memo sites in project_access.go use
them too. Callers get copies: Project.memoCopy re-allocates ParentProjectID,
and the user memo holds the raw row so the disabled/locked gate runs on
every hit rather than only on the first load.
2026-09-08 20:28:52 +02:00
kolaente e2fafd1317 fix(comments): stop returning the author's email when reading a single comment
ReadOne loaded the author with a raw Get, which keeps every column. The list endpoint already goes through getUsersOrLinkSharesFromIDs, which blanks the email and resolves link share authors; ReadOne now does the same.
2026-09-08 10:34:37 +02:00
kolaente c6983d7db5 fix(assignees): include public team members in project user search 2026-09-07 21:35:17 +02:00
kolaente b796e9f840 test(api-tokens): cover sha256 fast path, legacy backfill and null legacy columns 2026-09-06 20:29:27 +02:00
kolaente 9922b1e4f4 feat(api-tokens): verify tokens by indexed sha256 instead of pbkdf2
Every API-token request ran pbkdf2.Key(token, salt, 10000, 50, sha256):
~3 ms CPU per request. Tokens are 160-bit CSPRNG values, so a slow KDF
and salt add nothing; sessions.go already stores refresh tokens as
plain SHA-256 for the same reason. Lookup is now one indexed seek on
token_sha256, falling back to the legacy PBKDF2 scan only for rows
that have not been backfilled yet. A legacy match backfills the column
on its own autocommit session, since callers roll theirs back.
2026-09-06 20:29:27 +02:00
7c3b34f1bb perf(subscriptions): bind entity and user ids as parameters instead of literals (#3776)
`getSubscriptionsForEntitiesAndUser` built its two recursive-CTE queries
with the entity ids (`entityIDString`) and the user id (`sUserCond`)
interpolated into the SQL text. Every `GET /tasks/{id}` therefore sends
a statement PostgreSQL has never seen: the driver's prepared statement
cache cannot hit, and the planner runs on every call.

Measured on the performance-testing setup (pgx, 3000 simulated users):
the task variant of this CTE was planned once per call, 1.3 ms of
planning for 0.08 ms of execution, and it accounted for half of all
planning time on the database (`pg_stat_statements`: 1985 calls, 1985
plans, 2,558 ms planning vs 166 ms execution in a 75 s window).

## Change

Ids and the user id are bound as `?` parameters; the query text now only
varies with the number of ids, so it is prepared once per connection and
planned a handful of times before PostgreSQL switches to the generic
plan. Argument order follows placeholder order in each query, built up
with a small `add` helper next to the query. Semantics are unchanged;
`TestSubscription*` and `TestGetSubscriptionsForEntity*` pass.

## How to verify

1. Open a task and subscribe/unsubscribe to it and to its project;
notifications for comments and assignments still reach the subscribed
users.
2. On PostgreSQL with `pg_stat_statements.track_planning = on`: the
`WITH RECURSIVE project_hierarchy … t.id IN ($1)` row now shows `plans`
far below `calls`.

---------

Co-authored-by: kolaente <k@knt.li>
Co-authored-by: kolaente <hello@kolaente.de>
2026-09-06 08:25:32 +00:00
kolaente 1b8167d4cd perf(db): use the pgx driver for postgres
lib/pq sends every query as an unnamed prepared statement, so PostgreSQL
plans each statement from scratch. pgx via database/sql keeps a per-connection
statement cache and reuses named prepared statements instead, removing that
planning cost from the hot path.

xorm already ships a "pgx" driver mapping, so the switch is just the driver
name plus the blank import. The connection string format is unchanged: pgconn
parses the same URL and forwards unknown parameters (search_path) as runtime
parameters.

Named prepared statements do not work through a connection pooler in
transaction pooling mode unless it supports them (PgBouncer 1.21 and later,
with max_prepared_statements above 0). Those setups can raise that setting or
connect to PostgreSQL directly, which is no loss since Vikunja already pools
via database.maxopenconnections.

lib/pq was only used for QuoteIdentifier, which is now inlined.
2026-09-05 23:16:31 +02:00
kolaente fbe6e5eff3 fix(filters): keep date filter boundaries inside the driver's year range
Parsing the zero-date sentinel (`due_date > 0001-01-01 00:00`) in a
timezone east of Greenwich underflows into year 0 on the way to UTC. The
MySQL driver then refuses to encode the bound parameter with "year is not
in the range [1, 9999]: 0" and the whole task collection request fails.

The year clamp was gated on the MySQL dialect, so it only ran where the
dialect was already known at parse time and left other drivers storing BC
dates. Apply it unconditionally instead, and normalize the time entry
filter to UTC like the task filter already does.

Sentry API-OSS-2Q.
2026-09-05 20:24:08 +02:00
kolaente 72844bf715 fix: lock views before repairing task positions 2026-09-05 12:30:42 +02:00
kolaente a9a9945bc8 fix: lock project views before deleting a project's task positions 2026-09-05 12:30:42 +02:00