The api-token permission group key is derived from the route slug. Every
group is snake_case except "time-entries", whose URL slug carries a hyphen.
The frontend snake_cases request payloads, rewriting that group key to
"time_entries", which the backend then rejected — so a token granted the
Time Entries scope could not be saved.
Canonicalise group and path-segment names to snake_case where they are
derived, and normalise the group key on token validation and authorisation
so any token stored under the old hyphenated key keeps resolving. No data
migration is needed: the v2 time-entries resource has never shipped in a
release.
Set the just-logged-out flag before navigating, and skip the intermediate
router.push to login when redirecting to the IdP — otherwise Login.vue's
onBeforeMount consumed the flag before the logout round-trip landed, so the
single-provider auto-redirect fired and logged the user straight back in.
redirectToProviderOnLogout now reports whether it navigated, so logout can fall through to the login page when there's no static logout URL.
On X11/XWayland (Electron's default on Wayland sessions) the window had no
icon, so KDE Plasma showed the generic placeholder. Point BrowserWindow at
the packaged icon.png so the compositor has an icon to render.
When the app is hidden in the tray, closing then relaunching it triggered
the single-instance second-instance handler, which only called focus() — a
hidden window stays hidden on focus(), so the app appeared not to start
(notably on KDE Plasma Wayland where the tray icon may also be unreachable).
Call show() to surface it, and recreate the window if it no longer exists.
The desktop app ignored termination signals because the tray and embedded
express server keep the Electron event loop alive, forcing users to kill -9
on logout/shutdown. Add SIGINT/SIGTERM handlers that set isQuitting before
app.quit() so the hide-to-tray close handler doesn't swallow the quit.
node-gyp's undici 6.26.0 -> 6.27.0 and @electron/get's 7.27.2 -> 7.28.0,
each pinned within its major via overrides so only the security patch is
taken (an open >= range would jump across majors, e.g. to undici 8).
Resolves the 9 open desktop undici Dependabot alerts.
Newer devenv generates the devcontainer config at .devcontainer/devcontainer.json
instead of the legacy root-level .devcontainer.json, which left an untracked
duplicate. Track the new path so regeneration is a no-op.
Marking a task done via the list-view checkbox deferred the entire update
— including the network request — by 300ms to play the check animation. If
the page was torn down within that window (a refresh, tab close, or leaving
the app), the request was never sent and the save was silently lost. For
repeating tasks this is especially confusing: the due date never advances,
yet the checkbox un-checks itself anyway, so the failure looks identical to
success.
Fire the request immediately with the intended done value snapshotted, and
defer only the animation-coupled follow-up (result swap, pop sound, toast).
The optimistic v-model state already drives the check animation during the
300ms, so nothing visual is lost.
The v1 update was a whole-object POST /tasks/{id}: omitted scalars were
zeroed, so a status-only `veans update` silently wiped a task's
description and priority. The v1->v2 migration replaced that with
PATCH /tasks/{id} carrying a JSON Merge Patch built from only the
changed fields (client.TaskPatch, all-pointer + omitempty), which fixes
this by construction — absent fields are left untouched server-side.
Pin it with the acceptance tests from the issue: a title-only and a
status-only update must send only the field(s) they change, so the
stored description and priority survive.
veans is unreleased and targets bleeding-edge Vikunja, so the CLI now
speaks the Huma-backed /api/v2 exclusively (v1 is frozen and the kanban
bucket CRUD veans relies on only exists on v2).
- Transport: base path /api/v1 -> /api/v2 in Do/DoRaw; add a
content-type-aware path (DoMerge for application/merge-patch+json).
- Pagination: drop the x-pagination-total-pages header reader; every v2
list returns the {items,total,page,per_page,total_pages} envelope.
Decode it with a generic Paginated[T]/doList[T] and page until
page >= total_pages. Previously-single-GET lists (views, buckets,
comments, bots) are enveloped too — unwrap .items.
- Verbs: creates flip PUT -> POST (projects, labels, tokens, bot users,
shares, task create, comments, relations, assignees, label-attach,
bucket create); the bucket-task move flips POST -> PUT with a bare
{"task_id":N} body (URL owns project/view/bucket); task update moves
to PATCH merge-patch with a partial body.
- Errors: parse the RFC 9457 problem+json body (detail/title/code)
instead of v1's {code,message}; the status -> output.Code mapping is
unchanged.
- Discovery probes /api/v2/info, which doubles as the "new enough" check.
- Label search param s -> q; add views_buckets_tasks_put to the bot's
projects scope so the move is authorized regardless of route-init order.
Tests and the veans agent guide are updated for the new paths, verbs and
envelope. Verified end-to-end against a local v2 server: init, create,
show, list, claim, update and prime all work.
Port the standalone bucket list/create/update/delete from v1 to the
Huma-backed /api/v2, under /projects/{project}/views/{view}/buckets,
using v2 verb conventions (POST creates, PUT updates). The handlers
reuse the generic handler.Do* functions, so permissions are enforced
by the Bucket model's existing Can* methods.
Mirrors v1: no read-one route (the model has no ReadOne/CanRead), so
AutoPatch synthesises no PATCH. No model changes.
The tasks.read_all special case in CanDoAPIRoute only covered v1 paths.
Both GET /api/v2/tasks and GET /api/v2/projects/:project/tasks normalize
to the same tasks.read_all map key, but only one RouteDetail survives —
the project-scoped path overwrites the global one. The exact path
comparison then rejects the global endpoint with 401.
Extend the special case to include the v2 paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review point (#2950, comment 3444116036): when the surrounding task
<dialog> closed while the link prompt was open, the prompt was orphaned
and cleanup() never ran, leaking listeners and an unresolved promise.
Treat the prompt as a sub-modal of the task dialog: pressing Escape while
it is open now preventDefault()/stopPropagation()s the keydown so the
native modal <dialog> does not close on Escape, resolves the prompt with
'' (cancel) and runs cleanup() — only the prompt is dismissed, the task
dialog stays open. A one-shot 'cancel' listener on the enclosing dialog
backs this up in case the keydown handling is insufficient in some browser.
Tighten cleanup() so the prompt fully tears down regardless of how it
closes (Enter / Escape / click-outside): it now removes the scroll
listener, the document click listener and the dialog cancel listener, and
removes the element. handleClickOutside was hoisted so cleanup() can
remove it, closing the leaked-listener gap directly.
Adds an e2e asserting Escape cancels the prompt while the task dialog
stays open; the existing 'Enter creates the link' case still passes.
The Kanban task popup renders the description editor inside a native
<dialog> opened via showModal(), which lives in the browser's top-layer.
inputPrompt appended its URL <input> to document.body, so it was painted
behind the top-layer dialog (z-index cannot beat the top-layer) and could
not be focused through the dialog's focus trap. As a result clicking "Link"
in the popup did nothing, while it worked on the full task page (no modal).
Thread the TipTap editor through inputPrompt and append the prompt to
getPopupContainer(editor) — the open dialog ancestor when present, falling
back to document.body otherwise, so non-modal usage is unchanged. This is
the same helper the slash menu and mentions already use to escape the
top-layer (#1746).
Fixes#2940
The Kanban task detail opens as a native <dialog> via showModal(), which
paints in the browser top-layer. Floating UI appended to document.body
(or teleported to <body>) then renders behind the dialog regardless of
z-index, matching the bug class of #2940 / #1746 / #1899 / #1929.
- Emoji autocomplete popup: append to getPopupContainer(editor) (the open
dialog ancestor, else body), the same helper the slash menu and mentions
already use. Also switch its unmount to popupElement.remove() so it works
no matter which container it was appended to.
- Attachment dropzone overlay: teleport into the topmost open
dialog.modal-dialog instead of always <body>, mirroring Notification.vue,
so the drag-and-drop hint is visible while a task detail dialog is open.