Browse documentation

Frontend Specs

RankFlow AI — Admin Dashboard Frontend Specification

Document Version: 1.0.0

docs/specs/frontend/admin-spec-01-full.md
On this page

Document Version: 1.0.0 Date: 2026-06-13 Owner: Frontend Engineering Scope: All admin-facing screens, components, widgets, and interactions Format: Markdown (.md) — primary deliverable for version control and diffing Tech Stack: Next.js 14 (App Router), React 18, TypeScript, Tailwind CSS, tRPC, React Query (TanStack Query), Zod, Better Auth, Recharts, date-fns, Lucide React


Document Overview#

This specification is the single source of truth for all admin-facing frontend implementation. It documents the operational nerve center of the RankFlow AI platform — where the platform team monitors, controls, evaluates, and optimizes everything.

The admin dashboard is completely separate from the client dashboard. It uses the same design system (frontend-spec-01-design-system.md) but with denser layouts, more data per screen, dark sidebar option, and real-time feeds.

Critical constraints (non-negotiable, inherited from design system):

  • NO bold font weight. font-weight: 500 (Medium) is the maximum. Suisse Intl Medium only.
  • Ember Orange #EC652B is ONLY for: primary action buttons, CTA links, focus rings, positive outcome indicators. Never for text, backgrounds, error states, or status badges.
  • Page max-width: 1200px centered. Canvas background: Paper White #F6F6F8.
  • Sidebar: 240px wide. Client uses deep-ink (#011821). Admin uses carbon (#12161E) for visual distinction.
  • Section gap: 80px between major sections.
  • Card radius: 8px (border-radius-lg).
  • Medical compliance: All content for medical clients (CLINIC/HOSPITAL/DOCTOR) goes through 24h approval queue. No exceptions.

Admin-specific differences from client:

  • Denser layouts: tables with 15+ columns, KPI grids with 20+ cards
  • Dark sidebar (carbon #12161E) for visual distinction from client dashboard
  • Complex data tables: grouping, pivoting, column resizing, reordering, export
  • Complex charts: multi-series, stacked, comparative, historical trends
  • Real-time feeds: 10-second polling for jobs, alerts, system metrics
  • Impersonation: red banner always present when impersonating, session isolation
  • Audit trail: every action logged, every screen has audit trail access

Role access: All /admin/* routes are ADMIN only. Non-ADMIN users get 403 redirect to /dashboard.

Admin spec is split into 5 files to avoid truncation:

  • Part 1 (admin-spec-01-full.md): Sections 1–3 (KPI Overview, Client Management, Workflow Monitor)
  • Part 2 (admin-spec-02-ai-blog-prompts.md): Sections 4–6 (AI Evaluation, Blog Manager, Prompt Manager)
  • Part 3A (admin-spec-03a-system.md): Section 7 (System Configuration)
  • Part 3B (admin-spec-03b-alerts.md): Section 8 (Alert Manager)
  • Part 3C (admin-spec-03c-audit.md): Section 9 (Audit & Logs)
  • Part 3D (admin-spec-03d-data-governance.md): Section 10 (Data Governance)
  • Part 4A (admin-spec-04a-performance.md): Section 11 (Performance Monitor)
  • Part 4B (admin-spec-04b-admin-billing.md): Section 12 (Admin Billing)
  • Part 4C (admin-spec-04c-admin-reports.md): Section 13 (Admin Reports)
  • Part 4D (admin-spec-04d-security.md): Section 14 (Security)
  • Part 4E (admin-spec-04e-landing-pages.md): Section 15 (Admin Landing Pages)

Section 1 — Admin KPI Overview (/admin/kpi)#

Role access: ADMIN only. Non-ADMIN redirected to /dashboard with 403. Layout: Full-width grid of 20+ KPI cards (4 columns desktop, 2 tablet, 1 mobile) + alert summary panel + 4 charts. Sidebar: carbon (#12161E) background for admin visual distinction.


Widget 1.1 — KPI Card Grid#

1. Screen Name & Route: /admin/kpi — Admin KPI Overview. Role: ADMIN only.

2. Data to Show:

KPI Type Format Calculation Data Source
Active Clients number Integer with comma count(status = ACTIVE) admin.getKPIs
Active Websites number Integer with comma count(sites = ACTIVE) admin.getKPIs
Total Monthly Revenue currency INR with commas sum(all active subscriptions) admin.getRevenue
Avg Revenue Per Client currency INR with commas total revenue / active clients calculated client-side
Monthly Growth Rate percentage Signed %, 1 decimal (current - previous) / previous × 100 calculated client-side
Net Revenue Retention percentage %, 1 decimal (starting MRR + expansion - contraction - churn) / starting MRR admin.getRevenue
MRR currency INR with commas sum(monthly subscriptions) admin.getRevenue
Churn Rate percentage %, 1 decimal churned / total at start × 100 admin.getRevenue
LTV currency INR with commas avg revenue per client × avg lifespan calculated client-side
Revenue by Plan Tier chart data Stacked bar sum(revenue) per tier admin.getRevenue
Daily API Cost currency USD with 2 decimals sum(all API calls today) admin.getAPICosts
Client Health Score number 0–100, integer avg of all client health scores admin.getClientHealth
Content Approval Queue number Integer count(PENDING_REVIEW) admin.getKPIs
System Health Score number 0–100, integer composite(queue, failure rate, API cost) admin.getSystemHealth
Top Performing Clients list Top 5 names by revenue or engagement admin.getTopClients
Failed Payments (7d) number Integer count(failed in last 7 days) admin.getPaymentFailures
Trial Conversion Rate percentage %, 1 decimal converted / total trials × 100 admin.getTrialConversions
Social Post Engagement percentage %, 1 decimal total engagement / total posts × 100 admin.getSocialEngagement
Citation Health Score number 0–100, integer avg of all citation health scores admin.getCitationHealth
Avg Review Rating number 0–5, 1 decimal avg of all client review ratings admin.getKPIs
AI Response Accuracy percentage %, 1 decimal helpful responses / total × 100 admin.getAIEvaluation
Pending Decommission number Integer count(grace period clients) admin.getKPIs
Negative Review Unreplied number Integer count(negative > 24h) admin.getAlerts
NAP Mismatch Alerts number Integer count(clients with mismatch > 3) admin.getAlerts

Alert Thresholds per KPI:

KPI Warning Alert Critical
Monthly Growth Rate < 5% < 0%
Client Health Score < 70 < 60
Content Approval Queue > 10 > 25 > 50
System Health Score < 80 < 70 < 50
Failed Payments (7d) > 5 > 10 > 20
Trial Conversion Rate < 30% < 20%
Citation Health Score < 80 < 70 < 50
Avg Review Rating < 4.0 < 3.5
AI Response Accuracy < 85% < 80%
Negative Review Unreplied > 0 > 3 > 5
NAP Mismatch Alerts > 0 > 3 > 5
Daily API Cost > $50 > $100 > $200

3. Display Pattern:

  • Component: MetricCard (inherited from design system, adapted for dense admin layout).
  • Layout: CSS Grid, 4 columns (desktop), 2 columns (tablet), 1 column (mobile). Gap: space-4 (16px).
  • Container: card-white, radius-lg, shadow-subtle, padding 20px (compact vs client 24px).
  • Header: Label (caption, 12px, text-tertiary, uppercase) left-aligned. Threshold badge right-aligned if crossed.
  • Body: Value (heading or heading-lg, weight 500, text-primary).
  • Footer: Sparkline (mini line chart, 60px wide, 30px tall) or comparison text.
  • Threshold badge: If warning → ember-orange dot + "Warning". If alert → error-red dot + "Alert". If critical → error-red badge + "Critical".
  • Left border accent: If threshold crossed, left border 3px in threshold color (ember-orange for warning, error-red for alert/critical).
  • Status colors: forest-teal for positive, error-red for negative, ember-orange for warning, slate for neutral.

4. Backend Endpoint:

  • Primary: admin.getKPIs (tRPC query, adminProcedure).
  • Zod input schema: z.object({ period: z.enum(["7d", "30d", "90d"]).default("30d") }).
  • Zod output schema:
    z.object({
      totalClients: z.number(),
      activeClients: z.number(),
      activeWebsites: z.number(),
      totalMonthlyRevenue: z.number(),
      monthlyGrowthRate: z.number(),
      netRevenueRetention: z.number(),
      mrr: z.number(),
      churnRate: z.number(),
      ltv: z.number(),
      revenueByTier: z.array(z.object({ tier: z.string(), revenue: z.number() })),
      dailyApiCost: z.number(),
      clientHealthScore: z.number(),
      contentApprovalQueue: z.number(),
      systemHealthScore: z.number(),
      topPerformingClients: z.array(z.object({ name: z.string(), revenue: z.number(), engagement: z.number() })).max(5),
      failedPayments7d: z.number(),
      trialConversionRate: z.number(),
      socialPostEngagement: z.number(),
      citationHealthScore: z.number(),
      avgReviewRating: z.number(),
      aiResponseAccuracy: z.number(),
      pendingDecommission: z.number(),
      negativeReviewUnreplied: z.number(),
      napMismatchAlerts: z.number(),
    })
    
  • Auth: adminProcedure — requires role === "ADMIN". Returns 403 FORBIDDEN otherwise.

5. Fetch Frequency:

  • Initial load: On /admin/kpi mount.
  • Polling: Every 60 seconds (staleTime: 60s, refetchInterval: 60s).
  • Background refresh: On window focus (if staleTime exceeded).
  • Manual refresh: "Refresh All" button triggers utils.invalidate() for all dashboard queries.

6. Data Manipulations:

  • avgRevenuePerClient: totalMonthlyRevenue / activeClients → formatted to toLocaleString("en-IN").
  • monthlyGrowthRate: ((currentMRR - previousMRR) / previousMRR * 100).toFixed(1).
  • ltv: avgRevenuePerClient * avgClientLifespanMonths → formatted to toLocaleString("en-IN").
  • Threshold derivation: Client-side comparison of each KPI value against its warning/alert/critical threshold. Affects badge color and left border.
  • Formatting: Indian number format (en-IN) for currency and large numbers. Percentages with 1 decimal.

7. Why Structured This Way:

  • 60s polling: KPIs are high-velocity but not real-time. 60s balances freshness and server load.
  • 24 cards in 4-column grid: Admin needs dense information density. More cards than client dashboard (which has 8-10).
  • Left border for thresholds: Draws attention without overwhelming the card. Subtle but noticeable in a dense grid.
  • Sparklines in every card: Provides trend context without requiring full chart navigation. Admin scans quickly.
  • Indian number format: Target market is India. en-IN uses lakhs/crores.
  • No bold weight: Brand constraint. heading-lg at 40px with weight 500 is visually heavy enough.

8. Interaction Flows:

  • Hover: Card lifts (translateY(-1px), shadow-sm), 200ms transition. Click navigates to detail page for that metric (e.g., "Client Health Score" → /admin/clients with health filter).
  • Click: Each card is clickable, linking to the relevant detail page.
  • Context menu: None.
  • Loading: Skeleton card (pulsing rectangle for label, larger rectangle for value, thin line for sparkline). Same dimensions as loaded card.
  • Error: Inline error within card. Icon (alert triangle, error-red), "Data unavailable", "Retry" button (ghost). Other cards unaffected.

9. Error States:

  • Empty: If value === 0, show "0" as value, sparkline flat line at bottom, no trend badge. Not an error — just no data yet.
  • Loading: Skeleton (as described above).
  • Error: Inline error within card. Other cards show their data.
  • Partial: If trendData is missing but value present, show value + "Trend unavailable" text instead of sparkline.

10. Role-Based Variations:

  • ADMIN: Full access, all 24 cards, all thresholds, all navigation links.
  • CLIENT / EDITOR / VIEWER: No access. Redirected to /dashboard with 403.

Widget 1.2 — Alert Summary Panel#

1. Screen Name & Route: /admin/kpi — Alert Summary Panel (top of page, below KPI cards). Role: ADMIN only.

2. Data to Show:

  • Top 5 alerts by severity, each with: icon, title, count, severity badge, link to detail.
  • Severity: warning (yellow/ember-orange), alert (orange/ember-orange), critical (red/error-red).
  • Auto-dismiss: alerts resolve when metric returns to normal.
  • Fields per alert: id, severity, type, title, description, affectedClient (or "system"), count, status, createdAt.

3. Display Pattern:

  • Component: AlertSummaryPanel (composite: container + alert row list).
  • Layout: Horizontal scrollable row on desktop, vertical stack on mobile. Gap: space-3 (12px).
  • Container: card-white, radius-lg, shadow-subtle, padding 24px.
  • Header: "Active Alerts" (subheading, 20px, weight 500) + badge showing total count.
  • Alert row: Icon (left, 20px, color matches severity), title (body-sm, 14px, weight 500), description (caption, 12px, text-tertiary), severity badge (radius-full, pill), count badge (if > 1), link arrow (right).
  • Severity badge colors: warningrgba(236,101,43,0.12) bg, ember-orange text. alertrgba(236,101,43,0.20) bg, ember-orange text. criticalrgba(194,68,42,0.12) bg, error-red text.
  • Auto-dismiss: When alert resolves, row fades out (opacity 0, 300ms) and is removed from list.

4. Backend Endpoint:

  • Primary: admin.getAlerts (tRPC query, adminProcedure).
  • Zod input schema: z.object({ limit: z.number().default(5), status: z.enum(["active", "acknowledged", "resolved"]).default("active") }).
  • Zod output schema:
    z.array(z.object({
      id: z.string(),
      severity: z.enum(["warning", "alert", "critical"]),
      type: z.string(),
      title: z.string(),
      description: z.string(),
      affectedClient: z.string().nullable(),
      count: z.number().default(1),
      status: z.enum(["active", "acknowledged", "resolved"]),
      createdAt: z.string().datetime(),
    })).max(5)
    
  • Auth: adminProcedure. Returns 403 for non-admin.

5. Fetch Frequency:

  • Initial load: On /admin/kpi mount.
  • Polling: Every 60 seconds.
  • Auto-dismiss: Client-side removal when status changes to resolved on next fetch.

6. Data Manipulations:

  • Sorting: By severity (critical first, then alert, then warning), then by createdAt DESC.
  • Grouping: If multiple alerts of same type, group into single row with count > 1.
  • Filtering: Only status === "active" alerts shown in summary panel.

7. Why Structured This Way:

  • Top 5 only: Admin scans quickly. Full alert management is in /admin/alerts (Section 8).
  • Horizontal scrollable: Saves vertical space on dense admin page. Mobile stacks vertically.
  • Auto-dismiss: Reduces cognitive load. Admin doesn't need to manually clear resolved alerts.
  • Severity-first sorting: Critical alerts must be seen first. No scrolling to find them.

8. Interaction Flows:

  • Click: Alert row navigates to /admin/alerts with filter pre-applied for that alert type.
  • Hover: Row background shifts to rgba(1,24,33,0.02), 150ms transition.
  • Dismiss animation: Row fades out (opacity 0 → display none, 300ms, easing-exit).
  • Loading: Skeleton panel (3 rows of pulsing rectangles).
  • Error: Inline error banner. "Failed to load alerts. Retrying..." with spinner.
  • Empty: If no active alerts, show "All systems normal" with green check icon (forest-teal).

9. Error States:

  • Empty: "All systems normal" message with forest-teal check icon. Panel collapses to 48px height.
  • Loading: Skeleton (3 rows).
  • Error: Inline error within panel. Other widgets unaffected.
  • Partial: If some alerts load but severity metadata missing, show "Alert details unavailable" caption.

10. Role-Based Variations:

  • ADMIN: Full panel, all alerts, clickable links to detail.
  • Other roles: No access. Redirected to /dashboard.

Widget 1.3 — Revenue Trend Chart#

1. Screen Name & Route: /admin/kpi — Revenue Trend Chart (below alert panel). Role: ADMIN only.

2. Data to Show:

  • 12-month MRR trend line chart.
  • X-axis: Months (e.g., "Jan 2026", "Feb 2026").
  • Y-axis: INR revenue values.
  • Series: MRR line, ARR line (dashed), churn line (secondary axis or separate color).
  • Tooltip: Month, MRR value, change from previous month.

3. Display Pattern:

  • Component: AreaChart (Recharts).
  • Layout: Full-width card, 400px height.
  • Container: card-white, radius-lg, shadow-subtle, padding 24px.
  • Header: "Revenue Trend" (subheading, 20px, weight 500) + period selector (dropdown: 6m, 12m, 24m).
  • Chart colors: MRR line deep-indigo (#111A4A), ARR line sky-blue (#7EA7E9), churn line error-red (#C2442A).
  • Fill: MRR area fill opacity 12% deep-indigo.
  • Grid: Horizontal mist lines only. No vertical grid.
  • Axis text: caption (12px, text-tertiary).
  • Tooltip: card-white bg, shadow-sm, radius-lg, padding 12px.
  • Legend: caption size, horizontal, clickable to toggle series.

4. Backend Endpoint:

  • Primary: admin.getRevenue (tRPC query, adminProcedure).
  • Zod input schema: z.object({ period: z.enum(["6m", "12m", "24m"]).default("12m") }).
  • Zod output schema:
    z.object({
      mrr: z.array(z.object({ month: z.string(), value: z.number() })),
      arr: z.array(z.object({ month: z.string(), value: z.number() })),
      churn: z.array(z.object({ month: z.string(), value: z.number() })),
      newClients: z.array(z.object({ month: z.string(), count: z.number() })),
    })
    
  • Auth: adminProcedure.

5. Fetch Frequency:

  • Initial load: On mount.
  • Polling: Every 1 hour (refetchInterval: 3600000).
  • Period change: On dropdown selection, immediate refetch.

6. Data Manipulations:

  • Month formatting: date-fns format "MMM yyyy" from ISO date strings.
  • Currency formatting: value.toLocaleString("en-IN", { style: "currency", currency: "INR" }) in tooltip.
  • Change calculation: ((current - previous) / previous * 100).toFixed(1) for tooltip change indicator.
  • Series toggle: Client-side visibility state per series. No refetch on toggle.

7. Why Structured This Way:

  • 1-hour refresh: Revenue changes slowly. Real-time not needed. Hourly is sufficient for admin decision-making.
  • Area chart: Shows trend and magnitude simultaneously. Area fill gives weight to revenue volume.
  • Multi-series: MRR, ARR, and churn on one chart gives context. Admin sees correlation between churn and MRR dips.
  • Period selector: 6m/12m/24m covers short-term trends and long-term growth.

8. Interaction Flows:

  • Hover: Tooltip appears at data point. Shows exact value and month.
  • Legend click: Toggles series visibility. Hidden series line disappears with 200ms fade.
  • Period change: Dropdown change triggers immediate refetch with new period parameter.
  • Loading: Skeleton rectangle (same 400px height, pulsing).
  • Error: "Chart data unavailable" centered text with retry button.
  • Empty: "No revenue data for selected period" with suggestion to check billing setup.

9. Error States:

  • Empty: No data points for period → "No revenue data" message.
  • Loading: Skeleton rectangle.
  • Error: Centered error state with retry button.
  • Partial: If one series missing (e.g., churn data), show remaining series with "Partial data" caption.

10. Role-Based Variations:

  • ADMIN: Full chart, all series, period selector.
  • Other roles: No access.

Widget 1.4 — Client Status Distribution Chart#

1. Screen Name & Route: /admin/kpi — Client Status Distribution. Role: ADMIN only.

2. Data to Show:

  • Pie chart showing client distribution by status: ACTIVE, TRIAL, SUSPENDED, CANCELLED, PENDING.
  • Count and percentage per segment.
  • Center text: Total clients.

3. Display Pattern:

  • Component: PieChart (Recharts).
  • Layout: Half-width card (2-column grid on desktop), 300px height.
  • Container: card-white, radius-lg, shadow-subtle, padding 24px.
  • Header: "Client Status" (subheading, 20px, weight 500).
  • Colors: ACTIVE forest-teal, TRIAL sky-blue, SUSPENDED ember-orange, CANCELLED error-red, PENDING slate.
  • Labels: Outside segments with connector lines. Shows count + percentage.
  • Legend: Below chart, horizontal, caption size.
  • Center donut hole: Total client count (heading, 28px, weight 500).

4. Backend Endpoint:

  • Primary: admin.getKPIs (same as Widget 1.1, field clientStatusDistribution).
  • Zod output schema subset:
    z.object({
      clientStatusDistribution: z.array(z.object({
        status: z.enum(["ACTIVE", "TRIAL", "SUSPENDED", "CANCELLED", "PENDING"]),
        count: z.number(),
      })),
    })
    
  • Auth: adminProcedure.

5. Fetch Frequency:

  • Same as Widget 1.1: 60 seconds.

6. Data Manipulations:

  • Percentage: (count / totalClients * 100).toFixed(1) per segment.
  • Total: sum(count) for center text.
  • Sorting: By count descending.

7. Why Structured This Way:

  • Donut chart: Center text draws attention to total count. Segments show proportion.
  • Status colors: Each status has a semantic color from the badge system. Consistent with client table.
  • Half-width: KPI page has multiple charts. Donut needs less space than line chart.

8. Interaction Flows:

  • Hover: Segment enlarges slightly (outer radius + 4px). Tooltip shows status name, count, percentage.
  • Click: Navigates to /admin/clients with status filter pre-applied.
  • Legend click: Toggles segment visibility.
  • Loading: Skeleton circle.
  • Error: Centered error message.
  • Empty: "No clients yet" with CTA to marketing page.

9. Error States:

  • Empty: No clients → "No clients yet" + CTA.
  • Loading: Skeleton circle (pulsing).
  • Error: Centered error with retry.
  • Partial: If one status missing, show remaining segments.

10. Role-Based Variations:

  • ADMIN: Full chart, clickable segments.
  • Other roles: No access.

Widget 1.5 — API Cost Breakdown Chart#

1. Screen Name & Route: /admin/kpi — API Cost Breakdown. Role: ADMIN only.

2. Data to Show:

  • 7-day stacked bar chart by provider: Claude, OpenAI, DataForSEO, SerpAPI, Hyperbrowser, Firecrawl.
  • X-axis: Days (last 7).
  • Y-axis: Cost in USD.
  • Each day is a stacked bar with segments per provider.
  • Total cost per day shown on top of bar.

3. Display Pattern:

  • Component: BarChart (Recharts, stacked).
  • Layout: Full-width card, 350px height.
  • Container: card-white, radius-lg, shadow-subtle, padding 24px.
  • Header: "API Cost Breakdown (7 Days)" (subheading, 20px, weight 500) + total cost badge.
  • Colors: Claude deep-indigo, OpenAI sky-blue, DataForSEO forest-teal, SerpAPI lavender, Hyperbrowser ember-orange, Firecrawl mint.
  • Bar radius: 2px top corners.
  • Bar gap: 8px between bars.
  • Tooltip: card-white bg, shows provider breakdown per day.
  • Legend: Horizontal, caption size, clickable.

4. Backend Endpoint:

  • Primary: admin.getAPICosts (tRPC query, adminProcedure).
  • Zod input schema: z.object({ days: z.number().default(7) }).
  • Zod output schema:
    z.array(z.object({
      date: z.string(),
      claude: z.number(),
      openai: z.number(),
      dataforseo: z.number(),
      serpapi: z.number(),
      hyperbrowser: z.number(),
      firecrawl: z.number(),
      total: z.number(),
    }))
    
  • Auth: adminProcedure.

5. Fetch Frequency:

  • Initial load: On mount.
  • Polling: Every 5 minutes (refetchInterval: 300000).

6. Data Manipulations:

  • Date formatting: date-fns format "dd MMM" from ISO date strings.
  • Cost formatting: $${value.toFixed(2)} in tooltip.
  • Total calculation: sum(claude + openai + dataforseo + serpapi + hyperbrowser + firecrawl) per day.
  • Stacked bar rendering: Recharts stackId="a" on all Bar components.

7. Why Structured This Way:

  • 5-minute refresh: API costs change more frequently than revenue. 5m balances freshness and load.
  • Stacked bar: Shows daily total and provider breakdown simultaneously. Admin sees which provider drives costs.
  • Color coding: Each provider gets a consistent color across all admin screens.
  • Total badge: Header total gives immediate context without reading chart.

8. Interaction Flows:

  • Hover: Tooltip shows provider breakdown. Individual segment highlights on hover.
  • Legend click: Toggles provider visibility. No refetch.
  • Loading: Skeleton rectangle.
  • Error: Centered error with retry.
  • Empty: "No API usage in last 7 days" (rare, but possible for new platforms).

9. Error States:

  • Empty: No API usage → "No API usage recorded".
  • Loading: Skeleton.
  • Error: Centered error with retry.
  • Partial: If one provider missing, show remaining with "Partial data" caption.

10. Role-Based Variations:

  • ADMIN: Full chart, all providers, clickable.
  • Other roles: No access.

Section 2 — Client Management (/admin/clients)#

Role access: ADMIN only. Layout: Data table with filters + action bar + client detail modal/slide-over. Sidebar: carbon (#12161E).


Widget 2.1 — Client Table#

1. Screen Name & Route: /admin/clients — Client List Table. Role: ADMIN only.

2. Data to Show:

  • Columns: name, email, plan (tier), status, revenue, health score, last active, content queue count, NAP mismatch count, actions.
  • Status values: ACTIVE (green/forest-teal), TRIAL (blue/sky-blue), SUSPENDED (yellow/ember-orange), CANCELLED (red/error-red), PENDING (gray/slate).
  • Health score: 0–100, color-coded (>=80 green, 60-79 amber, <60 red).
  • Last active: relative time (e.g., "2 hours ago").
  • Content queue count: number of PENDING_REVIEW items.
  • NAP mismatch count: number of mismatched citations.
  • Actions per row: View (eye icon), Edit (pencil), Message (mail), Suspend (pause), Delete (trash).

3. Display Pattern:

  • Component: DataTable (inherited from design system, enhanced for admin).
  • Layout: Full-width table, no card wrapper (table edge-to-edge in page container).
  • Header row: paper-white bg, caption (12px, weight 500, uppercase, text-tertiary), border-bottom mist 1px.
  • Data rows: card-white bg alternating with paper-white (zebra striping for dense readability), border-bottom mist 1px, hover row-hover (rgba(1,24,33,0.02)).
  • Cell padding: 12px 16px (compact for density).
  • Status badge: radius-full, pill, color-coded per status value.
  • Health score: Circular mini progress (24px diameter), color-coded stroke.
  • Actions: Icon buttons (20px icons), space-2 (8px) gap. Hidden until row hover (desktop) or always visible (mobile).
  • Selection: Checkbox leftmost column. Batch action bar appears above table when items selected.
  • Batch actions: Change plan, Send message, Export CSV, Update status.
  • Pagination: Below table, space-4 (16px) margin-top. Page size: 50/100/200. Default: 50 per page.
  • Sortable: All columns except Actions. Sort icon (arrow up/down) on header. Active sort: text-primary + icon color ember-orange.
  • Filterable: Status dropdown, Plan dropdown, Health score range, NAP mismatch > 0 toggle, Content queue > 0 toggle, Date range (last active), Search (name/email).
  • Groupable: Group by plan tier (collapsible row groups with counts).
  • Column resize: Drag column border to resize. Minimum 80px per column.
  • Column reorder: Drag header to reorder columns. Persisted in localStorage key admin-client-table-columns.
  • Export: "Export CSV" button in batch action bar and table header. Downloads all filtered data.

4. Backend Endpoint:

  • Primary: admin.getClients (tRPC query, adminProcedure).
  • Zod input schema:
    z.object({
      page: z.number().default(1),
      pageSize: z.number().default(50),
      sortBy: z.enum(["name", "email", "plan", "status", "revenue", "healthScore", "lastActive", "contentQueue", "napMismatch"]).default("name"),
      sortOrder: z.enum(["asc", "desc"]).default("asc"),
      filters: z.object({
        status: z.array(z.enum(["ACTIVE", "TRIAL", "SUSPENDED", "CANCELLED", "PENDING"])).optional(),
        plan: z.array(z.enum(["STARTER", "STANDARD", "PREMIUM", "ENTERPRISE"])).optional(),
        healthScoreMin: z.number().optional(),
        healthScoreMax: z.number().optional(),
        hasNapMismatch: z.boolean().optional(),
        hasContentQueue: z.boolean().optional(),
        lastActiveFrom: z.string().datetime().optional(),
        lastActiveTo: z.string().datetime().optional(),
        search: z.string().optional(),
      }).optional(),
    })
    
  • Zod output schema:
    z.object({
      clients: z.array(z.object({
        id: z.string(),
        name: z.string(),
        email: z.string(),
        plan: z.enum(["STARTER", "STANDARD", "PREMIUM", "ENTERPRISE"]),
        status: z.enum(["ACTIVE", "TRIAL", "SUSPENDED", "CANCELLED", "PENDING"]),
        revenue: z.number(),
        healthScore: z.number(),
        lastActive: z.string().datetime(),
        contentQueueCount: z.number(),
        napMismatchCount: z.number(),
        createdAt: z.string().datetime(),
      })),
      totalCount: z.number(),
      totalPages: z.number(),
    })
    
  • Auth: adminProcedure.

5. Fetch Frequency:

  • Initial load: On /admin/clients mount.
  • Polling: Every 60 seconds (refetchInterval: 60000).
  • Filter/sort/page change: Immediate refetch with new params.
  • Background refresh: On window focus.

6. Data Manipulations:

  • Sorting: Client-side sort indicator only. Server-side sort via sortBy + sortOrder params.
  • Filtering: All filters sent to server. Server returns filtered set.
  • Search: Debounced 300ms. Searches name and email fields server-side.
  • Pagination: page and pageSize sent to server. Server returns paginated set.
  • Grouping: Client-side grouping after data loads. Group rows inserted between data rows. Collapsible.
  • Health score color: < 60error-red, 60-79ember-orange, >= 80forest-teal.
  • Revenue formatting: toLocaleString("en-IN", { style: "currency", currency: "INR" }).
  • Last active: date-fns formatDistanceToNow from ISO date.

7. Why Structured This Way:

  • 50 per page default: Admin manages hundreds of clients. 50 is dense but scannable. 200 option available for bulk operations.
  • Zebra striping: Dense tables with 11 columns need visual row separation. Alternating bg prevents eye-tracking errors.
  • Hidden actions until hover: Reduces visual clutter. 11 columns + actions = too much. Hover reveal on desktop.
  • Column resize/reorder: Admin preference varies. Some prioritize health score, others revenue. Persisted in localStorage.
  • Batch actions: Admin frequently updates multiple clients (e.g., plan changes). Selection + batch action bar is essential.
  • Group by plan: Revenue and operations analysis often done by plan tier. Collapsible groups keep table organized.

8. Interaction Flows:

  • Row click: Opens client detail slide-over (right panel, 480px wide).
  • View action: Navigates to /admin/clients/[id] (full page detail).
  • Edit action: Opens inline modal with ClientEditForm.
  • Message action: Opens "Send Message" modal with recipient pre-filled.
  • Suspend action: ConfirmDialog with "Suspend Client" title. On confirm, calls admin.updateClient with status: "SUSPENDED".
  • Delete action: ConfirmDialog with "Delete Client" title. Soft delete only (sets deletedAt). Requires confirmation + reason input.
  • Batch change plan: Select multiple rows → "Change Plan" dropdown in batch bar → select plan → confirm → calls admin.updateClient for each selected client.
  • Batch export: Select rows → "Export CSV" → downloads CSV with all columns.
  • Batch message: Select rows → "Send Message" → modal with recipient list pre-filled.
  • Filter sidebar: Slide-over from right (desktop), bottom sheet (mobile). Filter pills appear above table when active. Removable (X icon).
  • URL sync: Filters, sort, page sync to URL query params (?status=ACTIVE&plan=PREMIUM&page=2). Shareable and back-button supported.

9. Error States:

  • Empty: "No clients found" + "Adjust filters" suggestion + "Clear all filters" button.
  • Loading: Skeleton rows (10 rows, pulsing paper-white to mist).
  • Error: Full-page error state. Icon (alert triangle), title ("Failed to load clients"), description, "Retry" button (primary), "Contact support" link.
  • Partial: If some rows load but one column fails, show available data + inline error banner at top of affected section.
  • Background refetch error: Toast notification (error type), no UI change. Retry on next interval.

10. Role-Based Variations:

  • ADMIN: Full table, all actions, all batch operations, all filters.
  • Other roles: No access. Redirected to /dashboard.

Widget 2.2 — Client Detail Slide-Over#

1. Screen Name & Route: /admin/clients — Client Detail Slide-Over (right panel, 480px wide). Role: ADMIN only.

  • Also accessible at /admin/clients/[id] as full page with tabs.

2. Data to Show:

  • Profile card: name, email, phone, plan, status, join date, health score, last login, device count.
  • Subscription card: plan, price, renewal date, payment method, status, upgrade/downgrade history.
  • Usage stats: API calls (today/7d/30d), storage used, posts created, reviews received, content published, leads captured.
  • Activity log: Time-ordered list of all actions, filterable by type, searchable, exportable. Audit trail.
  • Team members: List with names, roles, last active.
  • Connected accounts: GBP, social platforms, citation directories — each with status.
  • Health score breakdown: NAP, citations, reviews, content, social, site — each sub-score with trend.
  • Impersonation: "View as Client" button.
  • Actions: Edit profile, upgrade/downgrade plan, update status, send message, view full audit log, export data, delete account.

3. Display Pattern:

  • Component: SlideOver (right-aligned panel, max-width 480px, full height).
  • Animation: Slide-in from right, 300ms, easing-standard.
  • Header: Client name (heading, 28px, weight 500) + close button (X, top-right).
  • Body: Scrollable if content exceeds 80vh. Gap: space-6 (24px) between sections.
  • Sections:
    • Profile Card: Avatar (initials or logo), name, email, phone, plan badge, status badge, join date, health score circular progress (48px), last login, device count.
    • Subscription Card: Plan name + price, renewal date, payment method icon + last 4 digits, status badge, upgrade/downgrade history (collapsible list).
    • Usage Stats: 6 mini metric cards in 3-column grid. API calls, storage, posts, reviews, content, leads.
    • Activity Log: Timeline-style list. Timestamp (caption), action (body-sm), actor (caption), result badge. Filter dropdown (action type). Search input. Export button.
    • Team Members: Avatar + name + role badge + last active. Max 5 visible, "Show all" link.
    • Connected Accounts: Icon + platform name + status badge + account name. GBP shows location count.
    • Health Score Breakdown: 6 sub-scores in vertical bar chart (NAP, citations, reviews, content, social, site). Each bar 0–100, color-coded. Trend arrow (up/down) next to each.
  • Action bar at bottom (sticky): "Edit" (secondary), "Impersonate" (accent), "Suspend" (danger), "Delete" (danger ghost).

4. Backend Endpoints:

  • admin.getClientById (query, adminProcedure):
    • Zod input: z.object({ id: z.string() }).
    • Zod output: Full Practice object with all relations (locations, members, gbpAccounts, socialAccounts, citations, subscriptions, invoices).
  • admin.getClientActivity (query, adminProcedure):
    • Zod input: z.object({ clientId: z.string(), page: z.number().default(1), pageSize: z.number().default(20), actionType: z.string().optional() }).
    • Zod output: z.array(AuditLog) with pagination metadata.
  • admin.getClientUsage (query, adminProcedure):
    • Zod input: z.object({ clientId: z.string(), period: z.enum(["today", "7d", "30d"]).default("7d") }).
    • Zod output: Usage stats object.
  • admin.impersonate (mutation, adminProcedure):
    • Zod input: z.object({ clientId: z.string() }).
    • Zod output: z.object({ success: z.boolean(), sessionToken: z.string(), expiresAt: z.string().datetime() }).
  • admin.stopImpersonation (mutation, adminProcedure):
    • Zod input: z.object({}).
    • Zod output: z.object({ success: z.boolean() }).
  • admin.updateClient (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string(), status: z.enum([...]).optional(), plan: z.enum([...]).optional(), name: z.string().optional(), email: z.string().email().optional() }).
    • Zod output: Updated Practice.
  • admin.deleteClient (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string(), reason: z.string().min(1) }).
    • Zod output: z.object({ success: z.boolean(), deletedAt: z.string().datetime() }).
  • admin.sendMessage (mutation, adminProcedure):
    • Zod input: z.object({ clientId: z.string(), subject: z.string(), body: z.string() }).
    • Zod output: z.object({ success: z.boolean(), messageId: z.string() }).
  • admin.exportClientData (mutation, adminProcedure):
    • Zod input: z.object({ clientId: z.string(), format: z.enum(["JSON", "CSV"]).default("JSON") }).
    • Zod output: z.object({ downloadUrl: z.string().url() }).
  • Auth: All adminProcedure.

5. Fetch Frequency:

  • Client detail: On slide-over open / page mount. 5 minutes (refetchInterval: 300000).
  • Activity log: On tab open. 60 seconds (refetchInterval: 60000).
  • Usage stats: On slide-over open. 5 minutes.
  • Connected accounts: On slide-over open. 5 minutes.
  • Health score: On slide-over open. 1 hour (refetchInterval: 3600000).

6. Data Manipulations:

  • Health score color: Per-sub-score, same as main health score.
  • Usage stats aggregation: Sum per period. Formatting per metric type.
  • Activity log filtering: Client-side filter by action type after server fetch. Search is server-side (actor name, action type).
  • Impersonation timer: Client-side countdown from expiresAt. 2-hour default.
  • Export formatting: JSON or CSV generation client-side from fetched data.

7. Why Structured This Way:

  • Slide-over (not modal): Client detail is information-dense. Modal would be too narrow. Slide-over gives 480px of width while keeping context of the list behind.
  • Full page variant: /admin/clients/[id] is for deep-linking and bookmarking. Same data, full page layout with tabs.
  • Activity log in detail: Admin needs audit context per client. Having it in detail saves navigation.
  • Health score breakdown: Sub-scores help diagnose why a client's health is low. Is it NAP? Citations? Content?
  • Impersonation in detail: "View as Client" is a primary admin action. Prominent placement in action bar.
  • Sticky action bar: Actions are always accessible even when scrolling through long activity logs.

8. Interaction Flows:

  • Open: Click row in table → slide-over opens with skeleton. Data loads, skeleton replaced.
  • Close: Click X, click backdrop, or press Escape. Panel slides out (300ms, easing-exit). Focus returns to table row.
  • Impersonate: Click "View as Client" → confirmation modal → admin.impersonate mutation → red banner appears at top of all pages → redirect to client dashboard (/dashboard).
  • Impersonation banner: "Impersonating [Client Name] — [MM:SS remaining] — Exit Impersonation". Red background (error-red at 10% opacity), red text. Always visible. Click "Exit" → admin.stopImpersonation → redirect back to /admin/clients/[id].
  • Edit profile: Click "Edit" → inline form fields appear → save calls admin.updateClient → toast confirmation → refetch detail.
  • Upgrade/downgrade plan: Click "Change Plan" → dropdown → confirm → admin.updateClient → toast → refetch.
  • Send message: Click "Message" → modal with subject/body → send → admin.sendMessage → toast.
  • Export data: Click "Export" → format selector → generate → admin.exportClientData → download link.
  • Delete account: Click "Delete" → confirmation modal + reason textarea → admin.deleteClient → toast → redirect to /admin/clients.
  • View full audit log: Click "View Full Audit Log" → navigates to /admin/audit with client filter pre-applied.

9. Error States:

  • Client not found: "Client not found" error page (404). Link back to /admin/clients.
  • Loading: Skeleton slide-over (pulsing rectangles for each section).
  • Error: Inline error per section. If profile fails, show error in profile card. Other sections load independently.
  • Partial: If activity log fails but profile loads, show profile + error in activity log section.
  • Impersonation error: If admin.impersonate fails, show toast error. Do not redirect.

10. Role-Based Variations:

  • ADMIN: Full detail, all actions, impersonation, delete, export.
  • Other roles: No access. Redirected to /dashboard.
  • Impersonation restrictions: During impersonation, billing changes, account deletion, plan changes are read-only. Buttons disabled with tooltip: "Not available during impersonation".

Section 3 — Workflow Monitor (/admin/workflows)#

Role access: ADMIN only. Layout: Stat cards (top) + job monitor table (middle) + queue management (bottom). Real-time: 10-second polling for jobs and queue depth. Sidebar: carbon (#12161E).


Widget 3.1 — Job Monitor Table#

1. Screen Name & Route: /admin/workflows — Job Monitor Table. Role: ADMIN only.

2. Data to Show:

  • Columns: job ID, type (citation-sync, gbp-post, social-post, content-generate, report-generate, email-send, etc.), status, queue, progress (%), started time, estimated completion, client (practice name), actions.
  • Status values: PENDING (gray/slate), RUNNING (blue/sky-blue), COMPLETED (green/forest-teal), FAILED (red/error-red), RETRYING (yellow/ember-orange), CANCELLED (red/error-red).
  • Progress: 0–100% linear progress bar (3px height) in cell. Color matches status.
  • Job type: Badge with icon. citation-syncLink icon, gbp-postMapPin icon, social-postShare2 icon, content-generateFileText icon, report-generateBarChart3 icon, email-sendMail icon.
  • Queue name: Text label (e.g., "gbp-post-publish", "social-post-publish").
  • Started: Relative time (e.g., "5 min ago") + absolute tooltip.
  • Estimated completion: Time remaining (e.g., "~2 min") or "—" if not estimable.
  • Client: Client name, linked to /admin/clients/[id].
  • Actions: Retry (FAILED only), Cancel (PENDING/RUNNING only), View Logs (modal), Clone (create new job from template).

3. Display Pattern:

  • Component: DataTable (enhanced for real-time).
  • Layout: Full-width table, no card wrapper.
  • Header row: paper-white bg, caption (12px, weight 500, uppercase, text-tertiary), border-bottom mist 1px.
  • Data rows: card-white bg, border-bottom mist 1px, hover row-hover.
  • Cell padding: 12px 16px.
  • Status badge: radius-full, pill, color-coded per status.
  • Progress bar: Inline in cell. 3px height, ember-orange fill for RUNNING, forest-teal for COMPLETED, error-red for FAILED.
  • Job type badge: Icon + text, radius-full, slate bg at 8% opacity, graphite text.
  • Actions: Icon buttons. Retry (circular arrow, forest-teal), Cancel (X, error-red), View Logs (document, midnight-teal), Clone (copy, slate).
  • Auto-refresh indicator: Small pulsing dot (forest-teal) next to table header. "Live — updating every 10s" caption.
  • Filter bar: Above table. Status multi-select, Queue dropdown, Type dropdown, Client dropdown, Date range picker.
  • Search: By job ID or client name. Debounced 300ms.
  • Queue depth indicator: Gauge widget above table. Warning >100 (yellow), Alert >500 (orange), Critical >1000 (red).
  • Failure rate indicator: Gauge widget above table. Warning >5%, Alert >15%, Critical >30%.
  • Job type distribution: Pie chart (by count) + bar chart (by execution time) in side panel (collapsible).

4. Backend Endpoints:

  • admin.getJobs (query, adminProcedure):
    • Zod input:
      z.object({
        page: z.number().default(1),
        pageSize: z.number().default(50),
        status: z.array(z.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "RETRYING", "CANCELLED"])).optional(),
        queue: z.string().optional(),
        type: z.string().optional(),
        clientId: z.string().optional(),
        dateFrom: z.string().datetime().optional(),
        dateTo: z.string().datetime().optional(),
        search: z.string().optional(),
      })
      
    • Zod output:
      z.object({
        jobs: z.array(z.object({
          id: z.string(),
          type: z.string(),
          status: z.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "RETRYING", "CANCELLED"]),
          queue: z.string(),
          progress: z.number().min(0).max(100),
          startedAt: z.string().datetime(),
          estimatedCompletion: z.string().datetime().nullable(),
          clientId: z.string(),
          clientName: z.string(),
          duration: z.number().nullable(), // seconds
          cost: z.number().nullable(), // USD
          attempts: z.number().default(1),
        })),
        totalCount: z.number(),
        totalPages: z.number(),
      })
      
  • admin.getQueueDepth (query, adminProcedure):
    • Zod input: z.object({ queue: z.string().optional() }).
    • Zod output: z.array(z.object({ name: z.string(), waiting: z.number(), active: z.number(), completed: z.number(), failed: z.number() })).
  • admin.retryJob (mutation, adminProcedure):
    • Zod input: z.object({ jobId: z.string() }).
    • Zod output: z.object({ success: z.boolean(), newJobId: z.string().nullable() }).
  • admin.cancelJob (mutation, adminProcedure):
    • Zod input: z.object({ jobId: z.string() }).
    • Zod output: z.object({ success: z.boolean() }).
  • admin.getJobLogs (query, adminProcedure):
    • Zod input: z.object({ jobId: z.string() }).
    • Zod output: z.object({ stdout: z.string(), stderr: z.string(), timestamps: z.array(z.string()) }).
  • Auth: All adminProcedure.

5. Fetch Frequency:

  • Jobs table: 10 seconds (refetchInterval: 10000). Real-time.
  • Queue depth: 10 seconds (refetchInterval: 10000). Real-time.
  • Job type distribution: 5 minutes (refetchInterval: 300000).
  • Failure rate: 10 seconds (derived from queue depth data).

6. Data Manipulations:

  • Status filtering: Multi-select. Server-side.
  • Queue filtering: Dropdown of all queue names. Server-side.
  • Type filtering: Dropdown of job types. Server-side.
  • Client filtering: PracticeSelect dropdown. Server-side.
  • Date range: Start/end date picker. Server-side.
  • Search: Job ID or client name. Server-side.
  • Progress: Rendered as percentage. Math.round(progress).
  • Duration: duration ? ${duration.toFixed(1)}s : "—".
  • Cost: cost ? $${cost.toFixed(2)} : "—".
  • Queue depth gauge: waiting count. Color thresholds: <100 neutral, 100-499 warning, 500-999 alert, >=1000 critical.
  • Failure rate: failed / (completed + failed) * 100. Rolling 1-hour window.
  • Job type distribution: Client-side aggregation from job list data.

7. Why Structured This Way:

  • 10-second polling: Job status changes rapidly. Admin needs near-real-time visibility for operational decisions. WebSocket not required — 10s HTTP polling is sufficient and simpler.
  • Progress bar in cell: Immediate visual feedback on job progress. No need to open detail to see progress.
  • Queue depth gauge: Global queue health at a glance. Admin knows if workers are overwhelmed.
  • Failure rate gauge: Critical operational metric. If failure rate spikes, admin needs to act immediately.
  • Job type icons: Visual recognition. Admin scans for specific job types quickly.
  • Retry/Cancel actions: Primary operational actions. Must be one-click accessible.
  • Auto-refresh indicator: Builds trust. Admin knows data is live.

8. Interaction Flows:

  • Retry (FAILED): Click retry icon → admin.retryJob mutation → toast "Job queued for retry" → table updates on next poll (status changes to PENDING or RETRYING).
  • Cancel (PENDING/RUNNING): Click cancel icon → ConfirmDialogadmin.cancelJob → toast "Job cancelled" → table updates on next poll (status changes to CANCELLED).
  • View Logs: Click logs icon → modal opens with full log output (see Widget 3.2).
  • Clone: Click clone icon → "Create Job" modal pre-filled with original job's type, queue, client, and payload. Editable. Submit creates new job.
  • Row click: Opens job detail slide-over (right panel, 480px). Shows full payload, result, logs, and retry history.
  • Filter bar: Slide-over from right (desktop). Filter pills appear above table. Removable.
  • URL sync: Filters, sort, page sync to URL query params.

9. Error States:

  • Empty: "No jobs in queue" + "All workflows completed" message.
  • Loading: Skeleton rows (10 rows) + pulsing auto-refresh indicator.
  • Error: Inline error banner at top of table. "Failed to load jobs. Retrying in 10s..." with spinner. Auto-retry on next poll.
  • Partial: If queue depth loads but job list fails, show gauge + error in table area.
  • Background refetch error: Toast notification. No UI change. Retry on next interval.

10. Role-Based Variations:

  • ADMIN: Full table, all actions, retry, cancel, clone, logs.
  • Other roles: No access.
  • Impersonation (admin): Job monitor is read-only during impersonation. Retry/cancel/clone disabled. Tooltip: "Not available during impersonation".

Widget 3.2 — Job Log Viewer#

1. Screen Name & Route: /admin/workflows — Job Log Viewer (modal). Role: ADMIN only.

2. Data to Show:

  • Full log output (stdout + stderr), timestamped lines.
  • Color-coded: info = midnight-teal, warning = ember-orange, error = error-red, success = forest-teal.
  • Search within logs: text search, highlight matches.
  • Download: raw log as text file.
  • Auto-refresh: 10 seconds for running jobs. Stops when job status is COMPLETED, FAILED, or CANCELLED.

3. Display Pattern:

  • Component: Modal (large, 960px max-width, 80vh height).
  • Header: "Job Logs — [Job ID]" (heading-sm, 24px, weight 500) + close button + download button + auto-refresh toggle.
  • Body: Monospace font (Suisse Intl Mono or fallback), 13px, line-height 1.5. Scrollable.
  • Log lines: Timestamp (caption, 11px, slate) + level badge + message.
  • Level badges: INFO (midnight-teal bg), WARN (ember-orange bg), ERROR (error-red bg), SUCCESS (forest-teal bg). All with radius-sm.
  • Search bar: Sticky at top of modal body. Input with search icon. Highlight matches in ember-orange background. "X / Y matches" counter.
  • Download button: Ghost button, download icon. Triggers text file download.
  • Auto-refresh toggle: Switch. On by default for RUNNING jobs. Off for terminal statuses.

4. Backend Endpoint:

  • admin.getJobLogs (query, adminProcedure):
    • Zod input: z.object({ jobId: z.string() }).
    • Zod output:
      z.object({
        jobId: z.string(),
        logs: z.array(z.object({
          timestamp: z.string(),
          level: z.enum(["INFO", "WARN", "ERROR", "SUCCESS"]),
          message: z.string(),
        })),
        stdout: z.string(),
        stderr: z.string(),
      })
      
  • Auth: adminProcedure.

5. Fetch Frequency:

  • Initial load: On modal open.
  • Auto-refresh: 10 seconds for RUNNING jobs. refetchInterval: 10000.
  • Stops: When job status changes to terminal (COMPLETED, FAILED, CANCELLED), auto-refresh stops. Toggle disabled.

6. Data Manipulations:

  • Search: Client-side text search in logs array. message.toLowerCase().includes(query.toLowerCase()).
  • Highlight: Matching text wrapped in <mark> with ember-orange bg.
  • Download: Client-side text file generation from logs array. Format: [timestamp] [level] message.
  • Color coding: Based on level field.

7. Why Structured This Way:

  • Large modal: Log output can be verbose. 960px width and 80vh height give ample reading space.
  • Monospace font: Log output is structured. Monospace preserves alignment and makes timestamps readable.
  • Color-coded levels: Immediate visual parsing. Admin spots errors in long logs.
  • Search within logs: Essential for debugging. Logs can be thousands of lines.
  • Auto-refresh for running jobs: Admin watches live job execution. No manual refresh needed.
  • Download: Admin may need to share logs with engineering or save for post-mortem.

8. Interaction Flows:

  • Open: Click "View Logs" in job table → modal opens → admin.getJobLogs query → logs render.
  • Close: Click X, click backdrop, or press Escape. Focus returns to "View Logs" button.
  • Search: Type in search bar → matches highlighted in real-time. Counter updates.
  • Download: Click download → browser downloads job-[id]-logs.txt.
  • Auto-refresh toggle: Click switch → toggles polling. If on, logs append new lines. If off, static.

9. Error States:

  • Empty logs: "No logs available for this job" (rare, but possible for very new jobs).
  • Loading: Skeleton log lines (10 lines of pulsing rectangles).
  • Error: "Failed to load logs" with retry button.
  • Partial: If stdout loads but stderr fails, show stdout + error for stderr.

10. Role-Based Variations:

  • ADMIN: Full logs, search, download, auto-refresh.
  • Other roles: No access.

Widget 3.3 — Queue Management#

1. Screen Name & Route: /admin/workflows — Queue Management (bottom section). Role: ADMIN only.

2. Data to Show:

  • Queue list: name, depth (waiting count), processing rate (jobs/min), failure rate (%), priority, status (active/paused), actions.
  • Actions: Pause, Resume, Clear.
  • Pause/Resume: Stops/starts processing for a queue. Shows status badge.
  • Clear: Removes all pending jobs (with confirmation). Completed jobs archived.
  • Priority adjustment: Drag-and-drop queue ordering, or numeric priority input.

3. Display Pattern:

  • Component: DataTable + QueueGauge + QueuePriorityList.
  • Layout: 2-column grid (desktop): queue table (left, 2/3) + priority list (right, 1/3). Stacked on mobile.
  • Queue table: Name, depth, processing rate, failure rate, status, actions.
  • Depth gauge: Inline bar. mist track, ember-orange fill. Width proportional to depth / 1000 (max).
  • Status badge: ACTIVE (green), PAUSED (amber).
  • Actions: Pause/Resume toggle button, Clear button (danger ghost).
  • Priority list: Drag-and-drop list. Each item: queue name, priority number (editable input), drag handle.
  • Confirmation modal for Clear: "Are you sure? This will remove all pending jobs from [queue name]. Completed jobs will be archived." + confirmation text input (type queue name to confirm).

4. Backend Endpoints:

  • admin.getQueues (query, adminProcedure):
    • Zod input: z.object({}).
    • Zod output:
      z.array(z.object({
        name: z.string(),
        depth: z.number(),
        processingRate: z.number(), // jobs per minute
        failureRate: z.number(), // percentage
        priority: z.number(),
        status: z.enum(["ACTIVE", "PAUSED"]),
      }))
      
  • admin.pauseQueue (mutation, adminProcedure):
    • Zod input: z.object({ name: z.string() }).
    • Zod output: z.object({ success: z.boolean(), status: z.literal("PAUSED") }).
  • admin.resumeQueue (mutation, adminProcedure):
    • Zod input: z.object({ name: z.string() }).
    • Zod output: z.object({ success: z.boolean(), status: z.literal("ACTIVE") }).
  • admin.clearQueue (mutation, adminProcedure):
    • Zod input: z.object({ name: z.string(), confirm: z.literal(true) }).
    • Zod output: z.object({ success: z.boolean(), clearedCount: z.number() }).
  • Auth: All adminProcedure.

5. Fetch Frequency:

  • Queue data: 10 seconds (refetchInterval: 10000). Real-time.
  • Priority list: On load + after drag-and-drop. No polling needed.

6. Data Manipulations:

  • Depth gauge: (depth / 1000 * 100).toFixed(0) for percentage width. Max 100%.
  • Processing rate: ${processingRate.toFixed(1)} jobs/min.
  • Failure rate: ${failureRate.toFixed(1)}%.
  • Priority: Editable number input. Client-side drag-and-drop reordering updates priority values (1, 2, 3...). Save sends to server.
  • Drag-and-drop: Using @dnd-kit/sortable or similar. Visual feedback during drag.

7. Why Structured This Way:

  • Real-time queue depth: Admin needs to see if queues are backing up. 10s polling matches job monitor.
  • Depth gauge: Visual representation of queue depth. Admin sees at a glance which queues are overloaded.
  • Pause/Resume: Emergency control. If a queue is causing problems, admin can pause it immediately.
  • Clear with confirmation: Destructive action. Typing queue name prevents accidental clears.
  • Priority drag-and-drop: Visual and intuitive. Admin reorders by priority. Lower number = higher priority.

8. Interaction Flows:

  • Pause: Click "Pause" → admin.pauseQueue → status changes to PAUSED → badge changes to amber → processing stops.
  • Resume: Click "Resume" → admin.resumeQueue → status changes to ACTIVE → badge changes to green.
  • Clear: Click "Clear" → confirmation modal (type queue name) → admin.clearQueue → toast "Cleared N jobs" → depth drops to 0.
  • Priority drag: Drag queue item → drop → priority numbers update → save button appears → click "Save Priority" → server updated.
  • Priority input: Edit number directly → save.

9. Error States:

  • Empty: "No queues configured" (rare, indicates system misconfiguration).
  • Loading: Skeleton table.
  • Error: Inline error per queue row. If one queue fails, others show data.
  • Partial: If depth loads but processing rate fails, show depth + "—" for rate.

10. Role-Based Variations:

  • ADMIN: Full control, pause, resume, clear, priority.
  • Other roles: No access.
  • Impersonation: Queue management is read-only during impersonation. Actions disabled.