Skip to content

Loading, Processing & Status States

Status: Canonical Applies to: All async surfaces across SageOx (recordings, transcriptions, deletions, setup flows, lists, detail views)


Purpose

This spec governs how SageOx communicates asynchronous state to users. It defines which states are visible, what they're called, how they look, and when they should be suppressed entirely.

The core principle: Calm status over busy spinners. Suppress noise. Be honest about failures.

SageOx runs significant async work (recording processing, transcription, summarization, git commits). Users need confidence that things are happening, not anxiety about when they'll finish.


Part 1: Audit Summary

Backend State Enums

Domain States Source
Recording pending uploading processing ready failed deleted api-go/internal/models/recording.go
Transcription pending processing completed failed web/src/lib/api/recordings.ts
Deletion pending confirmed completed failed workflow/.../secure-deletion/activities/constants.go
Operations pending running completed failed web/src/types/run-operation.ts
Waitlist pending approved invited rejected web/src/types/waitlist.ts
Notifications info warning error success web/src/types/notifications.ts

Frontend Components

Component Location Status vocabulary Visual pattern
StatusDot ui/StatusDot.tsx pending running succeeded failed warning info Colored dot, optional pulse
StatusBadge ui/StatusBadge.tsx Same as StatusDot Pill badge with icon + label
TranscriptionStatusBadge ui/TranscriptionStatusBadge.tsx pending processing completed failed Pill badge, semantic tokens
StatusIndicator recordings/RecordingsList.tsx uploading processing pending ready failed deleted Bare colored dot, title-only
DiscussionProcessingPipeline media/DiscussionProcessingPipeline.tsx completed in-progress pending failed per step Tufte pipeline with elapsed time
DeletionStatusBanner repo/DeletionStatusBanner.tsx pending confirmed completed failed Full-width Alert banner
GitLabRepoStatus gitlab/GitLabRepoStatus.tsx loading ready pending error unavailable Alert with action buttons
LoadingState ui/loading-state.tsx n/a (loading only) Centered spinner + message
Skeleton family ui/skeleton.tsx n/a (loading only) Pulse rectangles
CircularProgress ui/CircularProgress.tsx n/a (progress only) SVG ring with percentage
Progress ui/progress.tsx n/a (progress only) Radix progress bar
EmptyState (2 versions) ui/EmptyState.tsx, ui/empty-state/EmptyState.tsx n/a Icon + title + action

Part 2: Product Learnings

What SageOx Does Well

DiscussionProcessingPipeline is the gold standard for multi-step async visualization in this codebase. It follows Tufte principles -- data-dense, no decoration, shows elapsed time per step, uses color and motion sparingly. Every new pipeline visualization should study this component.

StatusDot and StatusBadge use semantic design tokens (ox-pending-*, ox-progress-*, ox-success-*, ox-error-*) consistently. These tokens encode meaning at the design system level, so future themes get correct status colors for free.

brand-in-product.md already defines the correct status labels: Captured, Processing, Pending, Uploading, Failed. These are intentional word choices that reinforce the SageOx brand every time a user sees them.

LoadingState has proper ARIA attributes (role="status", aria-live="polite", sr-only fallback text). This is the accessibility baseline every loading pattern must meet.

Skeleton system is comprehensive -- Skeleton, SkeletonText, SkeletonCard, SkeletonRow, SkeletonAvatar cover the common layout shapes without custom work per feature.

Patterns Worth Refining

StatusIndicator in RecordingsList uses raw Tailwind colors (bg-amber-500, bg-blue-500, bg-slate-400) instead of semantic tokens. It maps ready to "Captured" correctly in the label but uses bg-green-500 instead of bg-ox-success-500. The label is only exposed via title attribute, not visible text.

RecordingsListWidget leaks raw backend status strings to the UI at line 74: <span className="text-amber-500">{recording.status}</span>. A user seeing "processing" in amber text is reading a database enum, not a product label.

StatusDot uses succeeded where the brand spec says "Captured" and the recording model says ready. Three words for the same concept.

Patterns Worth Replacing

Two EmptyState components exist: ui/EmptyState.tsx (simple, uses py-16, large icon container) and ui/empty-state/EmptyState.tsx (comprehensive, with variants, forwardRef, secondary actions). The simple version violates UI density guidelines (oversized icon, too much padding). The comprehensive version is the canonical one.

RecordingsListWidget uses an inline <Loader2> spinner for loading state instead of LoadingState or a skeleton pattern. The card also uses a one-off empty state instead of the canonical EmptyState component.


Part 3: Senior UX Recommendations

1. Map All Backend States to User Labels

Every backend status must pass through a mapping layer before reaching the UI. Users should never see raw enum values.

Backend State User Label Rationale
ready (recording) Captured Brand-aligned. Reassures data safety.
processing (recording) Processing Factual, calm.
pending (recording) Suppressed User hasn't finished uploading; showing status adds anxiety.
uploading (recording) Uploading Only shown during active upload with progress.
failed (recording/transcription) Failed Honest. Don't soften.
deleted (recording) Never shown Soft-deleted items are not rendered.
completed (transcription) Suppressed Presence of transcript icon implies completion.
processing (transcription) Transcribing Distinct from recording processing.
pending (transcription) Suppressed in lists Sub-step; not relevant to user in list context.
pending (deletion) Deletion Pending High-stakes; must be visible and actionable.
confirmed (deletion) Deletion In Progress No user action possible; calm acknowledgment.
succeeded (generic) Captured or Suppressed Terminal success = no indicator needed in most cases.

2. Decide Loading Pattern by Context

Is the user waiting for a page/section to render?
  YES → Skeleton (matches the eventual layout shape)

Is the user waiting for a result they initiated?
  YES → Is there a known progress percentage?
    YES → Progress bar or CircularProgress
    NO  → LoadingState spinner with message

Is the state a persistent entity property (not a loading moment)?
  YES → StatusDot (list) or StatusBadge (detail)

Is the state a multi-step pipeline?
  YES → DiscussionProcessingPipeline pattern

3. Suppress Status When It Adds No Information

A completed recording with a ready status showing a green dot next to every row is visual noise. The absence of a status indicator communicates success. Only show status when the state is actionable, transient, or exceptional.

Show status when: - The entity is in a non-terminal state (processing, uploading, pending confirmation) - The entity has failed and the user should know - The state is actionable (deletion pending -- user can confirm or cancel)

Suppress status when: - The entity is in its expected terminal state (ready, completed, captured) - The sub-step is not relevant at the current view level (transcription pending in a list of recordings) - The entity is soft-deleted (don't render it at all)

4. Error Surfacing Hierarchy

Error Type Pattern Component Auto-dismiss
Context-specific (field validation, single entity failure) Inline text or badge StatusBadge with failed, inline red text No
Transient (network blip, retry succeeded, action completed) Toast notification Sonner toast Yes (5s)
Blocking (deletion workflow, setup required, system unavailable) Banner Alert component No

Never use a toast for a failure that requires user action. Never use a banner for a success.

5. Success Toast Rules

Success toasts confirm non-obvious outcomes. Suppress them when the UI already reflects the result.

Show toast Suppress toast
Action result is invisible (copied to clipboard, background sync completed) Result is immediately visible (item added to list, form closed, page navigated)
Destructive action completed (item deleted, member removed) Save button already shows saved state or form resets
Async action completed in background (export ready, webhook delivered) Inline status already changed (badge updated, dot color changed)

Rules: - Auto-dismiss in 5 seconds (default Sonner behavior). - Never stack more than 3 toasts simultaneously. - Copy formula: [Past participle] + [object] -- "Copied to clipboard", "Invite sent", "Recording deleted". - Never prefix with "Successfully". Say "Settings saved" not "Successfully saved settings". - Never use exclamation marks. "Invite sent" not "Invite sent!" - Place at bottom-right (Sonner default position). - Use toast.success() with CheckCircle icon (already wired in useToast.ts). - For undo-able actions, include an action button: toast.success("Member removed", { action: { label: "Undo", onClick: handleUndo } }).

Anti-patterns: - Showing "Saved" toast after every form submission when the form already closes or shows inline confirmation. - Showing success toast AND updating inline status for the same action (pick one). - Using toast.info() for success outcomes -- use toast.success().


Part 4: Canonical Design System Spec

Philosophy

SageOx communicates async state the way macOS communicates system state: at the edges, calmly, and only when relevant.

  1. Calm status over busy spinners -- A pulsing dot is calmer than a spinning wheel. Use the calmest indicator that communicates the state.
  2. Suppress noise -- Terminal success states don't need indicators. The absence of a badge is the status.
  3. Honest failures -- Say "Failed." Not "Something went wrong" or "We're having trouble." The user deserves directness.
  4. Brand in status -- "Captured" is not just a label; it's a brand moment. Every time a user sees it, they associate SageOx with safety.

Canonical States

Pending

  • Meaning: Waiting for an external trigger or prerequisite.
  • When to show: Deletion awaiting confirmation. Setup awaiting user action.
  • When NOT to show: Recording initialized but upload hasn't started (suppress entirely). Transcription queued (sub-step; suppress in lists).
  • Visual: StatusDot with pending status (muted dot, no pulse). StatusBadge with CircleDashed icon.
  • Allowed user actions: May have a CTA to trigger the next step.
  • Token: ox-pending-*

Uploading

  • Meaning: Data is actively transferring from client to server.
  • When to show: During active file upload. Progress bar preferred if bytes-transferred is known.
  • When NOT to show: After upload completes (transition to Processing automatically).
  • Visual: Progress bar or CircularProgress if percentage known. StatusDot with running and pulse if percentage unknown.
  • Allowed user actions: Cancel upload (if supported).
  • Token: ox-progress-*

Processing

  • Meaning: Server is working on something. No user action possible.
  • When to show: Recording processing pipeline. Transcription in progress (detail view only, label: "Transcribing").
  • When NOT to show: Don't use "Processing" as a generic loading label. Be specific: "Transcribing", "Summarizing", "Committing".
  • Visual: StatusDot with running status (pulsing dot). In detail views, use DiscussionProcessingPipeline for multi-step work.
  • Allowed user actions: None. Show elapsed time if available.
  • Token: ox-progress-*

Captured / Succeeded / Ready

  • Meaning: Terminal success. The entity is in its expected final state.
  • When to show: In brand-reinforcement contexts (first recording captured, onboarding milestones). In detail views as a static badge.
  • When NOT to show: In list rows where the majority of items are in this state. Showing a green dot on every row is noise.
  • Visual: When shown, use StatusDot with succeeded status (solid green dot, no pulse) or StatusBadge with CheckCircle icon. Label: Captured (recordings) or Completed (generic workflows).
  • Allowed user actions: Full entity interaction (view, edit, delete, share).
  • Token: ox-success-*

Failed

  • Meaning: Something went wrong. May be recoverable.
  • When to show: Always. Failures must never be hidden.
  • When NOT to show: Never suppress a failure.
  • Visual: StatusDot with failed status (solid red dot). StatusBadge with XCircle icon. In detail views, include error context.
  • Allowed user actions: Retry (if applicable), contact support, view error details.
  • Token: ox-error-*

Deleted

  • Meaning: Soft-deleted. The entity logically no longer exists.
  • When to show: Never in normal UI. Only visible in admin/audit views if they exist.
  • When NOT to show: All user-facing lists and detail pages. Filter deleted items at the query level.
  • Visual: N/A. If admin view exists, use StatusDot with info status and muted appearance.

Visual Treatment Rules

When to Use Each Loading Pattern

Pattern Use When Example
Skeleton Page/section first load; shape of content is known Recording list loading, card content loading
LoadingState (spinner + message) User-initiated action; result replaces current content "Loading recording..." after click
StatusDot (static or pulsing) Entity has a persistent state property shown in a list row Recording row with processing status
StatusBadge Entity state needs a label (detail view headers, highlighted status) "Processing" badge on recording detail page
Progress bar Known progress percentage, linear workflow File upload progress
CircularProgress Known progress percentage, compact space Upload widget
DiscussionProcessingPipeline Multi-step async workflow, detail view Recording detail: Record > Upload > Transcribe > Summarize > Commit

Hierarchy Rules

  1. Skeleton > Spinner for initial data fetches. Skeletons communicate layout stability; spinners communicate uncertainty.
  2. Static dot > Pulsing dot for states that don't actively change. Pulse implies "something is happening right now."
  3. No indicator > Any indicator for terminal success states in lists. Absence communicates normalcy.
  4. Banner > Toast for blocking states. Banners persist; toasts vanish.
  5. Pipeline > Badge for multi-step async work. Pipelines show progress through stages; badges show a single state.

Copy & Language Guidelines

Rule Example
Use past participles for completed steps "Recorded", "Uploaded", "Transcribed", "Summarized", "Committed"
Use present participles for in-progress labels "Uploading", "Processing", "Transcribing"
Use "Captured" (not "Ready" or "Done") for completed recordings Brand alignment per brand-in-product.md
Use "Failed" (not "Error" or "Something went wrong") for failures Honest, direct
Never use exclamation marks in status labels "Captured" not "Captured!"
Never use "Successfully" as a prefix "Captured" not "Successfully captured"
Never use "Please" in error states "Retry" not "Please try again"
Elapsed time uses compact format "2m 34s", "1h 5m" -- no verbose "2 minutes and 34 seconds"

Forbidden Language in Status Contexts

Forbidden Why Use Instead
"Loading..." as a visible label Vague; tells user nothing specific Skeleton (no text) or specific message ("Loading recordings")
"Something went wrong" Evasive, erodes trust "Failed" with specific context
"Oops" / "Uh oh" Unprofessional for a product handling team decisions State the failure directly
"Almost there" Promising something you don't know Show elapsed time instead
"Hang tight" Patronizing Suppress or show factual progress

Failure & Recovery

Scenario Auto-retry User action Recovery pattern
Upload chunk failure Yes (3 attempts, exponential backoff) None until exhausted, then "Retry Upload" Toast on auto-recovery; badge + retry button on exhaustion
Transcription failure No (pipeline step) "Retry" in detail view pipeline Red step in DiscussionProcessingPipeline, retry action on step
Network request failure Yes (React Query default) None unless persistent Toast on persistent failure
Deletion failure No Contact support DeletionStatusBanner with failed state
GitLab setup failure No "Try Again" button GitLabRepoStatus with error state + retry CTA

Error auto-clear rule: Toasts auto-dismiss after 5 seconds. Banners persist until state changes. Inline error badges persist until the entity state changes.

Lists vs Detail Views

Context Status visual What's shown What's forbidden
List row StatusDot (small, compact) Dot only. Label via title attribute. Suppress for terminal success. Full badges, inline text status, progress bars, pipeline visualizations
Detail header StatusBadge Badge with icon + label. Shown for all non-terminal states. Raw status strings, custom colored text
Detail body DiscussionProcessingPipeline Full pipeline for multi-step workflows. Elapsed times. Step-level retry actions. Multiple competing status indicators
Widget (sidebar card) Suppress or minimal Show status only for non-ready items: dot or short text. Full pipeline, large badges, progress bars

Part 5: System Boundaries

User-Visible vs Internal-Only States

Transition Visibility Rationale
pending -> uploading Internal User triggered upload; they know it's happening. Show upload progress, not a state label.
uploading -> processing Internal Automatic transition. User sees the pipeline advance.
processing -> ready User-visible The pipeline completes. Status suppressed in list (terminal). "Captured" in brand contexts.
ready -> deleted Internal Soft-delete. Item removed from UI entirely.
pending -> processing (transcription) Internal in lists Sub-step. Only visible in recording detail view pipeline.
pending -> confirmed (deletion) User-visible User explicitly confirmed. Banner updates.
confirmed -> completed (deletion) User-visible Entity removed. Final banner shown briefly, then redirect.

States That Must Never Be Surfaced

  • deleted -- Items in this state are filtered from all queries. No UI should render them.
  • Internal workflow states (Temporal activity retries, S3 multipart chunk IDs) -- These are operational, not product states.
  • Database enum values as labels -- "ready" is a database constraint value, not a user-facing word. Always map through the label layer.

Part 6: Design System Integration

Cross-References

Spec How This Spec Relates
Brand in Product Status labels ("Captured", "Processing", etc.) are defined there. This spec operationalizes them.
Lists & Pagination Status column treatment in list rows is governed by this spec. Lists spec governs column density.
UI Density & Confidence Restraint principles apply to status display. No oversized status icons. No redundant labels.

Additions to Lists & Pagination

The Lists spec says: "Status columns should be compact (dot or badge)." This spec clarifies:

  • Status column in lists uses StatusDot only -- never StatusBadge in a list row.
  • Suppress the dot for terminal success states -- a recording with status ready shows no dot. The absence is intentional.
  • Sort by status uses semantic ordering, not alphabetical: processing (1) > uploading (2) > pending (3) > ready (4) > failed (5). Active states sort first.
  • Status column header is "Status" -- not "State", not "Progress", not an icon-only header.

Canonical EmptyState

The comprehensive EmptyState in ui/empty-state/EmptyState.tsx is canonical. The simple version in ui/EmptyState.tsx should be deprecated and call sites migrated.

Canonical EmptyState rules: - Major variant (variant="major"): full-page empty states (no recordings, no repos) - Inline variant (variant="inline"): section-level empty states (no recent activity in a card) - Padding: py-12 for major, py-4 for inline (per UI Density spec) - Icon: w-24 h-24 container for major, w-12 h-12 for inline - Copy: Follow brand-in-product.md patterns ("No discussions yet. Capture a discussion to start building your team context.")


Part 7: Agent Rules

Decision Tree: What to Show

Building a new async surface?
|
|-- Does the entity have a status property?
|   |
|   |-- YES: Is it displayed in a list?
|   |   |-- YES: Use StatusDot. Suppress for terminal success.
|   |   |-- NO (detail view): Use StatusBadge for header.
|   |        Is it a multi-step pipeline? → Use DiscussionProcessingPipeline pattern.
|   |
|   |-- NO: Is the view loading data?
|       |-- YES: First load? → Skeleton.
|       |         User-initiated? → LoadingState spinner.
|       |-- NO: Is it empty? → EmptyState (canonical version).
|
|-- Does the entity show progress (percentage)?
    |-- YES: Known percentage → Progress bar or CircularProgress.
    |-- NO: Unknown duration → StatusDot with pulse, or LoadingState.

What Agents May Reuse

Component Import From Use For
StatusDot @/components/ui/StatusDot Any entity status in a list row
StatusBadge @/components/ui/StatusBadge Any entity status in a detail header
TranscriptionStatusBadge @/components/ui/TranscriptionStatusBadge Transcription status specifically
LoadingState @/components/ui/loading-state Centered spinner with message
Skeleton, SkeletonText, SkeletonCard, SkeletonRow, SkeletonAvatar @/components/ui/skeleton Layout-matching loading placeholders
Progress @/components/ui/progress Linear progress bars
CircularProgress @/components/ui/CircularProgress Compact circular progress
EmptyState @/components/ui/empty-state Zero-data states (use THIS one, not ui/EmptyState.tsx)
DiscussionProcessingPipeline @/components/media/DiscussionProcessingPipeline Multi-step pipeline visualization
DeletionStatusBanner @/components/repo/DeletionStatusBanner Deletion workflow banners

What Agents May NOT Do

  1. Invent new status names. All user-visible status labels are defined in this spec and brand-in-product.md. If a new status is needed, update this spec first.
  2. Create custom spinners. Use LoadingState or Skeleton. No custom <Loader2> wrappers, no CSS-only spinners, no animated SVGs.
  3. Create new loading patterns. The approved patterns are: Skeleton, LoadingState, StatusDot (pulsing), Progress, CircularProgress. No new categories.
  4. Show raw backend status strings in UI. Always map through labels. "processing" in the database → "Processing" (or a suppressed state) in the UI.
  5. Use non-semantic color tokens for status. Always use ox-pending-*, ox-progress-*, ox-success-*, ox-error-*, ox-warning-*. Never raw Tailwind colors (bg-amber-500, bg-red-500) for status indicators.
  6. Create a new EmptyState component. Use the canonical one from ui/empty-state/EmptyState.tsx.
  7. Show terminal success status in list rows. If the entity is in its expected final state, suppress the status indicator.
  8. Use toasts for failures requiring user action. Use inline badges or banners instead.

Lintable Constraints

These rules are mechanically verifiable in code review:

Constraint Check
No raw Tailwind status colors Grep for bg-amber-, bg-red-, bg-green-, bg-blue-, bg-slate- in status-related components. Must use ox-* tokens.
No raw status strings in JSX Grep for {recording.status} or {.*\.status} rendered directly in JSX text content. Must pass through label mapper.
No <Loader2> outside approved components Loader2 imports should only appear in LoadingState, StatusBadge, TranscriptionStatusBadge, DiscussionProcessingPipeline, and DeletionStatusBanner. New components should use LoadingState.
No duplicate EmptyState usage Imports of @/components/ui/EmptyState (the simple version) should migrate to @/components/ui/empty-state.
ARIA on loading states Any element with animate-spin or animate-pulse used as a loading indicator must have role="status" and either aria-label or a sibling sr-only element.