{"id":51146333,"url":"https://github.com/zonque/malady","last_synced_at":"2026-06-26T03:01:30.083Z","repository":{"id":363658253,"uuid":"1264160396","full_name":"zonque/malady","owner":"zonque","description":"🩺 Simple, self-hosted personal health tracker for various metrics","archived":false,"fork":false,"pushed_at":"2026-06-17T16:05:18.000Z","size":709,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-17T18:07:24.412Z","etag":null,"topics":["health-data","health-tracking","privacy-first","ruby-on-rails","self-hosted"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zonque.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-09T16:05:56.000Z","updated_at":"2026-06-17T16:07:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/zonque/malady","commit_stats":null,"previous_names":["zonque/malady"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/zonque/malady","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zonque%2Fmalady","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zonque%2Fmalady/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zonque%2Fmalady/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zonque%2Fmalady/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zonque","download_url":"https://codeload.github.com/zonque/malady/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zonque%2Fmalady/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34801014,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-26T02:00:06.560Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["health-data","health-tracking","privacy-first","ruby-on-rails","self-hosted"],"created_at":"2026-06-26T03:01:29.017Z","updated_at":"2026-06-26T03:01:30.074Z","avatar_url":"https://github.com/zonque.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Malady\n\n**Malady** is a privacy-first, multi-user health-metric tracker. Each person\nconfigures their own set of metrics — temperature, weight, medication, period,\nheart rate, anything — logs timestamped readings, and visualises them. Metric\ntypes are fully configurable and can be **changed after data has been entered\nwithout losing the original values**.\n\nBuilt with Ruby on Rails 8, Hotwire, and Tailwind CSS. Mobile-first, with an\noptional dark mode. Licensed under **AGPL-3.0** (see [License](#license)).\n\n![Malady dashboard](screenshots/dashboard.png)\n\n---\n\n## Table of contents\n\n- [Background](#background)\n- [Overview](#overview)\n- [Feature scope](#feature-scope)\n- [Tech stack](#tech-stack)\n- [Architecture](#architecture)\n- [Getting started (development)](#getting-started-development)\n- [Running the tests](#running-the-tests)\n- [Configuration (environment variables)](#configuration-environment-variables)\n- [Admin account](#admin-account)\n- [Grafana integration](#grafana-integration)\n- [Data export](#data-export)\n- [Importing from Daylio](#importing-from-daylio)\n- [Production \u0026 Docker Compose](#production--docker-compose)\n- [License](#license)\n- [Contributing](#contributing)\n\n---\n\n## Background\n\nA family member has been dealing with a stubborn, hard-to-pin-down malady, and\nwe'd love to find the patterns hiding in it. Being nerds, our instinct was\nobvious: collect the data, then go looking. We wanted something dead simple to\njot readings into throughout the day — at the desk, on the couch, on a phone in\na waiting room — and then sift through later for whatever the numbers are trying\nto tell us.\n\nSo we went looking for a tool. And looking. Everything was either a walled\ngarden, a subscription, or quietly shipping our most personal data off to\nsomeone else's cloud. Open source, self-hostable, and private? Crickets. So I\ndid the reasonable thing and started building one.\n\nRight now it does the basics and not much more — but that's the point. It'll\ngrow as real life shows us which features actually earn their keep, instead of\nthe ones we *think* we need. And, full disclosure: most of this is lovingly\nvibe-coded.\n\n**Got ideas?** We'd genuinely love to hear them. If you can think of a better way\nto track, slice, or visualise this kind of data — or you just want to poke at\nthe code — open an issue, send a PR, or start a discussion. Contributions,\nsuggestions, and \"have you considered…\" comments are all very welcome. See\n[Contributing](#contributing) to jump in.\n\n---\n\n## Overview\n\nMalady gives each user a personal dashboard of the health parameters they care\nabout. A *metric* is a typed column (e.g. \"Weight\" as a decimal in kg, \"Mood\" as\nan enumeration, \"Fasting\" as a yes/no). Users record *data points* against each\nmetric over time. Because real tracking needs evolve, a metric's type can be\nchanged later: Malady previews exactly how many existing values will convert,\nand on apply it re-projects values while always preserving the original input.\n\nAll data is strictly scoped to its owner. Visualisation is available either\nin-app or through Grafana via a read-only JSON API.\n\n## Feature scope\n\n- **Multi-user accounts** (Devise): sign up, sign in, password reset, email\n  confirmation, account lockout. Public sign-ups are gated by an env var.\n- **Per-user configurable metrics** with types: `decimal`, `integer`,\n  `percentage`, `boolean`, `enumeration`, `text`, and `text_block` — a Markdown\n  long-text / journal type, edited in a resizable textarea and rendered as Markdown.\n- **Type changes without data loss** — dry-run preview (how many convert / fail,\n  with sample failures) then a lossless apply that preserves every original value.\n- **Per-metric logging** with live updates via Hotwire / Turbo Streams.\n- **Edit \u0026 delete readings** — edit any logged value through the shared entry\n  form (a textarea for `text_block`); deletions ask for confirmation first.\n- **Dashboard** with drag-to-reorder metrics.\n- **Per-metric icons** — pick a searchable Bootstrap icon, shown on the\n  dashboard, metric page, overview, and quick-entry form.\n- **Dashboard \"Memories\"** — resurfaces past journal entries on their\n  anniversaries (1 / 3 / 6 / 9 / 12 months, then yearly), tucked behind a\n  collapsed accordion for privacy.\n- **Import from Daylio** — bring activities, mood, and journal notes in from a\n  Daylio CSV export (see [Importing from Daylio](#importing-from-daylio)).\n- **Mobile-first UI** with optional **dark mode**.\n- **Data export** as **JSON** and **CSV** (long format).\n- **Grafana integration** through a token-authenticated, read-only JSON API.\n- **Admin** area to administer user accounts (list, view, lock/unlock, confirm,\n  delete), with an env-bootstrapped admin user.\n- **Privacy first** — every query is owner-scoped; the API token is read-only.\n- **i18n-ready** (English only for now).\n- **UTC everywhere** in storage; timestamps are resolved to the viewer's local\n  time in the browser.\n\n## Tech stack\n\n- Ruby on Rails 8, Ruby 4.0\n- Hotwire (Turbo + Stimulus), Haml templates\n- Tailwind CSS v4 (class-based dark mode)\n- Redcarpet (Markdown for `text_block`), Bootstrap Icons (vendored webfont)\n- Devise (authentication)\n- **SQLite** for development \u0026 test, **PostgreSQL** for production\n- Minitest (model, controller/integration, and system tests)\n- `letter_opener` (dev email preview), `exception_notification` (prod alerts)\n\n## Architecture\n\nThe data model is an EAV (entity-attribute-value) design:\n\n- `Metric` — a typed column owned by a user (`data_type`, `unit`, `enum_options`, …).\n- `DataPoint` — one value per metric per timestamp. `value_text` holds the\n  **canonical string and is the source of truth**; `value_decimal` / `value_boolean`\n  are projections maintained for querying and charting.\n- `ValueCaster` — the single source of truth for validating and casting raw input\n  per metric type.\n- `MetricTypeChanger` — `dry_run` (preview) and `apply!` (re-project from\n  `value_text` in a transaction; originals never lost).\n\nThis keeps a metric's type change a matter of re-projecting the preserved\n`value_text`, so changing `text → decimal` (or back) never destroys data.\n\n## Getting started (development)\n\nPrerequisites: Ruby 4.0.x, Node, and a JS runtime (development uses SQLite, so no\ndatabase server is required).\n\n```bash\nbundle install\nbin/rails db:prepare      # create + migrate + seed (dev/test use SQLite)\nbin/dev                   # starts Rails AND the Tailwind CSS watcher (Procfile.dev)\n```\n\n\u003e **Important:** use `bin/dev`, not `bin/rails server` alone — `bin/dev` runs the\n\u003e Tailwind build/watch process. If you run the server without it, run\n\u003e `bin/rails tailwindcss:build` first or styles will be missing/stale.\n\n`bin/rails db:seed` creates a confirmed demo user in development:\n\n- **Email:** `demo@malady.test`\n- **Password:** `password123`\n\nThen visit \u003chttp://localhost:3000\u003e.\n\nTo fill an account with realistic **Faker** metrics and history:\n\n```bash\nbin/rails 'malady:demo_data' -- --email=demo@malady.test --days=60 --per-day=2 --create\n```\n\n`--create` makes a confirmed user if one doesn't exist, and `--days` / `--per-day`\nsize the generated history (defaults: 60 days, 2 readings/day). The legacy\npositional form `bin/rails 'malady:demo_data[email]'` still works.\n\n## Running the tests\n\n```bash\nbin/rails test          # model, service, controller \u0026 integration tests\nbin/rails test:system   # Capybara system tests (requires Chrome)\n```\n\nThe project uses Rails' native Minitest. Tests that sign a user in use the\n`confirmed_user` helper in `test/test_helper.rb` (Devise `:confirmable` blocks\nunconfirmed users).\n\n## Configuration (environment variables)\n\nAll runtime configuration is via environment variables.\n\n### Sign-ups\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `MALADY_ALLOW_SIGNUPS` | `false` | `true`/`1`/`yes`/`on` allows public sign-up. Any other value (or unset) closes registration (route returns 404 and the sign-up link is hidden). |\n\n### Admin account\n\n| Variable | Meaning |\n|---|---|\n| `MALADY_ADMIN_EMAIL` | Admin login email |\n| `MALADY_ADMIN_PASSWORD` | Admin password (min 6 chars, per Devise) |\n\nSee [Admin account](#admin-account).\n\n### Email (ActionMailer)\n\nDevelopment uses **letter_opener** (sent mail opens in the browser; nothing\nleaves the machine). Production uses SMTP, fully env-configured:\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `MALADY_HOST` | `localhost` | Host used in mailer URLs |\n| `MALADY_MAILER_SENDER` | `no-reply@malady.local` | Default \"from\" address |\n| `MALADY_SMTP_ADDRESS` | `localhost` | SMTP host |\n| `MALADY_SMTP_PORT` | `587` | SMTP port |\n| `MALADY_SMTP_USER_NAME` | — | SMTP username |\n| `MALADY_SMTP_PASSWORD` | — | SMTP password |\n| `MALADY_SMTP_DOMAIN` | `localhost` | HELO domain |\n| `MALADY_SMTP_AUTHENTICATION` | `plain` | SMTP auth method |\n| `MALADY_SMTP_STARTTLS` | `true` | Enable STARTTLS |\n\n### Exception notifications\n\n| Variable | Meaning |\n|---|---|\n| `MALADY_EXCEPTION_RECIPIENTS` | Comma-separated emails. When set, uncaught exceptions email these addresses (via `exception_notification`). When unset, the notifier is disabled. |\n\n### Database (production)\n\n| Variable | Default |\n|---|---|\n| `DATABASE_NAME` | `malady_production` |\n| `DATABASE_USER` | `malady` |\n| `DATABASE_PASSWORD` | — |\n| `DATABASE_HOST` | `localhost` |\n| `DATABASE_PORT` | `5432` |\n\nDevelopment and test use SQLite and ignore these.\n\n## Admin account\n\nThe admin can administer user accounts at **`/admin/users`** — list, view,\nlock/unlock, confirm, and delete users. (An admin cannot lock or delete their\nown account, to avoid lockout.)\n\nThe admin is **bootstrapped from the environment** and works even when public\nsign-ups are closed. Set `MALADY_ADMIN_EMAIL` and `MALADY_ADMIN_PASSWORD`, then:\n\n- **Just start the app** — the admin is auto-provisioned on boot (idempotent), or\n- run it explicitly: `bin/rails malady:ensure_admin` (also run by `bin/rails db:seed`).\n\nThe env vars are the **source of truth**: each run resets the admin's password to\n`MALADY_ADMIN_PASSWORD`. Rotate the password by changing the env var, not the DB.\n\n## Grafana integration\n\nMalady exposes a read-only, token-authenticated JSON API designed for Grafana's\n[Infinity datasource](https://grafana.com/grafana/plugins/yesoreyeram-infinity-datasource/):\n\n- `GET /api/v1/metrics` — the token owner's metrics (`slug`, `name`, `data_type`, `unit`)\n- `GET /api/v1/metrics/:slug/series?from=\u0026to=` — `[{ \"time\": \u003cISO-8601 UTC\u003e, \"value\": \u003cnumber\u003e }]`\n\nAuthenticate with your personal API token as a Bearer token:\n\n```\nAuthorization: Bearer \u003cyour-token\u003e\n```\n\nFind and rotate your token in-app at **`/api_token`** (linked from the dashboard).\nThe token is read-only and scoped to your own data.\n\n### Grafana via Docker Compose\n\nThe `docker-compose.yml` stack includes a **`grafana`** service with the Infinity\ndatasource plugin pre-installed. Once the stack is up:\n\n1. Open Grafana at \u003chttp://localhost:3001\u003e and log in (default `admin` / `admin`,\n   set via `GF_SECURITY_ADMIN_*` in `docker-compose.yml`).\n2. Add a datasource → **Infinity**.\n3. Under the datasource's **Authentication → HTTP headers**, add a header\n   `Authorization` with value `Bearer \u003cyour-token\u003e` (copy the token from Malady's\n   `/api_token` page).\n4. In a panel, add an **Infinity** query: type **JSON**, source **URL**,\n   URL `http://app:3000/api/v1/metrics/\u003cslug\u003e/series` (use the compose service\n   name `app`, not `localhost`), and parse the `time` / `value` fields.\n\n\u003e The plugin is downloaded on Grafana's first start, so that initial boot needs\n\u003e internet access.\n\n## Data export\n\nSigned in, you can export all your data:\n\n- **JSON:** `GET /export/json` — metrics with definitions and all data points.\n- **CSV:** `GET /export/csv` — long format: `metric_slug, metric_name, recorded_at, value, unit, note`.\n\nAll timestamps are UTC ISO-8601.\n\n## Importing from Daylio\n\nMigrating from [Daylio](https://daylio.net)? Export your entries as **CSV** from\nthe Daylio app, then import the file for a user from the command line:\n\n```bash\nbin/rails 'malady:import_daylio' -- --user=EMAIL_OR_ID --file=path/to/export.csv --dry-run\n```\n\n- `--user` accepts an email or numeric id; `--file` is the Daylio CSV.\n- `--dry-run` parses and reports what *would* be created without writing\n  anything — drop it to perform the import.\n\nThe importer maps Daylio's data onto Malady metrics:\n\n- **Activities → yes/no metrics** (a `true` reading per occurrence).\n- **Graded activities** named `good`/`medium`/`bad \u003cthing\u003e` are fuzzy-grouped\n  into a single **Choice** metric `\u003cthing\u003e` with `good`/`medium`/`bad` options\n  (e.g. `good sleep` / `medium sleep` → a `sleep` metric).\n- **Mood → a \"Mood\" Choice metric.**\n- **Notes → a \"Journal\" `text_block` metric**, with the note title as a Markdown\n  heading and Daylio's note HTML (`\u003cbr\u003e`, lists, bold/italic…) translated to\n  Markdown.\n\nImports are **idempotent**: metrics are reused by name and readings are\nde-duplicated by timestamp, so re-running won't create duplicates. The parser\nlives in `lib/daylio/` and has **no Rails dependencies** — it can be lifted out\nand used on its own.\n\n## Production \u0026 Docker Compose\n\nProduction uses PostgreSQL. The provided `docker-compose.yml` runs the full stack\n— a `db` (PostgreSQL 18) service and an `app` (Malady) service — and the **`app`\nservice's `environment:` block documents and sets every configuration variable**\n(see [Configuration](#configuration-environment-variables) for what each does).\n\nBefore first launch, edit `docker-compose.yml` and set at least:\n\n- `SECRET_KEY_BASE` — required in production; generate one with `bin/rails secret`.\n- `MALADY_ADMIN_EMAIL` / `MALADY_ADMIN_PASSWORD` — the admin account, auto-provisioned on boot.\n- `MALADY_HOST` — the public hostname (used in email links).\n- The `MALADY_SMTP_*` values if you want outgoing email in production.\n\n`DATABASE_HOST` is preset to `db` so the app reaches the Postgres container.\n\n```bash\ndocker compose run --rm app bin/rails db:prepare   # create + migrate (provisions the admin)\ndocker compose up -d                               # db + app (http://localhost:3000) + grafana (http://localhost:3001)\n```\n\nThe stack also includes **Grafana** (port 3001) for charting Malady data — see\n[Grafana via Docker Compose](#grafana-via-docker-compose).\n\nIn **development** (outside Docker), outgoing email is previewed in the browser\nvia [letter_opener](https://github.com/ryanb/letter_opener) — no SMTP server is\nneeded. Production sends via SMTP, configured through the `MALADY_SMTP_*` vars.\n\nThe admin is auto-provisioned on boot from `MALADY_ADMIN_*`, so once the stack is\nup you can sign in at `/users/sign_in` with those credentials.\n\n## License\n\nMalady is free software licensed under the **GNU Affero General Public License,\nversion 3 (AGPL-3.0)**. See the [LICENSE](LICENSE) file for the full text.\n\nThe AGPL's defining clause matters for a hosted app: **if you run a modified\nversion of Malady as a network service, you must offer its complete corresponding\nsource code to the users of that service.** Distribution and modification are\notherwise governed by the GPL family's copyleft terms — derivative works must also\nbe released under the AGPL-3.0.\n\nCopyright (C) the Malady contributors.\n\n## Contributing\n\nContributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the\nworkflow, coding standards, and testing expectations. By contributing you agree\nthat your contributions are licensed under the AGPL-3.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzonque%2Fmalady","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzonque%2Fmalady","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzonque%2Fmalady/lists"}