From 1e46cc102f2adb12a3e6633567bed1060e806104 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Sun, 9 Aug 2026 21:14:42 +0100 Subject: [PATCH 01/23] Add database backup script with 7-day retention and update roadmap --- README.md | 19 +++++++++++++++++++ docs/ROADMAP.md | 33 +++++++++++++++------------------ scripts/backup.sh | 30 ++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 18 deletions(-) create mode 100755 scripts/backup.sh diff --git a/README.md b/README.md index 553f895..ebe9929 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,25 @@ docker compose logs -f # view live logs docker compose ps # check container status ``` +### Database Backups + +Run the backup script manually: + +```bash +cd HomeOS +./scripts/backup.sh +``` + +Backups are saved to `~/homeos-backups/` with timestamps (e.g. `homeos-2026-08-09.db`). Backups older than 7 days are automatically deleted. + +To schedule daily backups at 3am on the Linux machine: + +```bash +crontab -e +# Add this line (adjust the path to your HomeOS directory): +0 3 * * * cd /path/to/HomeOS && ./scripts/backup.sh >> ~/homeos-backups/backup.log 2>&1 +``` + ### Run Tests ```bash diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7ad0e51..a8c14b7 100755 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -369,40 +369,37 @@ A complete HomeOS dashboard with live weather and polished UI. --- -# Phase 8 — Deployment +# Phase 8 — Deployment (COMPLETED) ## Goal Deploy HomeOS as a real kitchen appliance. -## Tasks +## Completed Tasks ### Task 1 — Dockerfiles -- Create backend Dockerfile (Python 3.12, install deps, run uvicorn). -- Create frontend Dockerfile (Node build step, serve with nginx). -- Update docker-compose.yml with build contexts, volumes, and env config. -- Verify `docker compose up` starts the full application. +- Created backend Dockerfile (Python 3.12, install deps, run Alembic migrations, start uvicorn). +- Created frontend Dockerfile (multi-stage: Node build + nginx serve). +- Created nginx.conf for static file serving and API proxying. +- Updated docker-compose.yml with networking, volumes, and environment config. ### Task 2 — Production Configuration -- Configure backend for production mode (debug off, optimised logging). -- Configure frontend nginx to serve static files and proxy API requests. -- Set up SQLite database volume for data persistence across container restarts. +- Added health checks for both containers (auto-restart on failure). +- Added gzip compression and static asset caching in nginx. +- Added memory limits (512MB backend, 128MB frontend). +- Set up SQLite database volume for data persistence. -### Task 3 — Mini PC Setup & Kiosk Mode +### Task 3 — Mini PC Setup & Kiosk Mode (SKIPPED) -- Install Ubuntu on Mini PC. -- Install Docker and Docker Compose. -- Deploy HomeOS containers. -- Configure Chromium in kiosk mode (fullscreen, no address bar, no cursor). -- Set up auto-start on boot. -- Configure automatic recovery after restart or crash. +- Not needed — using iPad as display via Safari "Add to Home Screen" instead of a connected monitor with kiosk browser. ### Task 4 — Database Backups -- Create a backup script that copies the SQLite database to a safe location. -- Schedule daily backups with cron. +- Created backup script (scripts/backup.sh) that copies SQLite database to ~/homeos-backups/ with timestamps. +- Keeps last 7 days of backups, deletes older ones automatically. +- Documented cron setup for daily scheduled backups. ## Deliverable diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..39964ed --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +BACKUP_DIR="$HOME/homeos-backups" +TIMESTAMP=$(date +%Y-%m-%d) +BACKUP_FILE="$BACKUP_DIR/homeos-$TIMESTAMP.db" +KEEP_DAYS=7 + +mkdir -p "$BACKUP_DIR" + +VOLUME_PATH=$(docker volume inspect homeos_sqlite-data -f '{{ .Mountpoint }}' 2>/dev/null) + +if [ -z "$VOLUME_PATH" ]; then + echo "Error: Could not find HomeOS Docker volume" + exit 1 +fi + +docker compose exec -T backend cp /app/data/homeos.db /tmp/homeos-backup.db 2>/dev/null +docker compose cp backend:/tmp/homeos-backup.db "$BACKUP_FILE" 2>/dev/null + +if [ $? -eq 0 ] && [ -f "$BACKUP_FILE" ]; then + echo "Backup saved: $BACKUP_FILE" +else + echo "Error: Backup failed" + exit 1 +fi + +find "$BACKUP_DIR" -name "homeos-*.db" -mtime +$KEEP_DAYS -delete 2>/dev/null + +BACKUP_COUNT=$(find "$BACKUP_DIR" -name "homeos-*.db" | wc -l) +echo "Backups retained: $BACKUP_COUNT" \ No newline at end of file From 721d37764567b83fae428a47bef622428314b983 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Sun, 9 Aug 2026 22:19:45 +0100 Subject: [PATCH 02/23] Add rotation duty tracker and auto-update deployment to future features --- docs/ROADMAP.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a8c14b7..16b76ba 100755 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -445,6 +445,14 @@ Example: --- +## Rotation Duty Tracker + +- Check dad's rotation duty schedule from an external source. +- Display upcoming duties for the next 7 days on the dashboard. +- Highlight today's duty if one is scheduled. + +--- + ## Prayer Times - Scrape prayer times from local mosque website. @@ -469,6 +477,15 @@ Example: --- +## Auto-Update Deployment + +- Create a script that checks GitHub for new changes on main branch. +- Auto-pull and rebuild Docker containers when updates are detected. +- Schedule with cron to check every few minutes. +- No manual SSH or pull needed — just merge to main and the Linux laptop updates itself. + +--- + ## Integration & End-to-End Tests - Add cross-module integration tests (e.g. full user flows across tasks, shopping, notes). From 47650539431007fea0d1cb209aa57b84bd4cbb48 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Sun, 9 Aug 2026 23:07:06 +0100 Subject: [PATCH 03/23] Add prayer times with Aladhan API, daily 1am refresh, and current prayer highlight --- .env.example | 7 +- README.md | 17 ++++- backend/app/api/v1/router.py | 2 + backend/app/core/scheduler.py | 18 +++++- backend/app/modules/prayer/__init__.py | 0 backend/app/modules/prayer/router.py | 11 ++++ backend/app/modules/prayer/schemas.py | 11 ++++ backend/app/modules/prayer/service.py | 88 ++++++++++++++++++++++++++ 8 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 backend/app/modules/prayer/__init__.py create mode 100644 backend/app/modules/prayer/router.py create mode 100644 backend/app/modules/prayer/schemas.py create mode 100644 backend/app/modules/prayer/service.py diff --git a/.env.example b/.env.example index 4e78333..a9e1d48 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,8 @@ CORS_ORIGINS=["http://localhost:5173"] # API API_V1_PREFIX=/api/v1 -# Weather (set your location coordinates) -WEATHER_LATITUDE=51.5074 -WEATHER_LONGITUDE=-0.1278 +# Weather and Prayer Times (set your location coordinates) +# Find your coordinates on Google Maps +WEATHER_LATITUDE=your_latitude +WEATHER_LONGITUDE=your_longitude WEATHER_CACHE_MINUTES=30 diff --git a/README.md b/README.md index ebe9929..d8887a0 100644 --- a/README.md +++ b/README.md @@ -74,13 +74,24 @@ cd HomeOS cp .env.example .env ``` -Edit `.env` and set your location for weather: +Edit `.env` and set your location coordinates. These are used for both weather and prayer times: ``` -WEATHER_LATITUDE=51.5074 -WEATHER_LONGITUDE=-0.1278 +WEATHER_LATITUDE=your_latitude +WEATHER_LONGITUDE=your_longitude ``` +To find your coordinates, search your city name on Google Maps and copy the latitude/longitude from the URL. + +#### Prayer Times + +Prayer times are fetched from the [Aladhan API](https://aladhan.com/prayer-times-api) using: + +- **Method 15** (Moonsighting Committee Worldwide) — closest to UK mosque timetables for Fajr +- **Hanafi school** — for later Asr times matching most UK mosques + +Times refresh automatically at 1am daily. The next upcoming prayer is highlighted on the dashboard. These are calculated astronomical times, so they may differ by a few minutes from your local mosque's posted times. + Start the app in the background: ```bash diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index c5a4a2a..d5c13d4 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -6,6 +6,7 @@ from app.modules.shopping.router import router as shopping_router from app.modules.notes.router import router as notes_router from app.modules.weather.router import router as weather_router +from app.modules.prayer.router import router as prayer_router v1_router = APIRouter() v1_router.include_router(health_router) @@ -14,3 +15,4 @@ v1_router.include_router(shopping_router) v1_router.include_router(notes_router) v1_router.include_router(weather_router) +v1_router.include_router(prayer_router) diff --git a/backend/app/core/scheduler.py b/backend/app/core/scheduler.py index 66bd4f7..ed1ee89 100644 --- a/backend/app/core/scheduler.py +++ b/backend/app/core/scheduler.py @@ -9,6 +9,16 @@ scheduler = AsyncIOScheduler() +async def run_prayer_refresh() -> None: + from app.modules.prayer.service import fetch_prayer_times + + logger.info("Scheduled prayer times refresh triggered") + try: + await fetch_prayer_times() + except Exception: + logger.exception("Scheduled prayer times refresh failed") + + async def run_rotation() -> None: from app.modules.tasks.rotation import rotate_tasks @@ -29,8 +39,14 @@ def setup_scheduler() -> None: id="weekly_task_rotation", replace_existing=True, ) + scheduler.add_job( + run_prayer_refresh, + trigger=CronTrigger(hour=1, minute=0), + id="daily_prayer_refresh", + replace_existing=True, + ) scheduler.start() - logger.info("Scheduler started: task rotation runs every Monday at midnight") + logger.info("Scheduler started: task rotation every Monday at midnight, prayer times refresh daily at 1am") def shutdown_scheduler() -> None: diff --git a/backend/app/modules/prayer/__init__.py b/backend/app/modules/prayer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/modules/prayer/router.py b/backend/app/modules/prayer/router.py new file mode 100644 index 0000000..320fa6d --- /dev/null +++ b/backend/app/modules/prayer/router.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter + +from app.modules.prayer.schemas import PrayerTimesResponse +from app.modules.prayer.service import get_prayer_times + +router = APIRouter(prefix="/prayer-times", tags=["prayer"]) + + +@router.get("", response_model=PrayerTimesResponse) +async def prayer_times(): + return await get_prayer_times() \ No newline at end of file diff --git a/backend/app/modules/prayer/schemas.py b/backend/app/modules/prayer/schemas.py new file mode 100644 index 0000000..d026f5d --- /dev/null +++ b/backend/app/modules/prayer/schemas.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class PrayerTime(BaseModel): + name: str + time: str + + +class PrayerTimesResponse(BaseModel): + prayers: list[PrayerTime] + current_prayer: str | None = None \ No newline at end of file diff --git a/backend/app/modules/prayer/service.py b/backend/app/modules/prayer/service.py new file mode 100644 index 0000000..dd77c17 --- /dev/null +++ b/backend/app/modules/prayer/service.py @@ -0,0 +1,88 @@ +from datetime import datetime + +import httpx + +from app.core.config import settings +from app.core.logging import get_logger +from app.modules.prayer.schemas import PrayerTime, PrayerTimesResponse + +logger = get_logger(__name__) + +ALADHAN_URL = "https://api.aladhan.com/v1/timings" +PRAYER_NAMES = ["Fajr", "Dhuhr", "Asr", "Maghrib", "Isha"] + +_cache: PrayerTimesResponse | None = None + + +def _to_12h(time_24h: str) -> str: + hour, minute = map(int, time_24h.split(":")) + period = "AM" if hour < 12 else "PM" + display_hour = hour % 12 or 12 + return f"{display_hour}:{minute:02d} {period}" + + +def _to_minutes(prayer: PrayerTime) -> int: + time_str = prayer.time.replace(" AM", "").replace(" PM", "") + hour, minute = map(int, time_str.split(":")) + if "PM" in prayer.time and hour != 12: + hour += 12 + if "AM" in prayer.time and hour == 12: + hour = 0 + return hour * 60 + minute + + +def _find_current_prayer(prayers: list[PrayerTime]) -> str | None: + now = datetime.now() + current_minutes = now.hour * 60 + now.minute + + current = None + for prayer in prayers: + if _to_minutes(prayer) <= current_minutes: + current = prayer.name + + return current + + +async def fetch_prayer_times() -> PrayerTimesResponse: + global _cache + + try: + params = { + "latitude": settings.WEATHER_LATITUDE, + "longitude": settings.WEATHER_LONGITUDE, + "method": 15, + "school": 1, + } + + async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client: + response = await client.get(ALADHAN_URL, params=params) + response.raise_for_status() + data = response.json() + + timings = data["data"]["timings"] + prayers = [] + for name in PRAYER_NAMES: + raw_time = timings.get(name, "") + if raw_time: + clean_time = raw_time.split(" ")[0] + prayers.append(PrayerTime(name=name, time=_to_12h(clean_time))) + + current_prayer = _find_current_prayer(prayers) + _cache = PrayerTimesResponse(prayers=prayers, current_prayer=current_prayer) + logger.info("Prayer times updated: %d prayers fetched", len(prayers)) + + except Exception: + logger.exception("Failed to fetch prayer times") + if _cache is None: + _cache = PrayerTimesResponse(prayers=[], current_prayer=None) + + return _cache + + +async def get_prayer_times() -> PrayerTimesResponse: + if _cache is not None: + return PrayerTimesResponse( + prayers=_cache.prayers, + current_prayer=_find_current_prayer(_cache.prayers), + ) + return await fetch_prayer_times() \ No newline at end of file From 2c59cf78ad9c5b4a603831985672726130308268 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Sun, 9 Aug 2026 23:10:22 +0100 Subject: [PATCH 04/23] Add pull-to-refresh to dashboard for weather, prayer times, and all cards --- frontend/src/components/PullToRefresh.tsx | 68 +++++++++++++++++++ frontend/src/components/index.ts | 1 + .../src/features/dashboard/DashboardPage.tsx | 29 +++++--- frontend/tsconfig.app.tsbuildinfo | 2 +- 4 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/PullToRefresh.tsx diff --git a/frontend/src/components/PullToRefresh.tsx b/frontend/src/components/PullToRefresh.tsx new file mode 100644 index 0000000..c113790 --- /dev/null +++ b/frontend/src/components/PullToRefresh.tsx @@ -0,0 +1,68 @@ +import { useRef, useState, useCallback } from "react"; + +interface PullToRefreshProps { + onRefresh: () => Promise | void; + children: React.ReactNode; +} + +const THRESHOLD = 80; + +export default function PullToRefresh({ onRefresh, children }: PullToRefreshProps) { + const [pullDistance, setPullDistance] = useState(0); + const [refreshing, setRefreshing] = useState(false); + const startY = useRef(0); + const pulling = useRef(false); + const containerRef = useRef(null); + + const handleTouchStart = useCallback((e: React.TouchEvent) => { + if (containerRef.current && containerRef.current.scrollTop === 0) { + startY.current = e.touches[0]!.clientY; + pulling.current = true; + } + }, []); + + const handleTouchMove = useCallback((e: React.TouchEvent) => { + if (!pulling.current) return; + const diff = e.touches[0]!.clientY - startY.current; + if (diff > 0) { + setPullDistance(Math.min(diff * 0.5, THRESHOLD + 20)); + } + }, []); + + const handleTouchEnd = useCallback(async () => { + if (!pulling.current) return; + pulling.current = false; + + if (pullDistance >= THRESHOLD) { + setRefreshing(true); + setPullDistance(THRESHOLD); + await onRefresh(); + setRefreshing(false); + } + setPullDistance(0); + }, [pullDistance, onRefresh]); + + return ( +
+
0 ? pullDistance : 0 }} + > + {refreshing ? ( + Refreshing... + ) : pullDistance >= THRESHOLD ? ( + Release to refresh + ) : pullDistance > 10 ? ( + Pull down to refresh + ) : null} +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 6f5fb92..ad2283f 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -5,6 +5,7 @@ export { default as EmptyState } from "./EmptyState"; export { default as IconButton } from "./IconButton"; export { default as Modal } from "./Modal"; export { default as LoadingSpinner } from "./LoadingSpinner"; +export { default as PullToRefresh } from "./PullToRefresh"; export { HomeIcon, CheckCircleIcon, diff --git a/frontend/src/features/dashboard/DashboardPage.tsx b/frontend/src/features/dashboard/DashboardPage.tsx index 6e0bb4b..816053f 100644 --- a/frontend/src/features/dashboard/DashboardPage.tsx +++ b/frontend/src/features/dashboard/DashboardPage.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; +import { PullToRefresh } from "../../components"; import WeatherCard from "./WeatherCard"; import TasksCard from "./TasksCard"; import ShoppingCard from "./ShoppingCard"; @@ -17,12 +18,24 @@ export default function DashboardPage() { return () => clearInterval(interval); }, [queryClient]); + const handleRefresh = useCallback(async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["weather"] }), + queryClient.invalidateQueries({ queryKey: ["prayer-times"] }), + queryClient.invalidateQueries({ queryKey: ["tasks"] }), + queryClient.invalidateQueries({ queryKey: ["shopping"] }), + queryClient.invalidateQueries({ queryKey: ["notes"] }), + ]); + }, [queryClient]); + return ( -
- - - - -
+ +
+ + + + +
+
); -} +} \ No newline at end of file diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 25ff50b..b8faa87 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file From 00788f601cc5974eedc53a183cd4fd064a0b3355 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Sun, 9 Aug 2026 23:15:35 +0100 Subject: [PATCH 05/23] Update roadmap: add post-launch features section with prayer times --- docs/ROADMAP.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 16b76ba..8c27e3c 100755 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -407,9 +407,23 @@ A fully working kitchen touchscreen system used by the family. --- +# Post-Launch Features (COMPLETED) + +Features added after the initial v1 release. + +## Prayer Times + +- Integrated Aladhan API with Method 15 (Moonsighting Committee) and Hanafi school for closest UK mosque times. +- Created backend prayer service with GET /api/v1/prayer-times endpoint. +- Added in-memory cache with daily refresh at 1am via APScheduler. +- Current prayer highlight recalculated on each request. +- Added pull-to-refresh on dashboard to manually update weather and prayer times. + +--- + # Future Expansion -After the first version is stable, new modules can be added. +New modules and features to be added. Possible future features: @@ -453,14 +467,6 @@ Example: --- -## Prayer Times - -- Scrape prayer times from local mosque website. -- Display daily prayer schedule on the dashboard. -- Highlight the next upcoming prayer. - ---- - ## CCTV Live Feed - Display live CCTV camera feeds on the dashboard. From 78172c059c2bff47d9f1a7170ffc9a3b4566abaf Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 12:30:38 +0100 Subject: [PATCH 06/23] Add auto-start after reboot instructions to README --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index d8887a0..2d206e0 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,22 @@ docker compose up --build -d Your data (members, tasks, shopping, notes) is stored on a Docker volume and is preserved across updates. +### Auto-start after reboot + +To ensure HomeOS starts automatically when the machine restarts: + +```bash +sudo systemctl enable docker +``` + +This makes Docker start on boot. The containers auto-start with Docker because they're configured with `restart: unless-stopped`. No need to run `docker compose up` again after a reboot. + +To verify Docker is running after a restart: + +```bash +sudo systemctl status docker +``` + ### Useful commands ```bash From 25de184dbba5225b90531ba61c2ae3a6b891810c Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 12:44:37 +0100 Subject: [PATCH 07/23] Add prayer times API client and TanStack Query hook for frontend --- docs/ROADMAP.md | 27 ++++++++++++++++++++++++--- frontend/src/api/prayer.ts | 7 +++++++ frontend/src/hooks/usePrayerTimes.ts | 10 ++++++++++ frontend/src/types/index.ts | 10 ++++++++++ frontend/tsconfig.app.tsbuildinfo | 2 +- 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 frontend/src/api/prayer.ts create mode 100644 frontend/src/hooks/usePrayerTimes.ts diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8c27e3c..b3154a0 100755 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -407,17 +407,38 @@ A fully working kitchen touchscreen system used by the family. --- -# Post-Launch Features (COMPLETED) +# Post-Launch Features Features added after the initial v1 release. -## Prayer Times +## Prayer Times (IN PROGRESS) + +### Completed Tasks + +#### Task 1 — Backend: Prayer Times API - Integrated Aladhan API with Method 15 (Moonsighting Committee) and Hanafi school for closest UK mosque times. - Created backend prayer service with GET /api/v1/prayer-times endpoint. - Added in-memory cache with daily refresh at 1am via APScheduler. - Current prayer highlight recalculated on each request. -- Added pull-to-refresh on dashboard to manually update weather and prayer times. + +#### Pull-to-Refresh + +- Added pull-to-refresh component on dashboard to manually update weather, prayer times, and all cards. + +### Remaining Tasks + +#### Task 2 — Frontend: Prayer Times Hook + +- Add PrayerTimes TypeScript interface. +- Create axios function for prayer times endpoint. +- Create TanStack Query hook (usePrayerTimes). + +#### Task 3 — Frontend: Prayer Times Bar + +- Display prayer times at the top of the dashboard. +- Highlight the current prayer. +- Touch-friendly layout with all 5 prayer times visible at a glance. --- diff --git a/frontend/src/api/prayer.ts b/frontend/src/api/prayer.ts new file mode 100644 index 0000000..bf36d9f --- /dev/null +++ b/frontend/src/api/prayer.ts @@ -0,0 +1,7 @@ +import apiClient from "./client"; +import type { PrayerTimes } from "../types"; + +export async function fetchPrayerTimes(): Promise { + const response = await apiClient.get("/prayer-times"); + return response.data; +} \ No newline at end of file diff --git a/frontend/src/hooks/usePrayerTimes.ts b/frontend/src/hooks/usePrayerTimes.ts new file mode 100644 index 0000000..f212e58 --- /dev/null +++ b/frontend/src/hooks/usePrayerTimes.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchPrayerTimes } from "../api/prayer"; + +export function usePrayerTimes() { + return useQuery({ + queryKey: ["prayer-times"], + queryFn: fetchPrayerTimes, + staleTime: 60 * 60 * 1000, + }); +} \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 479891c..4bd6f28 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -42,6 +42,16 @@ export interface Weather { icon: string; } +export interface PrayerTime { + name: string; + time: string; +} + +export interface PrayerTimes { + prayers: PrayerTime[]; + current_prayer: string | null; +} + export interface HealthResponse { app: string; version: string; diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index b8faa87..f8cba3c 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/prayer.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useprayertimes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file From 04d885e276ec0827eaec18966bbf4f931b04bedd Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 12:48:57 +0100 Subject: [PATCH 08/23] Add prayer times bar to dashboard with current prayer highlight --- .../src/features/dashboard/DashboardPage.tsx | 2 + .../src/features/dashboard/PrayerTimesBar.tsx | 57 +++++++++++++++++++ frontend/tsconfig.app.tsbuildinfo | 2 +- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 frontend/src/features/dashboard/PrayerTimesBar.tsx diff --git a/frontend/src/features/dashboard/DashboardPage.tsx b/frontend/src/features/dashboard/DashboardPage.tsx index 816053f..7450a89 100644 --- a/frontend/src/features/dashboard/DashboardPage.tsx +++ b/frontend/src/features/dashboard/DashboardPage.tsx @@ -1,6 +1,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect } from "react"; import { PullToRefresh } from "../../components"; +import PrayerTimesBar from "./PrayerTimesBar"; import WeatherCard from "./WeatherCard"; import TasksCard from "./TasksCard"; import ShoppingCard from "./ShoppingCard"; @@ -31,6 +32,7 @@ export default function DashboardPage() { return (
+ diff --git a/frontend/src/features/dashboard/PrayerTimesBar.tsx b/frontend/src/features/dashboard/PrayerTimesBar.tsx new file mode 100644 index 0000000..40ab6de --- /dev/null +++ b/frontend/src/features/dashboard/PrayerTimesBar.tsx @@ -0,0 +1,57 @@ +import { Card, LoadingSpinner } from "../../components"; +import { usePrayerTimes } from "../../hooks/usePrayerTimes"; + +export default function PrayerTimesBar() { + const { data, isLoading } = usePrayerTimes(); + + if (isLoading) { + return ( + + + + ); + } + + if (!data || data.prayers.length === 0) { + return null; + } + + return ( + +
+ {data.prayers.map((prayer) => { + const isCurrent = data.current_prayer === prayer.name; + return ( +
+ + {prayer.name} + + + {prayer.time} + +
+ ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index f8cba3c..067e820 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/prayer.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useprayertimes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/prayer.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/prayertimesbar.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useprayertimes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file From 7a8a53dd0f507f0446574835def606e05a7a2424 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 12:53:21 +0100 Subject: [PATCH 09/23] Update roadmap: mark prayer times feature as completed --- docs/ROADMAP.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b3154a0..5377aa6 100755 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -411,34 +411,30 @@ A fully working kitchen touchscreen system used by the family. Features added after the initial v1 release. -## Prayer Times (IN PROGRESS) +## Prayer Times (COMPLETED) -### Completed Tasks - -#### Task 1 — Backend: Prayer Times API +### Task 1 — Backend: Prayer Times API - Integrated Aladhan API with Method 15 (Moonsighting Committee) and Hanafi school for closest UK mosque times. - Created backend prayer service with GET /api/v1/prayer-times endpoint. - Added in-memory cache with daily refresh at 1am via APScheduler. - Current prayer highlight recalculated on each request. -#### Pull-to-Refresh +### Pull-to-Refresh - Added pull-to-refresh component on dashboard to manually update weather, prayer times, and all cards. -### Remaining Tasks - -#### Task 2 — Frontend: Prayer Times Hook +### Task 2 — Frontend: Prayer Times Hook -- Add PrayerTimes TypeScript interface. -- Create axios function for prayer times endpoint. -- Create TanStack Query hook (usePrayerTimes). +- Added PrayerTime and PrayerTimes TypeScript interfaces. +- Created axios function (fetchPrayerTimes). +- Created TanStack Query hook (usePrayerTimes) with 1-hour stale time. -#### Task 3 — Frontend: Prayer Times Bar +### Task 3 — Frontend: Prayer Times Bar -- Display prayer times at the top of the dashboard. -- Highlight the current prayer. -- Touch-friendly layout with all 5 prayer times visible at a glance. +- Created PrayerTimesBar displaying all 5 prayers in a horizontal row at the top of the dashboard. +- Current prayer highlighted in blue with tinted background. +- Added to DashboardPage above WeatherCard. --- From 10f371f6f57ea6f95e58649f9940137993f80fbf Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 13:08:15 +0100 Subject: [PATCH 10/23] Tune prayer times to match Masjid-e-Salaam timetable --- backend/app/modules/prayer/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/modules/prayer/service.py b/backend/app/modules/prayer/service.py index dd77c17..4fd72b6 100644 --- a/backend/app/modules/prayer/service.py +++ b/backend/app/modules/prayer/service.py @@ -52,6 +52,7 @@ async def fetch_prayer_times() -> PrayerTimesResponse: "longitude": settings.WEATHER_LONGITUDE, "method": 15, "school": 1, + "tune": "0,0,0,5,0,5,0,9,0", } async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client: From 73368d5be35928264af9573db6b367765b1a0398 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 13:22:46 +0100 Subject: [PATCH 11/23] Add Islamic calendar date to header from Aladhan API --- backend/app/modules/prayer/schemas.py | 3 ++- backend/app/modules/prayer/service.py | 5 ++++- frontend/src/features/dashboard/DashboardPage.tsx | 2 +- frontend/src/layouts/AppLayout.tsx | 8 +++++++- frontend/src/types/index.ts | 1 + 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/app/modules/prayer/schemas.py b/backend/app/modules/prayer/schemas.py index d026f5d..9b23233 100644 --- a/backend/app/modules/prayer/schemas.py +++ b/backend/app/modules/prayer/schemas.py @@ -8,4 +8,5 @@ class PrayerTime(BaseModel): class PrayerTimesResponse(BaseModel): prayers: list[PrayerTime] - current_prayer: str | None = None \ No newline at end of file + current_prayer: str | None = None + hijri_date: str | None = None \ No newline at end of file diff --git a/backend/app/modules/prayer/service.py b/backend/app/modules/prayer/service.py index 4fd72b6..80ad746 100644 --- a/backend/app/modules/prayer/service.py +++ b/backend/app/modules/prayer/service.py @@ -68,8 +68,11 @@ async def fetch_prayer_times() -> PrayerTimesResponse: clean_time = raw_time.split(" ")[0] prayers.append(PrayerTime(name=name, time=_to_12h(clean_time))) + hijri = data["data"]["date"]["hijri"] + hijri_date = f"{hijri['day']} {hijri['month']['en']} {hijri['year']}" + current_prayer = _find_current_prayer(prayers) - _cache = PrayerTimesResponse(prayers=prayers, current_prayer=current_prayer) + _cache = PrayerTimesResponse(prayers=prayers, current_prayer=current_prayer, hijri_date=hijri_date) logger.info("Prayer times updated: %d prayers fetched", len(prayers)) except Exception: diff --git a/frontend/src/features/dashboard/DashboardPage.tsx b/frontend/src/features/dashboard/DashboardPage.tsx index 7450a89..623ca81 100644 --- a/frontend/src/features/dashboard/DashboardPage.tsx +++ b/frontend/src/features/dashboard/DashboardPage.tsx @@ -32,8 +32,8 @@ export default function DashboardPage() { return (
- + diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index ad260b7..d83f562 100755 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { NavLink, Outlet, useLocation } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; +import { usePrayerTimes } from "../hooks/usePrayerTimes"; import { HomeIcon, CheckCircleIcon, @@ -35,6 +36,7 @@ function formatTime(date: Date) { export default function AppLayout() { const location = useLocation(); const [now, setNow] = useState(new Date()); + const { data: prayerData } = usePrayerTimes(); useEffect(() => { const interval = setInterval(() => setNow(new Date()), 60_000); @@ -46,6 +48,10 @@ export default function AppLayout() {
HomeOS
+ {prayerData?.hijri_date && ( + {prayerData.hijri_date} + )} + | {formatDate(now)} {formatTime(now)}
@@ -86,4 +92,4 @@ export default function AppLayout() {
); -} +} \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 4bd6f28..a6e9e55 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -50,6 +50,7 @@ export interface PrayerTime { export interface PrayerTimes { prayers: PrayerTime[]; current_prayer: string | null; + hijri_date: string | null; } export interface HealthResponse { From 97192dbcda31c1e4c2e89c7fe8e044d5a3d542a3 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 17:20:16 +0100 Subject: [PATCH 12/23] Add reusable long-press hook for touch edit interactions --- docs/ROADMAP.md | 39 +++++++++++++++++++++++++++--- frontend/src/hooks/useLongPress.ts | 28 +++++++++++++++++++++ frontend/tsconfig.app.tsbuildinfo | 2 +- 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 frontend/src/hooks/useLongPress.ts diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5377aa6..7cc5403 100755 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -432,9 +432,42 @@ Features added after the initial v1 release. ### Task 3 — Frontend: Prayer Times Bar -- Created PrayerTimesBar displaying all 5 prayers in a horizontal row at the top of the dashboard. +- Created PrayerTimesBar displaying all 5 prayers in a horizontal row on the dashboard. - Current prayer highlighted in blue with tinted background. -- Added to DashboardPage above WeatherCard. + +### Task 4 — Islamic Calendar Date + +- Added Hijri date to the prayer times API response from Aladhan. +- Displayed Islamic date in the app header alongside the Gregorian date. +- Tuned prayer time settings (Method 15, Hanafi, tune 0,0,0,5,0,5,0,9,0) to closely match Masjid-e-Salaam Preston timetable. + +--- + +## Long-Press Edit (IN PROGRESS) + +### Task 1 — Long-Press Hook + +- Create reusable useLongPress hook that detects 500ms finger hold. + +### Task 2 — Edit Tasks + +- Create EditTaskModal with pre-filled title and member assignment. +- Add backend PUT endpoint support (already exists). +- Wire long-press on TaskList rows to open edit modal. + +### Task 3 — Edit Shopping Items + +- Add long-press on ShoppingList rows to open existing EditItemModal. + +### Task 4 — Edit Notes + +- Create EditNoteModal with pre-filled content. +- Add long-press on NoteList cards to open edit modal. + +### Task 5 — Edit Members + +- Create EditMemberModal with pre-filled name and colour. +- Add long-press on MemberList rows to open edit modal. --- @@ -442,8 +475,6 @@ Features added after the initial v1 release. New modules and features to be added. -Possible future features: - ## AI Assistant - Voice commands. diff --git a/frontend/src/hooks/useLongPress.ts b/frontend/src/hooks/useLongPress.ts new file mode 100644 index 0000000..b9b5034 --- /dev/null +++ b/frontend/src/hooks/useLongPress.ts @@ -0,0 +1,28 @@ +import { useRef, useCallback } from "react"; + +export function useLongPress(onLongPress: () => void, delay = 500) { + const timerRef = useRef | null>(null); + const triggered = useRef(false); + + const start = useCallback(() => { + triggered.current = false; + timerRef.current = setTimeout(() => { + triggered.current = true; + onLongPress(); + }, delay); + }, [onLongPress, delay]); + + const cancel = useCallback(() => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }, []); + + return { + onTouchStart: start, + onTouchEnd: cancel, + onTouchMove: cancel, + wasLongPress: () => triggered.current, + }; +} \ No newline at end of file diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 067e820..694616c 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/prayer.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/prayertimesbar.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useprayertimes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/prayer.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/prayertimesbar.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/uselongpress.ts","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useprayertimes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file From 924cb52b6aab29762360c7db78fd8b1c6d5957f9 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 17:22:58 +0100 Subject: [PATCH 13/23] Add long-press edit for tasks with EditTaskModal --- frontend/src/features/tasks/EditTaskModal.tsx | 93 ++++++++++++++ frontend/src/features/tasks/TaskList.tsx | 120 ++++++++++-------- frontend/src/features/tasks/TasksPage.tsx | 40 ++++-- frontend/tsconfig.app.tsbuildinfo | 2 +- 4 files changed, 193 insertions(+), 62 deletions(-) create mode 100644 frontend/src/features/tasks/EditTaskModal.tsx diff --git a/frontend/src/features/tasks/EditTaskModal.tsx b/frontend/src/features/tasks/EditTaskModal.tsx new file mode 100644 index 0000000..81bf8e8 --- /dev/null +++ b/frontend/src/features/tasks/EditTaskModal.tsx @@ -0,0 +1,93 @@ +import { useState, useEffect } from "react"; +import { Modal, Button } from "../../components"; +import { useMembers } from "../../hooks/useMembers"; +import type { Task } from "../../types"; + +interface EditTaskModalProps { + open: boolean; + onClose: () => void; + onSave: (title: string, assignedTo: number | null) => void; + task: Task | null; +} + +export default function EditTaskModal({ open, onClose, onSave, task }: EditTaskModalProps) { + const [title, setTitle] = useState(""); + const [assignedTo, setAssignedTo] = useState(null); + const { data: members = [] } = useMembers(); + + useEffect(() => { + if (task) { + setTitle(task.title); + setAssignedTo(task.assigned_to); + } + }, [task]); + + function handleSave() { + const trimmed = title.trim(); + if (!trimmed) return; + onSave(trimmed, assignedTo); + } + + return ( + +
+
+ + setTitle(e.target.value)} + autoFocus + className="w-full min-h-[48px] px-4 rounded-xl border border-border bg-surface text-text text-lg dark:border-border-dark dark:bg-surface-dark-dim dark:text-text-dark focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ +
+ +
+ + {members.map((member) => ( + + ))} +
+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/features/tasks/TaskList.tsx b/frontend/src/features/tasks/TaskList.tsx index 0a7ac25..1007601 100644 --- a/frontend/src/features/tasks/TaskList.tsx +++ b/frontend/src/features/tasks/TaskList.tsx @@ -1,10 +1,12 @@ import { motion } from "framer-motion"; import { IconButton } from "../../components"; +import { useLongPress } from "../../hooks/useLongPress"; import type { Task } from "../../types"; interface TaskListProps { tasks: Task[]; onToggle: (id: number) => void; + onEdit: (task: Task) => void; onDelete: (id: number) => void; } @@ -36,61 +38,77 @@ function CheckCircle({ filled }: { filled: boolean }) { ); } -export default function TaskList({ tasks, onToggle, onDelete }: TaskListProps) { +function TaskRow({ task, onToggle, onEdit, onDelete }: { task: Task; onToggle: () => void; onEdit: () => void; onDelete: () => void }) { + const longPress = useLongPress(onEdit); + return ( -
- {tasks.map((task) => ( - - + + -
onToggle(task.id)} - className="flex-1 min-w-0 cursor-pointer" - > -

- {task.title} -

- {task.member && ( -
-
- - {task.member.name} - -
- )} +
{ + if (!longPress.wasLongPress()) onToggle(); + }} + className="flex-1 min-w-0 cursor-pointer" + > +

+ {task.title} +

+ {task.member && ( +
+
+ + {task.member.name} +
+ )} +
+ + } + variant="danger" + label={`Delete ${task.title}`} + onClick={onDelete} + /> + + ); +} - } - variant="danger" - label={`Delete ${task.title}`} - onClick={() => onDelete(task.id)} - /> - +export default function TaskList({ tasks, onToggle, onEdit, onDelete }: TaskListProps) { + return ( +
+ {tasks.map((task) => ( + onToggle(task.id)} + onEdit={() => onEdit(task)} + onDelete={() => onDelete(task.id)} + /> ))}
); -} +} \ No newline at end of file diff --git a/frontend/src/features/tasks/TasksPage.tsx b/frontend/src/features/tasks/TasksPage.tsx index 1b80d3a..7f90236 100644 --- a/frontend/src/features/tasks/TasksPage.tsx +++ b/frontend/src/features/tasks/TasksPage.tsx @@ -1,20 +1,32 @@ import { useState } from "react"; -import { useTasks, useCreateTask, useToggleTask, useDeleteTask } from "../../hooks/useTasks"; +import { useTasks, useCreateTask, useUpdateTask, useToggleTask, useDeleteTask } from "../../hooks/useTasks"; import { PageHeader, Button, EmptyState } from "../../components"; import AddTaskModal from "./AddTaskModal"; +import EditTaskModal from "./EditTaskModal"; import TaskList from "./TaskList"; +import type { Task } from "../../types"; export default function TasksPage() { const { data: tasks = [] } = useTasks(); const createTask = useCreateTask(); + const updateTask = useUpdateTask(); const toggleTask = useToggleTask(); const deleteTask = useDeleteTask(); - const [modalOpen, setModalOpen] = useState(false); + const [addModalOpen, setAddModalOpen] = useState(false); + const [editingTask, setEditingTask] = useState(null); - function handleSave(title: string, assignedTo: number | null) { + function handleCreate(title: string, assignedTo: number | null) { createTask.mutate( { title, assigned_to: assignedTo }, - { onSuccess: () => setModalOpen(false) }, + { onSuccess: () => setAddModalOpen(false) }, + ); + } + + function handleEdit(title: string, assignedTo: number | null) { + if (!editingTask) return; + updateTask.mutate( + { id: editingTask.id, data: { title, assigned_to: assignedTo } }, + { onSuccess: () => setEditingTask(null) }, ); } @@ -22,27 +34,35 @@ export default function TasksPage() {
setModalOpen(true)}>Add Task} + action={} /> {tasks.length === 0 ? ( setModalOpen(true)}>Add Task} + action={} /> ) : ( toggleTask.mutate(id)} + onEdit={(task) => setEditingTask(task)} onDelete={(id) => deleteTask.mutate(id)} /> )} setModalOpen(false)} - onSave={handleSave} + open={addModalOpen} + onClose={() => setAddModalOpen(false)} + onSave={handleCreate} + /> + + setEditingTask(null)} + onSave={handleEdit} + task={editingTask} />
); -} +} \ No newline at end of file diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 694616c..b8d0b25 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/prayer.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/prayertimesbar.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/uselongpress.ts","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useprayertimes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/members.ts","./src/api/notes.ts","./src/api/prayer.ts","./src/api/shopping.ts","./src/api/tasks.ts","./src/api/weather.ts","./src/components/button.tsx","./src/components/card.tsx","./src/components/emptystate.tsx","./src/components/iconbutton.tsx","./src/components/loadingspinner.tsx","./src/components/modal.tsx","./src/components/pageheader.tsx","./src/components/pulltorefresh.tsx","./src/components/icons.tsx","./src/components/index.ts","./src/features/dashboard/dashboardpage.tsx","./src/features/dashboard/notescard.tsx","./src/features/dashboard/prayertimesbar.tsx","./src/features/dashboard/shoppingcard.tsx","./src/features/dashboard/taskscard.tsx","./src/features/dashboard/weathercard.tsx","./src/features/notes/addnotemodal.tsx","./src/features/notes/notelist.tsx","./src/features/notes/notespage.tsx","./src/features/settings/addmembermodal.tsx","./src/features/settings/memberlist.tsx","./src/features/settings/settingspage.tsx","./src/features/shopping/additemmodal.tsx","./src/features/shopping/edititemmodal.tsx","./src/features/shopping/shoppinglist.tsx","./src/features/shopping/shoppingpage.tsx","./src/features/tasks/addtaskmodal.tsx","./src/features/tasks/edittaskmodal.tsx","./src/features/tasks/tasklist.tsx","./src/features/tasks/taskspage.tsx","./src/hooks/uselongpress.ts","./src/hooks/usemembers.ts","./src/hooks/usenotes.ts","./src/hooks/useprayertimes.ts","./src/hooks/useshopping.ts","./src/hooks/usetasks.ts","./src/hooks/useweather.ts","./src/layouts/applayout.tsx","./src/stores/themestore.ts","./src/types/index.ts"],"version":"5.8.3"} \ No newline at end of file From e3ce2ef5339905c69f1eb122f760983292c9643a Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 17:24:18 +0100 Subject: [PATCH 14/23] Add long-press edit for shopping items --- .../src/features/shopping/ShoppingList.tsx | 105 ++++++++++-------- 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/frontend/src/features/shopping/ShoppingList.tsx b/frontend/src/features/shopping/ShoppingList.tsx index 149bdf1..eafdd90 100644 --- a/frontend/src/features/shopping/ShoppingList.tsx +++ b/frontend/src/features/shopping/ShoppingList.tsx @@ -1,5 +1,6 @@ import { motion } from "framer-motion"; import { IconButton } from "../../components"; +import { useLongPress } from "../../hooks/useLongPress"; import type { ShoppingItem } from "../../types"; interface ShoppingListProps { @@ -46,55 +47,71 @@ function CheckBox({ checked }: { checked: boolean }) { ); } +function ShoppingRow({ item, onToggle, onEdit, onDelete }: { item: ShoppingItem; onToggle: () => void; onEdit: () => void; onDelete: () => void }) { + const longPress = useLongPress(onEdit); + + return ( + + + +
{ + if (!longPress.wasLongPress()) onToggle(); + }} + className="flex-1 min-w-0 cursor-pointer" + > +

+ {item.name} +

+
+ + } + label={`Edit ${item.name}`} + onClick={onEdit} + /> + } + variant="danger" + label={`Delete ${item.name}`} + onClick={onDelete} + /> +
+ ); +} + export default function ShoppingList({ items, onToggle, onEdit, onDelete }: ShoppingListProps) { return (
{items.map((item) => ( - - - -
onToggle(item.id)} - className="flex-1 min-w-0 cursor-pointer" - > -

- {item.name} -

-
- - } - label={`Edit ${item.name}`} - onClick={() => onEdit(item)} - /> - } - variant="danger" - label={`Delete ${item.name}`} - onClick={() => onDelete(item.id)} - /> -
+ item={item} + onToggle={() => onToggle(item.id)} + onEdit={() => onEdit(item)} + onDelete={() => onDelete(item.id)} + /> ))}
); -} +} \ No newline at end of file From 6216f616fa7cd56af09133ff8dfd0f4472903ab5 Mon Sep 17 00:00:00 2001 From: Muhammad Danial Khilji Date: Mon, 10 Aug 2026 17:25:39 +0100 Subject: [PATCH 15/23] Add long-press edit for notes with EditNoteModal --- frontend/src/features/notes/EditNoteModal.tsx | 51 ++++++++++++ frontend/src/features/notes/NoteList.tsx | 77 +++++++++++-------- frontend/src/features/notes/NotesPage.tsx | 40 +++++++--- frontend/tsconfig.app.tsbuildinfo | 2 +- 4 files changed, 128 insertions(+), 42 deletions(-) create mode 100644 frontend/src/features/notes/EditNoteModal.tsx diff --git a/frontend/src/features/notes/EditNoteModal.tsx b/frontend/src/features/notes/EditNoteModal.tsx new file mode 100644 index 0000000..acd5c72 --- /dev/null +++ b/frontend/src/features/notes/EditNoteModal.tsx @@ -0,0 +1,51 @@ +import { useState, useEffect } from "react"; +import { Modal, Button } from "../../components"; + +interface EditNoteModalProps { + open: boolean; + onClose: () => void; + onSave: (content: string) => void; + currentContent: string; +} + +export default function EditNoteModal({ open, onClose, onSave, currentContent }: EditNoteModalProps) { + const [content, setContent] = useState(currentContent); + + useEffect(() => { + setContent(currentContent); + }, [currentContent]); + + function handleSave() { + const trimmed = content.trim(); + if (!trimmed) return; + onSave(trimmed); + } + + return ( + +
+
+ +