# Build the "Quiet failure surfacing" pattern: `PipelineFailureNotice`

## Stack (edit this block to match your project)
- React 18+ with TypeScript: one function component, typed props, no `any`.
- Tailwind CSS, using your design system's tokens if you have them.
- Presentational only: data comes in through props, decisions go out through callbacks. No fetching or global state inside.
- If your codebase already has conventions (naming, shared components, tokens), follow them over this spec.

## What the pattern is for
A background failure becomes a visible, actionable state rather than an absence of output.

Use it when: Any invisible pipeline the user would otherwise assume had succeeded.

## What to build
A visible failure state for a background pipeline (sync, enrichment, scheduled job) that would otherwise just produce nothing. It says what failed, what is missing because of it, and what the person can do, with a capped retry. It renders in place of, or above, the results the pipeline feeds.

### Props
- `source`: { name: string; lastSuccessAt?: Date }, the pipeline or connector, e.g. "Billing sync".
- `status`: "ok" | "empty" | "failed" | "retrying" | "retry-exhausted" | "recovering", controlled; `empty` and `failed` are distinct values from the API, never inferred from a zero count.
- `error?`: { message: string; cause: "transient" | "auth" | "config" | "unknown"; at: Date }, the plain-language reason.
- `impact?`: string, what downstream output is missing or stale, e.g. "64 accounts are missing from this list; next week's report is on hold".
- `retry`: { attempt: number; max: number }, the visible ceiling.
- `onRetry()`, `onFix?()` (reconnect, open settings) and `onDismiss?()`: callbacks. The component never retries on its own.
- `fixLabel?`: string, the label for the real fix, e.g. "Reconnect billing".

### States it must render
- Empty (genuine): "No results. Billing sync last succeeded at 06:00", so a true zero is stated with its freshness.
- Failed: a red notice naming the source, the time, the reason and the impact, with Retry and the fix action; any partial results below are marked as incomplete.
- Retrying: "Retrying, attempt 2 of 3" with buttons disabled.
- Retry exhausted: "Retrying won't fix this" with the cause (e.g. an expired token), Retry removed, and the fix action made primary.
- Recovering: "Reconnected, syncing" with progress; then the notice clears and newly loaded items are briefly marked as "Was missing".

### Interactions
- Retry calls `onRetry` and is disabled once `attempt` reaches `max`.
- When `error.cause` is "auth" or "config", the fix action is primary from the start and Retry is secondary, because retrying cannot succeed.
- Dismiss collapses the notice to a persistent "1 source failing" chip, never removes it while the failure lasts.

### Edge cases
- Partial failure (some records loaded): show the results with an "Incomplete" header and the count missing, not a clean list.
- `lastSuccessAt` is missing: say "Has never synced successfully".
- Several failing sources: stack one line per source with a single summary header, not several full banners.
- Failure that resolves while the notice is open: switch to Recovering rather than vanishing, so the person sees it was fixed.

## Must avoid
Degrading to empty results, so the user reads a broken pipeline as "nothing found".

## Engineering constraint
Distinguish empty-result from error at the API level; retries need a user-visible ceiling.

## Accessibility
- The failed and retry-exhausted states use `role="alert"` once on entry; retry progress is polite.
- The notice is labelled by the source name so a screen-reader user can tell which pipeline broke.

## House rules
- Real `<button>`/`<input>` elements, full keyboard use, visible focus; announce status changes with `aria-live="polite"`; respect `prefers-reduced-motion`.
- All user-facing text comes from props (or a `labels` prop) so it can be translated.
- One consistent accent marks anything the AI made or is doing; amber means "needs a person", red means risky, green means done. Never rely on colour alone.
- Fits a 360 px wide container without horizontal scrolling.

## Acceptance checks
- [ ] Every state above can be rendered by passing props, with no internal fake data.
- [ ] Every interaction and edge case above behaves as described.
- [ ] Nothing in "Must avoid" can happen with this component.

## Deliver
1. The component file with its exported prop types.
2. A short usage example that renders each state.
