Admin Analytics Dashboards
The Admin Dashboard exposes five analytics sub-panels that consume aggregation APIs from the murchases, blogs, media, search, and alerts subgraphs. All panels require the PLATFORM_ADMIN role, which is automatically assigned to Organization Owners and Admins.
NOTE
This document covers the Revenue and Blog frontend components. For the Media, Search, and Alerts sub-tabs, see Media Analytics Dashboard, Search Engine Administration, and Workspaces & The Tower. For the underlying GraphQL APIs, see Analytics Aggregation API (revenue) and Blog Analytics API (content).
Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Admin Dashboard (/admin) โ
โ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โ
โ โOverviewโ โ Users โ โ Trash โ โHistory โ โAnalyti.โ โ
โ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โ
โ โผ โผ โ
โ UserListingPanel RevenueโBlogโMediaโSearchโAlerts โ
โ (nodes subgraph) โโโโโโโโโโโโฌโโโโโโโโโโ โ
โ โ Revenue โ Blog โ โ
โ โAnalytics โAnalyticsโ โ
โ โโโโโโโโโโโโผโโโโโโโโโโค โ
โ โ Media โ Search โ โ
โ โAnalytics โAnalyticsโ โ
โ โโโโโโโโโโโโดโโโโโโโโโโค โ
โ โ AlertsAnalytics โ โ
โ โโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
adminUsers revenueByDay blogPostsByDay
adminUser orderVolume blogEngagement
adminCount entityCounts blogStatus
topMalets topAuthors
Access Control Pipeline
Analytics queries use a two-layer auth model:
Gateway Role Injection โ The auth service's
/auth/validateendpoint returnsis_privileged: truefor users with anOWNERorADMINrole in theorg_memberstable. The gateway maps this torole: 'PLATFORM_ADMIN'in theuserheader forwarded to subgraphs.Subgraph Runtime Check โ Each analytics resolver calls
assertPlatformAdmin(actor), which verifiesactor.role === 'PLATFORM_ADMIN'. Unauthorized requests receive aForbiddenException.
// Gateway auth.context.ts โ role injection
ctx.user = {
id: data.user_id,
username: data.username,
email: data.email,
...(data.is_privileged ? { role: 'PLATFORM_ADMIN' } : {}),
};
Query Layer
All analytics queries are centralized in src/lib/queries/adminAnalytics.ts with full TypeScript interfaces. The file exports 11 queries across three subgraphs:
Nodes Subgraph (User Listing)
| Query | Purpose | Key Fields |
|---|---|---|
GET_ADMIN_USERS |
Cursor-paginated user list | AdminUserFilter with search, sort |
GET_ADMIN_USER_COUNT |
Total registered users | Scalar count |
GET_ADMIN_USER_DETAIL |
Single user with full stats | UserActivityStats embedded |
Murchases Subgraph (Revenue)
| Query | Purpose | Key Fields |
|---|---|---|
GET_REVENUE_BY_DAY |
Daily revenue time series | AnalyticsFilter (dateRange, currency) |
GET_ORDER_VOLUME_TRENDS |
Murchase volume by status | Status-grouped trends |
GET_PLATFORM_ENTITY_COUNTS |
Platform-wide KPI totals | Products, Services, Malets, Users |
GET_TOP_MALETS_BY_REVENUE |
Revenue leaderboard | Top N by total revenue |
Blogs Subgraph (Content)
| Query | Purpose | Key Fields |
|---|---|---|
GET_BLOG_POSTS_BY_DAY |
Post volume time series | BlogAnalyticsFilter (dateRange, maletId) |
GET_BLOG_STATUS_BREAKDOWN |
Status distribution | Published, Draft, Archived, Deleted |
GET_TOP_BLOG_AUTHORS |
Author leaderboard | postCount, totalViews, totalLikes |
GET_BLOG_ENGAGEMENT |
Engagement KPIs + top posts | avgViewsPerPost, avgLikesPerPost |
Component: UserListingPanel
File: src/lib/components/admin/UserListingPanel.svelte
A paginated user management table with:
- Debounced search (300ms) across name, email, and username
- Sortable columns:
CREATED_AT,DISPLAY_NAME,EMAILwith ASC/DESC toggle - Cursor pagination: Load More with
first/afterrelay-style pagination - Detail drawer: Slide-in panel showing full user profile +
UserActivityStatsgrid
query GetAdminUsers($first: Int, $after: String, $filter: AdminUserFilter) {
adminUsers(first: $first, after: $after, filter: $filter) {
edges {
node {
id displayName email avatarUrl createdAt isPrivate
activityStats {
followersCount followingCount collectionsCount
ordersCount bookingsCount blogPostsCount
}
}
cursor
}
pageInfo { hasNextPage endCursor }
totalCount
}
}
State Management
The component uses Svelte 5 runes for reactive state:
let users = $state<AdminUserNode[]>([]);
let totalCount = $state(0);
let endCursor = $state<string | null>(null);
let hasNextPage = $state(false);
let search = $state('');
let sortBy = $state<'CREATED_AT' | 'DISPLAY_NAME' | 'EMAIL'>('CREATED_AT');
let sortOrder = $state<'ASC' | 'DESC'>('DESC');
Component: RevenueAnalytics
File: src/lib/components/admin/RevenueAnalytics.svelte
Revenue and Murchase volume dashboard with:
- KPI cards: Total Revenue, Total Murchases, Average Murchase Value, Platform Entities โ each with a colored top accent bar
- Date range picker: 7d / 30d / 90d / 1y segment control
- Currency filter: Optional text input to scope by currency code (e.g.,
ZAR) - Charts: Revenue bar chart + Order volume line chart using
AdminChart - Entity breakdown: Inline pills showing Malets, Products, Services, Users, and order-status counts
- Leaderboard: Top Malets by Revenue table with medal emojis (๐ฅ๐ฅ๐ฅ)
Data Flow
RevenueAnalytics.svelte
โโ Promise.allSettled([
โ revenueByDay(filter) โ KPIs + revenue chart
โ orderVolumeTrends(filter) โ volume line chart
โ platformEntityCounts() โ entity breakdown pills
โ topMaletsByRevenue(10) โ leaderboard table
โ ])
โโ Graceful partial rendering (allSettled, not all)
IMPORTANT
Revenue amounts are stored in cents (integer). The component divides by 100 for display using Intl.NumberFormat with locale en-ZA.
Component: BlogAnalytics
File: src/lib/components/admin/BlogAnalytics.svelte
Blog content analytics dashboard with:
- KPI cards: Total Views, Total Likes, Avg Views/Post, Avg Likes/Post
- Post summary pills: Total posts, Published count, Draft count
- Charts: Post volume bar chart + CSS donut status breakdown (Published/Draft/Archived/Deleted with color-coded segments)
- Malet filter: Dropdown scoped to owned Malets, passed from parent
- Leaderboards: Top Authors table + Top Posts by Views table
Donut Chart
The status breakdown uses raw SVG <circle> elements with stroke-dasharray and stroke-dashoffset for a performant, dependency-free donut:
{#each statusBreakdown as segment, i}
<circle
cx="18" cy="18" r="15.9"
stroke={STATUS_COLORS[segment.status]}
stroke-dasharray="{pct} {100 - pct}"
stroke-dashoffset={-cumulativeOffset}
transform="rotate(-90 18 18)"
/>
{/each}
Analytics Sub-Tab Navigation
The Analytics tab uses a lightweight sub-tab switcher rather than full routing:
let analyticsSubTab = $state<'revenue' | 'blog' | 'media' | 'search' | 'alerts'>('revenue');
<div class="analytics-sub-tabs">
<button class:active={analyticsSubTab === 'revenue'}>Revenue</button>
<button class:active={analyticsSubTab === 'blog'}>Blog</button>
<button class:active={analyticsSubTab === 'media'}>Media</button>
<button class:active={analyticsSubTab === 'search'}>Search</button>
<button class:active={analyticsSubTab === 'alerts'}>Alerts</button>
</div>
{#if analyticsSubTab === 'revenue'}
<RevenueAnalytics />
{:else if analyticsSubTab === 'blog'}
<BlogAnalytics malets={malets} />
{:else if analyticsSubTab === 'media'}
<MediaAnalytics malets={malets} />
{:else if analyticsSubTab === 'search'}
<SearchAnalytics />
{:else if analyticsSubTab === 'alerts'}
<AlertsAnalytics />
{/if}
Error Handling
All five analytics components use the same resilient pattern:
Promise.allSettledfor parallel queries โ partial data renders even if one query fails- Error banner with retry button
- Loading spinner per-component
- Graceful empty states when no data is returned
File Map
| File | Purpose |
|---|---|
src/lib/queries/adminAnalytics.ts |
GraphQL queries + TypeScript interfaces |
src/lib/components/admin/UserListingPanel.svelte |
User management table + detail drawer |
src/lib/components/admin/RevenueAnalytics.svelte |
Revenue KPIs, charts, leaderboard |
src/lib/components/admin/BlogAnalytics.svelte |
Blog engagement KPIs, donut, leaderboards |
src/lib/components/admin/MediaAnalytics.svelte |
Media upload volume, storage, MIME distribution |
src/lib/components/admin/SearchAnalytics.svelte |
Search index composition, reconciliation |
src/lib/components/admin/AlertsAnalytics.svelte |
Alert delivery health, DLQ panel |
src/lib/queries/alerts.ts |
Alerts queries, types, display helpers |
src/routes/admin/+page.svelte |
Tab integration and sub-tab routing |
apps/auth/src/routes/validate.rs |
is_privileged field on auth validate |
apps/ngwenya-gateway/src/auth.context.ts |
PLATFORM_ADMIN role injection |
Related
- Workspaces & The Tower โ Tower architecture, all 5 analytics sub-tabs, access control, navigation
- Media Analytics Dashboard โ Dedicated Media sub-tab developer guide
- Alerts & Resilience โ Backend alerts subgraph, DLQ, delivery channels
- Analytics Aggregation API โ Backend revenue, Murchase volume, and entity count API reference
- Blog Analytics API โ Backend post volume, engagement, and author ranking API reference
- Search Engine Administration โ Backend search operations powering the Search sub-tab
- Edit History Audit Trail โ Field-level diff viewer for Products, Services, and Blog Posts
- Custom RBAC โ
PLATFORM_ADMINrole andMANAGE_ANALYTICSpermission definitions - Organization & Malet Management โ Organization context and
x-org-idheader mechanics - Alert Templates โ Email/SMS template maker in the Admin Dashboard Templates tab
- Search Configuration โ Per-vertical synonym management and Meilisearch index reconciliation
- Payments History โ Paginated Murchase table with filtering and detail drawer
- Inventory & Deprecation Tracking โ Product stock health monitoring and deprecated field usage tracking
- Platform Admin Provisioning โ How Tower access is controlled via dedicated
platform_adminstable and invite-chain