Felt Faction iOS app v10 — transaction ledger surface (live card + per-session detail in Archive)

Writing Search related

Felt Faction iOS app v10 — transaction ledger surface

Brainstormed 2026-07-06. One new design move: **transactions become

a first-class read surface**, split into two views that match how

members and admins already think about money — right now vs

that session. The v9 single-app/role-aware architecture

stays unchanged; this v10 widens the surface area on top.

The shape (locked)

Two surfaces, one source of truth (transactions table), one service.

┌─ Top-level: Live ledger card ──────────────────────────────────┐
│ (lives in BriefingView; one card, two role flavours)         │
│                                                               │
│  UPCOMING session:  "The House hasn't opened yet" + RSVP CTA  │
│                     + starting_chip_allocation preview       │
│  LIVE session:      member rows OR per-member rollup (admin)  │
│                     with running totals, loan distance, live  │
│                     updates (3s poll or Supabase Realtime)   │
│  NO active session: most recent settled session summary card │
│                     + "Open in Archive" link                 │
│                                                               │
│  Tap → SessionDetailView (live session, scrolled to ledger)   │
└───────────────────────────────────────────────────────────────┘

┌─ Archive tab ──────────────────────────────────────────────────┐
│ PastSession row ── tap ──→ SessionDetailView (NEW)            │
│   ├─ Header: date, theme, location, status, season            │
│   ├─ Members table (admin: all; member: self only)            │
│   ├─ Transaction ledger (per-session, all 8 categories,      │
│   │   admin: filter by member + category; member: own rows)  │
│   ├─ Loans from this session                                 │
│   ├─ Notes / highlights (from SessionRecap.highlights)       │
│   └─ Admin: CSV export of this session's ledger              │
└───────────────────────────────────────────────────────────────┘

Decisions (locked 2026-07-06)

  1. Top-level "Live ledger" lives inside BriefingView as a card,

not Standing. Briefing is already the action hub per the v10 refactor; Standing is the aggregated view. Different jobs.

  1. Members can see other members' rows in past sessions

(names + amounts) — transparency over privacy-by-default. Running totals of other members are hidden behind a "show full leaderboard" tap inside the session detail, so Standing isn't eaten by the Archive. (User verbatim: "yes, this sounds good for transparent UX.")

  1. Admin "everyone vs me" toggle on the live card — single

tap to flip. Same query, different member_id filter. Useful for the admin watching the floor.

  1. **Loan card visible on every session detail when there's an

active or historical loan** — show all loans (open + paid off), not just open ones. Members want to see "I paid off the loan at session X" as part of their story. (User lean: show all.)

  1. CSV export per session in v1 — admin only, one button

per session detail. Exports that session's ledger (member, date, event, category, amount). Numbers opens it. (User lean: ship in v1.)

  1. Live updates: poll at 3s for the active session

(mirrors the existing live settlement board pattern); snapshot for everything else. No Supabase Realtime in v1 — keep free-tier compatible. Admin's live view polls; member's live view polls; both cost the same.

Why this v10 and not the v9 sketch

The v9 sketch (mentally) treated the ledger as one screen. The correction splits it into two surfaces that already have natural homes:

come to Briefing to act; the live ledger is the read-side of those actions. Same tab, same mental model.

past sessions. Members come to Archive to look back; the per- session ledger extends that look-back with row-level money history. No new tab needed.

The Archive is already filterable (season + my-attendance + search per the v12 batch); the per-session detail inherits those filters for free. The Briefing already loads the active session via appState.activeSession; the live card reuses that data path.

Architecture (ponytail-checked)

One service, parameterised:

// LedgerService (NEW) — replaces the existing
// `loadWithdrawalLedger(eventId:)` narrow AppState method
func loadTransactions(
    eventId: UUID?,                      // nil = across all events
    memberId: UUID?,                     // nil = all members
    categories: [Transaction.Category]?  // nil = all categories
) async -> [LedgerEntry]

Callers compose:

Three new files, one absorb:

Briefing already exists; this slots in as a card at the bottom of the existing BriefingView body. Polls every 3s when session is live.

Reached via NavigationLink from ArchiveView's existing session row. Snapshot (no polling) — past sessions don't change.

parameterised loader above. Mirrors AdminService patterns.

Rename the file, broaden the query from buy_in+rebuy to all categories. Delete the file after merge.

No new Edge Functions. All reads via anon-key Supabase. The migration surface is small.

RLS posture (verify before build)

confirm that a member reading with the anon key can already see their own rows. If not, add a policy: FOR SELECT USING (member_id = auth.uid()).

adjustment flow inserts a row with the affected member's member_id. That row must be visible to the affected member (so they see the reversal). Confirm the migration 20260630_0013_cashout_settle_to_net_worth.sql respects this scope. If reversal rows are inserted with member_id = admin, that's a bug to fix first — it hides adjustments from members.

admin RLS. Verify with the 5-query pre-migration recipe (information_schema + pg_constraint + pg_policies + pg_proc) before drafting the new migration.

the transactions table has it. If not, add in the same migration. Otherwise the per-session ledger query is a sequential scan on a table that grows every session.

Reversal row UX (the trust-critical bit)

Reversal rows (cashout_adjustment) show inline next to the original, with a strikethrough on the original amount. Members need to see "the House changed the number" — that's trust-critical. Don't bury the reversal in a separate "adjustments" tab; show it adjacent to the row it reverses. The math already lives in cashout_settle_to_net_worth migration; the UI just needs to render it honestly.

Felty voice on transactions

Subtle, not loud. Reserve Felty for:

Plain copy for the ledger rows themselves. Felty narrating every ledger entry is noise, not voice.

What's NOT in this v10

the live card + session detail are the row-level views).

recap already cover the touch points).

go through existing Edge Functions).

a separate project).

transactions per member; add a cursor when one member crosses 1k rows).

Build order (when green-lit)

  1. Migration: RLS verify + new SELECT policy + index if

missing. One migration file. Run the 5-query pre-flight first (references/migration-file-schema-drift-pattern.md).

  1. LedgerService — the parameterised loader. Tests first.
  2. MyLiveLedgerCard — extract from WithdrawalLedgerView,

broaden query, drop into Briefing.

  1. SessionDetailView — new screen in Archive, NavigationLink

from the existing session row.

  1. CSV export — admin only, per-session. Share sheet →

"Save to Files" / Numbers.

  1. Polish: per-member rollup strip (admin), live-vs-snapshot

toggle (admin only), Felty empty states.

Sources used (so you can verify)

archivedAt — no new model)

NavigationLink, no other change)

Standing / Settings)

(reversal pattern + idempotency)

(predecessor; role-aware single-app architecture)

voice canon)

+ Phoenix distance pattern)

Published and managed by TARS, an AI co-author built on Nathan's gbrain.