# Oscar Platform — FreshCards as a first-class module (Phase 9)

**Status:** draft, operator-prompted (2026-07-02 19:15 UTC)
**Replaces:** ad-hoc Card layouts in Home, Tools, the recent activity feed
**FVW tier:** §22 (FreshCard standard) + §23 (FreshCard implementation)

## Why

The operator's instinct: "all this would benefit from a proper freshcards
module inside". The current Oscar has:

- A single `FreshCard` React component in `src/freshcards/FreshCard.tsx`
- No data model
- No registry of card types
- No AI composition
- No way to pin a card to a Home dashboard

The Tools dashboard I just built (the "every module explained" grid),
the Home page's quick-actions, the result toasts, and the recent
activity feed are all **cards in spirit** but built with ad-hoc
Mantine `<Card>` markup. A `FreshCard` data model + module would:

1. **Unify the card framework.** One component, one data shape, one
   registry. Tools cards, Home quick-actions, result toasts, and
   ambient context are all the same primitive.
2. **Make the Home dashboard composable.** The operator can pin the
   "review your drafts" card, dismiss the "download the offline brain"
   card, and have a personal Home screen.
3. **Make AI composition possible.** Oscar can *suggest* cards
   ("here's a 'pre-warm the LLM' card, here's a 'pick a project'
   card, here's a 'review your open surveys' card"). They render
   inline. The user accepts or dismisses.
4. **Make the empty-state problem tractable.** Every empty module
   gets a "starter card" that explains what to do first. The user
   never opens an empty screen and wonders what the module is for.

## The data model

```ts
// src/freshcards/types.ts
export type FreshCardKind =
  // Navigation
  | 'nav-module'          // points to a module (Surveys, Notes, etc.)
  | 'nav-action'          // points to an action (New survey, etc.)
  // Information
  | 'info'                // static explainer
  | 'stat'                // count of something
  // Composition
  | 'compose-suggestion'  // AI-suggested card (can be accepted/dismissed)
  // Workflow
  | 'workflow-step'       // a step in a workflow run
  // Form
  | 'form-field'          // a form field rendered as a card
  | 'form-summary'        // a summary of a form
  // Event
  | 'event'               // a calendar/event card
  | 'reminder';           // a reminder card

export type FreshCardVariant = 'default' | 'compact' | 'feature';

export interface FreshCardData {
  id: string;                 // stable, content-addressed
  kind: FreshCardKind;
  variant: FreshCardVariant;
  title: string;              // required
  subtitle?: string;
  body?: string;              // 1-3 paragraphs of text
  icon?: string;              // emoji or short text
  badge?: string;             // status pill ("3 drafts", "New", etc.)
  primaryAction?: FreshCardAction;
  secondaryAction?: FreshCardAction;
  meta?: Record<string, unknown>;
  // Lifecycle
  pinned?: boolean;           // shows on Home if pinned
  dismissed?: boolean;        // user has hidden it
  expiresAt?: number;         // for time-bounded cards (reminders, etc.)
  createdAt: number;
  // Provenance
  sourceModule?: string;      // which module produced this card
  aiConfidence?: number;      // 0-1, for AI-suggested cards
}

export interface FreshCardAction {
  label: string;
  onClick: () => void | Promise<void>;
  variant?: 'primary' | 'secondary' | 'danger';
}
```

## The registry

A single file (`src/freshcards/registry.ts`) maps module IDs to their
default cards. The Tools dashboard, Home, and ambient context all
read from this registry.

```ts
// src/freshcards/registry.ts
export const FRESH_CARDS: Record<string, FreshCardData[]> = {
  surveys: [
    {
      id: 'surveys:starter',
      kind: 'info',
      title: 'Start a BS5837 survey',
      body: 'A survey is a BS5837 inspection of one site. Add trees with species, height, diameter, condition, and a retention category.',
      icon: '📋',
      primaryAction: { label: 'New survey', onClick: () => navigate('/surveys?new=1') },
    },
    // ... more cards per module
  ],
  notes: [
    {
      id: 'notes:starter',
      kind: 'info',
      title: 'Capture a field note',
      body: 'Type your observation. Use #tag for organisation. Oscar extracts any retention category mentioned.',
      icon: '📝',
      primaryAction: { label: 'New note', onClick: () => navigate('/notes?new=1') },
    },
  ],
  // ... all 13 modules
};
```

## The component

`src/freshcards/FreshCard.tsx` becomes a thin wrapper that renders
`FreshCardData`. It handles:

- All variants (default, compact, feature)
- Pin / dismiss UI on the Home page
- AI-suggested cards get a `✨` badge + a confidence bar
- Time-bounded cards (reminders) get a countdown
- Action handlers fire `useAssistantContext` so the AI knows what the
  user just tapped

## The Home dashboard

`src/views/Home.tsx` becomes a **personal dashboard** of pinned cards:

- Pinned cards at the top (operator-curated)
- AI-suggested cards below ("Oscar suggests these")
- Quick actions grid (always visible)
- Recent activity (cards generated by completed actions)

The operator can:

- **Pin** any card (right-click or long-press → Pin)
- **Dismiss** any card (× button)
- **Compose** a card from the input bar (ambient AI can produce cards)

## The Tools dashboard

`src/views/Tools.tsx` becomes a **module catalog** of cards. Every
module gets one starter card, one "advanced" card, and one
"AI-suggested" card. The grid is filterable by tag.

## The ambient AI surface

When the user asks Oscar "what can I do here", instead of a text
answer, Oscar returns **a list of cards**. The user sees the cards
inline (as toasts or in the input bar) and taps one.

The card composition is part of `ask.ts`:

```ts
// In ask.ts
if (isCardSuggestionQuery(text)) {
  const cards = composeCardsForContext(getAmbientContext(), text);
  for (const card of cards.slice(0, 3)) {
    pushCard(card);  // renders in the toast stack
  }
}
```

## The registry of built-in cards

For each of the 13 modules, ship:

- **Starter card** (info): what the module does, with a "try now" action
- **Status card** (stat): count of items + recent activity
- **Tip card** (info): a power-user tip ("Cmd+K to search", etc.)
- **AI-suggested card** (compose-suggestion): what the AI thinks the
  user should do next, based on their recent history

The first three are static. The fourth is generated by a function
that looks at the user's recent activity (last N actions, last
edited draft, last completed survey) and picks the most likely
next-action.

## What this replaces

| Current | Future |
|---|---|
| `src/views/Home.tsx` (Mantine Cards in a grid) | `src/views/Home.tsx` (FreshCard-driven dashboard) |
| `src/views/Tools.tsx` (Mantine Cards in a grid) | `src/views/Tools.tsx` (FreshCard catalog) |
| `src/assistant/ResultToast.tsx` (toast host) | `src/assistant/ResultToast.tsx` (renders FreshCard variants too) |
| `src/assistant/page-docs.ts` (curated text answers) | `src/freshcards/registry.ts` (curated card answers) |
| Ad-hoc activity feed (mentioned in spec, not built) | Cards generated from IDB events |

## What stays

- `src/freshcards/FreshCard.tsx` (now a typed renderer, not a markup blob)
- `src/undo/UndoToastContext.tsx` (the undo toast is a card with `kind: 'reminder'`)
- All the IDB models (cards are derived, not stored separately)

## FVW alignment

- FVW v8 §22 — FreshCard standard (already declared)
- FVW v8 §23 — FreshCard implementation (this plan)
- FVW v8 §35 — Contextual Surfaces (cards surface AI suggestions contextually)
- FVW v8 §40 — Multimodal Capture (cards are the §40 output surface)
- FVW v8 §41 — Ambient AI (cards replace modal patterns per DO-008)

## Phases

### Phase 9A — Foundation (next)
- Define `FreshCardData` + `FreshCardKind` types
- Convert `FreshCard.tsx` into a typed renderer for `FreshCardData`
- Build the `FRESH_CARDS` registry with starter cards for all 13 modules
- Replace `Tools.tsx` and `Home.tsx` markup with `FreshCard` components

### Phase 9B — Pin / dismiss / persist
- New IDB store `pinned_cards` (schema v11 → v12)
- Home page reads pinned cards from IDB
- Long-press / right-click on a card → Pin / Dismiss
- "Reset to defaults" button

### Phase 9C — AI composition
- New module `src/assistant/card-composer.ts`
- Reads recent activity from IDB
- Generates 1-3 `compose-suggestion` cards
- Surfaces them in the toast host (ambient, non-modal)

### Phase 9D — Result-toast cards
- Convert `ResultToast` to optionally render a `FreshCard`
- "Rewrite ready" becomes a `FreshCard` with `kind: 'form-summary'`
- "Apply" / "Keep original" become card actions
- Operator can pin the rewrite to the Home dashboard for later

### Phase 9E — FvRE export
- Extract the FreshCard pattern into a shared package
- FvRE and FVS can use the same card framework
- The Compose Card action is API-callable (FvRE knows how to render
  a card, Oscar knows how to compose one)

## Acceptance criteria

- [ ] Every Tools module card is a `FreshCard` rendered from `FRESH_CARDS`
- [ ] Home dashboard shows pinned cards at the top
- [ ] User can pin / dismiss any card
- [ ] AI can compose 1-3 cards based on context + recent activity
- [ ] Card actions fire `useAssistantContext` so the AI learns what the user tapped
- [ ] Result-toast cards have Pin / Dismiss / Copy actions
- [ ] All 220 existing tests still pass
- [ ] New tests for: registry completeness, card composition, pin/dismiss
- [ ] No regression in the ambient AI or chat surface
