# 🤖 The bakeoff contract

Every app in the bakeoff answers the same requests the same way, so one
test suite and one measurement harness can drive all of them. This file
is binding. If something here seems wrong, say so in your `STATUS.md`
rather than deviating.

## Pages

| Request | Response |
|---|---|
| `GET /` | The whole list |
| `GET /active` | Only items not completed |
| `GET /completed` | Only completed items |
| `GET /<filter>?edit=<id>` | The same page, with that item's `li` given the class `editing` and its `input.edit` inside a form that posts to `/todos/<id>/edit` |
| `GET /static/base.css`, `GET /static/index.css` | `shared/base.css` and `shared/index.css`, byte for byte |
| `GET /healthz` | 200 |
| `GET /api/version` | 200, the body is the build version |

The filter links are `href="/"`, `href="/active"` and `href="/completed"`,
and the current one has the class `selected`. `#/` routes aren't used.

## Actions

Every action is an HTML form POST (`application/x-www-form-urlencoded`).
Without any special request headers, each one answers **303 See Other**
back to the current filter.

| Request | Fields | Effect |
|---|---|---|
| `POST /todos` | `title`, `filter` | Adds an item. The title is trimmed; if nothing is left, nothing is added |
| `POST /todos/<id>/toggle` | `filter` | Flips that item between active and completed |
| `POST /todos/<id>/edit` | `title`, `filter` | Renames it. The title is trimmed; if nothing is left, the item is deleted |
| `POST /todos/<id>/destroy` | `filter` | Deletes it |
| `POST /todos/toggle-all` | `filter` | Marks every item completed. If every item already was, marks them all active |
| `POST /todos/clear-completed` | `filter` | Deletes every completed item |

- `filter` is `all`, `active` or `completed`. The 303 goes to `/`,
  `/active` or `/completed` to match. A missing or unknown value goes
  to `/`.
- A POST naming an `<id>` that isn't in the caller's own list answers
  **404**. Nothing changes, in that list or anyone else's.
- A POST with an `Origin` header that isn't this app's own origin
  answers **403**. The app's own origin is `https://$CARLOS_HOST` on
  Carloku, and `http://<listen address>` when run with `--listen`. A
  POST with no `Origin` header is allowed. There's no CSRF token.
- Your JavaScript may call these same endpoints with a request header of
  your choosing (for example `Accept: application/json` or a custom
  header) and get HTML fragments or JSON back. Without that header, the
  behaviour above must not change.

## Markup

Start from `shared/reference.html`, which is TodoMVC's own template.
With JavaScript on, your page must look like it. The suite compares
screenshots, masking only `footer.info`.

- Every `li` in `ul.todo-list` carries `data-id="<id>"`, has the class
  `completed` when completed, and has `editing` while being edited.
- `section.main` and `footer.footer` are hidden when the list is empty
  (all filters considered). `button.clear-completed` is hidden when
  nothing is completed.
- The counter reads `<strong>N</strong> item left` or
  `<strong>N</strong> items left`.
- The footer line "Created by …" reads
  `Created by <a href="https://bakeoff.bfj.oncarlos.com">the CARLOS bakeoff</a> · <LANGUAGE>`,
  where `<LANGUAGE>` is your setup's name, e.g. `Rust` or
  `Go + Rastrillo`.

### Controls that must work without JavaScript

The suite drives a page with JavaScript off through exactly these:

| Action | Control |
|---|---|
| Add | `input.new-todo` in a form posting to `/todos`. Pressing Enter submits it |
| Toggle one | A submit control matching `[data-action="toggle"]` inside that item's `li`, in a form posting to `/todos/<id>/toggle` |
| Edit | A link `a[data-action="edit"]` inside that item's `li`, going to `?edit=<id>` on the current filter. The edit view's `input.edit` submits on Enter |
| Delete | `button.destroy`, a submit button in a form posting to `/todos/<id>/destroy` |
| Toggle all | A submit control matching `[data-action="toggle-all"]`, in a form posting to `/todos/toggle-all` |
| Clear completed | `button.clear-completed`, a submit button in a form posting to `/todos/clear-completed` |

These extra controls may be visible when JavaScript is off, styled to
fit. When JavaScript is on, the page must match the reference, so hide
or replace them.

### With JavaScript on

The page behaves like TodoMVC's own apps. Double-click a label to edit.
Enter or blur saves, and Escape cancels. An empty title deletes. Clicking
`input.toggle` or `input.toggle-all` takes effect without a full page
load, and the counter, filters, "Clear completed" button and toggle-all
state stay correct. `input.new-todo` is cleared after adding.

## Lists

- The whole app is one instance. Each visitor gets their own list through
  a cookie set on their first visit. The cookie holds a random value with
  at least 128 bits of randomness and is `HttpOnly; SameSite=Lax;
  Path=/`, plus `Secure` when served over https.
- A cookie value the app doesn't recognise, garbled or not, gets a fresh
  empty list, never an error.
- Titles are shown as text, never as HTML. Any Unicode is kept exactly as
  typed, and titles up to at least 10,000 characters work.
- `Content-Length`, when sent, is the number of bytes in the body.
