Skip to content

feat(rest): admin dashboard improvements (revert overview caches + bulk groups helper) - #42

Closed
aschumann-virtualcable wants to merge 21 commits into
masterfrom
dashboards-improves-rest-admin
Closed

feat(rest): admin dashboard improvements (revert overview caches + bulk groups helper)#42
aschumann-virtualcable wants to merge 21 commits into
masterfrom
dashboards-improves-rest-admin

Conversation

@aschumann-virtualcable

@aschumann-virtualcable aschumann-virtualcable commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What

Two orthogonal changes — both are admin-side only and both are required for the
dashboard to behave correctly:

  1. Source (src/uds/REST/methods/users_groups.py)
    • Drop the two REST-level caches introduced for the admin dashboard
      (_overview_cache plus its call sites in users_groups.py / services_pools.py).
    • Extract the previously inline bulk-groups computation into a dedicated
      bulk_groups_of_users() helper that prefetches the user groups in a single
      prefetch_related, regardless of N users.
  2. Admin bundle (src/uds/static/admin/*, src/uds/templates/uds/admin/index.html)
    • Rebuilt from VirtualCable/uds-admin-gui#integration-dashboard-etag at
      869207c. This is the same build the new admin dashboard PR will produce
      once merged, so deploying the bundle is a no-op transition.
    • Brings: dashboard drill-downs, ETag-412 friendly handling, create-via-POST,
      fallback_access unificado, license badge bajo @if (hasLicense), scroll-to-top,
      sidebar auto-expanding the section owning the active route.

The PR is self-contained: it reconciles integration/dashboard-rest-admin (source
revert + bulk groups) with a freshly rebuilt bundle from the matching admin branch.
Both old branches are obsolete and were dropped from the remote before this PR
was opened.

Why

Caches

The dashboard used to trigger two layers of caching on the admin REST endpoints:

  • _overview_cache: a 60s cached_overview decorator applied to authenticator
    user/group overviews and the pool user-service / restrained-pool overviews
    (commits 14cf720ba, 01b431a3b).
  • A query-level "summarise" cache on the service-pools overview (commit
    ca9648681, reverted here in f76fbd381).

The whole point of the admin is that an edit is visible immediately: a
change in one tab has to show up in the other tab the next time the second
admin refreshes, and a 60s cache broke that in the most user-visible place —
the dashboard. There is also no measurable benefit to cache: the pools
overview runs in ~120 ms on the demo dataset (300k pools) and ~90 ms locally
on 36, both well below the threshold where in-memory caching is justified.
The dashboard was caching the wrong thing on the wrong path.

Reverts in this PR:

  • f76fbd381 revert(rest): drop the admin overview caches — drops
    _overview_cache.py, the decorator, the import, and every call site
    (users_groups.py, services_pools.py, _overview_cache re-export).
  • ca9648681 perf(rest): drop the service pools overview cache, invalidate the rest on write — removes the query-level cache on the pools overview.

Bulk groups

The pre-cache Users.get_items was calling user.get_groups() per user, which
runs an extra round-trip per user on top of the user listing. That is O(N)
queries for an admin listing N users in an authenticator — at 1k users that is
~1k extra queries, ~6k for a 6-authenticator install.

The fix is to prefetch groups once and compute the resolved group list in
Python. The previous attempt (60bb42d35) inlined the logic in get_items(),
which made it untestable and hard to reason about. This PR extracts it as
bulk_groups_of_users():

  • One prefetch_related('groups') on the user queryset.
  • One prefetch_related('groups') on the parent user queryset (parents
    live in a different table).
  • One prefetch_related('groups') on the authenticator's metagroups.
  • Total: ~6 queries, independent of the number of users.

get_items now does the prefetch and calls the helper. as_user_item no
longer fills the groups field — it is filled in bulk after the items are
built, so get_item() (single user) still works the same way through
get_groups().

Bundle

The new admin SPA (dashboard drill-downs, 412 handling, create-POST, license
fix) is built and committed here. integration/dashboard-rest-admin already
shipped a stale bundle, and deploy-admin-bundle-bestof had a bundle that
predated the new fallback_access master work. This PR ships a bundle built
from the current admin branch (gui/admin 869207c) so the deployed
artifacts match the upcoming source merge by construction. Once the admin
PR is merged and the SPA is rebuilt, the next broker release will just
re-run the same build — no behavioural drift possible.

Tests

pytest21/21 pass, including two new ones:

  • tests/REST/methods/users_groups/test_users.py::UsersTest::test_users_overview_groups
    — for every user in the authenticator overview, the bulk-computed
    groups field equals set(user.get_groups()). Regression for the
    extracted helper.
  • tests/REST/methods/services_pools/test_services_pools.py::ServicePoolTest::test_overview_reflects_edit_immediately
    — list, edit a pool, list again, the new comments value is on the row.
    Regression for the overview cache removal: previously the second list
    returned the cached pre-edit payload.

Pairs with

  • VirtualCable/uds-admin-gui#integration-dashboard-etag — the source of the
    bundle in this PR. Must be merged first so subsequent bundle rebuilds
    match the new admin source.

aschumann-virtualcable and others added 21 commits July 20, 2026 15:51
The groups field was removed from the users overview because
User.get_groups() costs O(N) per-user queries (direct groups plus a
metagroup scan with two Count annotations each). External API consumers
may rely on it, so restore it computed in bulk: prefetch direct groups,
fetch parent users and metagroups once, and evaluate metagroup
membership in memory with the same semantics as get_groups() (parent
fallback, empty metagroup matches everyone, meta_if_any).

Cost is now a fixed ~6 queries regardless of user count. Detail
endpoint (get_item) is unchanged.

Adds test_users_overview_groups asserting overview groups match
per-user get_groups() output. Also includes rebuilt admin static
artifacts from the gui/admin dashboard branch.
The dashboard drill-down fans out to every authenticator at once for
users, groups and users-with-services, which change seldom yet dominate
load time. Memoize the assembled overview lists for a short TTL; flush=1
bypasses. cached_overview is generic and typed via a small Protocol so it
preserves the item type without leaking Any.
Rebuilt admin static bundle carrying the <html> theme backstop fix
(uds-admin-gui master-fix-admin-white-flash).
Extend the short-lived overview cache (previously only for authenticator
user/group lists) to the remaining slow dashboard drill-down tables:

- AssignedUserService.get_items and CachedService.get_items (per-pool,
  distinct key prefixes so the shared parent pool does not collide)
- ServicesPools.get_items (restrained pools), keyed on sumarize + odata

Move cached_overview into its own _overview_cache module so user_services
can share it without the users_groups <-> user_services import cycle;
users_groups re-exports it for existing callers.

Keys include parent.uuid and the odata filter; TTL is SHORT_CACHE_TIMEOUT
(60s) and flush=1 bypasses, matching the existing behavior.
# Conflicts:
#	src/uds/REST/methods/services.py
#	src/uds/REST/model/detail/__init__.py
#	src/uds/REST/model/master/__init__.py
#	tests/REST/_meta/test_crud_smoke.py
#	tests/REST/_meta/test_custom_methods.py
Extracting the query into _build_items left a kwargs reference behind,
breaking service pools listing with NameError: name 'kwargs' is not defined.
…t on write

The pool overview cache served stale data after an edit: it had a 60s TTL
and nothing cleared it on write, so a user saving a pool kept seeing the
old listing. Service pools go back to querying directly.

The remaining overviews (users, groups) are cleared on User/Group writes.
UserService is left out on purpose: its churn would keep the cache empty.
The merge into this branch resolved several files in favour of the stale
branch side, undoing master's ruff reformat (a230d8b) and regenerating
doc/api from a tree that predated the POST /collection endpoints and the
camelCase aliases. tests/fixtures/mfas.py also lost its @typing.override
decorators that way.

None of it belongs to the dashboard work, so those files go back to master.
user_services.py only gained an unused cached_overview import, dropped too.
Admin listings cannot be cached: an administrator who saves a change and
goes back to the list must see it. A short TTL only delays the stale read,
and invalidating locally does not help a second admin with the same page
open.

Removes _overview_cache and its use in authenticators and users_groups.
The bulk group computation stays: it removes N+1 queries without serving
stale data.
- Added new translation strings to `translations-fakejs.js` for improved localization.
- Removed duplicate translation entries and ensured consistency in the translation file.
- Updated script references in `index.html` to the latest versions for better performance and security.
Aligns the admin static bundle with gui/admin integration-dashboard-etag:
412 ConflictError UX, create via POST, license badge, and the unified
fallback_access endpoint. Reconciles the dashboard/cache-revert source
branch with the best-of bundle in a single branch.
@github-actions
github-actions Bot requested a review from dkmstr July 27, 2026 12:14
@dkmstr dkmstr closed this Jul 27, 2026
@aschumann-virtualcable
aschumann-virtualcable deleted the dashboards-improves-rest-admin branch July 29, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants