# Pages and cards

Part of [Build a custom machine](https://www.ownbox.io/sdk/guide).

A page is a function the Ownbox calls each time a person opens it. It answers **screen.json**: blocks the Ownbox
draws in its own look, phone first.

```python
@m.screen(m.home, title="Jobs")                    # /my/<slug>; owner_only=True for a page only the owner opens
def home(query, person):                           # it may take query, person and path, by name
    return {"v": 1, "title": "Jobs", "lede": "What is open today.", "blocks": [
        {"kind": "figures", "items": [{"value": 3, "label": "open"}]},
        {"kind": "list", "items": [{"text": "Fix the gutter", "href": m.home + "/jobs/41"}]},
        {"kind": "actions", "items": [{"run": "sync", "label": "Sync now"}]},      # a job it declared with m.every
        {"kind": "settings", "fields": [{"key": "min_quote", "label": "Smallest job", "type": "number", "min": 0}]}]}
```

**The page function** takes any of these, by name:

| Argument | Is |
|---|---|
| `query` | The page's query string as `{name: value}`: at most 10 names, each value at most 200 characters. |
| `person` | Who is viewing: `{"id", "role"}`. Pass `person["id"]` to `m.notify`. |
| `path` | The address asked for, for a page declared above it (`/my/<slug>/jobs/41` under `m.home + "/jobs"`). |

**screen.json** is `{"v": 1, "title", "lede"?, "blocks": [...]}`: a title of at most 120 characters, a lede of at most
300, and at most 40 blocks and 64 KB as JSON. Every block takes an optional `title` of at most 80 characters.

| Block | Fields | Limits |
|---|---|---|
| `text` | `text` | 2,000 characters; a blank line starts a paragraph. |
| `figures` | `items: [{value, label, hint?}]` | 1 to 6 items; `value` a number or 24 characters, `label` 40, `hint` 80. |
| `list` | `items: [{text, detail?, when?, href?}]`, `empty?` | 50 items; `text` 200, `detail` 300; `when` an ISO time. |
| `table` | `columns: [...]`, `rows: [[...]]` | 1 to 8 columns, 100 rows, 120 characters a cell. |
| `actions` | `items: [{run, label}]` | 1 to 3 buttons; `run` names a job it declared; `label` 60. |
| `settings` | `fields: [{key, label, type, help?, ...}]` | 12 fields; `label` 80, `help` 200. |

**Settings fields** are the only input a page takes. The Ownbox draws the form, checks each value, and saves it, for
the owner only, into the machine's own settings under the field's `key`: read it with `m.setting(key)`. A key its page
did not declare is never written.

| Type | Takes | The Ownbox checks |
|---|---|---|
| `text` | `max` (default 200, at most 500) | One line, no control characters. |
| `text` with `lines: true` | `max` (default 2,000, at most 5,000) | Newlines kept, no other control characters. |
| `number` | `min`, `max` | A number in range. |
| `toggle` | — | `true` or `false`. |
| `choice` | `choices`: 2 to 12, each at most 60 characters | One of them, exactly. |
| `url` | — | `https://` with a host, at most 2,000 characters. Shown as text, never as a link. |
| `items` | `fields` (1 to 4 sub-fields), `max` (1 to 50), `title?`, `toggle?`, `unique?` | A list of small records, each checked against its sub-fields. |

An `items` field saves `[{"id": "<the Ownbox's own>", "<sub-field>": value, ...}]`, newest last. Its sub-fields are any
type above except `items` and multi-line `text`, and none may be named `id`. A save made from an older copy of the list
is refused with the list as it is now, never merged quietly.

**The rules the Ownbox draws by:**

- Every string is plain text. The Ownbox escapes it, so markup shows as written.
- A link (`href`) is kept only when it is one of the machine's own pages, under `m.home`. Any other is dropped and the
  line kept.
- **A button runs one of its jobs** as if its time had come. The job does the work, never the page. At most one run of
  a job waits at a time. The Ownbox remembers the buttons a page drew for each role, not each person: a button drawn
  for one member is every member's.
- **An answer that fails a check** is not drawn. The page says *This page couldn't be shown*, with the reason, which
  names the field.
- A machine declares at most 20 pages and 10 cards.
- **The owner's AI reads its pages in words,** through the Ownbox's own tool for it. A page only the owner opens is
  never read for an AI.

**A card**, `m.panel(slot, title=, render=)`, draws on another screen's slot. `render()` answers screen.json of at most
6 blocks and no settings block, or an HTML body. It may take `person` by name. A card is drawn for anyone signed in:
check `person["role"]` for what only the owner should see. Today the Inbox's screen offers the slot `inbox`.

**`sdk.page(path, title=, lede=, body=)`** answers with HTML instead, for a machine written before screen.json. The
Ownbox keeps `p h2 h3 h4 ul ol li table thead tbody tr th td strong em b i br hr blockquote code pre small span div` and
`a` (a link to its own pages only). It cuts `script`, `style`, `iframe`, `form`, `img` and the like with everything
they hold, and drops every attribute but `class` (`card`, `quiet`, `figure`, `row`), `colspan` and `rowspan`.

Those are every block and every field type a page can hold: the Ownbox refuses any other kind. Their limits are
listed again under [Limits](https://www.ownbox.io/sdk/guide/limits).
