Developer Docs

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:

  1. Gateway Role Injection โ€” The auth service's /auth/validate endpoint returns is_privileged: true for users with an OWNER or ADMIN role in the org_members table. The gateway maps this to role: 'PLATFORM_ADMIN' in the user header forwarded to subgraphs.

  2. Subgraph Runtime Check โ€” Each analytics resolver calls assertPlatformAdmin(actor), which verifies actor.role === 'PLATFORM_ADMIN'. Unauthorized requests receive a ForbiddenException.

// 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, EMAIL with ASC/DESC toggle
  • Cursor pagination: Load More with first/after relay-style pagination
  • Detail drawer: Slide-in panel showing full user profile + UserActivityStats grid
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.allSettled for 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