Skip to content

[feat] Front-end recovery UI for failed indexing after successful on-chain tx #267

@realproject7

Description

@realproject7

Problem

When a storyline/plot tx succeeds on-chain but the indexer API call fails (500, network error, browser crash), the user sees an error with no way to retry. The on-chain tx is permanent but the DB record is missing. The backfill cron will eventually catch it, but it won't have client-side metadata (genre, language, content fallback).

Reference Implementation

dropcast has a battle-tested pattern for this exact scenario:

  • hooks/useCreateIntent.ts — localStorage-based intent storage with tx hash persistence + retry logic
  • app/create/page.tsx (lines ~3346–3485) — Recovery modal UI ("Finalize Campaign" / "Resume Creation")
  • hooks/useFundCampaign.ts — Transaction error parsing (distinguishes user-rejected vs sent-but-failed)

Key pattern (from dropcast):

  1. Before tx: Save intent to localStorage (content, metadata, indexer route)
  2. After tx confirms: Persist tx hash to localStorage
  3. Indexer call: Attempt POST to indexer API
  4. If indexer fails: Intent stays in localStorage with error + retry count
  5. On page reload: Recovery UI appears — "Your story was published on-chain but indexing failed. Retry?"
  6. User clicks retry: Re-sends the indexer POST with the same tx hash + metadata
  7. On success: Clear localStorage intent, redirect to story page
  8. 409 / duplicate: Treat as idempotent success (indexer upserts by tx_hash anyway)

Implementation

1. Create src/hooks/usePublishIntent.ts

localStorage-based intent that tracks:

interface PublishIntent {
  txHash: string | null;
  content: string;
  metadata: Record<string, string>;  // genre, language, etc.
  indexerRoute: string;
  uploadKeyPrefix: string;
  createdAt: number;
  retryCount: number;
  lastError: string | null;
}
  • saveIntent() — called before wallet confirmation
  • persistTxHash(hash) — called after tx confirms
  • clearIntent() — called after successful indexing
  • readPendingIntent() — check for unfinished intents on page load

2. Integrate into usePublish.ts

Wire the intent lifecycle into the existing 5-state flow:

  • Before step 2 (confirming): saveIntent()
  • After step 3 (pending → receipt): persistTxHash(hash)
  • After step 4 (indexing success): clearIntent()
  • On indexer error: keep intent, set error

3. Recovery UI on create/chain pages

On mount, check readPendingIntent(). If a pending intent exists (has txHash but indexer never succeeded):

  • Show a recovery banner/modal: "Your previous story was published on-chain but indexing failed"
  • Show tx hash (linked to explorer)
  • "Retry Indexing" button → re-sends POST to indexer with saved metadata
  • "Dismiss" button → clears intent (user accepts backfill cron will handle it, minus metadata)
  • Max retry count (5) with warning after exceeded

4. Error classification

  • User rejected tx: Clear intent immediately (no recovery needed)
  • Tx sent but receipt poll failed: Persist intent with txHash, show recovery
  • Tx confirmed, indexer 500/network error: Persist intent, show recovery
  • Indexer 409 / duplicate: Treat as success, clear intent

Files to modify

  • src/hooks/usePublishIntent.ts — new: localStorage intent management
  • src/hooks/usePublish.ts — integrate intent lifecycle
  • src/app/create/page.tsx — add recovery UI on mount
  • src/app/story/[storylineId]/chain/page.tsx — add recovery UI for plot chaining too

Reference files (dropcast — read these for the full pattern)

  • /Users/cho/Projects/dropcast/hooks/useCreateIntent.ts
  • /Users/cho/Projects/dropcast/hooks/useFundCampaign.ts
  • /Users/cho/Projects/dropcast/app/create/page.tsx (lines ~3346–3485)

Acceptance criteria

  • Intent saved to localStorage before wallet confirmation
  • Tx hash persisted after on-chain confirmation
  • Intent cleared after successful indexing
  • Recovery UI appears on page reload when pending intent exists
  • Retry button re-sends indexer POST with all metadata (genre, language, content)
  • Dismiss button clears the intent
  • User-rejected tx clears intent (no false recovery)
  • Works for both storyline creation and plot chaining
  • npm run typecheck passes

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/T3Assigned to T3 builder agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions