diff --git a/README.md b/README.md index f2585bd..d526386 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Sproutly takes the guesswork out of seed starting. Enter your plant varieties on - **Seed Library** — manage plant varieties with frost-relative timing, germination days, sun/water requirements - **Garden Tracker** — log growing batches and track status from `planned` → `germinating` → `seedling` → `potted up` → `hardening off` → `garden` → `harvested` - **Year Timeline** — visual calendar showing when each variety's stages fall across the year -- **Ntfy Notifications** — daily summary push notifications to your phone, configurable time and topic +- **Ntfy Notifications** — daily summary push notifications to your phone, configurable time, topic, and authentication - **Settings** — set your last frost date, fall frost date, location, timezone, and notification preferences ## Stack @@ -45,7 +45,7 @@ Access the app at **http://localhost:8053** ### First Steps 1. Go to **Settings** and enter your last frost date — this anchors all planting schedule calculations -2. Optionally configure an [ntfy](https://ntfy.sh) topic for push notifications +2. Optionally configure an [ntfy](https://ntfy.sh) topic for push notifications — supports no auth, username/password, or API key/token 3. Browse the pre-loaded **Seed Library** (12 common vegetables, herbs, and flowers included) 4. Start logging batches in **My Garden** as you sow seeds @@ -97,7 +97,18 @@ Key endpoints: - `GET/PUT /api/settings/` — app settings - `POST /api/notifications/test` — send test ntfy notification - `POST /api/notifications/daily` — trigger daily summary +- `GET /api/notifications/log` — recent notification history + +## Ntfy Authentication + +For private ntfy servers or access-controlled topics, the Settings page supports three auth modes: + +| Mode | When to use | +|------|-------------| +| None | Public ntfy.sh topics | +| Username & Password | ntfy server with basic auth enabled | +| API Key / Token | ntfy account access token (generate in ntfy account settings) | ## Status -This project is in early development. Core infrastructure and UI prototype are functional. UI design and feature set are evolving based on user feedback. +Core infrastructure is functional. UI design and feature set are evolving based on user feedback. diff --git a/backend/models.py b/backend/models.py index 2aa769c..487445a 100644 --- a/backend/models.py +++ b/backend/models.py @@ -86,6 +86,9 @@ class Settings(Base): notification_time = Column(String(5), default="07:00") timezone = Column(String(50), default="UTC") location_name = Column(String(100)) + ntfy_username = Column(String(200)) + ntfy_password = Column(String(200)) + ntfy_api_key = Column(String(200)) class NotificationLog(Base): diff --git a/backend/routers/notifications.py b/backend/routers/notifications.py index b6397e7..f4d6051 100644 --- a/backend/routers/notifications.py +++ b/backend/routers/notifications.py @@ -1,3 +1,4 @@ +import base64 from datetime import date, timedelta from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session @@ -96,16 +97,25 @@ async def send_ntfy(settings: Settings, title: str, message: str, db: Session, p server = (settings.ntfy_server or "https://ntfy.sh").rstrip("/") url = f"{server}/{settings.ntfy_topic}" + headers = { + "Title": title, + "Priority": priority, + "Tags": "seedling", + } + if settings.ntfy_api_key: + headers["Authorization"] = f"Bearer {settings.ntfy_api_key}" + elif settings.ntfy_username: + creds = base64.b64encode( + f"{settings.ntfy_username}:{settings.ntfy_password or ''}".encode() + ).decode() + headers["Authorization"] = f"Basic {creds}" + try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.post( url, content=message.encode("utf-8"), - headers={ - "Title": title, - "Priority": priority, - "Tags": "seedling", - }, + headers=headers, ) resp.raise_for_status() diff --git a/backend/schemas.py b/backend/schemas.py index c3e9c4b..cde2a6c 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -78,6 +78,9 @@ class SettingsUpdate(BaseModel): notification_time: Optional[str] = "07:00" timezone: Optional[str] = "UTC" location_name: Optional[str] = None + ntfy_username: Optional[str] = None + ntfy_password: Optional[str] = None + ntfy_api_key: Optional[str] = None class SettingsOut(SettingsUpdate): diff --git a/mysql/init.sql b/mysql/init.sql index 50d450e..29a80ee 100644 --- a/mysql/init.sql +++ b/mysql/init.sql @@ -41,7 +41,10 @@ CREATE TABLE IF NOT EXISTS settings ( ntfy_server VARCHAR(200) DEFAULT 'https://ntfy.sh', notification_time VARCHAR(5) DEFAULT '07:00', timezone VARCHAR(50) DEFAULT 'UTC', - location_name VARCHAR(100) + location_name VARCHAR(100), + ntfy_username VARCHAR(200), + ntfy_password VARCHAR(200), + ntfy_api_key VARCHAR(200) ); CREATE TABLE IF NOT EXISTS notification_log ( diff --git a/nginx/html/css/style.css b/nginx/html/css/style.css index e883e81..49b7b48 100644 --- a/nginx/html/css/style.css +++ b/nginx/html/css/style.css @@ -628,3 +628,9 @@ a:hover { text-decoration: underline; } .main-content { margin-left: 0; padding: 1rem; } .stats-row { grid-template-columns: 1fr 1fr; } } + +/* Ntfy auth toggle */ +.hidden { display: none !important; } +.auth-toggle { display: flex; gap: 1.25rem; flex-wrap: wrap; } +.auth-toggle-option { display: flex; align-items: center; gap: 0.35rem; cursor: pointer; font-size: 0.9rem; } +.auth-toggle-option input[type="radio"] { accent-color: var(--green-mid); } diff --git a/nginx/html/index.html b/nginx/html/index.html index 0c546ea..c2533c1 100644 --- a/nginx/html/index.html +++ b/nginx/html/index.html @@ -182,6 +182,37 @@ Subscribe to this topic in the ntfy app +