{"id":45436439,"url":"https://github.com/jasonpuglisi/e-note-ion","last_synced_at":"2026-06-06T05:03:45.340Z","repository":{"id":339870536,"uuid":"1163657790","full_name":"JasonPuglisi/e-note-ion","owner":"JasonPuglisi","description":"Automation for Vestaboard displays — with emotion","archived":false,"fork":false,"pushed_at":"2026-03-26T19:57:11.000Z","size":3191,"stargazers_count":0,"open_issues_count":21,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-27T08:24:23.723Z","etag":null,"topics":["cron","docker","home-automation","python","scheduler","split-flap","vestaboard"],"latest_commit_sha":null,"homepage":"https://github.com/JasonPuglisi/e-note-ion","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JasonPuglisi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-22T00:11:31.000Z","updated_at":"2026-03-26T19:57:17.000Z","dependencies_parsed_at":"2026-03-12T00:01:22.659Z","dependency_job_id":null,"html_url":"https://github.com/JasonPuglisi/e-note-ion","commit_stats":null,"previous_names":["jasonpuglisi/e-note-ion"],"tags_count":148,"template":false,"template_full_name":null,"purl":"pkg:github/JasonPuglisi/e-note-ion","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JasonPuglisi%2Fe-note-ion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JasonPuglisi%2Fe-note-ion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JasonPuglisi%2Fe-note-ion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JasonPuglisi%2Fe-note-ion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JasonPuglisi","download_url":"https://codeload.github.com/JasonPuglisi/e-note-ion/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JasonPuglisi%2Fe-note-ion/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290742,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["cron","docker","home-automation","python","scheduler","split-flap","vestaboard"],"created_at":"2026-02-22T03:09:03.589Z","updated_at":"2026-04-06T00:11:57.080Z","avatar_url":"https://github.com/JasonPuglisi.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# E•NOTE•ION\n\n![E•NOTE•ION](https://raw.githubusercontent.com/JasonPuglisi/e-note-ion/main/assets/social-preview.png)\n\n[![CI](https://github.com/JasonPuglisi/e-note-ion/actions/workflows/ci.yml/badge.svg)](https://github.com/JasonPuglisi/e-note-ion/actions/workflows/ci.yml)\n\nA self-hosted, code-first content scheduler for Vestaboard split-flap\ndisplays. Define your board content as version-controlled JSON — cron\nschedules, templated messages, live data integrations, and a priority queue —\nwith no web UI or cloud dependency required. Supports both the\n[Note](https://shop.vestaboard.com/products/note) (3×15) and the Flagship\n(6×22).\n\n\u003e This project is primarily agent-developed using [Claude](https://claude.ai),\n\u003e with human design, decision-making, guidance, and review. See\n\u003e [Philosophy](#philosophy) for more on the approach.\n\n## Who this is for\n\nE•NOTE•ION is built for developers and power users who want to treat their\nboard like infrastructure: content in files, schedules in cron, secrets in env\nvars, deploys in Docker.\n\nIf you'd prefer a friendlier experience — a web UI, drag-and-drop scheduling,\nand a polished setup flow — check out\n[FiestaBoard](https://github.com/Fiestaboard/FiestaBoard), which nails that\nuse case beautifully.\n\n## See also\n\nThe Vestaboard community has built a lot of great tooling:\n\n| Project | What it does well |\n|---|---|\n| [FiestaBoard](https://github.com/Fiestaboard/FiestaBoard) | Full-featured self-hosted app with a web UI and a rich scheduling experience |\n| [Vestaboard+](https://www.vestaboard.com/vestaboard-plus) | Official cloud subscription with Zapier/IFTTT integration and a curated app marketplace |\n| [jparise/vesta](https://github.com/jparise/vesta) | Clean Python library for the Vestaboard API — great if you want to build your own tooling |\n| [natekspencer/hacs-vestaboard](https://github.com/natekspencer/hacs-vestaboard) | Home Assistant integration for triggering board updates from automations |\n| [Zapier](https://zapier.com) / [IFTTT](https://ifttt.com) | No-code workflow triggers via Vestaboard+ — lowest barrier to entry |\n| MCP servers | Emerging tools for LLM-driven board updates from Claude and other agents |\n\n## Running with Docker (recommended)\n\nPre-built multi-arch images (`linux/amd64`, `linux/arm64`) are published to\nthe GitHub Container Registry on each release.\n\nFirst copy `config.example.toml` to `config.toml` and fill in your API keys\nand settings (see [Configuration](#configuration) below). Then run:\n\n```bash\ndocker run -d \\\n  --name e-note-ion \\\n  --restart unless-stopped \\\n  -v /path/to/config.toml:/app/config.toml:ro \\\n  ghcr.io/jasonpuglisi/e-note-ion:latest\n```\n\nTo mount personal content, add a volume pointing at `/app/content/user`:\n\n```bash\n  -v /path/to/your/content:/app/content/user \\\n```\n\nDisplay model, public mode, and enabled contrib content are all configured in\n`config.toml` under `[scheduler]` — no environment variables needed for these\nsettings. See [Configuration](#configuration) for details.\n\nContrib integrations require their own API keys and configuration — see\n[`content/README.md`](content/README.md) for details.\n\n### Unraid\n\nAn Unraid Docker template is available in a\n[separate repository](https://github.com/JasonPuglisi/unraid-templates).\nIt exposes config file path, user content directory, timezone, and webhook\nport as UI fields.\n\n### Viewing container logs\n\nSome integrations print important messages to stdout during startup or\noperation — for example, an authentication code and URL you need to visit\nto complete an OAuth flow. Check the container logs to see these messages.\n\n**Docker:**\n```bash\ndocker logs e-note-ion\n# or follow live:\ndocker logs -f e-note-ion\n```\n\n**Unraid:** In the Unraid web UI, go to **Docker** → click the container\nicon next to **e-note-ion** → **Logs**.\n\n### Integrations that require interactive auth\n\nSome integrations (e.g. Trakt.tv) use an **OAuth device code flow**: the\nscheduler prints a short code and URL to the container logs, you visit\nthe URL on any device and approve access, and tokens are automatically\nsaved to `config.toml`. No browser on the scheduler host is required.\n\n**For this to work, `config.toml` must be mounted read-write** (not `:ro`)\nso the scheduler can persist the tokens:\n\n```bash\n# Correct — read-write (required when using auth-based integrations):\n-v /path/to/config.toml:/app/config.toml\n\n# Wrong — read-only prevents token persistence:\n-v /path/to/config.toml:/app/config.toml:ro\n```\n\nUntil auth is complete, templates from that integration are silently skipped\nand the display shows other content normally. See each integration's sidecar\ndoc under [`content/contrib/`](content/contrib/) for setup details.\n\n## Configuration\n\nCopy `config.example.toml` to `config.toml` and fill in your values:\n\n```bash\ncp config.example.toml config.toml\n# edit config.toml — add your Vestaboard API key and any integration settings\n```\n\n`config.toml` is git-ignored and contains secrets — never commit it.\n\nKey `[scheduler]` settings:\n\n| Key | Default | Description |\n|---|---|---|\n| `model` | `\"note\"` | Display model: `\"note\"` (3×15) or `\"flagship\"` (6×22) |\n| `public` | `false` | When `true`, skip templates marked `private = true` (for shared/guest-visible spaces). Can be toggled at runtime via `POST /webhook/scheduler` with `{\"action\": \"public\"}` or `{\"action\": \"private\"}` |\n| `content_enabled` | _(absent)_ | Content filter for cron-scheduled templates in both `user/` and `contrib/`: absent = all user loads, no contrib; `[\"*\"]` = all user + all contrib; `[\"bart\", \"my_quotes\"]` = only matching stems from either directory. **Webhook-only integrations (`plex`, `message`, `notion`) are unaffected — their webhooks fire regardless of this setting.** |\n| `timezone` | system TZ | IANA timezone for cron job scheduling (e.g. `\"America/Los_Angeles\"`) |\n| `min_hold` | `60` | Minimum seconds any message stays on display before a high-priority (≥8) queued message can interrupt it. Set to `0` to disable (not recommended for physical displays). |\n\n## Health monitoring\n\nWhen the [webhook listener](#configuration) is enabled, a health endpoint is\navailable at `GET /health`. It returns a JSON summary of all registered\nintegration statuses, useful for uptime monitoring (e.g. UptimeRobot).\n\n**Authentication:** A credential is auto-generated on first startup — check\nthe container logs for the plaintext secret. Pass it as\n`X-Webhook-Secret: \u003csecret\u003e` (preferred) or `?secret=\u003csecret\u003e`.\n\n**HTTP status codes:**\n- `200` — all integrations healthy (or unknown)\n- `503` — one or more integrations degraded or errored\n\n**Response format:**\n```json\n{\n  \"status\": \"healthy\",\n  \"uptime_seconds\": 3600,\n  \"integrations\": {\n    \"weather\": {\n      \"status\": \"healthy\",\n      \"last_success\": \"2026-01-01T12:00:00+00:00\",\n      \"last_expected_empty\": null,\n      \"last_error\": null,\n      \"last_error_message\": null,\n      \"success_rate\": 1.0,\n      \"total_events\": 10,\n      \"registered_at\": \"2026-01-01T08:00:00+00:00\"\n    }\n  }\n}\n```\n\nEach integration tracks the last 10 events in a rolling buffer. Status levels:\n`healthy` (no errors), `degraded` (mixed), `error` (all errors), `unknown`\n(no events yet). Expected empty data (e.g. nothing playing, no events today)\ncounts as healthy — only API failures trigger degraded/error.\n\nA periodic health summary also logs to the console every hour, showing\nnon-healthy integrations and their recent error rates. Health state is\nin-memory and resets on container restart.\n\n## Installing from PyPI\n\n**Requirements:** Python 3.14+\n\n```bash\npip install e-note-ion\n```\n\nCreate a config file and run:\n\n```bash\ncp config.example.toml config.toml  # fill in your API key\ne-note-ion                           # or: e-note-ion --config /path/to/config.toml\n```\n\nUse `e-note-ion --help` for CLI options.\n\n## Running from source\n\n**Requirements:** Python 3.14+, [uv](https://github.com/astral-sh/uv)\n\n```bash\nuv sync\ncp config.example.toml config.toml  # fill in your API key\nuv run e-note-ion\n```\n\nDisplay model, public mode, and content filter are set in `config.toml`\nunder `[scheduler]`. See [Configuration](#configuration) for details.\n\n## Content files\n\nContent is defined as JSON files in two directories:\n\n- **`content/contrib/`** — bundled community-contributed content, disabled by\n  default. Enable via `[scheduler].content_enabled` in `config.toml`.\n- **`content/user/`** — personal content. Loaded automatically when\n  `content_enabled` is absent; filtered alongside contrib when it is set.\n  Git-ignored; mount your own directory here or symlink to a private repo.\n\nSee [`content/README.md`](content/README.md) for the full content format\nreference, including template fields, variables, color squares, priority\nguidelines, schedule overrides, and available integrations.\n\n## Philosophy\n\n**Content as code.** Board messages live in JSON files alongside your other\ndotfiles and configs. They're version-controlled, diff-able, and deployable\nthe same way as everything else. There's no database to back up, no UI state\nto sync, and no vendor lock-in — just files, cron, and a single Python\nprocess.\n\n**An AI development experiment.** E•NOTE•ION is also an ongoing exploration of\nagentic software development. Most of the implementation is written by Claude,\nwith a human setting direction, reviewing plans, and making architectural\ncalls. The goal isn't to remove the human — it's to see how far thoughtful\nhuman–AI collaboration can go on a real project with real constraints.\n\n## Development\n\n```bash\nuv sync\nuv run pre-commit install\n```\n\nRun the full check suite before committing:\n\n```bash\nuv run ruff check .\nuv run ruff format --check .\nuv run pyright\nuv run bandit -c pyproject.toml -r .\nuv run pip-audit\nuv run pre-commit run pretty-format-json --all-files\nuv run pytest\n```\n\nAll checks are also enforced as pre-commit hooks.\n\n### Integration tests\n\nIntegration tests hit the real APIs and are excluded from the default `pytest`\nrun. To run them locally:\n\n```bash\ncp .env.example .env\n# fill in your API keys — bare values, no surrounding quotes\nuv run pytest -m integration -v\n```\n\nRequired keys:\n\n| Key | Where to get it |\n|---|---|\n| `VESTABOARD_VIRTUAL_API_KEY` | [web.vestaboard.com](https://web.vestaboard.com) → Developer → Virtual Boards |\n| `CALENDAR_URL` | Google/iCloud: secret-address `.ics` URL (see `content/contrib/calendar.md`) |\n| `CALENDAR_CALDAV_URL` | `https://caldav.icloud.com/` for iCloud CalDAV |\n| `CALENDAR_USERNAME` | Apple ID email address |\n| `CALENDAR_PASSWORD` | App-specific password from [appleid.apple.com](https://appleid.apple.com) |\n| `BART_API_KEY` | [api.bart.gov/api/register.aspx](https://api.bart.gov/api/register.aspx) |\n| `DISCOGS_TOKEN` | [discogs.com/settings/developers](https://www.discogs.com/settings/developers) |\n| `TRAKT_CLIENT_ID` | [trakt.tv/oauth/applications](https://trakt.tv/oauth/applications) → your app |\n| `TRAKT_CLIENT_SECRET` | same app page |\n| `TRAKT_ACCESS_TOKEN` | run Trakt auth flow once and copy from `config.toml` |\n| `TMDB_API_READ_ACCESS_TOKEN` | [themoviedb.org/settings/api](https://www.themoviedb.org/settings/api) (optional; enhances Plex/Trakt metadata) |\n| `CALENDAR_CARDDAV_URL` | `https://contacts.icloud.com/` for iCloud birthday integration |\n| `PARCEL_API_KEY` | [web.parcelapp.net](https://web.parcelapp.net) → API key (requires Parcel Premium) |\n| `DIVING_NDBC_STATION` | [ndbc.noaa.gov](https://www.ndbc.noaa.gov/) station ID (e.g. `46221`) |\n| `DIVING_LAT` | Station latitude |\n| `DIVING_LON` | Station longitude |\n| `YNAB_API_KEY` | [app.ynab.com/settings/developer](https://app.ynab.com/settings/developer) → Personal Access Token |\n| `YNAB_BUDGET_ID` | Budget UUID from the YNAB web app URL |\n\n`.env` is git-ignored — never commit it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjasonpuglisi%2Fe-note-ion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjasonpuglisi%2Fe-note-ion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjasonpuglisi%2Fe-note-ion/lists"}