Frontend Specs
RankFlow AI — Admin Dashboard Frontend Specification
Document Version: 1.0.0
docs/specs/frontend/admin-spec-01-full.mdOn this page
- Document Overview
- Section 1 — Admin KPI Overview (/admin/kpi)
- Widget 1.1 — KPI Card Grid
- Widget 1.2 — Alert Summary Panel
- Widget 1.3 — Revenue Trend Chart
- Widget 1.4 — Client Status Distribution Chart
- Widget 1.5 — API Cost Breakdown Chart
- Section 2 — Client Management (/admin/clients)
- Widget 2.1 — Client Table
- Widget 2.2 — Client Detail Slide-Over
- Section 3 — Workflow Monitor (/admin/workflows)
- Widget 3.1 — Job Monitor Table
- Widget 3.2 — Job Log Viewer
- Widget 3.3 — Queue Management
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
#EC652Bis ONLY for: primary action buttons, CTA links, focus rings, positive outcome indicators. Never for text, backgrounds, error states, or status badges. - Page max-width:
1200pxcentered. Canvas background: Paper White#F6F6F8. - Sidebar:
240pxwide. Client usesdeep-ink(#011821). Admin usescarbon(#12161E) for visual distinction. - Section gap:
80pxbetween 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
/dashboardwith 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, padding20px(compact vs client24px). - Header: Label (
caption, 12px,text-tertiary, uppercase) left-aligned. Threshold badge right-aligned if crossed. - Body: Value (
headingorheading-lg, weight 500,text-primary). - Footer: Sparkline (mini line chart, 60px wide, 30px tall) or comparison text.
- Threshold badge: If warning →
ember-orangedot + "Warning". If alert →error-reddot + "Alert". If critical →error-redbadge + "Critical". - Left border accent: If threshold crossed, left border 3px in threshold color (
ember-orangefor warning,error-redfor alert/critical). - Status colors:
forest-tealfor positive,error-redfor negative,ember-orangefor warning,slatefor 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— requiresrole === "ADMIN". Returns403 FORBIDDENotherwise.
5. Fetch Frequency:
- Initial load: On
/admin/kpimount. - Polling: Every 60 seconds (
staleTime: 60s,refetchInterval: 60s). - Background refresh: On window focus (if
staleTimeexceeded). - Manual refresh: "Refresh All" button triggers
utils.invalidate()for all dashboard queries.
6. Data Manipulations:
avgRevenuePerClient:totalMonthlyRevenue / activeClients→ formatted totoLocaleString("en-IN").monthlyGrowthRate:((currentMRR - previousMRR) / previousMRR * 100).toFixed(1).ltv:avgRevenuePerClient * avgClientLifespanMonths→ formatted totoLocaleString("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-INuses lakhs/crores. - No bold weight: Brand constraint.
heading-lgat 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/clientswith 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
trendDatais missing butvaluepresent, 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
/dashboardwith 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, padding24px. - 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:
warning→rgba(236,101,43,0.12)bg,ember-orangetext.alert→rgba(236,101,43,0.20)bg,ember-orangetext.critical→rgba(194,68,42,0.12)bg,error-redtext. - 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/kpimount. - Polling: Every 60 seconds.
- Auto-dismiss: Client-side removal when
statuschanges toresolvedon next fetch.
6. Data Manipulations:
- Sorting: By severity (critical first, then alert, then warning), then by
createdAtDESC. - 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/alertswith 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-tealcheck 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, padding24px. - Header: "Revenue Trend" (
subheading, 20px, weight 500) + period selector (dropdown: 6m, 12m, 24m). - Chart colors: MRR line
deep-indigo(#111A4A), ARR linesky-blue(#7EA7E9), churn lineerror-red(#C2442A). - Fill: MRR area fill opacity 12%
deep-indigo. - Grid: Horizontal
mistlines only. No vertical grid. - Axis text:
caption(12px,text-tertiary). - Tooltip:
card-whitebg,shadow-sm,radius-lg, padding 12px. - Legend:
captionsize, 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-fnsformat"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
periodparameter. - 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, padding24px. - Header: "Client Status" (
subheading, 20px, weight 500). - Colors: ACTIVE
forest-teal, TRIALsky-blue, SUSPENDEDember-orange, CANCELLEDerror-red, PENDINGslate. - Labels: Outside segments with connector lines. Shows count + percentage.
- Legend: Below chart, horizontal,
captionsize. - Center donut hole: Total client count (
heading, 28px, weight 500).
4. Backend Endpoint:
- Primary:
admin.getKPIs(same as Widget 1.1, fieldclientStatusDistribution). - 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/clientswith 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, padding24px. - Header: "API Cost Breakdown (7 Days)" (
subheading, 20px, weight 500) + total cost badge. - Colors: Claude
deep-indigo, OpenAIsky-blue, DataForSEOforest-teal, SerpAPIlavender, Hyperbrowserember-orange, Firecrawlmint. - Bar radius: 2px top corners.
- Bar gap: 8px between bars.
- Tooltip:
card-whitebg, shows provider breakdown per day. - Legend: Horizontal,
captionsize, 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-fnsformat"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 allBarcomponents.
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_REVIEWitems. - 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-whitebg,caption(12px, weight 500, uppercase,text-tertiary), border-bottommist1px. - Data rows:
card-whitebg alternating withpaper-white(zebra striping for dense readability), border-bottommist1px, hoverrow-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 colorember-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
localStoragekeyadmin-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/clientsmount. - 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+sortOrderparams. - Filtering: All filters sent to server. Server returns filtered set.
- Search: Debounced 300ms. Searches
nameandemailfields server-side. - Pagination:
pageandpageSizesent to server. Server returns paginated set. - Grouping: Client-side grouping after data loads. Group rows inserted between data rows. Collapsible.
- Health score color:
< 60→error-red,60-79→ember-orange,>= 80→forest-teal. - Revenue formatting:
toLocaleString("en-IN", { style: "currency", currency: "INR" }). - Last active:
date-fnsformatDistanceToNowfrom 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:
ConfirmDialogwith "Suspend Client" title. On confirm, callsadmin.updateClientwithstatus: "SUSPENDED". - Delete action:
ConfirmDialogwith "Delete Client" title. Soft delete only (setsdeletedAt). Requires confirmation + reason input. - Batch change plan: Select multiple rows → "Change Plan" dropdown in batch bar → select plan → confirm → calls
admin.updateClientfor 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-whitetomist). - 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
Practiceobject with all relations (locations,members,gbpAccounts,socialAccounts,citations,subscriptions,invoices).
- Zod input:
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.
- Zod input:
admin.getClientUsage(query,adminProcedure):- Zod input:
z.object({ clientId: z.string(), period: z.enum(["today", "7d", "30d"]).default("7d") }). - Zod output: Usage stats object.
- Zod input:
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() }).
- Zod input:
admin.stopImpersonation(mutation,adminProcedure):- Zod input:
z.object({}). - Zod output:
z.object({ success: z.boolean() }).
- Zod input:
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.
- Zod input:
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() }).
- Zod input:
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() }).
- Zod input:
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() }).
- Zod input:
- 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.impersonatemutation → 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-redat 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/auditwith 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.impersonatefails, 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-sync→Linkicon,gbp-post→MapPinicon,social-post→Share2icon,content-generate→FileTexticon,report-generate→BarChart3icon,email-send→Mailicon. - 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-whitebg,caption(12px, weight 500, uppercase,text-tertiary), border-bottommist1px. - Data rows:
card-whitebg, border-bottommist1px, hoverrow-hover. - Cell padding:
12px 16px. - Status badge:
radius-full, pill, color-coded per status. - Progress bar: Inline in cell. 3px height,
ember-orangefill for RUNNING,forest-tealfor COMPLETED,error-redfor FAILED. - Job type badge: Icon + text,
radius-full,slatebg at 8% opacity,graphitetext. - 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(), })
- Zod input:
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() })).
- Zod input:
admin.retryJob(mutation,adminProcedure):- Zod input:
z.object({ jobId: z.string() }). - Zod output:
z.object({ success: z.boolean(), newJobId: z.string().nullable() }).
- Zod input:
admin.cancelJob(mutation,adminProcedure):- Zod input:
z.object({ jobId: z.string() }). - Zod output:
z.object({ success: z.boolean() }).
- Zod input:
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()) }).
- Zod input:
- 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:
waitingcount. 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.retryJobmutation → toast "Job queued for retry" → table updates on next poll (status changes to PENDING or RETRYING). - Cancel (PENDING/RUNNING): Click cancel icon →
ConfirmDialog→admin.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 Monoor fallback), 13px, line-height 1.5. Scrollable. - Log lines: Timestamp (
caption, 11px,slate) + level badge + message. - Level badges:
INFO(midnight-tealbg),WARN(ember-orangebg),ERROR(error-redbg),SUCCESS(forest-tealbg). All withradius-sm. - Search bar: Sticky at top of modal body. Input with search icon. Highlight matches in
ember-orangebackground. "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(), })
- Zod input:
- 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
logsarray.message.toLowerCase().includes(query.toLowerCase()). - Highlight: Matching text wrapped in
<mark>withember-orangebg. - Download: Client-side text file generation from
logsarray. Format:[timestamp] [level] message. - Color coding: Based on
levelfield.
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.getJobLogsquery → 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.
misttrack,ember-orangefill. 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"]), }))
- Zod input:
admin.pauseQueue(mutation,adminProcedure):- Zod input:
z.object({ name: z.string() }). - Zod output:
z.object({ success: z.boolean(), status: z.literal("PAUSED") }).
- Zod input:
admin.resumeQueue(mutation,adminProcedure):- Zod input:
z.object({ name: z.string() }). - Zod output:
z.object({ success: z.boolean(), status: z.literal("ACTIVE") }).
- Zod input:
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() }).
- Zod input:
- 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/sortableor 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.