Serving data

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