# Penny customer-feedback decision seed

Penny is a self-hosted customer-feedback decision product for one B2B software
business. Its current maturity is a **read-only UI port and D1 data spine**,
not a released product: the interface reads real durable records through Hono
DTOs, but it does not create sessions or mutate product data.

One Cloudflare Worker serves the Vite-built React application and Hono API.
`/health` checks D1 through Drizzle plus the required R2 and Queue bindings.
The public portal reads only customer-safe boards, published requests, approved
roadmap projections, and published releases. The operator app reads the D1
workspace only after the Worker validates an allow-listed operator session.

Buyer-owned public identity lives in `src/ext/config.ts`. It supplies the
visible workspace name, portal description, footer-credit toggle, browser title,
and generic not-found branding without exposing the server-only operator or
origin configuration to the browser bundle. Research material under `research/`
records visual provenance only; no static prototype pages ship with the app.
Verified runtime captures and their clean-clone provenance live in
[`visual/`](visual/README.md).

Read `CONTRACT.md` for the intended product guarantees, `docs/read-spine.md`
for the current route and contract-area boundary, and `HANDOVER.md` for the
work still required before a buyer-facing release.

## Local development

```bash
pnpm install
pnpm dev
```

The generated `wrangler.toml` contains a Miniflare-only D1 identifier and is
ignored by git. `pnpm setup` and `pnpm deploy` are deliberately not implemented
yet, so this port has not been presented as a deployed release.

With the local Vite address open, verify:

1. `/` shows published feedback; `/portal/feedback/:slug`, `/portal/roadmap`,
   and `/portal/updates` consume the public read API. `/portal` and
   `/portal/feedback` redirect to `/`; unknown portal routes use the app 404.
2. `/portal/feedback/new` is a fresh unavailable state. It collects no input
   because requester identity and moderation are not configured.
3. `/signin` accurately states that native magic-link issuance is unavailable.
4. `/app` redirects an unauthenticated browser to `/signin`. A real,
   allow-listed D1 session can read the operator workspace; the product does
   not yet provide a way to issue that session.
5. `/health` returns `{"status":"ok"}` only after local D1, R2, and Queue
   checks succeed.

## Screen and API map

| Client route | Current read surface | API |
| --- | --- | --- |
| `/` | published feedback and board browse | `GET /api/public/boards`, `GET /api/public/requests` |
| `/portal/feedback/:slug` | published request and approved comments | `GET /api/public/requests/:slug` |
| `/portal/roadmap` | approved customer-safe roadmap | `GET /api/public/roadmap` |
| `/portal/updates` and `/:slug` | published updates, including the honest empty feed | `GET /api/public/releases`, `GET /api/public/releases/:slug` |
| `/app/overview` | attention decisions, unreviewed evidence, active roadmap, recent releases | `GET /api/operator/overview` |
| `/app/feedback` | evidence inbox | `GET /api/operator/evidence` |
| `/app/decisions` and `/:slug` | decision queue and comparable brief | `GET /api/operator/decisions`, `GET /api/operator/decisions/:slug` |
| `/app/roadmap` | internal roadmap read view | `GET /api/operator/roadmap` |
| `/app/releases` and `/:slug` | draft/published releases and targets | `GET /api/operator/releases`, `GET /api/operator/releases/:slug` |
| `/app/customers` and `/:slug` | operator-only account context | `GET /api/operator/accounts`, `GET /api/operator/accounts/:slug` |

All operator reads require the same valid D1 session. Loading, retryable
failure, unavailable action, empty, missing-record, and expired-session states
are deliberately rendered; no action is simulated with browser state or
fixture data.

## Validate

```bash
pnpm verify
```

The current gate runs real TypeScript and lint checks, the idempotent local-D1
sample importer, Worker trust-boundary and read-route tests, and the
Worker/Vite production build. It does not yet provide complete product or
baseline contract coverage, browser accessibility checks, load tests,
previous-release migration verification, a clean deployment workflow, or
release packaging.

The read UI has also been exercised from a disposable clean clone at all
mounted routes and representative 390, 768, 1280, and 1536 pixel viewports.
That browser proof is recorded in [`visual/README.md`](visual/README.md); it is
local runtime evidence, not a claim of deployed release readiness.
