A widget renders whatever JSON is currently set as its feed. Update the feed
and the hub streams it to the native phone app, which invokes 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: you POST JSON to the widget’s feed. Your producer runs wherever you install it, including on the Mac if it needs local data. Preen does not schedule or sandbox that producer; its setup and permissions are separate from widget action consent.
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 the hub accepted the feed update; connected stream
subscribers receive it without waiting for a poll. It does not acknowledge
that a phone has rendered it.
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 native app subscribes to GET /api/data/:id/stream, an SSE stream that
sends the current feed and subsequent updates. It reconnects interrupted
streams and also fetches GET /api/data/:id immediately, then roughly every
10 seconds, as a freshness backup. Both the direct LAN connection and the
encrypted relay support streamed data.
The manifest still stores refreshMs (default 1500 ms), but the current
iOS app does not use it to schedule data delivery. Lowering it will not make
the stream faster or change the backup polling interval. Control update volume
in your producer instead.
onData can receive the same snapshot more than once, including from the
backup poll or after reconnecting. Render idempotently. The feed keeps the
latest value and does not replay every update missed while disconnected; use
the datastore for history you need to retain.
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);
});