Skip to content

feat(ui): rebuild the admin dashboard around the instance - #7302

Merged
gustavosbarreto merged 1 commit into
masterfrom
feat/admin-dashboard
Oct 2, 2026
Merged

gustavosbarreto merged 1 commit into
masterfrom
feat/admin-dashboard

Conversation

@gustavosbarreto

@gustavosbarreto gustavosbarreto commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Summary

The admin dashboard showed six counters and a recent-sessions table, which repeat what a namespace already shows. It becomes Instance, and its content depends on the edition.

<AdminInstance>                   pages/admin/Instance.tsx
  Enterprise
    <NeedsAttention>              member requests (Approve/Reject inline),
                                  license expiring, device limit near or reached
    <LicensePanel>                accepted devices vs limit, expiry, issued to
    <AccessPanel>                 users, namespaces, instance admins, sign-in methods
    <ServerPanel>                 version, SSH and API endpoints
  Cloud
    <InstanceNumbers>             users, namespaces, devices, online now, active sessions
    <AccessPanel> <ServerPanel>

On Enterprise, a namespace owner who is not an instance admin can invite someone, and that account waits for an instance admin's approval. The dashboard lists these requests together with the namespace each person is joining. Cloud has open signup and no approval step, so it shows instance figures and no list of people.

Admin users list:

+ tabs: All | Awaiting approval (Enterprise) | Admins | Unconfirmed, in ?subset=
+ search on All matches name, username or email; on a tab, username only
- "Reject account" opened the "Delete user" dialog
+ ApproveAccountDialog / RejectAccountDialog, shared with the dashboard

Also:

  • deviceCapacity in utils/license now holds the 90% / limit rule, and DeviceLimitBanner uses it too.
  • useServerInfo reads /api/info, and useSshEndpoint now goes through it.
  • Approving, rejecting and creating a user now refresh the counts that depend on them.

Everything uses endpoints that already exist; the backend is unchanged.

Evidence

  • Before: six counters (registered users, registered, online, pending and rejected devices, active sessions) and a recent-sessions table.
    After: Enterprise shows pending members with "joining acme" / "joining dev", a License panel (accepted devices, Expires, Issued to) and Access/Server. Cloud shows the numbers strip with Access/Server.
  • Instance.test.tsx replaces the old dashboard test and covers both editions: member requests with their namespace, approve and reject hitting the right endpoints, the empty state waiting for the license, the grace-period and device-limit notices, unlimited and non-expiring licenses, the Access counts and links, server info, a failed request leaving the rest of the page, and the Cloud figures with no people or license.
  • AdminUsers.test.tsx: the filter sent for each ?subset=, a subset combined with the search under and, awaiting approval ignored outside Enterprise, and Reject on a pending account opening the reject dialog.
  • Console suite: 2675 passed (240 files).

Merge Danger

Door: two-way

UI only. Reverting the PR restores the old page.

Blast Radius: admin console

  • The admin nav item is renamed "Dashboard" → "Instance", and the route moves from /admin/dashboard to /admin/instance with no redirect. A workspace tab or bookmark saved on the old path lands on a missing route.
  • To find which namespace each pending member is joining, the dashboard requests the details of up to 10 namespaces, one call per namespace. The admin namespace list returns members empty, so there is no single call for this. A member added to a namespace beyond those 10 is listed without "joining …".
  • The users list search changed scope (name/username/email on All).

@gustavosbarreto
gustavosbarreto requested a review from a team as a code owner October 2, 2026 14:57
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 5m 57s —— View job


Code Review Complete

I reviewed 25 files and posted 3 inline issues: 1 Standards violation, 1 Standards judgement call and 1 Correctness defect. The Spec axis did not run (see below).

Standards

pages/admin/__tests__/Instance.test.tsx:20-34 builds its own mockAdminUser object literal. react-conventions puts shared test factories in src/tests/factories.ts.

Correctness

In pages/admin/instance/NeedsAttention.tsx:187-191, settled does not wait for the stats query that capacity comes from. If stats are still loading or the request failed, an instance at its device limit shows "Nothing needs your attention."

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

  • Validation dropped one candidate below the confidence bar: bare userEvent.click without userEvent.setup() in the new tests. It breaks a react-conventions testing rule, but the console already has 113 such calls.
  • Validation also dropped a judgement call that ApproveAccountDialog/RejectAccountDialog duplicate each other. They follow the same pattern as DeleteUserDialog and DeleteNamespaceDialog.
  • No cap dropped any findings.

To request another review round, comment /review.

Comment thread ui/apps/console/src/pages/admin/instance/NeedsAttention.tsx Outdated
Comment thread ui/apps/console/src/pages/admin/__tests__/Instance.test.tsx Outdated
Comment thread ui/apps/console/src/pages/admin/Instance.tsx Outdated
@gustavosbarreto
gustavosbarreto force-pushed the feat/admin-dashboard branch 2 times, most recently from 62cfb30 to d5138bb Compare October 2, 2026 16:58
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 8m 53s —— View job


Code Review Complete

I reviewed 35 files and posted 3 inline issues: no Standards violations, 2 Standards judgement calls and 1 Correctness defect. The Spec axis did not run.

Standards

The worst one is Duplicated Code. pages/admin/users/index.tsx:234-256 is the third near-identical copy of the segmented filter tablist (the others are in containers and ssh-identities). InstanceNumbers.tsx's Metric and Panel.tsx's Count already disagree on placeholders and number formatting.

Correctness

hooks/useAdminUserMutations.ts:37-42: useDeleteUser doesn't invalidate getNamespacesAdmin, but the server deletes the namespaces the user owns. For up to 5 minutes (the staleTime), the Instance Namespaces count and the namespace list still include the deleted namespaces.

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

  • All three earlier findings are fixed in the current head. The settled fix now waits on both the license and stats requests, through useDeviceCapacity.
  • Validation dropped one candidate below the confidence bar. Instance.test.tsx writes its admin stats and /info responses as inline literals; react-conventions wants a factory in src/tests/factories.ts. Other console tests, FirstRunGate, SetupGuard and GeneralSettings, already inline the same shapes.
  • /admin/dashboard now has no route. The PR description already calls this out, and the old path also persists in the workspaceTabs localStorage entry. A single <Route path="/admin/dashboard" element={<Navigate to="/admin/instance" replace />} /> would cover both stored tabs and bookmarks.
  • No cap dropped any findings.

To request another review round, comment /review.

Comment thread ui/apps/console/src/hooks/useAdminUserMutations.ts
Comment thread ui/apps/console/src/pages/admin/users/index.tsx Outdated
Comment thread ui/apps/console/src/pages/admin/instance/InstanceNumbers.tsx Outdated
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 5m 36s —— View job


Code Review Complete

I reviewed 36 files and posted 4 inline issues: 3 Standards judgement calls (no violations) and 1 Correctness defect. The Spec axis did not run.

Standards

The most significant judgement call is Divergent Change. pages/admin/users/index.tsx:244-253 writes out its own copy of the search-scope rule, which also lives in buildUsersFilter (hooks/useAdminUsers.ts:46-65). The other two:

  • No test covers the new three-field search on the All tab.
  • Intl.NumberFormat is declared in both InstanceNumbers.tsx and Panel.tsx.

Correctness

pages/admin/instance/AccessPanel.tsx:27-29 shows "Sign-in methods: None" while /api/info is loading and after it fails. useServerInfo returns an empty list in both cases and doesn't expose isLoading.

Spec

The Spec axis did not run: the PR references no issue to review against.

Additional notes

  • Every finding from earlier rounds is fixed in the current head, and none was raised again.
  • Validation dropped one possible hard violation at 75 confidence, below the 80 bar: the doc comment /** One choice in a FilterTabs row. */ on FilterTab<T> (components/common/FilterTabs.tsx). It comes close to restating the declaration. It could state the contract instead, for example that value must be unique within a row because it is the key and the selection identity.
  • Two more judgement calls were dropped on purpose:
    • No direct unit test for deviceCapacity. Its boundaries are already covered through the DeviceLimitBanner and Instance tests.
    • DeviceCapacity["state"] is mapped to a tone in three places.
  • I checked the backend side of the user filter: the field names, the bool/eq/contains operators, the not-confirmed value, how and is applied, and the 8-item limit. I also checked the invalidation operation IDs, expires_at in seconds with ≤ 0 meaning never expires, and a negative device limit meaning unlimited. All of these match the server.
  • No cap dropped any findings.

To request another review round, comment /review.

Comment thread ui/apps/console/src/pages/admin/instance/AccessPanel.tsx
Comment thread ui/apps/console/src/pages/admin/users/index.tsx Outdated
Comment thread ui/apps/console/src/pages/admin/users/__tests__/AdminUsers.test.tsx
Comment thread ui/apps/console/src/pages/admin/instance/InstanceNumbers.tsx Outdated
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 10m 17s —— View job


Code Review Complete

I reviewed 37 files and posted 2 inline issues, both Standards judgement calls. There are no Standards violations and no Correctness defects. The Spec axis did not run.

Standards

The most significant finding is Duplicated Code. pages/admin/users/__tests__/AdminUsers.test.tsx:123-126 and pages/admin/__tests__/Instance.test.tsx:38-44 each decode the request filter by hand with JSON.parse(atob(raw)), when the existing src/tests/decodeB64url helper already does this. Raw atob doesn't undo the base64url substitution that toBase64Json applies, so a future filter that encodes to a - or _ would make these tests throw.

The other judgement call: instance/AccessPanel.tsx:28-34 copies Count's —/… placeholder rule inline.

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

  • Every finding from earlier rounds is fixed in the current head, and none was raised again.
  • Validation dropped three candidates:
    • Tautology in a test (confidence 72, below the 80 bar): the "searches name, username and email" test in AdminUsers.test.tsx:156-159 builds its expected value with .map() instead of writing a literal. It imports no implementation logic, and the last round suggested this exact form.
    • Unconfirmed tab also listing awaiting-approval accounts: not confirmed. server/api/services/register.go:122-126 sets status=confirmed before it sets awaiting_approval=true, so the two never overlap. One inconsistency stands outside this PR: migrations/010_users_awaiting_approval.tx.up.sql still describes these accounts as not-confirmed.
    • deviceCapacity doc comment restating its signature: not confirmed. It points to the contract on DeviceCapacity, which is stated in full.
  • I checked these against the backend and found them correct: the filter fields and operators, the invalidation operation IDs, the ?subset= fallback outside Enterprise, the page reset on a tab change, the expires_at "Never" rule and the device-limit boundaries.
  • No cap dropped any findings.

To request another review round, comment /review.

Comment thread ui/apps/console/src/pages/admin/users/__tests__/AdminUsers.test.tsx
Comment thread ui/apps/console/src/pages/admin/instance/AccessPanel.tsx Outdated
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@gustavosbarreto
gustavosbarreto force-pushed the feat/admin-dashboard branch 2 times, most recently from a7ca565 to 49d0730 Compare October 2, 2026 19:26
Comment thread ui/apps/console/src/pages/admin/instance/LicensePanel.tsx Outdated
Comment thread ui/apps/console/src/pages/admin/__tests__/Instance.test.tsx
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 5m 48s —— View job


Code Review Complete

I reviewed 38 files and posted 3 inline issues:

  • Standards: 1 hard violation, 2 judgement calls.
  • Correctness: no defects.
  • Spec: did not run.

Standards

The violation is in hooks/useAdminNamespaces.ts:80-84. The doc comment on useAwaitingMemberNamespaces explains why the hook needs the workaround: "The namespace list does not carry members, so…". code-style.md puts that kind of reason in the commit message, and the commit message already has it. The judgement calls:

  • FilterTabs.tsx:16 explains why arrow keys select a tab immediately ("since a filter has nothing to preview"). That reason isn't in the commit message.
  • The test for the reject dialog on a row is filed under describe("subset tabs") in AdminUsers.test.tsx:305. The file already has a describe("user rows") where it belongs.

Spec

The Spec axis did not run because the PR doesn't reference an issue to review against.

Additional notes

  • Every finding from earlier rounds is fixed in the current head, and none was raised again.
  • Validation dropped two candidates that earlier rounds had already settled:
    • The Panel doc's "fixed height so titles line up" clause.
    • The lack of a direct unit test for deviceCapacity. One new point came up: its documented limit = 0 case and the "ok" side of the 90% threshold are not tested anywhere, including through the page tests.
  • The Correctness pass found nothing new. It checked:
    • LicensePanel progress bar: aria-valuenow is clamped and aria-valuetext gives the real count. A limit of 0 shows no bar and doesn't divide by zero.
    • /stats: the UI reads the fields the CE and Cloud handlers return, and registered_devices/online_devices count accepted devices only.
    • /info: useServerInfo reads the fields the CE and Cloud handlers return.
    • Namespace members: awaiting_approval exists on the member response.
    • Admin nav: /admin/instance and /admin/instance-api-keys don't collide.
    • No licence installed: NeedsAttention behaves correctly.
  • No cap dropped any findings.

To request another review round, comment /review.

Comment thread ui/apps/console/src/hooks/useAdminNamespaces.ts
Comment thread ui/apps/console/src/components/common/FilterTabs.tsx Outdated
Comment thread ui/apps/console/src/pages/admin/users/__tests__/AdminUsers.test.tsx Outdated
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 2m 44s —— View job


Code Review Complete

I reviewed 39 files and posted 2 inline issues:

  • Standards: no violations, 1 judgement call.
  • Correctness: 1 defect.
  • Spec: did not run (see below).

Standards

The judgement call is in pages/admin/users/__tests__/AdminUsers.test.tsx:112-115, which also applies at 214 and 315. The file writes the edition mock out inline three times. Instance.test.tsx, in the same PR, wraps that same step in a setEdition helper.

Correctness

pages/admin/instance/NeedsAttention.tsx:52-57 handles "at the limit" and "over the limit" with one message: "New devices can't be accepted." When accepted devices exceed the limit, firewall.go:45 refuses every connection to every device, but the notice only says new accepts are blocked.

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

  • Every finding from earlier rounds is fixed in the current head, and none was raised again. The commit message now carries every reason the comment ban moved out of the code.
  • Present before this branch, now used in more places: useDeviceCapacity.ts:19 reads license.data?.features.devices. When the stored license fails to parse, GET /admin/api/license answers 200 {} (cloud/internal/admin/routes/license.go:28), so features is undefined and .devices throws. The old DeviceLimitBanner had the same read. license.installedLicense?.features.devices would avoid the crash. The branch did not introduce this, so it isn't inline.
  • Validation dropped these candidates:
    • ApproveAccountDialog/RejectAccountDialog duplicating each other. Round one already settled this: they follow DeleteUserDialog.
    • Edition gating of awaiting_approval spread across several isEnterprise() checks (Repeated Switches). No defect sits behind it, and a maintainer would be unlikely to change it.
  • No cap dropped any findings.

To request another review round, comment /review.

Comment thread ui/apps/console/src/pages/admin/instance/NeedsAttention.tsx
Comment thread ui/apps/console/src/pages/admin/users/__tests__/AdminUsers.test.tsx Outdated
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 8m 55s —— View job


Code Review Complete

I reviewed 40 files and posted 2 inline issues:

  • Standards: no violations, 1 judgement call.
  • Correctness: 1 defect.
  • Spec: did not run (see below).

Standards

The judgement call is in pages/admin/__tests__/Instance.test.tsx:320-326. The device-limit rows set only the limit. The accepted count that decides each case (9) comes from the file-level stats fixture about 300 lines up, so the value the test asserts on is out of view.

Correctness

pages/admin/instance/NeedsAttention.tsx:192-197: if a license's starts_at is in the future, the panel says "Nothing needs your attention." But CanAcceptDevice (cloud/internal/license/evaluator.go:37) refuses every accept until the start date, and nothing in the UI reads starts_at.

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

  • Every finding from earlier rounds is fixed in the current head, and none was raised again.
  • Not posted inline: both Standards agents suggested making "past the limit" its own DeviceCapacity state rather than splitting "over" again inside capacityNotice. The last round already offered that as an option, so it is not raised again. One related point stays open: DeviceLimitBanner still tells the past-the-limit case "New devices can't connect", while Instance now says connections are refused. A dedicated state would let both say the same thing.
  • Validation dropped these candidates:
    • A limit of 0 renders "All 0 licensed devices are in use." (confidence 75, below the 80 bar). The page does reach this branch. Nothing in the repo shows a real license is ever issued with devices: 0.
    • The grace-period text says the instance still works, but the evaluator refuses connections whenever IsExpired() is true, grace period included. The string predates this branch and the License page already shows it. LicenseGuard also redirects before Instance mounts, as the commit message explains.
    • The warns about %s table could use the object form. This is folded into the judgement call above.
  • These came back clean on the Correctness pass:
    • the capacity boundaries against CanAcceptDevice/CanConnectDevice (at the limit, past it, unlimited, limit 0);
    • registered_devices and the evaluator counting devices with the same query;
    • the 90% threshold not dividing by zero;
    • the DeviceLimitBanner refactor;
    • the LicensePanel clamping;
    • useDeviceCapacity's failure handling and hook rules.
  • No cap dropped any findings.

To request another review round, comment /review.

Comment thread ui/apps/console/src/pages/admin/instance/NeedsAttention.tsx Outdated
Comment thread ui/apps/console/src/pages/admin/__tests__/Instance.test.tsx Outdated
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 2m 22s —— View job


Code Review Complete

I reviewed 40 files and posted 1 inline issue:

  • Standards: no violations, 1 judgement call.
  • Correctness: no defects.
  • Spec: did not run (see below).

Standards

The judgement call is in pages/admin/__tests__/Instance.test.tsx:324-327. The not-yet-started row matches its notice with /^This license starts on .+\./. That pattern would also accept the "Now" and "Never" sentinels, or the wrong timestamp field. The suggestion pins the date at noon UTC in mid-month, so the row can assert the exact string.

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

  • Every finding from earlier rounds is fixed in the current head, and none was raised again.
  • Correctness checks on the new start-date notice that came back clean:
    • starts_at is in Unix seconds (cloud/pkg/lic/license.go).
    • 0 and -1 both count as already started, and the UI shows no notice for either.
    • The notice says accepts are refused but connections are not, which matches the backend: CanAcceptDevice checks IsStarted() and CanConnectDevice doesn't.
    • LicenseGuard lets a not-yet-started license through to the page.
    • The notice counts toward count, so "Nothing needs your attention." can't show alongside it.
    • The notice keys don't collide.
    • The factory's default starts_at is in the past, so the other rows stay deterministic.
  • Validation dropped these candidates:
    • Date.now() read during render in startNotice. InstanceApiKeys.tsx:29 already does the same, and the only effect is that the notice can stay up for one extra render after the start date.
    • Moving the start-date rule into utils/license.ts. It is a single comparison, and nothing has gone wrong because of where it lives.
  • No cap dropped any findings.

To request another review round, comment /review.

Comment thread ui/apps/console/src/pages/admin/__tests__/Instance.test.tsx Outdated
The six counters and the recent sessions repeated what a namespace
already shows. The page becomes Instance and differs by edition.

Enterprise leads with what waits on the instance admin: members a
non-admin namespace owner added, who stay awaiting approval until an
instance admin approves them, and a license that is expiring or out of
devices. Cloud has open signup and no approval step, so it shows the
instance's figures and no list of people.

The admin namespace list returns members empty, so finding the
namespace a pending member is joining takes one detail request per
namespace. The scan stops at 10 namespaces; a member of a namespace
past that is listed without "joining".

The user filter grammar holds a single operator for all conditions, so
it cannot express (name OR username OR email) AND subset. Search on the
All tab matches the three fields; under a tab it falls back to username.

expires_at <= 0 is a license that never expires on the server, shown as
Never rather than the date or "Now".

The Cloud device and session figures link nowhere: the admin pages for
them were removed.

LicenseGuard already sends an expired license, grace period included,
and a failed license fetch to the license page. The grace-period notice
and the failed-license line on Instance only show when a background
refetch turns stale data into that state while the page is open.

A figure checks failure before loading because a failed list request
still reports a zero total, which would otherwise read as the answer.

FilterTabs selects a tab as soon as the arrow keys reach it (automatic
activation): a filter has nothing to preview before it applies.

Past the device limit the cloud firewall refuses every connection, not
just new accepts, so that case gets its own notice.

A license whose starts_at is in the future refuses every device accept
until then, so it gets a notice of its own.
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 2m 8s —— View job


Code Review Complete

I reviewed 40 files on Standards (repo rules, React conventions, tests, code smells) and on Correctness (security and runtime defects). I found no new issues. The Spec axis did not run because the PR references no issue to review against.


@gustavosbarreto
gustavosbarreto enabled auto-merge (rebase) October 2, 2026 22:10
@gustavosbarreto
gustavosbarreto merged commit 8e131d3 into master Oct 2, 2026
32 checks passed
@gustavosbarreto
gustavosbarreto deleted the feat/admin-dashboard branch October 2, 2026 22:50
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.

1 participant