Skip to content

joshuamyers-dev/sleeplab

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

310 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SleepLab

GNU GPL v3 Frontend Backend

SleepLab is a local-first sleep therapy dashboard for importing and exploring ResMed CPAP data. It includes:

  • A React + Vite frontend in frontend/
  • A FastAPI backend in api/
  • A PostgreSQL-backed importer in importer/ for ResMed DATALOG folders

Screenshots

SleepLab dashboard screenshot 1 SleepLab dashboard screenshot 2 SleepLab dashboard screenshot 3

Stack

  • Frontend: React 19, Vite, TypeScript, Tailwind
  • Backend: FastAPI, SQLAlchemy, Uvicorn
  • Database: PostgreSQL 16
  • Workspace tooling: Nx

Requirements

  • Node.js 20+
  • npm
  • Python 3.12
  • PostgreSQL 16

Self-Hosting With Docker Compose

SleepLab can run as a self-hosted Docker stack with:

  • PostgreSQL
  • FastAPI backend
  • Nginx-served frontend
  • automatic schema migrations at API startup
  • a prebuilt Docker image, so no local image build is required

Key files:

The default self-hosted image is:

joshuaaaronmyers/sleeplab:latest

Required Configuration

Create an env file for deployment by copying .env.selfhost.example.

Set at minimum:

  • SECRET_KEY

Optional but commonly needed:

  • OPENAI_API_KEY
  • CORS_ALLOWED_ORIGINS
  • API_URL

Recommended values for a local/self-hosted machine:

SECRET_KEY=replace-me-with-a-long-random-secret
OPENAI_API_KEY=
CORS_ALLOWED_ORIGINS=*
API_URL=http://localhost:8000

The self-hosted compose stack always uses the internal Postgres DSN:

postgresql+psycopg2://cpap:cpap@postgres:5432/cpap

For the default self-hosted setup, CORS_ALLOWED_ORIGINS is * so the frontend can talk to the API regardless of whether you access it via localhost, 127.0.0.1, or a LAN hostname/IP. If you expose the app publicly, tighten that value to your actual frontend origin(s).

Start The Stack

docker compose up -d

If you want the newest published image first:

docker compose pull
docker compose up -d

View Logs

docker compose logs -f

Stop The Stack

docker compose down

Copy-Paste docker-compose.yml

If you want to self-host quickly on a server, you can use this docker-compose.yml directly:

services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: cpap
      POSTGRES_USER: cpap
      POSTGRES_PASSWORD: cpap
    volumes:
      - sleeplab_postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U cpap -d cpap"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: joshuaaaronmyers/sleeplab:latest
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql+psycopg2://cpap:cpap@postgres:5432/cpap
      SECRET_KEY: replace-me-with-a-long-random-secret
      OPENAI_API_KEY: ""
      CORS_ALLOWED_ORIGINS: "*"
      API_URL: http://localhost:8000
      API_HOST: 0.0.0.0
      API_PORT: 8000
    ports:
      - "8080:8080"
      - "8000:8000"

volumes:
  sleeplab_postgres_data:

Then start it with:

docker compose up -d

docker run Command

If you already have PostgreSQL running separately, you can run just the SleepLab app container:

docker run -d \
  --name sleeplab \
  --restart unless-stopped \
  -p 8080:8080 \
  -p 8000:8000 \
  -e DATABASE_URL="postgresql+psycopg2://USER:PASSWORD@HOST:5432/cpap" \
  -e SECRET_KEY="replace-me-with-a-long-random-secret" \
  -e OPENAI_API_KEY="" \
  -e CORS_ALLOWED_ORIGINS="*" \
  -e API_URL="http://localhost:8000" \
  joshuaaaronmyers/sleeplab:latest

Notes:

  • docker run does not include PostgreSQL. You must provide your own database.
  • API_URL should be the URL the browser will use to reach the API.
  • If the app is exposed publicly, replace CORS_ALLOWED_ORIGINS="*" with your real frontend origin(s).

Default Self-Hosted URLs

  • Frontend: http://localhost:8080
  • API: http://localhost:8000

What The Compose File Does

  • starts PostgreSQL with a named volume
  • pulls joshuaaaronmyers/sleeplab:latest
  • exposes the frontend on 8080
  • exposes the API on 8000
  • waits for Postgres to become healthy
  • runs migrations automatically at API startup

Persistence

Database data is stored in the named volume:

  • sleeplab_postgres_data

Upgrade Workflow

git pull
docker compose pull
docker compose up -d

Migrations run automatically through server.py when the API starts.

Troubleshooting

  • If Docker Compose says the image is missing, run docker login and docker compose pull.
  • If the frontend loads but API requests fail, verify API_URL and CORS_ALLOWED_ORIGINS.
  • If the API container exits early, inspect docker compose logs app for DB or migration errors.
  • If AI summaries are unavailable, check GET /llm/health for provider status and confirm the relevant env vars are set (see AI Summaries below).
  • If you are deploying to a Linux server, use the published multi-arch image tag rather than an old locally built arm-only image.

Quick Start

1. Install dependencies

npm install
cd frontend && npm install

2. Start Postgres

The repo includes a local Postgres service inside Docker Compose:

docker compose up -d postgres

Default database settings from docker-compose.yml:

  • Database: cpap
  • Username: cpap
  • Password: cpap
  • Port: 5432

The API currently connects to:

postgresql+psycopg2://localhost/cpap

That is defined in api/database.py. If your local database setup differs, update that file or add your own configuration layer.

3. Apply schema migrations

Run the SQL files in migrations/ against the cpap database in order:

psql -d cpap -f migrations/001_add_auth.sql
psql -d cpap -f migrations/002_scope_sessions_per_user.sql
psql -d cpap -f migrations/003_add_public_ids.sql
psql -d cpap -f migrations/004_reset_uuid_ids.sql
psql -d cpap -f migrations/005_add_user_profile_fields.sql

4. Run the app

Start frontend and backend together:

npm run dev

Or run them separately:

npm run api
npm run frontend

Default local URLs:

  • Frontend: http://127.0.0.1:5173
  • API: http://127.0.0.1:8000

Timezones

Two IANA timezone settings control how session data is interpreted and displayed.

Variable Default Purpose
MACHINE_TZ UTC The timezone your CPAP machine is set to. The importer uses this to correctly interpret the naive local timestamps embedded in EDF files before storing them as UTC in the database.
DISPLAY_TZ UTC The timezone used to format all time labels in the UI — plot axes, event timeline, session start time. Set this to your local timezone for accurate display.

Both values must be valid IANA timezone names (e.g. America/New_York, Europe/London, Australia/Sydney).

Set them in your .env file or docker-compose.yml:

MACHINE_TZ=America/New_York
DISPLAY_TZ=America/New_York

If your machine is set to the same timezone as your display, both values will be identical. If you travel with your CPAP and don't update the machine clock, set MACHINE_TZ to the machine's home timezone and DISPLAY_TZ to wherever you want times displayed.

Re-importing after changing MACHINE_TZ: The importer attaches the timezone at import time. If you change MACHINE_TZ after sessions are already in the database, re-run the importer with --from to update affected sessions.

Auth

SleepLab uses bearer-token auth.

  • POST /auth/register and POST /auth/login return { token, user }
  • The frontend stores the JWT in browser localStorage
  • Authenticated API requests send Authorization: Bearer <token>

Relevant files:

Importing Data

SleepLab imports ResMed SD card data from a DATALOG folder.

In the UI:

  1. Create an account or log in.
  2. Open the import screen.
  3. Select the DATALOG folder from the SD card.
  4. The frontend uploads the files in batches to the API.
  5. The API runs the importer in the background and writes parsed sessions into Postgres.

The upload/import endpoints are implemented in api/routers/upload.py, and the importer lives in importer/import_sessions.py.

You can also run the importer manually:

cd importer
python3 import_sessions.py --datalog /absolute/path/to/DATALOG --user-id <user-uuid>

Optional filters:

python3 import_sessions.py --datalog /absolute/path/to/DATALOG --user-id <user-uuid> --folder 20241215
python3 import_sessions.py --datalog /absolute/path/to/DATALOG --user-id <user-uuid> --from 20250101

SleepHQ Import

Sessions can be pulled directly from SleepHQ without an SD card.

Setup

Add your SleepHQ OAuth credentials and team ID in Settings → SleepHQ Integration, or set them in .env:

SLEEPHQ_CLIENT_ID=your-client-id
SLEEPHQ_CLIENT_SECRET=your-client-secret
SLEEPHQ_TEAM_ID=your-team-id   # optional — auto-resolved if omitted

OAuth credentials are available from your SleepHQ developer/account settings.

Sync from the UI

Open Import → Sync from SleepHQ and click Sync now. The last 30 days of sessions are fetched and written to the database. Sessions imported this way use a sleephq-{id} session ID to avoid collisions with SD card imports.

CLI

cd importer
python3 sleephq_import.py --user-id <user-uuid> --days 30

# Explicit date range
python3 sleephq_import.py --user-id <user-uuid> --from 2024-01-01 --to 2025-01-01

# Dry run — fetch and map without writing to the database
python3 sleephq_import.py --user-id <user-uuid> --days 30 --dry-run

The importer retries automatically on rate-limit (HTTP 429) responses and pauses between paginated requests, so long historical back-fills work without manual intervention.

AI Summaries

AI-generated session and trend summaries are powered by any OpenAI-compatible LLM backend. The provider is selected automatically based on environment variables — existing deployments with OPENAI_API_KEY continue to work with no changes.

Provider detection

LLM_PROVIDER Backend Required env vars
openai / auto-detected OpenAI cloud OPENAI_API_KEY, OPENAI_MODEL (default gpt-4o)
ollama / default when no key Local Ollama OLLAMA_BASE_URL (default http://localhost:11434/v1), OLLAMA_MODEL (default llama3.1:8b)
litellm LiteLLM proxy LITELLM_BASE_URL (default http://localhost:4000/v1), LITELLM_MODEL (default gpt-4o-mini)
custom Any OpenAI-compatible endpoint LLM_BASE_URL, LLM_API_KEY, LLM_MODEL

Health check

GET /llm/health

Returns the active provider, base URL, model, and whether the backend is reachable.

Self-hosted with Ollama

# docker-compose.yml
services:
  app:
    environment:
      LLM_PROVIDER: ollama
      OLLAMA_BASE_URL: http://ollama:11434/v1
      OLLAMA_MODEL: llama3.1:8b
  ollama:
    image: ollama/ollama:latest
    restart: unless-stopped
    volumes:
      - ollama_data:/root/.ollama

Without any LLM configuration, core dashboard features still work but AI summary endpoints return an error message instead of generated output.

Project Layout

api/         FastAPI application
frontend/    React/Vite client
importer/    ResMed EDF parsing and import pipeline
migrations/  SQL migrations

Contributing

For local development:

  1. Install dependencies:
npm install
cd frontend && npm install
  1. Start Postgres:
docker compose up -d postgres
  1. Run the app:
npm run dev

Useful commands:

npm run api
npm run frontend
cd frontend && npm run build
cd frontend && npm run lint

Before opening a PR, make sure:

  • the frontend builds successfully
  • lint passes for the frontend
  • any README or env changes are documented
  • self-hosting changes are reflected in docker-compose.yml and .env.selfhost.example where relevant

Useful Commands

npm run dev
npm run api
npm run frontend
cd frontend && npm run build
cd frontend && npm run lint

Notes

  • The backend reads DATABASE_URL from environment and falls back to a local development default in api/database.py.
  • The backend uses a fallback development JWT secret if SECRET_KEY is not set. Set a real SECRET_KEY outside local development.

Acknowledgements

This project depends on open-cpap-parser as a key functional module for multi-manufacturer CPAP data parsing. All SleepyHead/OSCAR-derived binary parsing code lives in open-cpap-parser — SleepLab itself does not implement any direct derivative of SleepyHead or OSCAR.

open-cpap-parser is a derivative of the free and open-source software SleepyHead, developed and copyright by Mark Watkins (Jedimark) (C) 2011-2018, and of OSCAR (Open Source CPAP Analysis Reporter), which is itself a derivative of SleepyHead. The binary-format parsing logic in open-cpap-parser's Rust extension module is ported from OSCAR.

Per Mark Watkins' redistribution request, any derivative of this work must mention clearly in its advertising material, software installer, and about screens that it "is based on the free and open-source software SleepyHead, developed and copyright by Mark Watkins (C) 2011-2018." Referencing "GPL software" alone is not sufficient. See NOTICE.md for the full redistribution notice and third-party copyright statements.

License

GNU General Public License v3.0 (GPL-3.0)

This project is licensed under the GNU General Public License v3.0. See LICENSE.md for the full license text.

This project incorporates open-cpap-parser as a key functional module, which is a derivative of OSCAR and the free and open-source software SleepyHead, developed and copyright by Mark Watkins (Jedimark) (C) 2011-2018. Both SleepyHead and OSCAR are distributed under the GPL-3.0, which this project inherits. All other components are permissively licensed (MIT, BSD-3-Clause, Apache-2.0) and are compatible with GPL-3.0.

About

SleepLab is a local-first CPAP therapy dashboard for importing, storing, and analysing ResMed sleep data. It combines a React/Vite frontend for charts and session insights, a FastAPI backend for APIs and auth, and a PostgreSQL-backed importer that processes ResMed data into a self-hostable web app.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors