Klippy saves notes from the chat. This lesson gives them a page: list, search and delete, with plain HTML forms and no browser JavaScript. It reads the same D1 table the agent writes, through the same query layer.
Verified remix 3.1.0wrangler 4.157.0
Start from d-06 on GitHub. Starting here, or behind? Get the code at that tag:
git clone https://github.com/ryanyogan/klippy cd klippy git checkout d-06
1. The prompt
You practised this one in lesson 4. Fresh session, plan mode:
Goal: add a Notes page that lists, searches and deletes the notes Klippy saves, so I can tidy them without the chat. Use the remix skill: read guides 08 (data and validation) and 09 (forms and mutations), and the data-table and data-schema READMEs first. Constraints: - Same D1 database and the same `notes` table as the agent: import it from @klippy/db, query with remix/data-table through the data-table-d1 driver. No raw SQL strings, no new tables. - Native forms: search is a GET form with ?q (at most 100 characters), delete is a POST form per note that redirects with 303. Validate q and the id with remix/data-schema. - Signed-in only (the d-06 requireUser). Touch only apps/web/**. Plan first, then wait for my go-ahead. Done when typecheck and remix doctor pass, signed-out /notes redirects to /login, and a cross-site delete gets 403. Report the files and outputs.
One table, one definition. Importing it from
@klippy/dbmeans the agent and the page can't drift apart.GET to search, POST to delete. A link that deletes can be followed by a prefetcher or a crawler. A 303 after the POST means a refresh doesn't delete again.
2. What to expect
New: app/db.ts and app/actions/notes/ (controller, schema, page). Changed: routes, router, the layout's nav, wrangler.jsonc (the same klippy-db, same id) and package.json.
Every input is parsed with
data-schemafirst. Bad input is a 4xx, never a 500.like('body', …)binds the value. A typed%only widens the search; it never becomes SQL.
Our run (real; signed in with a test cookie made locally from our .dev.vars, and three seeded notes):
Also checked: no match says No notes match "zebra".; a 101-character search is a 400; a bad id is a 404; a cross-site delete is a 403; a signed-out delete goes to /login; a delete answers 303 to /notes?deleted=1, which shows "Note deleted." in a status line.
Privacy, again. Notes have no owner column. The allowlist from lesson 6 is what keeps them yours.
3. Review it
The table comes from
@klippy/db, not redefined. The D1database_idmatches the agent'swrangler.jsonc.Every input goes through
data-schemabefore use.Delete is POST + 303. No GET deletes anything.
Each Delete button's name says which note.
Reject: string-built SQL (
"… LIKE '%" + q + "%'"), a second copy of the notes table, browser JavaScript for this page.
Steer prompts:
You built the search SQL by joining strings. Use like('body', `%${q}%`) from remix/data-table so the value is bound.Delete is a link (GET). Make it a POST form, and redirect with 303 after deleting.You redefined the notes table in apps/web. Import it from @klippy/db so both Workers share one definition.Your repo matches tag d-07 (changes from d-06).