Skip to content

Commit e6c1bdf

Browse files
authored
Merge pull request #5 from danialkhilji/develop
Release v1.3: calendar, birthdays, reminders, weather animations, mobile app
2 parents 68f3a76 + 4e32a1b commit e6c1bdf

93 files changed

Lines changed: 2819 additions & 1526 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
11
# Changelog
22

3+
## v1.3
4+
5+
### New Features
6+
- Dashboard calendar with expandable modal, month/year dropdowns, swipe navigation, and date selection
7+
- Task reminders with date/time picker and overdue highlighting on dashboard
8+
- Recurring tasks (daily/weekly/monthly) with automatic reset via APScheduler
9+
- Calendar-task integration — select any date to see its tasks
10+
- Birthday tracking via calendar — add birthdays for anyone, yearly repeat, upcoming birthdays card on dashboard
11+
- Calendar as core system — unified /api/v1/calendar/by-date endpoint returning tasks and birthdays
12+
- Animated weather card backgrounds (sunny, cloudy, rainy, snowy, stormy, windy) with CSS keyframes
13+
- Enhanced weather data — feels like, rain chance, daily high/low from Open-Meteo
14+
- Mobile app via PWA + Tailscale — responsive UI, installable on phone, remote access
15+
- Custom app icon (Figma-designed HomeOS icon)
16+
- Quick-add shopping items with emoji buttons and custom items in Settings
17+
18+
### Improvements
19+
- Removed dark mode (unused by family)
20+
- Auto-scroll focused input into view when keyboard opens in modals
21+
- Weather card vertically and horizontally centred
22+
- Responsive font sizes (14px phone, 18px tablet)
23+
- Calendar grid stays fixed while tasks/birthdays scroll independently
24+
- Add Birthday and Today buttons always visible at bottom of calendar
25+
26+
### Infrastructure
27+
- Moved birthdays module into calendar module
28+
- Moved calendar components to features/calendar/ folder
29+
- Moved project docs to private KnowledgeBase repo
30+
- 119 backend tests total
31+
32+
---
33+
334
## v1.2
435

536
### New Features

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,47 @@ To verify Docker is running after a restart:
127127
sudo systemctl status docker
128128
```
129129

130+
### Remote Access with Tailscale
131+
132+
Access HomeOS from your phone outside your home WiFi using Tailscale (free VPN).
133+
134+
**On the Linux laptop (HomeOS server):**
135+
136+
```bash
137+
curl -fsSL https://tailscale.com/install.sh | sh
138+
sudo tailscale up
139+
```
140+
141+
Note the Tailscale IP shown (e.g. `100.x.x.x`).
142+
143+
**On your phone:**
144+
145+
1. Install Tailscale from App Store (iPhone) or Play Store (Android)
146+
2. Sign in with the same account used on the Linux laptop
147+
3. Open `http://100.x.x.x` in your phone browser (use the Tailscale IP from above)
148+
149+
Tailscale runs in the background — once set up, your phone can reach HomeOS from anywhere without opening ports or exposing your home network.
150+
151+
### Install on Phone (Add to Home Screen)
152+
153+
HomeOS is a PWA — it can be installed on your phone's home screen like a native app.
154+
155+
**iPhone (Safari):**
156+
157+
1. Open HomeOS in Safari (use your home WiFi IP or Tailscale IP)
158+
2. Tap the Share button (square with arrow)
159+
3. Scroll down and tap "Add to Home Screen"
160+
4. Tap "Add"
161+
162+
**Android (Chrome):**
163+
164+
1. Open HomeOS in Chrome
165+
2. Tap the three-dot menu
166+
3. Tap "Add to Home screen" or "Install app"
167+
4. Tap "Add"
168+
169+
The app opens fullscreen without a browser address bar, with the HomeOS icon on your home screen.
170+
130171
### Useful commands
131172

132173
```bash
@@ -219,5 +260,6 @@ HomeOS/
219260
│ ├── layouts/ # App shell and navigation
220261
│ ├── stores/ # Zustand state management
221262
│ └── types/ # Shared TypeScript types
222-
└── docs/ # Vision, roadmap, engineering guidelines
263+
├── scripts/ # Backup and utility scripts
264+
└── CHANGELOG.md # Release history
223265
```

backend/app/api/v1/router.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
from app.modules.weather.router import router as weather_router
99
from app.modules.prayer.router import router as prayer_router
1010
from app.modules.shopping.store_router import router as store_router
11+
from app.modules.shopping.quick_add_router import router as quick_add_router
12+
from app.modules.calendar.birthday_router import router as birthdays_router
13+
from app.modules.calendar.calendar_router import router as calendar_router
1114

1215
v1_router = APIRouter()
1316
v1_router.include_router(health_router)
1417
v1_router.include_router(members_router)
1518
v1_router.include_router(tasks_router)
1619
v1_router.include_router(shopping_router)
1720
v1_router.include_router(store_router)
21+
v1_router.include_router(quick_add_router)
1822
v1_router.include_router(notes_router)
1923
v1_router.include_router(weather_router)
2024
v1_router.include_router(prayer_router)
25+
v1_router.include_router(birthdays_router)
26+
v1_router.include_router(calendar_router)

backend/app/core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Settings(BaseSettings):
1414

1515
APP_NAME: str = "HomeOS"
1616
DEBUG: bool = True
17-
VERSION: str = "1.2"
17+
VERSION: str = "1.3"
1818

1919
DATABASE_URL: str = f"sqlite+aiosqlite:///{BASE_DIR / 'homeos.db'}"
2020

backend/app/core/scheduler.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ async def run_prayer_refresh() -> None:
1919
logger.exception("Scheduled prayer times refresh failed")
2020

2121

22+
async def run_recurrence_reset() -> None:
23+
from app.modules.tasks.recurrence import reset_recurring_tasks
24+
25+
logger.info("Scheduled recurring task reset triggered")
26+
async with async_session_factory() as session:
27+
try:
28+
await reset_recurring_tasks(session)
29+
await session.commit()
30+
except Exception:
31+
await session.rollback()
32+
logger.exception("Scheduled recurring task reset failed")
33+
34+
2235
async def run_rotation() -> None:
2336
from app.modules.tasks.rotation import rotate_tasks
2437

@@ -39,14 +52,20 @@ def setup_scheduler() -> None:
3952
id="weekly_task_rotation",
4053
replace_existing=True,
4154
)
55+
scheduler.add_job(
56+
run_recurrence_reset,
57+
trigger=CronTrigger(hour=0, minute=1),
58+
id="daily_recurrence_reset",
59+
replace_existing=True,
60+
)
4261
scheduler.add_job(
4362
run_prayer_refresh,
4463
trigger=CronTrigger(hour=1, minute=0),
4564
id="daily_prayer_refresh",
4665
replace_existing=True,
4766
)
4867
scheduler.start()
49-
logger.info("Scheduler started: task rotation every Monday at midnight, prayer times refresh daily at 1am")
68+
logger.info("Scheduler started: recurrence reset daily at 00:01, task rotation every Monday at midnight, prayer times refresh daily at 1am")
5069

5170

5271
def shutdown_scheduler() -> None:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from app.modules.calendar.birthday_models import Birthday
2+
3+
__all__ = ["Birthday"]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from datetime import datetime
2+
3+
from sqlalchemy import String, Integer, DateTime, func
4+
from sqlalchemy.orm import Mapped, mapped_column
5+
6+
from app.core.database import Base
7+
8+
9+
class Birthday(Base):
10+
__tablename__ = "birthdays"
11+
12+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
13+
name: Mapped[str] = mapped_column(String(200), nullable=False)
14+
month: Mapped[int] = mapped_column(Integer, nullable=False)
15+
day: Mapped[int] = mapped_column(Integer, nullable=False)
16+
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from fastapi import APIRouter, Depends, Query
2+
from sqlalchemy.ext.asyncio import AsyncSession
3+
4+
from app.core.database import get_db
5+
from app.modules.calendar.birthday_schemas import BirthdayCreate, BirthdayResponse, UpcomingBirthdayResponse
6+
from app.modules.calendar import birthday_service as service
7+
8+
router = APIRouter(prefix="/birthdays", tags=["birthdays"])
9+
10+
11+
@router.get("", response_model=list[BirthdayResponse])
12+
async def list_birthdays(db: AsyncSession = Depends(get_db)):
13+
return await service.get_all_birthdays(db)
14+
15+
16+
@router.get("/upcoming", response_model=list[UpcomingBirthdayResponse])
17+
async def upcoming_birthdays(
18+
days: int = Query(7, ge=1, le=365),
19+
db: AsyncSession = Depends(get_db),
20+
):
21+
return await service.get_upcoming_birthdays(db, days)
22+
23+
24+
@router.get("/by-date", response_model=list[BirthdayResponse])
25+
async def birthdays_by_date(
26+
month: int = Query(..., ge=1, le=12),
27+
day: int = Query(..., ge=1, le=31),
28+
db: AsyncSession = Depends(get_db),
29+
):
30+
return await service.get_birthdays_by_date(db, month, day)
31+
32+
33+
@router.post("", response_model=BirthdayResponse, status_code=201)
34+
async def create_birthday(data: BirthdayCreate, db: AsyncSession = Depends(get_db)):
35+
return await service.create_birthday(db, data)
36+
37+
38+
@router.delete("/{birthday_id}")
39+
async def delete_birthday(birthday_id: int, db: AsyncSession = Depends(get_db)):
40+
await service.delete_birthday(db, birthday_id)
41+
return {"message": "Birthday deleted"}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from datetime import datetime
2+
3+
from pydantic import BaseModel, Field
4+
5+
6+
class BirthdayCreate(BaseModel):
7+
name: str = Field(min_length=1, max_length=200)
8+
month: int = Field(ge=1, le=12)
9+
day: int = Field(ge=1, le=31)
10+
11+
12+
class BirthdayResponse(BaseModel):
13+
id: int
14+
name: str
15+
month: int
16+
day: int
17+
created_at: datetime
18+
19+
model_config = {"from_attributes": True}
20+
21+
22+
class UpcomingBirthdayResponse(BaseModel):
23+
id: int
24+
name: str
25+
month: int
26+
day: int
27+
days_until: int
28+
29+
model_config = {"from_attributes": True}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from datetime import date
2+
3+
from sqlalchemy import select
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from app.core.exceptions import NotFoundError
7+
from app.modules.calendar.birthday_models import Birthday
8+
from app.modules.calendar.birthday_schemas import BirthdayCreate, UpcomingBirthdayResponse
9+
10+
11+
async def get_all_birthdays(db: AsyncSession) -> list[Birthday]:
12+
result = await db.execute(select(Birthday).order_by(Birthday.month, Birthday.day))
13+
return list(result.scalars().all())
14+
15+
16+
async def get_birthdays_by_date(db: AsyncSession, month: int, day: int) -> list[Birthday]:
17+
result = await db.execute(
18+
select(Birthday).where(Birthday.month == month, Birthday.day == day)
19+
)
20+
return list(result.scalars().all())
21+
22+
23+
async def get_upcoming_birthdays(db: AsyncSession, days: int = 7) -> list[UpcomingBirthdayResponse]:
24+
today = date.today()
25+
result = await db.execute(select(Birthday))
26+
all_birthdays = list(result.scalars().all())
27+
28+
upcoming = []
29+
for bday in all_birthdays:
30+
try:
31+
this_year = date(today.year, bday.month, bday.day)
32+
except ValueError:
33+
continue
34+
35+
if this_year < today:
36+
this_year = date(today.year + 1, bday.month, bday.day)
37+
38+
days_until = (this_year - today).days
39+
if 0 <= days_until <= days:
40+
upcoming.append(UpcomingBirthdayResponse(
41+
id=bday.id,
42+
name=bday.name,
43+
month=bday.month,
44+
day=bday.day,
45+
days_until=days_until,
46+
))
47+
48+
upcoming.sort(key=lambda b: b.days_until)
49+
return upcoming
50+
51+
52+
async def create_birthday(db: AsyncSession, data: BirthdayCreate) -> Birthday:
53+
birthday = Birthday(name=data.name, month=data.month, day=data.day)
54+
db.add(birthday)
55+
await db.flush()
56+
await db.refresh(birthday)
57+
return birthday
58+
59+
60+
async def delete_birthday(db: AsyncSession, birthday_id: int) -> None:
61+
result = await db.execute(select(Birthday).where(Birthday.id == birthday_id))
62+
birthday = result.scalar_one_or_none()
63+
if not birthday:
64+
raise NotFoundError("Birthday", birthday_id)
65+
await db.delete(birthday)

0 commit comments

Comments
 (0)