# Feedback to Snakkbar: from the panel to the developers and back

People working in the panel (the hosted one and every embedded admin) can tell Snakkbar about a
bug, something unclear or a feature they miss. They draw on the screen to show what they mean and
write a message; the report reaches the developers with a picture of the panel, and the developers'
answer comes back to the person in the panel. The developers' tools (Claude Code, a script, CI)
fetch what is open over the API, fix it, and answer.

## In the panel

- **The bug in Pixel's head** (packages/admin/src/feedback/, as in Declarely) opens the drawing
  tool. On a phone Pixel's sheet closes first, so the panel is free to draw on.
- **Answers** show at the top of Pixel's chat (`FeedbackAnswers`): what the person wrote, the
  status and Snakkbar's answer, until «Ok, takk». While Pixel is closed, a dot on its bubble says an
  answer has come. The person's reports are fetched when the panel loads, when the tab comes back
  into view, and every five minutes.
- **The drawing tool** lies over the whole admin. Tools: freehand (H), line (L), rectangle (R), and
  the hand (M), which lets the panel be used under the drawing (open a menu, then mark it). Three
  colours, undo and redo (Ctrl+Z, Ctrl+Shift+Z or Ctrl+Y). Pictures can be pasted, dropped or picked
  (at most three, 8 MB each). Ctrl+Enter sends. On a phone the card folds to its toolbar, and
  «Skriv melding» opens the message (the panel is copied then, before the keyboard squeezes it).
- **Sending** closes the tool at once. The admin's frame is copied with
  [modern-screenshot](https://github.com/qq15725/modern-screenshot) (without the tool and the
  tooltips), the drawing is laid on top, and it is posted as WebP (PNG where the browser cannot
  write WebP). A notice says «Takk! Tilbakemelding #N er sendt», or offers to try again.
- **Pixel** can do it too: when someone describes a problem with Snakkbar itself, Pixel offers to
  send it, writes it up in their words and opens the drawing tool with it (`start_feedback`); the
  person points and sends. `my_feedback` lists their reports and answers.

With every report go: the message, the panel's address and open view (`inbox`, `email`,
`settings/profile`, …), the frame's size, the user agent, the build (`VERCEL_GIT_COMMIT_SHA`, short)
and what the browser says about itself: language, time zone, screen, pixel ratio, touch, colour
scheme, reduced motion, online, and the last ten script errors in the tab. Nothing else the person
did is recorded.

## The developers' API

`/api/v1/platform/feedback`, authenticated with a **platform key** (`Authorization: Bearer
sk_platform_…`) or a signed-in system owner. Make keys in the panel under
**Systemeier → Tilbakemeldinger → API-nøkler for utviklere**; the key is shown once and only its
hash is kept. A tenant's `sk_snakkbar_…` key never gets in, and a platform key opens nothing but
these routes. 120 requests a minute per key.

### `GET /platform/feedback`

| Query | |
| --- | --- |
| `status` | `open` (`new` and `planned`, the default in the panel), `all`, one status, or several separated by commas. Leaving it out means all |
| `tenantId` | Only this customer's |
| `since` | Created on or after this date (`2026-11-01`) |
| `before` | Paging: numbers below this (`nextBefore` from the previous page) |
| `limit` | 1–200, default 50 |

```json
{
  "items": [
    {
      "id": "6c1f…",
      "number": 42,
      "status": "new",
      "message": "Lagre-knappen under Nettsted og chat er grå selv om jeg har endret noe.",
      "tenant": { "id": "amatec", "name": "Amatec" },
      "author": { "operatorId": "…", "userId": "…", "name": "Kari Nordmann", "email": "kari@amatec.no" },
      "page": { "url": "https://snakkbar.app/panel?tenant=amatec&view=settings/site", "view": "settings/site", "viewport": "1440x900", "userAgent": "Mozilla/5.0 …" },
      "appVersion": "fef8627",
      "browser": { "language": "nb-NO", "timeZone": "Europe/Oslo", "screen": "1920x1080", "pixelRatio": 1, "errors": [{ "at": "…", "message": "TypeError: …", "source": "/admin.js:1:2345" }] },
      "screenshotUrl": "https://…/feedback/amatec/….webp?token=…",
      "attachmentUrls": [],
      "response": null,
      "respondedAt": null,
      "responseSeenAt": null,
      "createdAt": "2026-11-04T09:12:00Z",
      "updatedAt": "2026-11-04T09:12:00Z"
    }
  ],
  "nextBefore": null
}
```

Picture links work for ten minutes. Behind the key instead:
`GET /platform/feedback/{id or number}/screenshot` and `/attachments/{1–3}`.

### `GET /platform/feedback/{id or number}`

One report, `{ "feedback": … }`. `42` and `#42` both work.

### `PATCH /platform/feedback/{id or number}`

```json
{ "status": "done", "response": "Takk! Det var en feil, og den er rettet nå." }
```

`status` is `new`, `planned` (accepted, coming), `declined` (not now) or `done`. `response` (at most
4000 characters, Markdown) is shown to the person in the panel until they have read it; a changed
answer shows again, `null` or `""` removes it. A status change alone is not announced. Send the body
as UTF-8 (a body with U+FFFD is refused: that is what a Windows shell makes of æ, ø and å). Every
answer is written to the audit log (`feedback.respond`).

### A round with curl

```sh
KEY=sk_platform_…
curl -s -H "Authorization: Bearer $KEY" "https://snakkbar.app/api/v1/platform/feedback?status=open"
curl -s -H "Authorization: Bearer $KEY" -o 42.webp "https://snakkbar.app/api/v1/platform/feedback/42/screenshot"
curl -s -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json; charset=utf-8" \
  --data '{"status":"done","response":"Rettet i dag. Takk for at du sa fra!"}' \
  "https://snakkbar.app/api/v1/platform/feedback/42"
```

## The person's side of the API

For a custom panel built on the SDK (`client.feedback`):

| Call | |
| --- | --- |
| `POST /operator/feedback` | multipart: `message` (required), `screenshot`, `attachments` (≤ 3), `pageUrl`, `view`, `viewport`, `context` (JSON, ≤ 12 kB). Images are PNG, JPEG, WebP or GIF, checked by their bytes, ≤ 8 MB. 30 an hour per person. Answers `201 { "feedback": { "id", "number" } }` |
| `GET /operator/feedback` | `{ items, unseen }`: the person's own reports (across their tenants when signed in to the hosted panel), newest first |
| `POST /operator/feedback/seen` | `{ ids? }`: the answers have been read (all when `ids` is left out). A system owner acting as someone reads without marking |

## Storage

`feedback` and `platform_api_keys` (supabase/migrations/20261104140000_feedback.sql), read and
written by the server only; pictures in the private `feedback` bucket under `<tenant>/<uuid>.<ext>`.
Reports are numbered across all tenants. A report outlives the operator who sent it (their name and
e-mail are kept on it); deleting a tenant keeps its reports without the link.
