Commit Graph
16094 Commits
Author SHA1 Message Date
kolaente 64044dba06 feat(projects): show undone task counts in the sidebar
Opt-in via a new general setting, off by default.
2026-10-02 16:54:04 +02:00
kolaente bd9583d310 feat(projects): load project task counts and refresh them after task writes 2026-10-02 16:52:15 +02:00
kolaente 4b96d977c8 chore(client): regenerate for project task counts 2026-10-02 16:50:46 +02:00
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
kolaente 6cea424122 test(attachments): provide Pinia for date formatting 2026-10-02 09:48:17 +02:00
kolaente 9bcfb534f3 fix(datepicker): confirm typed times with Enter 2026-10-02 09:48:17 +02:00
kolaente 47c530d12d fix(dates): respect time format in date tooltips 2026-10-02 09:48:17 +02:00
kolaenteandTink 90129559d0 feat(account): save settings as a merge patch of the changed fields
Settings saves no longer re-read the account and PUT the merged result.
They PATCH only the edited fields, the server merges them, and the cache
gets the same edits applied. Demo mode drops the language in the form.
2026-10-02 09:47:32 +02:00
kolaenteandTink e23c45e753 chore(client): regenerate for the user settings read and patch 2026-10-02 09:47:32 +02:00
kolaenteandTink d851612413 feat(api): read the current user's general settings on v2
A GET next to the existing PUT lets AutoPatch expose PATCH
/user/settings/general, so clients can change single settings without
re-sending the rest.
2026-10-02 09:47:32 +02:00
kolaenteandTink a673881780 test(settings): cover a setting changed elsewhere while the form is open 2026-10-02 09:47:32 +02:00
kolaenteandTink e4e96103ce fix(settings): only save the settings that changed onto the server's current settings
PUT /user/settings/general replaces every setting, and the general settings form sent the snapshot it took on open, reverting settings changed elsewhere such as the sidebar width. The sidebar drag and frontend toggles had the same problem with this tab's cached account.

Both mutations now share one save: it checks the request identity, re-reads /user, overlays only the edited fields and PUTs. The form diffs its edits against what it last saved, ignores a submit while a save is in flight and keeps edits made during one.

Refs #4014
2026-10-02 09:47:32 +02:00
kolaenteandTink 74d987f831 fix(client): hand mutations their request context and never toast its aborts
Mutation functions now receive the request context the wrapper captured, so they can fence a multi-request mutation without capturing a second one. A request-context abort is internal: it never toasts, even while the context is still current, and skips the follow-up refetch.
2026-10-02 09:47:32 +02:00
Frederick [Bot] 76761de3e0 chore(i18n): update translations via Crowdin 2026-10-02 00:11:30 +00:00
kolaente 3530c751aa refactor(frontend): drop the repeated rate limit check around ApiConfig
Addresses pr-swarm finding (conventions).
2026-10-01 22:12:22 +02:00
kolaente e279ebc2dd fix(frontend): show a rate limit message when booting hits a 429
The boot error screen told the user to check the API URL even when the
first user request was only rate limited.

Refs #4014
2026-10-01 22:12:22 +02:00
Frederick [Bot] 12c1aaeedc [skip ci] Updated yaegi symbols 2026-10-01 15:59:52 +00:00
Frederick [Bot] 879215681a [skip ci] Updated swagger docs 2026-10-01 15:59:52 +00:00
kolaente 2e5543323b fix(auth): keep recoverable sessions after a failed refresh
checkAuth() latched the stored JWT as failed on any refresh error, so a
network error or 5xx left the user logged out until a reload, and a token
another tab stored meanwhile was never adopted.

- doRefresh reads the response itself and throws a typed
  RefreshTokenError (network, server, rate-limited, rejected); the store
  switches on its kind instead of guessing at the error's cause.
- Network errors and 5xx no longer latch the JWT; the next navigation
  refreshes again. A 5xx gets one immediate retry so a blip recovers
  inside the router guard and keeps the deep link.
- renewToken no longer logs out an expired session on a transient
  failure.
- A token another tab stores while our refresh is in flight is adopted
  in doRefresh, for every caller.
- A 429 with Retry-After holds all refreshes against that API server
  until it passes (capped at ten minutes), then a single scheduled retry
  restores the session and leaves the login page for the saved route.
  Without the header the old page-load latch stays.

Fixes #4052
2026-10-01 17:34:38 +02:00
kolaente ecfbd5b7ec fix(routes): send and expose Retry-After on rate-limited responses
Every 429 from the rate limiters now carries Retry-After so clients know
when the window ends. The basic-auth limiter keys on aligned buckets, so
its value comes from the bucket end. Retry-After isn't CORS-safelisted,
so the CORS config exposes it for the desktop app and frontends on
another origin.
2026-10-01 17:34:38 +02:00
kolaente 0bb6a34607 docs(api): declare web.HTTPError and all refresh token 401 codes on v1 refresh
The 401 body is a web.HTTPError with a code, not a models.Message, and
RefreshSession also answers 16002, 16003 and 16004, not only 16005.
2026-10-01 17:34:38 +02:00
kolaenteandTink e9f983cab0 refactor(queries): let comments and time entries use the server page size
pageSizeFor only floored the configured size at 1, and every caller passed
the server's own max_items_per_page. Drop it, stop sending per_page, key the
pages by page number only and take per_page from the response via
toPaginated.
2026-10-01 17:29:11 +02:00
kolaenteandTink 87a56436c9 refactor(queries): drop the stale v2 per_page maximum
277c97f68 replaced the 1000 schema maximum with a silent cap at
service.maxitemsperpage, so pageSizeFor only needs to keep per_page at
least 1.
2026-10-01 17:29:11 +02:00
kolaenteandTink c0a8f25ac4 refactor(queries): let full-list sweeps use the server page size
The v2 API caps per_page at service.maxitemsperpage (default 50) instead
of honouring 1000, and fetchAllPages already follows total_pages, so the
per_page these sweeps sent had no effect.
2026-10-01 17:29:11 +02:00
kolaenteandTink 51475b829c refactor(caldav-tokens): adopt shared pagination helpers for the token list
Nest list pages under a list key to match the sibling query modules, and
swap the inline envelope normalisation and delete cache rewrite for
toPaginated and removeFromPages.
2026-10-01 17:29:11 +02:00
kolaenteandTink fc7037519f refactor(caldav-tokens): paginate the settings token table
Binds the table to the route's ?page=, clamps the page back in range once
real data has loaded (not against placeholder data), and locks the delete
buttons while a page switch is loading. Renders <Pagination> once there is
more than one page. Stops sending per_page so the server default applies.
2026-10-01 17:29:11 +02:00
kolaenteandTink b7969bc4e2 refactor(caldav-tokens): page the token list query on the server
Replaces the fetchAllPages sweep with one page of 25 per query, keyed
by page. Deleting a token rewrites total and total_pages on every
cached page before the list prefix is invalidated.
2026-10-01 17:29:11 +02:00
kolaenteandTink 76fabd86a7 test(caldav-tokens): pin paged token list and delete across pages 2026-10-01 17:29:11 +02:00
kolaenteandTink 82a263cbe9 fix(caldav-tokens): honour page in the v2 token list
The handler returned every token on every page while reporting
total_pages, so paging through the list repeated the same rows.

Fixes #4001
2026-10-01 17:29:11 +02:00
kolaenteandTink 35344e3650 test(bots): cover opening an out-of-range bot page directly
Pin the clamp for ?page=99.
2026-10-01 17:29:11 +02:00
kolaenteandTink 1104f6d817 fix(bots): show a newly created bot on the page it lands on
With more than one page of bots, a new bot was appended to a page the user
wasn't on, with no feedback. Jump to the last page and name the bot in a
success message.
2026-10-01 17:29:11 +02:00
kolaenteandTink f823910655 fix(bots): lock the bot list while the previous page is shown
keepPreviousData left stale bot cards actionable during a page switch.
2026-10-01 17:29:11 +02:00
kolaenteandTink 832c710835 refactor(bots): paginate the bot settings page 2026-10-01 17:29:11 +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 1e2eaae22b refactor(bots): query the bot list one page at a time 2026-10-01 17:29:11 +02:00
kolaenteandTink ae5a7f4d8c test(bots): cover paging through the bot settings page end to end 2026-10-01 17:29:11 +02:00
kolaenteandTink 3df20db8f8 test(bots): pin the paged bot list and cross-page mutations 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 2e9d5db02f refactor(api-tokens): paginate the token settings and bot token lists 2026-10-01 17:29:11 +02:00
kolaenteandTink d847cda81a refactor(api-tokens): query token lists page by page 2026-10-01 17:29:11 +02:00
kolaenteandTink dc3fa209ae test(api-tokens): pin route-driven paging of the token settings page 2026-10-01 17:29:11 +02:00
kolaenteandTink 78829e35f0 test(api-tokens): pin paged token lists and delete 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 ee0dda91a5 feat(webhooks): paginate the webhook list 2026-10-01 17:29:11 +02:00
kolaenteandTink 346bfb16b1 refactor(webhooks): key the webhook list by page instead of fetching every page 2026-10-01 17:29:11 +02:00
kolaenteandTink d40bd9c2f2 test(webhooks): pin paged list requests and delete across pages 2026-10-01 17:29:11 +02:00
kolaenteandTink e763ed6325 fix(teams): clamp an out-of-range team list page
?page= beyond the last page showed an empty list with no way back.
2026-10-01 17:29:10 +02:00
kolaenteandTink 6bef60e7b8 test(teams): assert the two team pages cover every seeded team once
The pagination e2e case only checked row counts.
2026-10-01 17:29:10 +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