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.
- Calm status over busy spinners -- A pulsing dot is calmer than a spinning wheel. Use the calmest indicator that communicates the state.
- Suppress noise -- Terminal success states don't need indicators. The absence of a badge is the status.
- Honest failures -- Say "Failed." Not "Something went wrong" or "We're having trouble." The user deserves directness.
- 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:
StatusDotwithpendingstatus (muted dot, no pulse).StatusBadgewithCircleDashedicon. - 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:
Progressbar orCircularProgressif percentage known.StatusDotwithrunningandpulseif 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:
StatusDotwithrunningstatus (pulsing dot). In detail views, useDiscussionProcessingPipelinefor 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
StatusDotwithsucceededstatus (solid green dot, no pulse) orStatusBadgewithCheckCircleicon. 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:
StatusDotwithfailedstatus (solid red dot).StatusBadgewithXCircleicon. 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
StatusDotwithinfostatus 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¶
- Skeleton > Spinner for initial data fetches. Skeletons communicate layout stability; spinners communicate uncertainty.
- Static dot > Pulsing dot for states that don't actively change. Pulse implies "something is happening right now."
- No indicator > Any indicator for terminal success states in lists. Absence communicates normalcy.
- Banner > Toast for blocking states. Banners persist; toasts vanish.
- 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
StatusDotonly -- neverStatusBadgein a list row. - Suppress the dot for terminal success states -- a recording with status
readyshows 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¶
- 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. - Create custom spinners. Use
LoadingStateorSkeleton. No custom<Loader2>wrappers, no CSS-only spinners, no animated SVGs. - Create new loading patterns. The approved patterns are: Skeleton, LoadingState, StatusDot (pulsing), Progress, CircularProgress. No new categories.
- Show raw backend status strings in UI. Always map through labels.
"processing"in the database →"Processing"(or a suppressed state) in the UI. - 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. - Create a new EmptyState component. Use the canonical one from
ui/empty-state/EmptyState.tsx. - Show terminal success status in list rows. If the entity is in its expected final state, suppress the status indicator.
- 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. |
Related¶
- Brand in Product -- Status labels source of truth
- Lists & Pagination -- List density and status column treatment
- UI Density & Confidence -- Visual restraint principles
- UX Lint: Lists & Pagination -- Enforcement checklist for list UIs