# Edit the board in the browser

A published board is static by default: it shows the files as they were at
build time, and a drag only stages a move. Three steps make the same page
edit the board:

### Name the server in docs.yaml

  `kanban.api` in the site's `docs.yaml` names the board server, and
  `agents.yaml` configures it. See [Two keys turn the board live](#two-keys-turn-the-board-live).

### Run the server

  `folio-board-server api` runs beside the checkout that holds the board.

### Open the link it prints

  The link carries a token new on every start. See
  [The server prints the link that opens the board](#the-server-prints-the-link-that-opens-the-board).

The server writes the card file, checks the whole board, and commits. The
files in git stay the source of truth; the page reads them back after every
save.

![A live board: the filter bar shows the live mark](./kanban-live-board.png)

## Two keys turn the board live

The site that renders the board names the server. For the server on this
page, the URL is `http://127.0.0.1:<port>` or `http://localhost:<port>`, the
two hosts it answers. An `https://` origin with no path names a hosted server
instead (`board.server.hosted`): the board then offers **Sign in with
GitHub** and keeps the session in the browser for 8 hours. Any other value
fails the build.

```yaml
# docs.yaml
kanban:
  source: board
  routes:
    public: true
  api:
    url: http://127.0.0.1:4350
```

The project whose `agents.yaml` names the board configures the server:

```yaml
# agents.yaml
board:
  source: board
  server:
    bind: 127.0.0.1:4350                  # the default; loopback only
    origins: ["http://localhost:4340"]    # the pages allowed to call it
    read_only: false                      # true refuses every write
```

| Key | Default | Meaning |
| --- | --- | --- |
| `board.server.bind` | `127.0.0.1:4350` | The address the server listens on. Use `127.0.0.1`: the server answers only `127.0.0.1` and `localhost` in `Host`. |
| `board.server.origins` | `[]` | Browser origins allowed to call the server, matched exactly. Empty, no page can call it. |
| `board.server.read_only` | `false` | `true` answers every write with `403 read_only`. |

A site built without `kanban.api` carries no server address and no editor
code. Removing the key and rebuilding returns the page to the static board.

## The server prints the link that opens the board

```bash
# REPOSITORY is Folio Board's git URL. The build fetches Folio Docs from its
# public repository.
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --locked \
  --git "$REPOSITORY" folio-board-server
folio-board-server api --project-dir .
```

```text
board: /home/ana/widgets/board
api: http://127.0.0.1:4350
open: http://localhost:4340/kanban/#board-token=3f9c…
token: 3f9c…
```

The token is a random 256-bit value, new on every start. Open the `open:`
link: the page reads the token from the address, keeps it in the tab's
session storage, and removes it from the address bar. The part after `#` is
never sent to a server. A new tab or a restarted server needs the link again.
`--token HEX` fixes the value, for scripted demos.

**The server never pushes**

  It commits in the board's own checkout. Push the board branch as you would
  after `folio-agents board move --commit`.

## Every save is one commit

A live card has no edit mode. Each value in the dialog is its own control:
click it, change it, and press Enter or leave the field. The value shows at
once with a mark while the commit runs; a refused save rolls back and says
why under the control.

| Gesture | What the card file gets | Commit message |
| --- | --- | --- |
| Drag a card to another column | `status:` | `board: <id> <from> -> <to>` |
| Edit the title, milestone, track, type, tags, priority, size, assignee, parent, blocked by or created | that frontmatter field | `board: update <id>` |
| Edit the description | the body before the first machine heading | `board: describe <id>` |
| Tick or untick a criterion | its checkbox | `board: check criterion on <id>` |
| Rewrite a criterion | its text | `board: update criterion on <id>` |
| Add a criterion | one open line under `## Acceptance criteria` | `board: add criterion on <id>` |
| Add a comment | one line at the end of `## Comments` | `board: comment on <id>` |
| Add a trail note | one line at the end of `## Trail` | `board: trail <id>` |
| Add an artifact | one `artifacts:` entry | `board: attach <kind> to <id>` |
| Remove an artifact | that `artifacts:` entry | `board: detach <kind> from <id>` |

Comments and trail lines carry today's date and the server's actor, the
slugified git `user.name` of the board checkout. No request names an actor.
A move shows **Undo**, which commits the move back.

The artifact tile takes a URL, `#42` for a pull request, or the path of a
file the board already carries, from the card's directory or as
`board/cards/<id>/…`. A file elsewhere in the project is refused; attach it
with [`folio-agents board attach`](/docs/reference/cli#attach).
Only entries listed under `artifacts:` can be removed; a file in the card's
directory stays until the file goes.

![Adding an artifact on a live card](./kanban-live-attach.png)

## The description opens in a rich editor

**Edit description** opens the card's description in place. Selecting text
shows a bar with bold, italic, inline code, a link, two heading levels, two
kinds of list and a quote; `/` at the start of a line inserts a block, such as
a code block or a divider. Cmd or Ctrl+Enter saves. The server serves the
editor itself, at `/editor/v1.js`, so a static site never loads it.

![The description editor on a live card](./kanban-live-editor.png)

The editor opens as plain Markdown source instead, with a line that says
why, in three cases:

- the description uses a component, such as `<Tabs>`;
- the rich editor would rewrite part of the description;
- the editor did not load.

Only the description reaches the editor. The frontmatter, the criteria, the
comments and the trail keep their own controls, and the server refuses a
description that would change them, such as one that adds a `## Trail`
heading. Pasted plain
text joins its lines into one paragraph line, as
[`board fmt`](/docs/reference/cli#fmt) writes them.

## A card that changed is refused, not merged

Every edit except a comment, a trail note and an attached artifact carries
the hash of the card the page last read. When the card file changed since,
the save is refused with `412 precondition_failed` and the card as it is now.
The page shows the current card; make the edit again. A comment, a trail note
and an attached artifact need no hash: each adds a dated line or an entry and
changes no text the page showed. Adding a criterion needs the hash, like every
other edit.

A commit to another card is not a conflict. The server re-reads the board
and retries up to three times, then answers `409 stale`.

Each save must leave a valid board, and a refused save writes nothing. A
`parent` that makes a cycle gets `400 invalid_input`. A description that would
change another section, such as one that adds a `## Trail` heading, gets
`422 board_invalid`.

## Without the server the board stays static

When the server does not answer, or the tab has no token, the filter bar
shows **read-only**. The board then works as a static board: moves are staged
in the browser and **Export moves** prints the `folio-agents board move`
commands.

## The server answers loopback only

These rules hold for the local server. A hosted server answers its public
host and takes a GitHub session instead; the
[Board API](/docs/reference/api#a-hosted-github-server-adds-three-sign-in-routes)
lists its routes.

- It listens on a loopback address; any other `bind` is a config error.
- A request whose `Host` is not `127.0.0.1:<port>` or `localhost:<port>` gets
  `421 misdirected`, which stops DNS rebinding.
- Only the pages in `origins` pass CORS, and no cookies are used.
- Every route except `/editor/v1.js` needs `Authorization: Bearer <token>`.
- A request body is at most 256 KB.

## What the browser cannot do

**Not available in this release**

  Creating, deleting and archiving cards and editing `board.yaml`. Use
  `folio-agents board add` and a hand edit.

The routes the page calls are listed in the [Board API](/docs/reference/api).
