Browse documentation

Frontend Specs

Section 9 — Audit & Logs (`/admin/audit`)

Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard.

docs/specs/frontend/admin-spec-03c-audit.md
On this page

Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard. Layout: 2-tab interface: Audit Trail, System Logs. 1200px max-width. Sidebar: carbon (#12161E) background. Notes: All audit events are immutable and retained for 90 days (configurable). System logs retained for 30 days. Export available for both.


9.1 Audit Trail Table#

1. Purpose#

Immutable log of every significant action taken by any user on the platform. Admins trace "who did what, when, from where." Used for compliance, security investigations, and debugging.

2. Visual Layout#

Section A: Filters Bar (top, full width)

  • Date range: "Today", "Last 7 days", "Last 30 days", "Custom" (default: Last 7 days)
  • User filter: Searchable multi-select dropdown (all users, sorted by most recent activity)
  • Role filter: Multi-select (ADMIN, CLIENT, EDITOR, VIEWER, SYSTEM)
  • Action type filter: Multi-select dropdown (login, logout, create, update, delete, export, impersonate, etc. — 20+ types)
  • Entity filter: Select (Client, Content, Lead, User, Config, Alert, etc.)
  • Search: "Search by IP, user agent, or description"
  • "Export CSV" button (right-aligned)
  • "Export JSON" button (secondary, for programmatic analysis)

Section B: Audit Table (below, full width)

  • Data table with 8 columns (horizontal scroll on mobile):
    • Timestamp: text-sm text-muted, ISO format or relative (toggle in header). "2026-06-15 14:32:05 IST" or "2 min ago"
    • User: Avatar (24px) + name + role badge (e.g., "John Doe" + ADMIN pill)
    • Action: Badge with action type. CREATE (green), UPDATE (blue), DELETE (red), LOGIN (gray), EXPORT (amber), IMPERSONATE (purple)
    • Entity: Entity type + name. e.g., "Client: Acme Corp" or "Content: Blog Post #123"
    • Description: One-line summary. "Updated API key for OpenAI integration" or "Approved blog post for Dr. Smith"
    • IP Address: text-sm mono font. 127.0.0.1 or 203.0.113.45
    • User Agent: Truncated browser string. "Chrome 125 / macOS" (parsed from UA string)
    • Details: "View" link → expands row or opens modal
  • Row expansion: Click "View" → row expands below showing full JSON diff (before/after for updates) or full payload (for creates/deletes). JSON formatted with syntax highlighting (dark background, bg-gray-900, text-green-400 for additions, text-red-400 for deletions).
  • Impersonation indicator: If action taken during impersonation, row has a purple left border + IMPERSONATING badge next to user name.
  • System actions: SYSTEM role shows robot icon instead of avatar + SYSTEM badge in gray.
  • Pagination: 50 rows per page. Pagination controls at bottom: "Previous | 1 2 3 ... 10 | Next" + "Showing 1–50 of 1,247 events".

Section C: Stats Bar (above table)

  • Horizontal row: "Total events: {N}" | "By users: {N}" | "By system: {N}" | "Impersonation events: {N}" | "Failed actions: {N}"
  • "Failed actions" in red if > 0.

3. Data Source (tRPC endpoint)#

admin.getAuditTrail.useQuery({
  dateRange: z.object({ from: z.date(), to: z.date() }),
  userIds: z.array(z.string().uuid()).optional(),
  roles: z.array(z.enum(["ADMIN", "CLIENT", "EDITOR", "VIEWER", "SYSTEM"])).optional(),
  actionTypes: z.array(z.string()).optional(), // e.g., ["CREATE", "UPDATE", "DELETE"]
  entityTypes: z.array(z.string()).optional(), // e.g., ["CLIENT", "CONTENT", "USER"]
  search: z.string().optional(),
  page: z.number().min(1).default(1),
  pageSize: z.number().min(1).max(100).default(50),
}, { refetchInterval: 30000 }); // 30s frequent

admin.exportAuditTrail.useMutation({
  dateRange: z.object({ from: z.date(), to: z.date() }),
  format: z.enum(["CSV", "JSON"]).default("CSV"),
  filters: z.object({ /* same as query filters */ }).optional(),
});

4. Zod Schema#

const AuditActionTypeSchema = z.enum([
  "CREATE", "UPDATE", "DELETE", "LOGIN", "LOGOUT",
  "EXPORT", "IMPORT", "IMPERSONATE", "END_IMPERSONATE",
  "APPROVE", "REJECT", "PUBLISH", "UNPUBLISH",
  "SCHEDULE", "CANCEL", "PAUSE", "RESUME",
  "ROTATE_KEY", "TOGGLE_FEATURE", "TOGGLE_MAINTENANCE",
  "ACKNOWLEDGE_ALERT", "RESOLVE_ALERT", "TEST_CONNECTION",
  "SEND_EMAIL", "BULK_ACTION", "CONFIG_CHANGE",
]);

const AuditEntityTypeSchema = z.enum([
  "CLIENT", "CONTENT", "LEAD", "USER", "CONFIG",
  "ALERT", "INTEGRATION", "BILLING", "SUBSCRIPTION",
  "PROMPT", "BLOG_POST", "WORKFLOW", "QUEUE",
  "SYSTEM", "API_KEY",
]);

const AuditTrailItemSchema = z.object({
  id: z.string().uuid(),
  timestamp: z.date(),
  userId: z.string().uuid().optional(), // null for SYSTEM
  userName: z.string().optional(),
  userRole: z.enum(["ADMIN", "CLIENT", "EDITOR", "VIEWER", "SYSTEM"]).optional(),
  userAvatar: z.string().url().optional(),
  action: AuditActionTypeSchema,
  entityType: AuditEntityTypeSchema,
  entityId: z.string().optional(),
  entityName: z.string().optional(),
  description: z.string(),
  ipAddress: z.string().ip().optional(),
  userAgent: z.string().optional(),
  impersonatingUserId: z.string().uuid().optional(), // if action during impersonation
  impersonatingUserName: z.string().optional(),
  payload: z.record(z.any()).optional(), // full before/after diff for updates
  before: z.record(z.any()).optional(),
  after: z.record(z.any()).optional(),
  success: z.boolean().default(true),
  errorMessage: z.string().optional(),
  metadata: z.object({
    requestId: z.string().optional(),
    sessionId: z.string().optional(),
    durationMs: z.number().optional(),
  }).optional(),
});

const AuditTrailResponseSchema = z.object({
  items: z.array(AuditTrailItemSchema),
  total: z.number(),
  userCount: z.number(),
  systemCount: z.number(),
  impersonationCount: z.number(),
  failedCount: z.number(),
  hasMore: z.boolean(),
});

5. Fetch Frequency#

  • Frequent (30000ms / 30s): Audit trail grows continuously. 30s polling keeps it fresh for real-time monitoring.
  • On demand: Filter changes, pagination, export, row expansion.
  • SSE: When new audit event occurs (any action by any user), push to table if it matches current filters. If table is on page 1, prepend new row with highlight animation. If on another page, show "New events" badge on page 1 link.

6. Data Manipulations#

  • Timestamp formatting: Default ISO YYYY-MM-DD HH:mm:ss with timezone. Toggle to relative: "2 min ago".
  • User agent parsing: Raw UA → parsed browser/OS. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..." → "Chrome 125 / macOS". Unknown → "Unknown browser".
  • Action badge colors:
    • CREATE, APPROVE, PUBLISH, RESUME: bg-emerald-50 + text-emerald-700
    • UPDATE, SCHEDULE, CONFIG_CHANGE: bg-blue-50 + text-blue-700
    • DELETE, REJECT, UNPUBLISH, CANCEL: bg-red-50 + text-red-700
    • LOGIN, LOGOUT: bg-gray-50 + text-gray-700
    • EXPORT, IMPORT, BULK_ACTION: bg-amber-50 + text-amber-700
    • IMPERSONATE, END_IMPERSONATE: bg-purple-50 + text-purple-700
  • Diff rendering: For UPDATE actions, render before/after JSON side-by-side in expanded row. Changes highlighted: green for additions, red for deletions, amber for modifications.
  • Impersonation highlight: Purple left border (4px) + IMPERSONATING badge. Hover shows tooltip: "Action performed while impersonating {clientName} ({clientEmail})".
  • Failed action: Row background bg-red-50 + red left border + FAILED badge. Description strikethrough with error message below.
  • IP geolocation: If IP is not localhost, show country flag emoji (🇮🇳, 🇺🇸, etc.) next to IP. Uses free IP geolocation API or pre-computed data.
  • Pagination: 50 per page. If total > 1000, show "1 2 3 ... 98 99 100" style pagination with ellipsis.

7. Rationale#

  • Comprehensive audit trail: Every action, every user, every entity. This is the compliance backbone. If a client asks "who published my blog post without approval?" the answer is in this table.
  • 30s polling: Audit events are frequent (every user action generates one). 30s keeps the feed fresh without overwhelming the server.
  • JSON diff for updates: Seeing "API key changed" is vague. Seeing the old key masked and the new key masked with timestamp is actionable. Full JSON diff for complex updates (config changes, entity edits).
  • Impersonation tracking: Critical for security. If an admin impersonates a client and deletes content, the audit trail shows both the admin's identity and the impersonated client's identity. No repudiation.
  • Failed action logging: Failed logins, failed exports, failed API calls — all logged. Security team can detect brute force or abuse patterns.
  • User agent parsing: Detects suspicious logins (e.g., admin logging in from an unknown browser or OS). Flagged if different from normal pattern.
  • Geolocation: Flags logins from unexpected countries. If admin always logs in from India and suddenly from Brazil, that's a red flag.
  • Export: Compliance requirements often need audit trails exported for auditors. CSV for humans, JSON for programmatic analysis.
  • System actions: Background jobs, scheduled tasks, automated processes — all logged as SYSTEM so admins can trace automated changes (e.g., "System cancelled expired trial at 00:00").

8. Interaction Flows#

  • Filter change: Change date range or user → table reloads with loader → URL updates with query params (shareable).
  • Row expand: Click "View" → row expands with JSON diff. Click again or "Collapse" → row collapses.
  • Export CSV: Click "Export CSV" → mutation → returns download URL → browser downloads. If large date range, shows "Preparing export..." spinner for up to 5 seconds.
  • Export JSON: Same as CSV but JSON format. Includes full payload objects.
  • User drill-down: Click user name → navigates to /admin/clients (if client) or user detail modal (if admin). Shows filtered audit trail for that user.
  • Entity drill-down: Click entity name → navigates to entity detail (e.g., /dashboard/content/[id] for content). If entity was deleted, shows "Entity deleted" toast.
  • Impersonation hover: Hover over IMPERSONATING badge → tooltip shows full impersonation details: admin name, client name, session start time, session duration.
  • Failed action expand: Click "View" on failed row → shows error message + stack trace (if available) + request ID for debugging.
  • Real-time new row: If new audit event matches filters and user is on page 1, new row appears at top with subtle fade-in animation (green pulse for 2 seconds).
  • Impersonation: Read-only. No actions to take on audit trail. Can view, filter, export, expand. All existing data accessible.

9. Error States#

  • Loading: Skeleton table (10 rows with shimmer).
  • Empty (filtered): "No audit events match your filters. Try expanding the date range or clearing filters."
  • Empty (no data): "No audit events recorded yet. Activity will appear here as users interact with the platform."
  • Export fail: Toast "Export failed. Date range too large — max 90 days per export." or "Export failed. Try again in 30 seconds."
  • Large dataset: If > 10,000 events in selected range, banner: "Large dataset detected. Consider narrowing date range for faster loading." with "Load anyway" button.
  • SSE disconnect: Subtle indicator: "Live updates paused" → auto-reconnect.
  • Permission denied: Non-ADMIN never reaches this page (redirected at route level).

10. Role-Based Variations#

  • ADMIN: Full access — filter, view, export, expand, drill-down.
  • Other roles: No access.
  • Impersonation: Read-only. Full audit trail visible. Cannot export (button disabled). Cannot modify or delete audit events (immutable by design).

9.2 System Log Viewer#

1. Purpose#

View and search raw application logs (structured logging, not just text). Admins debug production issues, trace error cascades, and monitor application health. Logs are structured JSON from the backend (Pino, Winston, or similar).

2. Visual Layout#

Section A: Log Level Filter (top, full width)

  • Toggle group: ERROR (red) | WARN (amber) | INFO (blue) | DEBUG (gray) | TRACE (light gray)
  • Default: ERROR + WARN selected. Click to toggle individual levels.
  • "Live tail" toggle: ON/OFF. When ON, new logs stream in at bottom. When OFF, static view.
  • "Search logs" input: search across message, error, stack trace, metadata.
  • "Time range": "Last 15 min", "Last 1 hr", "Last 4 hr", "Last 24 hr", "Custom"
  • "Export" button (secondary)
  • "Clear" button (clears current view, doesn't delete logs)

Section B: Log Feed (below, full width)

  • Terminal-style feed (monospace font, font-mono, text-sm). Dark background option: bg-gray-900 + text-gray-100 (toggle in header).
  • Each log line is a compact row:
    • [2026-06-15 14:32:05.123] timestamp in text-gray-500
    • [ERROR] level badge: red pill for ERROR, amber for WARN, blue for INFO, gray for DEBUG
    • [api.content.generate] module name in text-cyan-400 (dark mode) or text-blue-600 (light mode)
    • Message: main log message in text-primary
    • Right: "Expand" icon (chevron down)
  • Expanded log: Click row → expands below showing full JSON payload:
    • requestId, userId, sessionId, durationMs, stackTrace, metadata, context
    • Stack trace formatted with clickable file paths (if source maps available, link to source)
    • Error objects: error.message, error.code, error.stack
    • Context: url, method, headers, body (truncated for large payloads)
  • Color coding by level:
    • ERROR: Red left border, red background tint bg-red-50
    • WARN: Amber left border, amber background tint bg-amber-50
    • INFO: No border, white background
    • DEBUG: Gray left border, gray background bg-gray-50
    • TRACE: Light gray, very subtle

Section C: Live Tail Mode (when enabled)

  • New log lines append at bottom.
  • Auto-scroll: Follows new logs. User can scroll up to pause auto-scroll. "Resume tail" button appears when paused.
  • Max 1000 lines in viewport. Older lines auto-purged from DOM (virtualized scrolling for performance).
  • "{N} new logs" badge in header when new logs arrive while user scrolled up.

Section D: Log Stats (above feed)

  • Mini sparkline bar chart showing log volume per level over selected time range.
  • "Total: {N} | ERROR: {N} | WARN: {N} | INFO: {N} | Rate: {N}/min"
  • Error rate > 10/min shows red warning.

3. Data Source (tRPC endpoint)#

admin.getSystemLogs.useQuery({
  levels: z.array(z.enum(["ERROR", "WARN", "INFO", "DEBUG", "TRACE"])).default(["ERROR", "WARN"]),
  search: z.string().optional(),
  timeRange: z.object({ from: z.date(), to: z.date() }).optional(),
  module: z.string().optional(), // filter by module name
  requestId: z.string().optional(), // trace a specific request
  userId: z.string().uuid().optional(), // trace a specific user's activity
  page: z.number().min(1).default(1),
  pageSize: z.number().min(1).max(500).default(100),
}, { refetchInterval: 10000 }); // 10s real-time for live tail

admin.getLiveLogs.useSubscription({ // WebSocket/SSE for live tail
  levels: z.array(z.enum(["ERROR", "WARN", "INFO", "DEBUG", "TRACE"])).default(["ERROR", "WARN"]),
});

admin.exportSystemLogs.useMutation({
  timeRange: z.object({ from: z.date(), to: z.date() }),
  levels: z.array(z.enum(["ERROR", "WARN", "INFO", "DEBUG", "TRACE"])),
  format: z.enum(["JSON", "TEXT"]).default("JSON"),
});

4. Zod Schema#

const LogLevelSchema = z.enum(["ERROR", "WARN", "INFO", "DEBUG", "TRACE"]);

const SystemLogItemSchema = z.object({
  id: z.string().uuid(),
  timestamp: z.date(),
  level: LogLevelSchema,
  message: z.string(),
  module: z.string().default("app"), // e.g., "api.content.generate", "worker.job", "auth.middleware"
  requestId: z.string().optional(),
  userId: z.string().uuid().optional(),
  sessionId: z.string().optional(),
  durationMs: z.number().optional(),
  stackTrace: z.string().optional(),
  error: z.object({
    name: z.string(),
    message: z.string(),
    code: z.string().optional(),
    stack: z.string().optional(),
  }).optional(),
  metadata: z.record(z.any()).optional(),
  context: z.object({
    url: z.string().optional(),
    method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional(),
    statusCode: z.number().optional(),
    userAgent: z.string().optional(),
    ip: z.string().optional(),
  }).optional(),
});

const SystemLogResponseSchema = z.object({
  logs: z.array(SystemLogItemSchema),
  total: z.number(),
  errorCount: z.number(),
  warnCount: z.number(),
  infoCount: z.number(),
  ratePerMinute: z.number(),
  hasMore: z.boolean(),
});

5. Fetch Frequency#

  • Real-time (10000ms / 10s): System logs are high-volume. 10s polling with live tail via SSE/WebSocket for streaming.
  • On demand: Filter changes, search, pagination, time range, module filter, request ID trace.
  • Live tail (SSE): When "Live tail" is ON, subscribe to SSE stream. New logs pushed immediately. Client appends to feed.
  • Static: When "Live tail" is OFF, 10s polling is sufficient.

6. Data Manipulations#

  • Timestamp: ISO format with milliseconds: 2026-06-15 14:32:05.123. Always UTC, with local timezone note: "All times UTC. Local: 14:32:05 IST".
  • Level badge: Colored pill. ERROR = red, WARN = amber, INFO = blue, DEBUG = gray, TRACE = light gray.
  • Module name: Shortened if too long. [api.content.generate][api.content.gen] with full name on hover.
  • Message truncation: Long messages truncated to 120 chars with "..." + expand to see full.
  • Stack trace: Formatted with line breaks. File paths in text-blue-400 (dark mode) or text-blue-600 (light mode). If source maps available, link to GitHub source (or local source if dev).
  • Request tracing: If requestId present, click → filters entire log view to show only logs with that requestId → "Trace request: {id}" header appears with "Clear trace" button.
  • User tracing: If userId present, click user name → filters to that user's logs across all sessions.
  • Rate calculation: (total logs in time range) / (time range minutes). Error rate: errorCount / total * 100.
  • Virtualized scrolling: For live tail with 1000+ lines, use virtualized list (react-window or similar) to maintain 60fps.
  • Dark mode toggle: Switch between light (bg-white) and dark (bg-gray-900) terminal theme. Preference saved to localStorage.

7. Rationale#

  • Terminal-style interface: Logs are developer/debugger tools. Terminal aesthetic is the right mental model. Monospace, compact, color-coded levels.
  • Live tail: Essential for watching a deployment or debugging an active incident. "Live tail ON" → watch errors stream in real-time as you fix things.
  • Structured logs: JSON logs with requestId, userId, module enable powerful tracing. Not just "something broke" but "request 12345 failed in module api.content.generate at 14:32:05 for user abc-123".
  • 10s polling + SSE: Polling for static views, SSE for live tail. Best of both worlds. SSE for real-time streaming without polling overhead.
  • Request tracing: One request can generate 5–10 log lines across modules. Filter by requestId to see the full trace. Essential for debugging distributed actions.
  • Error rate sparkline: Visual indicator of log health. If error rate spikes after a deployment, immediate visual feedback.
  • Dark mode: Developers prefer dark mode for log viewing. Reduces eye strain during long debugging sessions. Saved preference.
  • Virtualized scrolling: 1000+ log lines would crash the DOM without virtualization. Only visible rows rendered.
  • Module filtering: When debugging content generation, filter to api.content.* and worker.content.* to reduce noise.

8. Interaction Flows#

  • Live tail ON: Toggle switch → feed clears → subscribes to SSE → new logs stream in → auto-scrolls to bottom → "Live tail active" badge in header.
  • Live tail OFF: Toggle → unsubscribes from SSE → static 10s polling resumes → "Live tail paused" badge.
  • Scroll up: Pause auto-scroll → "Resume tail" button appears in bottom-right corner → click → jumps to bottom and resumes.
  • Expand log: Click row → expands with full JSON → click again or "Collapse" → collapses.
  • Trace request: Click requestId in any log → entire view filters to that request → all related logs shown chronologically → "Clear trace" button in header.
  • Search: Type in search box → 300ms debounce → feed reloads with filtered results → "Searching..." loader.
  • Filter by level: Click level toggle → feed reloads. Multi-select: can show ERROR + WARN only, or all levels.
  • Export: Click "Export" → choose format (JSON lines or plain text) → mutation → download starts.
  • Clear view: Click "Clear" → all visible logs removed → "View cleared. New logs will appear below." (doesn't delete server logs).
  • Dark mode toggle: Click moon/sun icon → theme switches → preference saved to localStorage.
  • Impersonation: Read-only. All log viewing, filtering, tracing, export accessible. No modifications possible (logs are immutable).

9. Error States#

  • Loading: Terminal-style loader: > Loading logs... with blinking cursor.
  • Empty (filtered): > No logs match your filters. Try expanding time range or adjusting levels. in terminal style.
  • Empty (no logs): > No logs available. Check logging configuration. (system misconfiguration).
  • SSE disconnect: "Live tail disconnected. Reconnecting..." with retry button. Auto-reconnects every 5s.
  • Large dataset: > 10,000 logs in range → banner: "Large log volume. Loading first 1000. Use narrower time range for full view."
  • Export fail: Toast "Export failed. Max export size: 100MB. Narrow time range."
  • Rate limit: If user searches too frequently (debounce helps), no explicit error — just throttled naturally.
  • No logs for trace: "No logs found for request ID {id}. Request may have expired from log retention (30 days)."

10. Role-Based Variations#

  • ADMIN: Full access — live tail, filter, search, trace, export, dark mode, clear view.
  • Other roles: No access.
  • Impersonation: Read-only. All viewing features work. Export button disabled (logs may contain cross-client data). No modifications.