Files
pad/internal/models
xarmian 0c5ec04fac feat(admin): extend user list with aggregations + sort/filter (TASK-1544) (#599)
* feat(admin): extend user list with aggregations + sort/filter (TASK-1544)

GET /admin/users now returns per-user workspace_count, storage_bytes,
last_write_at, and a computed status pill (disabled / no-workspace /
inactive / active, with documented precedence). Adds sort and filter
knobs so the table can scale beyond the existing fixed offset/limit.

Store layer:

- AdminUserSearchParams gains Role, Sort, Order, ActiveWithinDays,
  HasWorkspaces, Disabled. Pointer types where tri-state ("no filter"
  vs. "filter to false") matters.

- AdminUserListEntry wraps models.User with WorkspaceCount, StorageBytes,
  Status — returned in AdminUserSearchResult.Users.

- SearchUsers SQL rewritten: LEFT JOIN against grouped subqueries so
  one user owning N workspaces with M attachments each still produces
  exactly one row (no aggregation explosion). Both subqueries filter
  deleted_at IS NULL to match WorkspaceStorageUsage's existing
  definition. Allow-listed sort clause prevents injection.

- computeAdminUserStatus exported for unit tests; precedence locked in
  by TestComputeAdminUserStatus.

- TestSearchUsersAggregations covers workspace_count + storage_bytes +
  status across a three-user fixture and each new filter/sort knob.

Model + scanner:

- models.User gains LastWriteAt. userColumns + scanUser updated; the
  legacy callers (GetUser, ListUsers, etc.) inherit the new field for
  free via the shared scanner.

Handler:

- handleAdminListUsers accepts the new params: role, disabled,
  has_workspaces, active_within_days, sort, order. Tri-state bools
  only fire when the query param is present. Response now embeds
  workspace_count / storage_bytes / last_write_at / status.

Part of PLAN-1542. Frontend consumption lands in T1548 (cheap columns)
and T1549 (sort/filter UI).

* fix: address Codex review on TASK-1544

- Tri-state bool parsing in handler now uses strconv.ParseBool — accepts
  the canonical truthy/falsy variants ("True"/"TRUE"/"t"/"1" and the
  parallel falses), and silently ignores garbage values rather than
  treating them as false. Closes the "disabled=TRUE silently means
  enabled-only" surprise.

- SearchUsers count query no longer joins the storage aggregation when
  HasWorkspaces isn't an active filter. The page query still needs both
  joins (the row carries the data), but a typical "give me a count"
  call no longer scans every live attachment. The workspace_count join
  remains conditional on HasWorkspaces filtering.

Status threshold (>30d vs >=30d): the documented spec and impl both say
">30d" — no change.
2026-05-20 16:50:16 -04:00
..
2026-03-26 01:52:36 +00:00