API

Pushing data

How live JSON reaches a widget — via the MCP, or from any script.

A widget renders whatever JSON is currently set as its feed. Update the feed and the phone picks it up on its next poll, re-invoking the widget’s onData — the page never reloads.

From the MCP

push_data sets the feed to an arbitrary JSON object the widget understands:

// push_data
{ "id": "wgt_abc123", "data": { "tempC": 21.4, "available": true } }

The data is opaque to the hub — it’s the contract between your widget’s onData renderer and whatever is producing values. Push again to update.

From a script (no MCP) — feeding the widget

A Preen widget never calls out; something has to push values in. That producer is the feed side of the widget’s backend, and it can be anything that speaks HTTP — a cron job, a sensor daemon, a CI step, a Raspberry Pi. There’s no Preen SDK to install and no code of yours runs on the Mac: you just POST JSON to the widget’s feed.

Get the feed’s URL and token once with get_data_endpoint:

// get_data_endpoint
{ "id": "wgt_abc123" }
// → { "url": "https://<mac-lan-ip>:<port>/api/data/wgt_abc123", "token": "…" }

Use the returned url verbatim — it’s the widget’s LAN data path (/api/data/:id), reachable from any host on your network. Don’t hand-build an /admin/… URL: the admin surface is localhost-only and isn’t the producer write path.

Then POST JSON with the bearer token:

curl -X POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tempC": 22.0, "available": true}'

The body becomes the feed wholesale — the same object your onData renderer receives. A 200 means it’s stored; the phone shows it on its next poll.

Treat the token like a secret. It’s your hub’s write token, and it can push data to any widget’s feed — not just this one — so don’t hand it out per widget. Keep it in an env var or your secrets store, never in the widget HTML.

TLS. The hub serves HTTPS with a self-signed certificate (the paired phone pins it). A strict client will reject that cert unless you trust the Mac’s certificate explicitly; for a quick local producer, curl -k skips verification.

Keep it fed (cron / launchd)

A backend is usually just this POST on a timer — read once, push once, and let your scheduler handle the loop:

#!/usr/bin/env bash
# preen-feed.sh — run from cron/launchd. PREEN_URL + PREEN_TOKEN come from
# get_data_endpoint; export them in the job's environment.
set -euo pipefail
cpu=$(ps -A -o %cpu | awk '{ s += $1 } END { print int(s) }')
curl -fsS -X POST "$PREEN_URL" \
  -H "Authorization: Bearer $PREEN_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"cpu\": $cpu, \"available\": true}" || true

Feeding several widgets is just several POSTs — one get_data_endpoint (URL + token) per widget id. If a push fails, the widget keeps showing its last value; push {"available": false} when you’d rather the renderer drop to an explicit “no data” state.

How fast it updates

The phone polls GET /api/data/:id every refreshMs (set per widget at publish time; default 1500 ms). So an update is visible within one poll interval. Lower refreshMs for snappier widgets, higher to be gentle — the native app does the polling, not your widget.

Shape your payload for the widget

Because data arrives as a whole object each time, design a flat, self-describing shape and have the renderer tolerate missing keys:

{ "cpu": 34, "gpu": 12, "mem": 61, "available": true }
window.PREEN.onData((d) => {
  d = d || {};
  if (d.available === false) return;
  set("cpu", d.cpu); set("gpu", d.gpu); set("mem", d.mem);
});