Developer Docs

Workspaces & The Tower

Mallnline organizes its administrative interfaces into Workspaces โ€” role-specific dashboards named after physical spaces within the virtual mall. Each workspace serves a distinct persona and has its own subdomain in the production architecture.

Workspace Architecture

Workspace Subdomain Persona Frontend Route Description
The Deck deck.mallnline.com Malet Owners /dashboard, /[malet]/manage/* Per-Malet management โ€” dashboard, analytics, payments, inventory, trash, edit history
The Studio studio.mallnline.com Developers /dev SDK docs, API references, webhook management
The Tower tower.mallnline.com Platform Admins /tower Platform-wide admin dashboard โ€” analytics, users, templates, search config, deprecation

NOTE

In the current monolith frontend, workspaces are routes within the main SvelteKit app. In the production subdomain architecture, each workspace becomes its own entry point with shared authentication via uID.

The Tower (`/tower`)

The Tower is the platform-wide admin dashboard. It is restricted to internal Mallnline administrators (is_privileged users only). Non-privileged users are redirected to The Deck.

IMPORTANT

The Tower is designed to eventually live at tower.mallnline.com as its own standalone app. No customer-facing features should live in The Tower. Owner-specific features belong in The Deck.

Access Control

The Tower uses a client-side is_privileged gate:

Authenticated? โ”€โ”€โ”€yesโ”€โ”€โ†’ is_privileged? โ”€โ”€โ”€yesโ”€โ”€โ†’ Full Tower Dashboard
                 โ”‚                         โ”‚
                 no                        no โ†’ Redirect to /dashboard
                 โ”‚
                 โ””โ”€โ†’ Redirect to /auth?redirect=/tower

The legacy /admin route redirects to /tower for backward compatibility.

Navigation Entry Points

The Tower link is visible only to privileged users:

Location Component Selector Gated By
Side Navigation SideNav.svelte [data-testid="sidenav-admin-link"] $currentUser?.is_privileged
User Menu UserMenu.svelte [data-testid="user-menu-admin-link"] $currentUser?.is_privileged
Footer SoleContent.svelte [data-testid="sole-admin-link"] $currentUser?.is_privileged
Settings AdminSection.svelte [data-testid="settings-admin-link"] Always visible (link goes to /tower)

Tab Architecture

The Tower uses a primary/secondary tab system with platform-only tabs:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Overview โ”‚ Users โ”‚ Analytics                โ”‚ More โ–พ โ”‚
โ”‚                                                  โ”‚
โ”‚  โ”Œโ”€ More Dropdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚ Templates โ”‚ Search Config โ”‚ Deprecation โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

State type:

let activeTab = $state<
  'overview' | 'users' | 'analytics' |
  'templates' | 'search' | 'deprecation'
>('overview');

NOTE

Deck-scoped tabs (Payments, Inventory, Trash, Edit History) were extracted to flat sub-routes in the ngwenya-deck standalone app in the Deck โ†” Tower Separation epic.

The Deck (`deck.mallnline.com`)

The Deck is the Malet Owner standalone workspace. It provides per-malet tools for managing products, analytics, and operations.

Deck Sub-Routes

Route Purpose Key Component
/dashboard Owner overview with quick actions Standalone page
/analytics Per-malet Blog & Media analytics BlogAnalytics, MediaAnalytics
/payments Murchase history for owned Malets PaymentsHistory
/inventory Stock levels for owned products InventoryPanel
/trash Soft-deleted items with restore TrashBin
/history Edit history with activity feed ActivityFeedTimeline

Deck Query Scoping

Deck sub-routes use scoped queries to ensure owners only see their own data:

// Uses maletId: { in: $maletIds } filter on @FilterableField
export const GET_OWNED_MALET_PRODUCTS = gql`
  query GetOwnedMaletProducts($maletIds: [String!]!, $first: Int!) {
    products(paging: { first: $first }, filter: { maletId: { in: $maletIds } }) {
      edges { node { id name basePrice status totalInventory ... } }
    }
  }
`;

Analytics Sub-Tabs (Tower โ€” Platform-Wide)

The Tower's Analytics tab contains five sub-views showing platform-wide data:

Sub-Tab Component Data Source Key Visualizations
Revenue RevenueAnalytics.svelte GET_MY_OWNED_MALETS, orders queries KPI cards, revenue/volume charts, entity breakdown, top Malets leaderboard
Blog BlogAnalytics.svelte Blog posts per Malet Engagement KPIs, post volume chart, status donut, top authors/posts
Media MediaAnalytics.svelte Media subgraph Upload volume, storage growth, MIME distribution, processing health, top Malets by storage
Search SearchAnalytics.svelte Faceted search, reconciliation, synonyms Index composition donut, categories, Malet leaderboard, tag cloud, synonym coverage, reconciliation health
Alerts AlertsAnalytics.svelte alertLogs, dlqSummary (alerts subgraph) Delivery health donut, channel distribution bars, event type breakdown, recent delivery feed, DLQ health panel

Analytics Sub-Tabs (Deck โ€” Per-Malet)

The Deck's /analytics route shows per-malet analytics (Blog + Media only):

let activeSubTab = $state<'blog' | 'media'>('blog');

Platform-wide analytics (Revenue, Search, Alerts) remain exclusively in The Tower.

Search Analytics Data Sources

The Search Analytics dashboard is unique in that it composes from three separate query patterns rather than a single subgraph analytics resolver:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚    SearchAnalytics       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                         โ”‚
โ”‚  1. Zero-query Facet    โ”‚โ”€โ”€โ†’ SEARCH_ITEMS (q="", limit=0)
โ”‚     Search              โ”‚    โ†’ type, category, malet, section facets
โ”‚                         โ”‚
โ”‚  2. Reconciliation      โ”‚โ”€โ”€โ†’ GET_RECONCILIATION_STATUS
โ”‚     Status              โ”‚    โ†’ lastRun, added, removed, unchanged, errors
โ”‚                         โ”‚
โ”‚  3. Synonym Coverage    โ”‚โ”€โ”€โ†’ GET_SEARCH_SYNONYMS
โ”‚                         โ”‚    โ†’ groups per vertical
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

This pattern avoids adding dedicated analytics resolvers to the search subgraph โ€” it reuses existing operational queries and performs client-side aggregation.

Organization Analytics

Separate from The Tower, the Organization Management page (/orgs/[slug]/manage) has its own Analytics tab implemented by OrgAnalyticsPanel.svelte. This tab provides org-scoped metrics:

  • KPI Cards: Total Members, Total Activity, Active Contributors, Teams
  • Activity Over Time: 30-day bar chart from organizationAuditEvents
  • Role Distribution: Donut chart of OWNER/ADMIN/MEMBER ratios
  • Event Type Breakdown: Horizontal bars for MEMBER_ADDED, ROLE_CHANGED, etc.
  • Top Contributors: Leaderboard with medal emojis
  • Teams Table: Sub-group listing from organizationTeams

Like Search Analytics, this uses client-side aggregation from existing queries rather than dedicated analytics resolvers.

Utility Functions

Both analytics components share common formatting utilities:

function formatNumber(n: number): string {
  if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
  if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
  return n.toString();
}

function formatDuration(ms: number): string {
  if (ms < 1000) return `${ms}ms`;
  return `${(ms / 1000).toFixed(1)}s`;
}

function timeAgo(dateStr: string): string {
  const diff = Date.now() - new Date(dateStr).getTime();
  const mins = Math.floor(diff / 60000);
  if (mins < 60) return `${mins}m ago`;
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return `${hrs}h ago`;
  return `${Math.floor(hrs / 24)}d ago`;
}

These are tested in tests/searchAnalytics.test.ts (15 tests), tests/orgAnalytics.test.ts (16 tests), and tests/alertsAnalytics.test.ts (22 tests).

Alerts Analytics Data Sources

The Alerts Analytics dashboard consumes from the alerts subgraph's AlertLog module via GraphQL:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚      AlertsAnalytics         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                             โ”‚
โ”‚  1. dlqSummary              โ”‚โ”€โ”€โ†’ Aggregated status counts
โ”‚                             โ”‚    (PENDING/DELIVERED/FAILED/DEAD + total)
โ”‚                             โ”‚
โ”‚  2. alertLogs(filter)       โ”‚โ”€โ”€โ†’ Paginated delivery logs
โ”‚                             โ”‚    โ†’ channel, status, eventType, recipient
โ”‚                             โ”‚    โ†’ client-side aggregation for charts
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Alerts-specific utilities in src/lib/queries/alerts.ts:

// Aggregate logs by any field (channel, status, eventType)
function aggregateByField<T>(logs: AlertLog[], field: string): { key: T; count: number }[]

// Mask recipients for privacy display
function truncateRecipient(r: string): string  // j***@e***.com or ***4567

// Calculate percentage with 1 decimal
function percentOf(part: number, total: number): string

Display label maps provide human-readable names, colors, and icons:

  • CHANNEL_LABELS / CHANNEL_COLORS / CHANNEL_ICONS โ€” EMAIL, SMS, PUSH, IN_APP
  • STATUS_LABELS / STATUS_COLORS โ€” PENDING, DELIVERED, FAILED, DEAD
  • EVENT_TYPE_LABELS โ€” 12 event types (notify_email, order_status_changed, org_invite_created, etc.)

File Reference

File Purpose
src/routes/tower/+page.svelte Tower page โ€” tabs, data fetching, layout
src/routes/admin/+page.svelte Legacy redirect โ†’ /tower
ngwenya-deck/src/routes/dashboard/+page.svelte Deck dashboard with quick actions
ngwenya-deck/src/routes/analytics/+page.svelte Per-malet Blog & Media analytics
ngwenya-deck/src/routes/payments/+page.svelte Murchase history for owned Malets
ngwenya-deck/src/routes/inventory/+page.svelte Inventory health for owned products
ngwenya-deck/src/routes/trash/+page.svelte Soft-deleted items with restore
ngwenya-deck/src/routes/history/+page.svelte Edit history with activity feed
src/lib/components/admin/RevenueAnalytics.svelte Revenue sub-tab (Tower)
src/lib/components/admin/BlogAnalytics.svelte Blog sub-tab (Tower + Deck)
src/lib/components/admin/MediaAnalytics.svelte Media sub-tab (Tower + Deck)
src/lib/components/admin/SearchAnalytics.svelte Search sub-tab (Tower)
src/lib/components/admin/AlertsAnalytics.svelte Alerts sub-tab (Tower)
src/lib/queries/admin.ts GET_OWNED_MALET_PRODUCTS scoped query
src/lib/queries/alerts.ts Alerts queries, types, display helpers
src/lib/components/admin/OrgAnalyticsPanel.svelte Org manage analytics tab
src/routes/lobby/SideNav.svelte Owner Tools & Platform sections
src/lib/components/UserMenu.svelte Deck + Tower buttons in user dropdown
src/routes/lobby/SoleContent.svelte Footer Deck + Tower links
src/lib/utils/adminUtils.ts isAdmin() helper
tests/searchAnalytics.test.ts Search analytics utility tests
tests/orgAnalytics.test.ts Org analytics utility tests
tests/alertsAnalytics.test.ts Alerts analytics utility tests (22 tests)