feat: Phase 5b — dashboard for tenants and platform operators - #7
Closed
TheMeinerLP wants to merge 13 commits into
Closed
feat: Phase 5b — dashboard for tenants and platform operators#7TheMeinerLP wants to merge 13 commits into
TheMeinerLP wants to merge 13 commits into
Conversation
… client Adds the ui/ module (Nuxt 4, ssr:false, Vue 3, Tailwind 4 via @nuxt/ui, VueUse, eslint-plugin-vuejs-accessibility) that the platform and tenant dashboards build on next. - Client-only OIDC (oidc-client-ts, Authorization Code + PKCE) with tokens kept in an in-memory store rather than localStorage/sessionStorage, since an XSS bug in the UI would otherwise leak bearer access to a tenant's data; silent renew covers the reload cost. - A typed client for every api module REST/SSE endpoint (tenants, sources, maps, renders, hostings, plus the two SSE progress/log streams), with request/response types mirrored field-for-field from the actual Java records and one ApusApiError shape for every failure mode the api module can produce. - UI-side role helpers mirroring ApusPrincipal/TenantAccess -- convenience for what to show, not enforcement; the api module remains the sole enforcement point. - A base layout/nav that shows the platform link only for platform-admin, and the one required page (signed-in user + tenant). - Vitest unit tests for the API client, role logic, SSE framing, and JWT decoding (47 tests). Not wired into the Gradle build (no build.gradle.kts, not in settings.gradle.kts) -- it's a self-contained pnpm project with no shared build inputs/outputs with the Java modules; see ui/README.md for the build/test commands and the full reasoning behind every decision above.
Adds the tenant-level view over the api module's REST/SSE surface (design spec §11.2): read-only source/hosting listings, a map list with a role-gated render trigger, and a render history with live percent/ETA progress and log tailing over SSE. Progress display treats an unknown percent/ETA (api sends -1 with degraded: true) as an honest "unknown" state rather than a fabricated 0% or invented duration, and every SSE stream closes itself on view-unmount or terminal render phase.
Adds the platform-admin dashboard (design spec §11.2): a tenant list showing observed storage usage against quota, a form to create tenants with their initial quota and allowed hosting domains, and a notice explaining why cluster-wide job visibility isn't available yet. Hidden from non-platform-admins as a UX convenience only -- the api module's 403 remains the actual enforcement point. Storage usage is presented strictly as an observation (Ceph enforces the quota, not Apus), and the allowed-domains help text spells out what the list actually gates, so a bare "*" isn't set by accident. Editing an existing tenant's quota or domains, and a cluster-wide render listing, both need endpoints the api module does not expose yet (only GET/POST /api/tenants exist, and GET /api/renders is scoped to the caller's own tenant) -- reported upstream rather than worked around here.
Nuxt registers components under nested directories with a directory prefix (app/components/layout/AppHeader.vue -> LayoutAppHeader), but layouts/default.vue referenced <AppHeader /> and AppHeader.vue itself referenced <AppNav /> -- both unprefixed. Neither vue-tsc nor nuxt build flags an unresolved component tag; Vue's template compiler treats it as a plain, empty custom element instead. The header (and therefore the nav inside it) silently failed to render at runtime. Add a Nuxt-environment Vitest project (vitest.nuxt.config.ts, tests/nuxt/) that actually mounts the default layout via @nuxt/test-utils' mountSuspended and asserts the header/nav content is present with no "Failed to resolve component" warning -- a real render, not a typecheck, so this class of bug fails a test run again if it recurs.
AppNav.vue only ever showed a "Platform" link (platform-admin). An account with a tenant role (tenant-owner/-operator/-viewer) had no way into /tenant except a typed-in URL. Add a "Tenant" link gated on canReadTenant(), mirroring the existing platform-admin gate.
…nders cluster-wide
Closes two gaps between design spec §10.3's role table ("platform-admin
darf Tenants anlegen/ändern/löschen, Quotas, clusterweite Sicht") and
what the api module actually implemented, both flagged by the platform
dashboard work rather than worked around:
- PATCH /api/tenants/{name}: TenantRepository gained update(Tenant), used
by a new TenantController#update that changes storage quota/maxObjects
and allowedHostingDomains on an existing tenant (partial-update
semantics, null leaves a field untouched). platform-admin only, same
manual role check as list/create.
- GET /api/renders/cluster: BlueMapRenderController#listCluster walks
every Tenant via TenantRepository (cluster-wide reach, exactly like
TenantController already has) and lists renders in each tenant's own
namespace, tagging each with its owning tenant (ClusterRenderResponse).
Does not touch TenantResolver -- a platform-admin is not necessarily a
member of any tenant, so resolving *a* namespace for it would be wrong.
Both endpoints keep the module's one binding security rule: the tenant
comes from the token or, for the cluster endpoint, from iterating every
Tenant resource -- never from anything the caller supplies. Added
BlueMapRenderControllerTest#listClusterRejectsEveryNonPlatformAdminRole,
which asserts tenant-owner/-operator/-viewer and a roleless caller are
all rejected, not just "no roles at all".
…o utils/ Nuxt's components/ auto-scan registers every file under it as a component, including plain .ts logic modules -- formatTimestamp.ts, renderProgress.ts, and sseController.ts under components/tenant/ showed up as pseudo-components (TenantFormatTimestamp etc.) in .nuxt/components.d.ts. Harmless (nothing referenced those names as a component) but a smell inherent to putting non-component files under components/, called out in both files' own comments as a leftover of an earlier file-scope restriction. Move them to app/utils/, alongside the project's other framework-free logic, and update every import site and test accordingly.
… the platform dashboard
Removes the two honest "not available yet" notices the platform
dashboard shipped with and wires the real endpoints added in the
preceding api commit:
- PlatformEditTenantForm.vue (new): per-tenant inline form for storage
quota, max objects, and allowed hosting domains, calling
PATCH /api/tenants/{name} via the new ApusApiClient#updateTenant.
Wired into TenantList.vue behind an "Edit quota / domains" toggle, one
open at a time; removes the "quota/domains can only be set at
creation" notice from CreateTenantForm.vue, which is no longer true.
- PlatformClusterRenderTable.vue (new, replaces ClusterJobsNotice.vue):
fetches GET /api/renders/cluster via the new
ApusApiClient#listClusterRenders and lists every tenant's renders with
phase/progress/start time. Deliberately a snapshot, not a live view --
the per-render SSE stream still resolves its namespace through the
caller's own tenant claim, which a platform-admin viewing another
tenant's render does not have; a "Refresh" button re-fetches instead.
Also finishes moving components/platform/domainValidation.ts to
app/utils/ (paired with storageUsage.ts's move in the layout-fix commit),
closing out the same components/-as-logic-folder smell for the platform
side that the preceding refactor commit closed for the tenant side.
…onfig The strings the secret scanner flagged were not only in old commits: the phase 1 plan carried them in a code sample, and .gitguardian.yaml listed them in plaintext, which made the exemption file a finding of its own. The plan sample now shows a placeholder, and the exemptions are SHA256 digests.
This was referenced Aug 9, 2026
Contributor
Author
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 5b of Apus: the dashboard. Stacked on PR #6.
Nuxt 4 in SPA mode, Vue 3, Tailwind 4, Nuxt UI — matching the
launchpadhouse standard. Two levels, separated by the role in the token: tenants manage their own sources, maps, renders and hosting; platform operators manage tenants, quotas and allowed domains.Two things done deliberately
Frontend role checks are convenience, not security. The dashboard hides what a role may not do, but enforcement stays in the API. Nothing here is built to look like the frontend enforces rights — that invites dropping the server-side check later.
Progress is displayed honestly. Telemetry can degrade without the render being at risk; the API then reports
-1anddegraded: true. The UI says so instead of drawing a bar at zero or inventing a value. That distinction is why phase 1 has a contract test in the first place.Tokens live in memory only — not
localStorage, notsessionStorage. A bearer JWT in Web Storage is scrapable by any XSS for as long as it sits there. Cost: a hard reload drops it, covered by silent renewal against the broker's own session.Bugs found while building
A runtime error in the shared layout. Nuxt registers components from subdirectories with a directory prefix, so
components/layout/AppHeader.vueisLayoutAppHeaderin templates. The layout referenced<AppHeader />, which compiles to aresolveComponentthat fails at runtime — and neithervue-tscnornuxt buildreports it. Both parallel agents hit this independently.It is now guarded by a test that actually mounts the layout and fails if a component does not resolve. That guard was itself validated: the fix was reverted, the test failed exactly as the original bug manifested, then the fix was restored. A typecheck demonstrably does not catch this.
Two API gaps that made the platform level unusable. There was no way to change an existing tenant's quota (only
GETandPOST /api/tenants), and no cluster-wide view of running renders —GET /api/rendersalways resolves the namespace from the caller's own tenant. Both contradict spec §10.3, which grantsplatform-adminexactly those abilities. The agent surfaced them as visible notices rather than faking a working screen, and both are now implemented.The cluster-wide endpoint is the one place in the API that crosses tenant boundaries, so it is hard-restricted to
platform-adminwith a test proving every other role is refused. That kind of endpoint is precisely the hole that had to be closed three times already, in phases 2a, 3 and 5a.Testing
107 frontend tests plus lint, typecheck and a production build with a runtime smoke test; API tests green including the new endpoints.
Known gaps