- Replace tailwindcss v3 with tailwindcss v4 + @tailwindcss/vite plugin
- Create dedicated tailwind.css entry with granular imports (skip preflight)
- Use CSS-first config with prefix(tw) instead of JS config
- Switch from PostCSS plugin to Vite plugin for better performance
- Update class prefix from tw- (dash) to tw: (colon) in all Vue files
- Remove @tailwind directives from global.scss
- Delete tailwind.config.js (replaced by CSS directives)
- Update stylelint at-rules for v4 directives
Async event handlers (via Watermill) from the previous test can hold
SQLite connections, starving the next test's fixture setup PATCH request.
Three changes fix this:
1. Track in-flight event handler goroutines with a WaitGroup.
2. Call WaitForPendingHandlers() in the test endpoint before
truncating/inserting data.
3. Navigate the browser to about:blank in fixture teardown to stop
notification polling and other frontend requests between tests.
When multiple avatar components mount with alternating sizes (e.g. 48
and 20), invalidateAvatarCache clears pending requests for ALL sizes of
the same user, causing each call to create a new HTTP request instead of
reusing the pending one.
I noticed this issue when I open a task with 20 comments, the avatar
requests make the service OOM.
<img width="733" height="551" alt="image"
src="https://github.com/user-attachments/assets/db45db31-294c-4363-ad27-38d454b5a6a2"
/>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wraps all tables that were missing overflow handling in a
`has-horizontal-overflow` div to prevent horizontal overflow on mobile
viewports.
Affected components:
- Sessions.vue
- ApiTokens.vue
- ProjectSettingsWebhooks.vue
- LinkSharing.vue
- UserTeam.vue
- EditTeam.vue
- ProjectSettingsViews.vue
Fixes https://github.com/go-vikunja/vikunja/issues/2331
Add pnpm override to force all transitive rollup dependencies to use
the direct dependency version (4.59.0), eliminating the old 2.79.2
pulled in by workbox-build.
Make the "create new" option in multiselect dropdowns visually distinct
from regular search results by adding a green plus icon and making the
hint text always visible instead of only on hover.
word-break: break-all breaks text at any character, causing mid-word
breaks even when the word could fit on the next line. overflow-wrap:
break-word wraps at word boundaries first and only breaks mid-word
when a single word exceeds the container width.
Replace FormField with Password component for new password input:
- Provides real-time validation feedback (8-72 char requirement)
- Remove redundant password confirmation field
- Disable save button when form is invalid (validation errors or empty fields)
Replace innerHTML with DOM API calls in inputPrompt.ts. The oldValue
parameter (sourced from a link's href attribute in the TipTap editor)
was interpolated directly into an HTML string, allowing stored XSS if
an attacker crafted a malicious href. Using document.createElement and
setting .value as a property ensures the value is never parsed as HTML.
TipTap's setContent() parses strings as HTML via DOMParser, allowing
crafted ?filter= URL parameters to inject SVG phishing buttons, anchor
tags, and formatted content into the trusted UI.
Use ProseMirror JSON document format instead of raw strings so the
filter value is always set as a text node, bypassing HTML parsing
entirely.
- Verifies transparent retry and JWT rotation on 401 with code 11
- Verifies no retry for 401 with non-JWT error code
- Verifies current session appears on sessions settings page
- Increases rate limit for e2e test API to prevent 429 errors
- Session model, type interface, and API service
- Sessions settings page showing active sessions with device info,
IP address, last active time, and current session indicator
- Auth store updated to use cookie-based refresh tokens for user
sessions and JWT-based renewal for link shares
- refreshToken() uses Web Locks API to coordinate across browser
tabs — only one tab performs the refresh, others adopt the result
- 401 response interceptor with automatic retry: detects expired JWT
(error code 11), refreshes the token, and replays the request
- Interceptor gated to user JWTs only (link shares skip refresh)
- checkAuth() attempts cookie refresh when JWT is expired, allowing
seamless session resumption after short TTL expiry
- Proactive token refresh on page focus/visibility via composable
- renewToken() tolerates refresh failures when JWT is still valid
The loading screen was dismissed as soon as auth check completed, but
before Vue Router's initial navigation finished. This caused route.name
to be undefined momentarily, making showAuthLayout evaluate to false
and briefly flashing the NoAuthWrapper (login shell) before the
authenticated layout appeared.
Wait for router.isReady() before setting ready = true so the loading
screen stays visible until the route is fully resolved.
The DatepickerInline quick-select buttons (Tomorrow, etc.) inside the
reminder popup no longer auto-save. Update existing tests to click
Confirm after selecting a date.
Refs #2208
- Test that clicking a date in the absolute reminder picker does not
auto-save, only saves when Confirm is clicked
- Test that the Confirm button is visible when task has no due date
(defaultRelativeTo is null)
Refs #2208
Prevent reminders from being saved to the API until the user clicks
Confirm, matching the behavior of the due/start/end date pickers.
- Remove @update:modelValue handler on DatepickerInline so date/time
changes only update local state
- Change Confirm button condition from showFormSwitch to activeForm so
it renders even when defaultRelativeTo is null
- Add confirmAndClose function that handles both absolute and relative
reminder forms
- Remove debounce (useDebounceFn) since saves are now user-initiated
Refs #2208
Tasks whose start date is before and end date is after the visible
gantt range were not shown because the API filter only checked whether
individual date fields fell within the range. Add a fourth filter
clause to match tasks that fully encompass the visible date range.
Fixesgo-vikunja/vikunja#2269
Without explicit Cache-Control headers, browsers may heuristically cache
API JSON responses. This causes stale data to be served on normal page
refresh (F5) — for example, projects newly shared with a team not
appearing until the user performs a hard refresh (Ctrl+Shift+R).
Add Cache-Control: no-store to all API responses via middleware and
configure the service worker's NetworkOnly strategy to explicitly bypass
the browser HTTP cache for API requests.
Ref: https://community.vikunja.io/t/team-members-cannot-see-project/1876
logout() calls checkAuth() to clear the authenticated flag and user
info. But since checkAuth() likely ran within the last minute (on the
navigation that triggered logout), the now-working throttle would
return early, leaving authenticated=true in the store despite the
token being removed. This could cause Login.vue to redirect back
to home.
Resetting lastUserInfoRefresh to null before checkAuth() ensures
the logout flow always runs fully.
When the setUser call is skipped for an already-loaded user,
info.value.exp was never updated with the renewed token's expiry.
This caused useRenewTokenOnFocus to compute a stale expiresIn,
eventually forcing a logout even though the token was still valid.
Now we always update exp from the JWT, even when skipping the
full setUser call.
The JWT token only contains id, username, type, and exp — not the
display name. Previously, every route navigation called setUser() with
this incomplete data, causing the display name to flash to the username
before the API call completed and restored the full name.
Now we skip setUser() if a complete user object with the same ID is
already loaded.
Fixes https://github.com/go-vikunja/vikunja/issues/2270
The throttle checked `lastUserInfoRefresh > now + 1 minute` which is
never true since lastUserInfoRefresh is set to the current time.
Changed to `now - 1 minute` so the check actually prevents redundant
API calls within a one-minute window.