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);
});