# KV Key Inventory

**Last updated:** 2026-05-16 by CC
**Source backend:** Cloudflare KV bound as `env.OUR_KV` (see `/functions/our/data.js`)
**Read/write surface:** `/our/data?key=<name>` (GET/PUT/POST/DELETE)
**Purpose:** canonical map of every KV key used across the app. Reference when reading from KV in new code, when an audit needs to know what's stored where, or when migrating a feature.

## Conventions

- Keys are lowercase, dash-separated, no spaces. Pattern: `^[a-z0-9][a-z0-9_-]{0,63}$` (per `isValidKey` in `data.js`).
- Per-entity keys use `<feature>-<entity-id>` (e.g. `bet-attended-2026-05-16-pit-phi`).
- Per-month metrics use `<feature>-YYYY-MM` (e.g. `odds-api-usage-2026-05`).
- Per-game outcome lives at `prediction-outcome-{game-id}` so the static `predictions.json` doesn't need a git push to score a result.
- The KV is the source of truth for runtime state. Static JSON files (`/data/*.json`, `predictions.json`) are seeds + fallbacks.

---

## Singleton keys (one value, not per-entity)

| Key | Shape | Producer | Consumer |
|---|---|---|---|
| `calendar` | `[{ id, title, date, time, who, notes, duration_min, completed }, ...]` | /our/inbox.html (manual entry) · planning chat | /our/index.html · /personal/calendar/current.html · /personal/status-tracker/current.html · /index.html `loadCalendarSchedule()` |
| `announcements` | `[{ id, text, date, priority }]` | admin | /win/ · /our/ |
| `bce-feed` | `[{ game_id, score, conditions_met, classification }, ...]` | /bets/edges/blowout-correlation/ scanner | dashboard pages |
| `cc-activity-current` | `{ task, status, ts }` | CC writes during long ops | /index.html CC activity strip |
| `cc-prompt-queued` | `[{ id, prompt, queued_at, source }]` | /index.html "queue task for CC" button | next CC session (read at start) |
| `filament-status` | `{ rolls: [{ id, color, brand, weight_remaining_g, status, notes }], last_updated }` | /personal/3d-printing/filament-admin.html | /win/ filament tile · /personal/3d-printing/dashboard.html |
| `inbox` | `[{ id, text, added, added_by }, ...]` | /our/inbox.html capture | /our/ · planning processor |
| `calendar-inbox` | `[{ id, text, added, added_by, hint }, ...]` | /our/ · /win/ capture row | planning processor (manual review) |
| `messages-from-win` | `[{ id, text, added }]` | /win/message-dad.html | /index.html Win feedback queue · /win/ replies tile |
| `sam-replies-to-win` | `[{ to_msg_id, reply, ts }]` | /index.html Win-feedback admin | /win/message-dad.html |
| `ripening` | `[{ id, title, body, captured_at, status }]` | thoughts-inbox processing | "Ripening" surface (Sam's hub) |
| `sam-followed-teams` | `{ pirates: 'follow' \| 'mute' \| null, ... }` | per-team Follow/Mute buttons | bet-reminder filtering · "Where you are" lean-toward |
| `thoughts-inbox` | `[{ id, text, captured_at }]` | Sam mid-conversation captures | recalibration sessions |
| `trip-pittsburgh-2026-hotel` | `{ name, address, check_in, check_out, status, saved_at }` | /personal/trips/pittsburgh-2026-05-15.html form | /our/ today tile · trip page |

## Per-entity prefix keys

| Prefix | Suffix shape | Producer | Consumer |
|---|---|---|---|
| `bet-attended-{game-id}` | `{ attended: bool, ts }` | game-page "I attended" button | hub · prediction log |
| `bet-placed-{game-id}-{tier}` | `{ amount, book, ts }` | portfolio-tracker state machine | weekend tracker totals |
| `bet-reminder-{game-id}` | `{ active: bool, set_at }` | game-page "Remind me to bet" | Sam's hub Where-You-Are panel · auto-removed 90 min after first pitch |
| `customer-messages-{customer-id}` | `[{ id, text, from, ts }]` | /jon/message-sam.html · /customer/message-sam.html | admin reply UI |
| `customer-requests-{customer-id}` | `[{ id, text, status, ts }]` | /customer/request-feature.html | admin pipeline |
| `earn-task-{task-id}` | `{ name, amount, status, completed_at, paid_at }` | /personal/win-earn-admin/ | /win/earn.html · hub Win-Earnings tile |
| `factor-track-{slug}` | `{ attempts, hits, last_seen, notes }` | prediction-log scorer | factor accuracy reports |
| `invite-sent-{recipient}` | `{ method, ts, link }` | /invites/ | invites page "last sent" timestamp |
| `manual-odds-{game-id}-{book}-{market}` | `{ gameId, book, market, line, entered_at }` | /bets/games/_live-widgets.js manual override modal | live-widgets odds display (overrides API) |
| `manual-odds-index-{game-id}` | `[key1, key2, ...]` | _live-widgets append after manual save | _live-widgets loadManual() — list without KV-list API |
| `odds-api-usage-{YYYY-MM}` | `{ month, calls, first_call, last_call }` | /functions/bets/odds-fetch.js | /bets/api-usage/ dashboard |
| `portfolio-feed-{date-slug}` | `[{ bet_id, outcome, actually_placed, book, notes }]` | /bets/portfolio/track/{date}-weekend.html state machine | weekend tracker P/L computation |
| `prediction-outcome-{game-id}` | `{ outcome: 'win' \| 'loss' \| 'push' \| 'pending', verified_at, score }` | game-page scoring UI · CC scoring scripts | /bets/calendar.html · /personal/status-tracker/current.html · prediction-log |
| `survey-completed-{customer-id}-{job-id}` | `{ ts, version }` | /jon/survey.html submit | gates the price-reveal flow |
| `survey-response-{customer-id}-{job-id}` | `{ q1, q2, ..., nps, comments, submitted_at }` | /jon/survey.html submit | admin analytics · price-reveal page |
| `whats-new-read-{customer-id}` | `{ last_read_ts }` | /jon/whats-new.html | unread badge logic |
| `win-color-vote` | `{ red: n, blue: n, ... }` | /win/ theme switcher | aggregated stats |
| `win-ideas-new-{ts}` | `{ idea_text, ripeness, captured }` | /win/ capture row | Sam admin promotes to /win/ideas state |
| `win-cards-*` `win-rcs-*` `win-sports-*` etc. | per-prefix | /win/ capture | /win/ tile + Sam admin |

## Cached read-through keys (function-managed)

| Key | TTL | Producer | Consumer |
|---|---|---|---|
| `odds-{sport}-{event_id}-{markets}` | 15 min | /functions/bets/odds-fetch.js | live-widgets odds fetch |
| `scores-{sport}-{date}` | 60 sec | /functions/bets/scores-fetch.js | live-widgets score fetch |

## Schema rules + conventions

- **Validate keys before writing.** `isValidKey` in `data.js` allows `[a-zA-Z0-9_-]` up to 64 chars. Don't include date separators like `:` in keys.
- **TTL via `expirationTtl`.** Use Cloudflare's KV TTL for cache-style data; manual cleanup for content-style data.
- **Schema versioning.** When changing the shape of a key, bump a `version` field inside the value if backward-compatibility matters. Most keys are small enough to migrate inline.
- **Per-entity index pattern.** When the runtime needs to list per-entity keys but KV has no list API (e.g. all manual-odds for a game), maintain an `{prefix}-index-{entity}` array alongside the entries.
- **Static seed + KV override** is the load-bearing pattern: static JSON file under `/data/` or `/bets/prediction-log/predictions.json` for the canonical seed; KV for runtime mutations that shouldn't require a git push.

## Audit query (every key produced/consumed in repo)

```bash
# producers
grep -rohE "kvPut\([\`'\"]([a-z][a-z0-9_-]+)" --include="*.html" --include="*.js" | sort -u
# consumers
grep -rohE "kvGet\([\`'\"]([a-z][a-z0-9_-]+)" --include="*.html" --include="*.js" | sort -u
# fetch-style
grep -rohE "key=([a-z][a-z0-9_-]+)" --include="*.html" --include="*.js" | sort -u
```

Run periodically and reconcile against this doc. Anything in the grep results not in this doc → either add it here or fix the typo.
