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) — a widget’s “backend”
A Preen widget never calls out; something has to push values in. That producer is 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 grants write access to that one widget’s feed (and only that 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);
});