Skip to content

Releases: PulsHealth/pulshealth

Release list

0.2.0

Choose a tag to compare

@smwade smwade released this 18 Sep 17:32
f4e0fc1

Upgrading: move the checkout to v0.2.0 (git pull, or
git checkout v0.2.0), set PULS_VERSION=0.2.0 (or track latest), then
make pull up; migrate applies 014_device_tokens.sql and re-runs
099_read_roles.sh. No .env changes are required — the shared
PULS_TOKEN keeps working exactly as before, and PULS_MULTI_USER defaults
to off. The checkout step is not optional: 0.2.0's ingest records
device_token_id on every batch, a column only 014 adds, so the new
images on a 0.1.0 checkout answer every upload with a 500 (the app keeps its
anchors and retries, so nothing is lost, but nothing syncs either).

Added

  • Per-device tokens (make devices ARGS='issue --user <uuid> --name <label>', list, rename, revoke; SRV-8). Each is stored only as its
    SHA-256, bound to one user, revocable on its own and stamped with its last
    use. A request that presents one acts as that user: X-User-ID may be
    absent or equal, anything else is 403 before the body is read. Every
    batches row now records which device wrote it (device_token_id, NULL
    for the shared token) and the per-batch log line carries token_id.
    Migration 014_device_tokens.sql.
  • Per-request user scoping on the product API (SRV-11, the API side;
    the web viewer's switcher and the MCP server's user argument below
    ride on it). Every
    /v1 route takes an optional user=<uuid> query parameter; absent, the
    request is answered for PULS_USER_ID exactly as before. GET /v1/users
    lists the users the deployment answers for — name, e-mail, createdAt,
    lastSync, batches, uploadedSamples from the batches log — plus
    default and multiUser. /openapi.json describes the parameter on
    every scoped operation.
  • PULS_MULTI_USER (.env, default false) decides whether user= may
    name anyone but the default. Off, another user is 403 multi-user reads are disabled, never a quiet answer for the default user; a value that
    is not a UUID is 400; neither charges the auth-failure limiter. Turning it
    on means the one static PULS_API_TOKEN — the token docs/ai.md says to
    hand to a ChatGPT Action — reads every user on the server, so it stays
    off until you want that.
  • puls-export --user <uuid> (default $PULS_USER_ID, else none) picks
    whose data to export, and a 403 is explained the way a 401 is.
  • web: a user switcher when the database holds more than one user;
    ?user=<uuid> picks one (SRV-11).
  • MCP: list_users, and a user argument on every tool; PULS_USER_ID
    pins an instance to one person (PULS_MCP_USER_ID for the Compose
    service). Needs the product API's user parameter and /v1/users (SRV-11).
  • GET /v1/summary?range=7d|14d|30d|90d on the product API: the last N
    calendar days as one short markdown page (activity, heart, sleep,
    workouts, body, coverage) for pasting into a chat that has no MCP
    connection; format=json for the numbers. The MCP server exposes it as
    get_summary (AI-6).

Changed

  • PULS_TOKEN is optional. PULS_ALLOW_SHARED_TOKEN (default true) turns
    the shared token off once every phone has its own; empty PULS_TOKEN does
    the same. Ingest logs its auth mode at startup and warns when nothing at
    all could authenticate. scripts/bootstrap.sh and make pairing accept
    that mode instead of dying on an empty token.
  • A device-token lookup that fails because the database is unreachable is
    503 authentication unavailable, never 401, and is not charged to the
    auth-failure limiter; neither is a 403 user mismatch.
  • The grafana role loses SELECT on device_tokens (revoked by
    099_read_roles.sh on every run).
  • The api_reader role gains SELECT on batches (for /v1/users; the
    table holds no credential). No operator action: 099_read_roles.sh
    re-runs on the next docker compose up -d.
  • web: Next.js 16.3.5 (from 16.3.4).

0.1.0

Choose a tag to compare

@smwade smwade released this 15 Sep 15:37

The first tagged release, and the one that first publishes
ghcr.io/pulshealth/{ingest,api,mcp,web} — before it, a compose install had
nothing to pull and had to build from the checkout. Everything below shipped
together; there is no earlier release to diff against.

Added

  • The Puls Sync Protocol, v1. One gzipped NDJSON POST plus optional read
    endpoints, specified in docs/protocol/ with JSON Schemas, a fixture corpus
    with expected outcomes, an offline schema checker (tools/protocol-check)
    and a Python reference receiver. The receiver's smoke test doubles as a
    conformance runner against any implementation:
    smoke_test.py --url <url> --token <token>.
  • The reference backend. Go ingest and product API over PostgreSQL 17 /
    TimescaleDB, with Grafana dashboards for the data and for ingest health.
    Ingest is idempotent per sample UUID, upserts aggregates and activity rings,
    and connects as a scoped DML-only role rather than the superuser.
  • Schema migrations that apply themselves. The migrate service runs
    before every app service on docker compose up -d, records each file in
    schema_migrations with a checksum, refuses an edited or missing applied
    file, and requires an explicit baseline for a database that predates it.
  • A one-command quickstart. scripts/bootstrap.sh generates the secrets,
    starts the stack and prints the pairing QR; make wraps the rest. --lan
    trades TLS for a phone on the same Wi-Fi, on request only.
  • Read-only MCP server (server/mcp), 11 tools over the product API in
    stdio and streamable-HTTP modes, so Claude Desktop, Claude Code, Cursor and
    ChatGPT can answer questions from the data. It never touches Postgres.
  • Export. GET /v1/export streams CSV or JSONL; tools/puls-export is a
    dependency-free CLI over it.
  • Product API endpoints an analyst asks for first: daily sleep, bounded raw
    samples, workout series, state of mind, activity rings, daily metrics that
    resolve the iPhone/Watch double-count.
  • One type vocabulary. docs/protocol/catalog.json is rendered from the
    Swift HealthTypeCatalog, and the web catalog is generated from the JSON, so
    the two published lists cannot drift.
  • Self-hosting safety rails. Auth-failure rate limiting on both ingest and
    the product API (failed attempts only, never successful ones), a /healthz
    on each that answers from a two-second cache rather than the pool, an
    optional password on the web viewer, loopback binds by default, and an
    opt-in backup service with a documented restore drill.
  • Open-source hygiene. Apache-2.0 with NOTICE and TRADEMARK.md,
    SECURITY.md, CONTRIBUTING.md with DCO sign-off, a code of conduct, issue
    and PR templates, AGENTS.md, llms.txt, and a CI gate that fails on
    owner-specific content in tracked files.