code quality fixes
Build and Push Docker Image / build-and-push (pull_request) Successful in 58s
Python Code Quality / python-code-quality (pull_request) Successful in 10s
Python Test / python-test (pull_request) Successful in 18s

This commit is contained in:
Brian Bjarke Jensen
2025-11-11 20:43:47 +01:00
parent 696de14e6b
commit a2b78f7e96
17 changed files with 236 additions and 249 deletions
+8 -8
View File
@@ -81,14 +81,14 @@ uv run uvicorn src.baby_monitor.main:app --reload --host 0.0.0.0 --port 8000
### Environment Variables ### Environment Variables
| Variable | Default | Description | | Variable | Default | Description |
| ---------------- | ------------ | --------------------------------------------- | | ---------------- | ------------ | ---------------------------------------------- |
| `ENVIRONMENT` | `production` | Set to `development` to enable API docs | | `ENVIRONMENT` | `production` | Set to `development` to enable API docs |
| `ADMIN_PASSWORD` | _(required)_ | Admin user password | | `ADMIN_PASSWORD` | _(required)_ | Admin user password |
| `ADMIN_USERNAME` | `admin` | Admin username | | `ADMIN_USERNAME` | `admin` | Admin username |
| `DATA_DIR` | `/data` | Directory for SQLite database (SQLite only) | | `DATA_DIR` | `/data` | Directory for SQLite database (SQLite only) |
| `REDIS_URI` | _(optional)_ | Redis connection URI for distributed tokens | | `REDIS_URI` | _(optional)_ | Redis connection URI for distributed tokens |
| `POSTGRES_URI` | _(optional)_ | PostgreSQL connection URI for database storage| | `POSTGRES_URI` | _(optional)_ | PostgreSQL connection URI for database storage |
### Storage Options ### Storage Options
@@ -15,7 +15,7 @@ from baby_monitor.models.db.child_parent import ChildParent
class DatabaseChildRepository(ChildRepositoryInterface): class DatabaseChildRepository(ChildRepositoryInterface):
"""Database implementation for child data access. """Database implementation for child data access.
Works with both SQLite and PostgreSQL databases. Works with both SQLite and PostgreSQL databases.
""" """
@@ -15,7 +15,7 @@ from baby_monitor.repositories.interfaces import (
class DatabaseChildInvitationRepository(ChildInvitationRepositoryInterface): class DatabaseChildInvitationRepository(ChildInvitationRepositoryInterface):
"""Database implementation of child invitation repository. """Database implementation of child invitation repository.
Works with both SQLite and PostgreSQL databases. Works with both SQLite and PostgreSQL databases.
""" """
@@ -26,10 +26,10 @@ class DatabaseChildInvitationRepository(ChildInvitationRepositoryInterface):
def _get_now_utc(self, reference_dt: datetime) -> datetime: def _get_now_utc(self, reference_dt: datetime) -> datetime:
""" """
Get current UTC time matching the timezone awareness of reference. Get current UTC time matching the timezone awareness of reference.
Args: Args:
reference_dt: A datetime from DB to match timezone format reference_dt: A datetime from DB to match timezone format
Returns: Returns:
Current UTC time (naive for SQLite, aware for PostgreSQL) Current UTC time (naive for SQLite, aware for PostgreSQL)
""" """
@@ -84,9 +84,7 @@ class DatabaseChildInvitationRepository(ChildInvitationRepositoryInterface):
def verify_invitation(self, code: str) -> bool: def verify_invitation(self, code: str) -> bool:
"""Verify if an invitation code is valid and not expired.""" """Verify if an invitation code is valid and not expired."""
invitation = ( invitation = (
self.db.query(ChildInvitation) self.db.query(ChildInvitation).filter(ChildInvitation.code == code).first()
.filter(ChildInvitation.code == code)
.first()
) )
if not invitation: if not invitation:
@@ -16,7 +16,7 @@ from baby_monitor.models.db.child_parent import ChildParent
class DatabaseDiaperChangeRepository(DiaperChangeRepositoryInterface): class DatabaseDiaperChangeRepository(DiaperChangeRepositoryInterface):
"""Database implementation for diaper change log data access. """Database implementation for diaper change log data access.
Works with both SQLite and PostgreSQL databases. Works with both SQLite and PostgreSQL databases.
""" """
@@ -16,7 +16,7 @@ from baby_monitor.models.db.child_parent import ChildParent
class DatabaseFeedingRepository(FeedingRepositoryInterface): class DatabaseFeedingRepository(FeedingRepositoryInterface):
"""Database implementation for feeding log data access. """Database implementation for feeding log data access.
Works with both SQLite and PostgreSQL databases. Works with both SQLite and PostgreSQL databases.
""" """
@@ -19,7 +19,7 @@ from baby_monitor.repositories.interfaces import (
class DatabaseInvitationRepository(InvitationRepositoryInterface): class DatabaseInvitationRepository(InvitationRepositoryInterface):
"""Database implementation for managing invitation tokens. """Database implementation for managing invitation tokens.
Works with both SQLite and PostgreSQL databases. Works with both SQLite and PostgreSQL databases.
""" """
@@ -30,10 +30,10 @@ class DatabaseInvitationRepository(InvitationRepositoryInterface):
def _get_now_utc(self, reference_dt: datetime) -> datetime: def _get_now_utc(self, reference_dt: datetime) -> datetime:
""" """
Get current UTC time matching the timezone awareness of reference. Get current UTC time matching the timezone awareness of reference.
Args: Args:
reference_dt: A datetime from DB to match timezone format reference_dt: A datetime from DB to match timezone format
Returns: Returns:
Current UTC time (naive for SQLite, aware for PostgreSQL) Current UTC time (naive for SQLite, aware for PostgreSQL)
""" """
@@ -82,9 +82,7 @@ class DatabaseInvitationRepository(InvitationRepositoryInterface):
Returns: Returns:
True if valid and not consumed, False otherwise True if valid and not consumed, False otherwise
""" """
invitation = self.db.query(Invitation).filter( invitation = self.db.query(Invitation).filter(Invitation.token == token).first()
Invitation.token == token
).first()
if not invitation: if not invitation:
return False return False
@@ -110,9 +108,7 @@ class DatabaseInvitationRepository(InvitationRepositoryInterface):
Returns: Returns:
True if successfully consumed, False if invalid or already used True if successfully consumed, False if invalid or already used
""" """
invitation = self.db.query(Invitation).filter( invitation = self.db.query(Invitation).filter(Invitation.token == token).first()
Invitation.token == token
).first()
if not invitation: if not invitation:
return False return False
@@ -143,8 +139,6 @@ class DatabaseInvitationRepository(InvitationRepositoryInterface):
else: else:
# No invitations, use naive UTC (safe default) # No invitations, use naive UTC (safe default)
now_utc = datetime.now(UTC).replace(tzinfo=None) now_utc = datetime.now(UTC).replace(tzinfo=None)
self.db.query(Invitation).filter( self.db.query(Invitation).filter(Invitation.expires_at < now_utc).delete()
Invitation.expires_at < now_utc
).delete()
self.db.commit() self.db.commit()
@@ -16,7 +16,7 @@ from baby_monitor.models.db.child_parent import ChildParent
class DatabaseSleepRepository(SleepRepositoryInterface): class DatabaseSleepRepository(SleepRepositoryInterface):
"""Database implementation for sleep log data access. """Database implementation for sleep log data access.
Works with both SQLite and PostgreSQL databases. Works with both SQLite and PostgreSQL databases.
""" """
@@ -11,7 +11,7 @@ from baby_monitor.models.db.user import User
class DatabaseUserRepository(UserRepositoryInterface): class DatabaseUserRepository(UserRepositoryInterface):
"""Database-based user repository. """Database-based user repository.
Works with both SQLite and PostgreSQL databases. Works with both SQLite and PostgreSQL databases.
""" """
+7 -19
View File
@@ -10,7 +10,7 @@ from baby_monitor.models.child import CreateChildRequest, ChildResponse
from baby_monitor.routers.auth import verify_token from baby_monitor.routers.auth import verify_token
from baby_monitor.repositories.interfaces import ( from baby_monitor.repositories.interfaces import (
ChildRepositoryInterface, ChildRepositoryInterface,
ChildInvitationRepositoryInterface ChildInvitationRepositoryInterface,
) )
from baby_monitor.repositories.dependencies import ( from baby_monitor.repositories.dependencies import (
get_child_repository, get_child_repository,
@@ -45,9 +45,7 @@ class RedeemChildInvitationRequest(BaseModel):
def create_child( def create_child(
request: CreateChildRequest, request: CreateChildRequest,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> ChildResponse: ) -> ChildResponse:
"""Create a new child for the authenticated user.""" """Create a new child for the authenticated user."""
child = child_repo.create( child = child_repo.create(
@@ -63,9 +61,7 @@ def create_child(
@router.get("", response_model=list[ChildResponse]) @router.get("", response_model=list[ChildResponse])
def get_user_children( def get_user_children(
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> list[ChildResponse]: ) -> list[ChildResponse]:
"""Get all children for the authenticated user.""" """Get all children for the authenticated user."""
children = child_repo.get_by_user_id(user_id) children = child_repo.get_by_user_id(user_id)
@@ -76,9 +72,7 @@ def get_user_children(
def get_child( def get_child(
child_id: int, child_id: int,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> ChildResponse: ) -> ChildResponse:
"""Get a specific child by ID.""" """Get a specific child by ID."""
child = child_repo.get_by_id(child_id) child = child_repo.get_by_id(child_id)
@@ -99,9 +93,7 @@ def update_child(
child_id: int, child_id: int,
request: CreateChildRequest, request: CreateChildRequest,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> ChildResponse: ) -> ChildResponse:
"""Update a child's information.""" """Update a child's information."""
# First check if child exists and belongs to user # First check if child exists and belongs to user
@@ -132,9 +124,7 @@ def update_child(
def create_child_invitation( def create_child_invitation(
request: CreateChildInvitationRequest, request: CreateChildInvitationRequest,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
invitation_repo: Annotated[ invitation_repo: Annotated[
ChildInvitationRepositoryInterface, ChildInvitationRepositoryInterface,
Depends(get_child_invitation_repository), Depends(get_child_invitation_repository),
@@ -176,9 +166,7 @@ def create_child_invitation(
def redeem_child_invitation( def redeem_child_invitation(
request: RedeemChildInvitationRequest, request: RedeemChildInvitationRequest,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
invitation_repo: Annotated[ invitation_repo: Annotated[
ChildInvitationRepositoryInterface, ChildInvitationRepositoryInterface,
Depends(get_child_invitation_repository), Depends(get_child_invitation_repository),
+4 -12
View File
@@ -29,9 +29,7 @@ def create_diaper_change(
diaper_repo: Annotated[ diaper_repo: Annotated[
DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> DiaperChangeResponse: ) -> DiaperChangeResponse:
"""Create a new diaper change log entry.""" """Create a new diaper change log entry."""
# Verify the child belongs to the authenticated user # Verify the child belongs to the authenticated user
@@ -68,9 +66,7 @@ def get_diaper_change(
diaper_repo: Annotated[ diaper_repo: Annotated[
DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> DiaperChangeResponse: ) -> DiaperChangeResponse:
"""Get a specific diaper change log by ID.""" """Get a specific diaper change log by ID."""
diaper_change = diaper_repo.get_by_id(diaper_change_id) diaper_change = diaper_repo.get_by_id(diaper_change_id)
@@ -92,9 +88,7 @@ def update_diaper_change(
diaper_repo: Annotated[ diaper_repo: Annotated[
DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> DiaperChangeResponse: ) -> DiaperChangeResponse:
"""Update an existing diaper change log.""" """Update an existing diaper change log."""
diaper_change = diaper_repo.get_by_id(diaper_change_id) diaper_change = diaper_repo.get_by_id(diaper_change_id)
@@ -129,9 +123,7 @@ def delete_diaper_change(
diaper_repo: Annotated[ diaper_repo: Annotated[
DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> None: ) -> None:
"""Delete a diaper change log.""" """Delete a diaper change log."""
diaper_change = diaper_repo.get_by_id(diaper_change_id) diaper_change = diaper_repo.get_by_id(diaper_change_id)
+6 -18
View File
@@ -29,9 +29,7 @@ def create_feeding(
feeding_repo: Annotated[ feeding_repo: Annotated[
FeedingRepositoryInterface, Depends(get_feeding_repository) FeedingRepositoryInterface, Depends(get_feeding_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> FeedingResponse: ) -> FeedingResponse:
"""Create a new feeding log entry.""" """Create a new feeding log entry."""
# Verify the child belongs to the authenticated user # Verify the child belongs to the authenticated user
@@ -53,9 +51,7 @@ def get_active_feeding(
feeding_repo: Annotated[ feeding_repo: Annotated[
FeedingRepositoryInterface, Depends(get_feeding_repository) FeedingRepositoryInterface, Depends(get_feeding_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> FeedingResponse | None: ) -> FeedingResponse | None:
"""Get the current active feeding (where end_time is null) for the user.""" """Get the current active feeding (where end_time is null) for the user."""
feedings = feeding_repo.get_by_user_id(user_id) feedings = feeding_repo.get_by_user_id(user_id)
@@ -92,9 +88,7 @@ def get_feeding(
feeding_repo: Annotated[ feeding_repo: Annotated[
FeedingRepositoryInterface, Depends(get_feeding_repository) FeedingRepositoryInterface, Depends(get_feeding_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> FeedingResponse: ) -> FeedingResponse:
"""Get a specific feeding log by ID.""" """Get a specific feeding log by ID."""
feeding = feeding_repo.get_by_id(feeding_id) feeding = feeding_repo.get_by_id(feeding_id)
@@ -116,9 +110,7 @@ def update_feeding(
feeding_repo: Annotated[ feeding_repo: Annotated[
FeedingRepositoryInterface, Depends(get_feeding_repository) FeedingRepositoryInterface, Depends(get_feeding_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> FeedingResponse: ) -> FeedingResponse:
"""Update a feeding log entry.""" """Update a feeding log entry."""
feeding = feeding_repo.get_by_id(feeding_id) feeding = feeding_repo.get_by_id(feeding_id)
@@ -130,9 +122,7 @@ def update_feeding(
verify_child_access(child_repo, feeding["child_id"], user_id) verify_child_access(child_repo, feeding["child_id"], user_id)
# Update the feeding # Update the feeding
feeding_type_value = ( feeding_type_value = request.feeding_type.value if request.feeding_type else None
request.feeding_type.value if request.feeding_type else None
)
updated_feeding = feeding_repo.update( updated_feeding = feeding_repo.update(
feeding_id=feeding_id, feeding_id=feeding_id,
start_time=request.start_time, start_time=request.start_time,
@@ -153,9 +143,7 @@ def delete_feeding(
feeding_repo: Annotated[ feeding_repo: Annotated[
FeedingRepositoryInterface, Depends(get_feeding_repository) FeedingRepositoryInterface, Depends(get_feeding_repository)
], ],
child_repo: Annotated[ child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> None: ) -> None:
"""Delete a feeding log entry.""" """Delete a feeding log entry."""
feeding = feeding_repo.get_by_id(feeding_id) feeding = feeding_repo.get_by_id(feeding_id)
+11 -33
View File
@@ -26,12 +26,8 @@ router = APIRouter(prefix="/api/sleep", tags=["sleep"])
def create_sleep( def create_sleep(
request: CreateSleepRequest, request: CreateSleepRequest,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
sleep_repo: Annotated[ sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)],
SleepRepositoryInterface, Depends(get_sleep_repository) child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
],
child_repo: Annotated[
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> SleepResponse: ) -> SleepResponse:
"""Create a new sleep log entry.""" """Create a new sleep log entry."""
# Verify the child belongs to the authenticated user # Verify the child belongs to the authenticated user
@@ -48,12 +44,8 @@ def create_sleep(
@router.get("/active", response_model=SleepResponse | None) @router.get("/active", response_model=SleepResponse | None)
def get_active_sleep( def get_active_sleep(
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
sleep_repo: Annotated[ sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)],
SleepRepositoryInterface, Depends(get_sleep_repository) child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
],
child_repo: Annotated[
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> SleepResponse | None: ) -> SleepResponse | None:
"""Get the current active sleep (where end_time is null) for the user.""" """Get the current active sleep (where end_time is null) for the user."""
sleeps = sleep_repo.get_by_user_id(user_id) sleeps = sleep_repo.get_by_user_id(user_id)
@@ -74,9 +66,7 @@ def get_active_sleep(
@router.get("", response_model=list[SleepResponse]) @router.get("", response_model=list[SleepResponse])
def get_user_sleeps( def get_user_sleeps(
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
sleep_repo: Annotated[ sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)],
SleepRepositoryInterface, Depends(get_sleep_repository)
],
) -> list[SleepResponse]: ) -> list[SleepResponse]:
"""Get all sleep logs for the authenticated user's children.""" """Get all sleep logs for the authenticated user's children."""
sleeps = sleep_repo.get_by_user_id(user_id) sleeps = sleep_repo.get_by_user_id(user_id)
@@ -87,12 +77,8 @@ def get_user_sleeps(
def get_sleep( def get_sleep(
sleep_id: int, sleep_id: int,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
sleep_repo: Annotated[ sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)],
SleepRepositoryInterface, Depends(get_sleep_repository) child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
],
child_repo: Annotated[
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> SleepResponse: ) -> SleepResponse:
"""Get a specific sleep log by ID.""" """Get a specific sleep log by ID."""
sleep = sleep_repo.get_by_id(sleep_id) sleep = sleep_repo.get_by_id(sleep_id)
@@ -111,12 +97,8 @@ def update_sleep(
sleep_id: int, sleep_id: int,
request: UpdateSleepRequest, request: UpdateSleepRequest,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
sleep_repo: Annotated[ sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)],
SleepRepositoryInterface, Depends(get_sleep_repository) child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
],
child_repo: Annotated[
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> SleepResponse: ) -> SleepResponse:
"""Update an existing sleep log.""" """Update an existing sleep log."""
sleep = sleep_repo.get_by_id(sleep_id) sleep = sleep_repo.get_by_id(sleep_id)
@@ -142,12 +124,8 @@ def update_sleep(
def delete_sleep( def delete_sleep(
sleep_id: int, sleep_id: int,
user_id: Annotated[int, Depends(verify_token)], user_id: Annotated[int, Depends(verify_token)],
sleep_repo: Annotated[ sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)],
SleepRepositoryInterface, Depends(get_sleep_repository) child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
],
child_repo: Annotated[
ChildRepositoryInterface, Depends(get_child_repository)
],
) -> None: ) -> None:
"""Delete a sleep log.""" """Delete a sleep log."""
sleep = sleep_repo.get_by_id(sleep_id) sleep = sleep_repo.get_by_id(sleep_id)
+12 -4
View File
@@ -192,12 +192,20 @@
Cancel Cancel
</button> </button>
</form> </form>
<div id="skipOption" style="text-align: center; margin-top: 20px; display: none;"> <div
<p style="color: #666; font-size: 14px; margin-bottom: 10px;"> id="skipOption"
style="text-align: center; margin-top: 20px; display: none"
>
<p style="color: #666; font-size: 14px; margin-bottom: 10px">
Want to accept a child share invitation first? Want to accept a child share invitation first?
</p> </p>
<button type="button" class="button secondary" onclick="skipToHome()" style="margin: 0;"> <button
type="button"
class="button secondary"
onclick="skipToHome()"
style="margin: 0"
>
Skip for now Skip for now
</button> </button>
</div> </div>
+6 -5
View File
@@ -267,7 +267,8 @@
let allDiaperChanges = []; let allDiaperChanges = [];
let allChildren = []; let allChildren = [];
let currentDaysRange = parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7; let currentDaysRange =
parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
let selectedChildId = null; // null means "All Children" let selectedChildId = null; // null means "All Children"
// Restore last selected child from localStorage // Restore last selected child from localStorage
@@ -640,8 +641,8 @@
// Create 8 three-hour buckets // Create 8 three-hour buckets
for (let i = 7; i >= 0; i--) { for (let i = 7; i >= 0; i--) {
const periodStart = new Date(now); const periodStart = new Date(now);
periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0); periodStart.setHours(periodStart.getHours() - i * 3, 0, 0, 0);
const periodEnd = new Date(periodStart); const periodEnd = new Date(periodStart);
periodEnd.setHours(periodEnd.getHours() + 3); periodEnd.setHours(periodEnd.getHours() + 3);
@@ -675,8 +676,8 @@
// Show 8-hour aggregates for 3 days (9 buckets total) // Show 8-hour aggregates for 3 days (9 buckets total)
for (let i = 8; i >= 0; i--) { for (let i = 8; i >= 0; i--) {
const periodStart = new Date(now); const periodStart = new Date(now);
periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0); periodStart.setHours(periodStart.getHours() - i * 8, 0, 0, 0);
const periodEnd = new Date(periodStart); const periodEnd = new Date(periodStart);
periodEnd.setHours(periodEnd.getHours() + 8); periodEnd.setHours(periodEnd.getHours() + 8);
+53 -43
View File
@@ -304,7 +304,8 @@
let allFeedings = []; let allFeedings = [];
let allChildren = []; let allChildren = [];
let currentDaysRange = parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7; let currentDaysRange =
parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
let selectedChildId = null; // null means "All Children" let selectedChildId = null; // null means "All Children"
let barChartInstance = null; let barChartInstance = null;
@@ -672,15 +673,15 @@
} }
// Only use completed feedings // Only use completed feedings
const completedFeedings = filteredFeedings.filter(f => f.end_time); const completedFeedings = filteredFeedings.filter((f) => f.end_time);
// If 24 hours selected, show 3-hour aggregates // If 24 hours selected, show 3-hour aggregates
if (currentDaysRange === 1) { if (currentDaysRange === 1) {
// Create 8 three-hour buckets // Create 8 three-hour buckets
for (let i = 7; i >= 0; i--) { for (let i = 7; i >= 0; i--) {
const periodStart = new Date(now); const periodStart = new Date(now);
periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0); periodStart.setHours(periodStart.getHours() - i * 3, 0, 0, 0);
const periodEnd = new Date(periodStart); const periodEnd = new Date(periodStart);
periodEnd.setHours(periodEnd.getHours() + 3); periodEnd.setHours(periodEnd.getHours() + 3);
@@ -699,13 +700,13 @@
const feedingDate = new Date(f.start_time); const feedingDate = new Date(f.start_time);
return feedingDate >= periodStart && feedingDate < periodEnd; return feedingDate >= periodStart && feedingDate < periodEnd;
}); });
const feedingDurations = periodFeedings.map(feeding => { const feedingDurations = periodFeedings.map((feeding) => {
const start = new Date(feeding.start_time); const start = new Date(feeding.start_time);
const end = new Date(feeding.end_time); const end = new Date(feeding.end_time);
return (end - start) / 60000; // Convert to minutes return (end - start) / 60000; // Convert to minutes
}); });
durations.push(feedingDurations); durations.push(feedingDurations);
feedingCounts.push(periodFeedings.length); feedingCounts.push(periodFeedings.length);
} }
@@ -713,8 +714,8 @@
// Show 8-hour aggregates for 3 days (9 buckets total) // Show 8-hour aggregates for 3 days (9 buckets total)
for (let i = 8; i >= 0; i--) { for (let i = 8; i >= 0; i--) {
const periodStart = new Date(now); const periodStart = new Date(now);
periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0); periodStart.setHours(periodStart.getHours() - i * 8, 0, 0, 0);
const periodEnd = new Date(periodStart); const periodEnd = new Date(periodStart);
periodEnd.setHours(periodEnd.getHours() + 8); periodEnd.setHours(periodEnd.getHours() + 8);
@@ -739,13 +740,13 @@
const feedingDate = new Date(f.start_time); const feedingDate = new Date(f.start_time);
return feedingDate >= periodStart && feedingDate < periodEnd; return feedingDate >= periodStart && feedingDate < periodEnd;
}); });
const feedingDurations = periodFeedings.map(feeding => { const feedingDurations = periodFeedings.map((feeding) => {
const start = new Date(feeding.start_time); const start = new Date(feeding.start_time);
const end = new Date(feeding.end_time); const end = new Date(feeding.end_time);
return (end - start) / 60000; // Convert to minutes return (end - start) / 60000; // Convert to minutes
}); });
durations.push(feedingDurations); durations.push(feedingDurations);
feedingCounts.push(periodFeedings.length); feedingCounts.push(periodFeedings.length);
} }
@@ -770,13 +771,13 @@
const feedingDate = new Date(f.start_time); const feedingDate = new Date(f.start_time);
return feedingDate >= date && feedingDate < nextDay; return feedingDate >= date && feedingDate < nextDay;
}); });
const feedingDurations = dayFeedings.map(feeding => { const feedingDurations = dayFeedings.map((feeding) => {
const start = new Date(feeding.start_time); const start = new Date(feeding.start_time);
const end = new Date(feeding.end_time); const end = new Date(feeding.end_time);
return (end - start) / 60000; // Convert to minutes return (end - start) / 60000; // Convert to minutes
}); });
durations.push(feedingDurations); durations.push(feedingDurations);
feedingCounts.push(dayFeedings.length); feedingCounts.push(dayFeedings.length);
} }
@@ -794,7 +795,10 @@
} }
// Find the maximum number of feedings in any period // Find the maximum number of feedings in any period
const maxFeedings = Math.max(...chartData.durations.map(d => d.length), 0); const maxFeedings = Math.max(
...chartData.durations.map((d) => d.length),
0,
);
// If no feedings at all, show empty chart // If no feedings at all, show empty chart
if (maxFeedings === 0) { if (maxFeedings === 0) {
@@ -802,11 +806,13 @@
type: "bar", type: "bar",
data: { data: {
labels: chartData.labels, labels: chartData.labels,
datasets: [{ datasets: [
label: "No Data", {
data: new Array(chartData.labels.length).fill(0), label: "No Data",
backgroundColor: "rgba(102, 126, 234, 0.3)", data: new Array(chartData.labels.length).fill(0),
}], backgroundColor: "rgba(102, 126, 234, 0.3)",
},
],
}, },
options: { options: {
responsive: true, responsive: true,
@@ -833,30 +839,32 @@
// Create color palette for different feedings // Create color palette for different feedings
const colors = [ const colors = [
'rgba(102, 126, 234, 0.8)', "rgba(102, 126, 234, 0.8)",
'rgba(118, 75, 162, 0.8)', "rgba(118, 75, 162, 0.8)",
'rgba(255, 159, 64, 0.8)', "rgba(255, 159, 64, 0.8)",
'rgba(76, 175, 80, 0.8)', "rgba(76, 175, 80, 0.8)",
'rgba(244, 67, 54, 0.8)', "rgba(244, 67, 54, 0.8)",
'rgba(156, 39, 176, 0.8)', "rgba(156, 39, 176, 0.8)",
'rgba(102, 126, 234, 0.6)', "rgba(102, 126, 234, 0.6)",
'rgba(118, 75, 162, 0.6)', "rgba(118, 75, 162, 0.6)",
'rgba(255, 159, 64, 0.6)', "rgba(255, 159, 64, 0.6)",
'rgba(76, 175, 80, 0.6)', "rgba(76, 175, 80, 0.6)",
]; ];
// Create datasets - one for each feeding position // Create datasets - one for each feeding position
const datasets = []; const datasets = [];
for (let feedingIndex = 0; feedingIndex < maxFeedings; feedingIndex++) { for (let feedingIndex = 0; feedingIndex < maxFeedings; feedingIndex++) {
const dataForFeeding = chartData.durations.map(periodDurations => const dataForFeeding = chartData.durations.map(
periodDurations[feedingIndex] || 0 (periodDurations) => periodDurations[feedingIndex] || 0,
); );
datasets.push({ datasets.push({
label: `Feeding ${feedingIndex + 1}`, label: `Feeding ${feedingIndex + 1}`,
data: dataForFeeding, data: dataForFeeding,
backgroundColor: colors[feedingIndex % colors.length], backgroundColor: colors[feedingIndex % colors.length],
borderColor: colors[feedingIndex % colors.length].replace('0.8', '1').replace('0.6', '1'), borderColor: colors[feedingIndex % colors.length]
.replace("0.8", "1")
.replace("0.6", "1"),
borderWidth: 1, borderWidth: 1,
}); });
} }
@@ -897,9 +905,9 @@
font: { font: {
size: 12, size: 12,
}, },
callback: function(value) { callback: function (value) {
return Math.round(value) + 'm'; return Math.round(value) + "m";
} },
}, },
grid: { grid: {
color: "rgba(0, 0, 0, 0.05)", color: "rgba(0, 0, 0, 0.05)",
@@ -920,17 +928,19 @@
size: 13, size: 13,
}, },
callbacks: { callbacks: {
label: function(context) { label: function (context) {
const minutes = Math.round(context.parsed.y); const minutes = Math.round(context.parsed.y);
return `${context.dataset.label}: ${minutes}m`; return `${context.dataset.label}: ${minutes}m`;
}, },
footer: function(tooltipItems) { footer: function (tooltipItems) {
const periodIndex = tooltipItems[0].dataIndex; const periodIndex = tooltipItems[0].dataIndex;
const totalMinutes = chartData.durations[periodIndex].reduce((sum, val) => sum + val, 0); const totalMinutes = chartData.durations[
periodIndex
].reduce((sum, val) => sum + val, 0);
const feedingCount = chartData.feedingCounts[periodIndex]; const feedingCount = chartData.feedingCounts[periodIndex];
return `Total: ${Math.round(totalMinutes)}m (${feedingCount} feeding${feedingCount !== 1 ? 's' : ''})`; return `Total: ${Math.round(totalMinutes)}m (${feedingCount} feeding${feedingCount !== 1 ? "s" : ""})`;
} },
} },
}, },
}, },
}, },
+112 -83
View File
@@ -547,15 +547,15 @@
const sessionCounts = []; // For reference const sessionCounts = []; // For reference
// Only use completed sleep sessions // Only use completed sleep sessions
const completedSessions = sleepSessions.filter(s => s.end_time); const completedSessions = sleepSessions.filter((s) => s.end_time);
// If 24 hours selected, show 3-hour aggregates // If 24 hours selected, show 3-hour aggregates
if (timeRange === 1) { if (timeRange === 1) {
// Create 8 three-hour buckets // Create 8 three-hour buckets
for (let i = 7; i >= 0; i--) { for (let i = 7; i >= 0; i--) {
const periodStart = new Date(now); const periodStart = new Date(now);
periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0); periodStart.setHours(periodStart.getHours() - i * 3, 0, 0, 0);
const periodEnd = new Date(periodStart); const periodEnd = new Date(periodStart);
periodEnd.setHours(periodEnd.getHours() + 3); periodEnd.setHours(periodEnd.getHours() + 3);
@@ -574,11 +574,12 @@
const startDate = new Date(session.start_time); const startDate = new Date(session.start_time);
return startDate >= periodStart && startDate < periodEnd; return startDate >= periodStart && startDate < periodEnd;
}); });
const sessionDurations = periodSessions.map(session => const sessionDurations = periodSessions.map(
calculateDuration(session.start_time, session.end_time) / 60 (session) =>
calculateDuration(session.start_time, session.end_time) / 60,
); );
durations.push(sessionDurations); durations.push(sessionDurations);
sessionCounts.push(periodSessions.length); sessionCounts.push(periodSessions.length);
} }
@@ -586,8 +587,8 @@
// Show 8-hour aggregates for 3 days (9 buckets total) // Show 8-hour aggregates for 3 days (9 buckets total)
for (let i = 8; i >= 0; i--) { for (let i = 8; i >= 0; i--) {
const periodStart = new Date(now); const periodStart = new Date(now);
periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0); periodStart.setHours(periodStart.getHours() - i * 8, 0, 0, 0);
const periodEnd = new Date(periodStart); const periodEnd = new Date(periodStart);
periodEnd.setHours(periodEnd.getHours() + 8); periodEnd.setHours(periodEnd.getHours() + 8);
@@ -612,11 +613,12 @@
const startDate = new Date(session.start_time); const startDate = new Date(session.start_time);
return startDate >= periodStart && startDate < periodEnd; return startDate >= periodStart && startDate < periodEnd;
}); });
const sessionDurations = periodSessions.map(session => const sessionDurations = periodSessions.map(
calculateDuration(session.start_time, session.end_time) / 60 (session) =>
calculateDuration(session.start_time, session.end_time) / 60,
); );
durations.push(sessionDurations); durations.push(sessionDurations);
sessionCounts.push(periodSessions.length); sessionCounts.push(periodSessions.length);
} }
@@ -641,29 +643,39 @@
const startDate = new Date(session.start_time); const startDate = new Date(session.start_time);
return startDate >= date && startDate < nextDay; return startDate >= date && startDate < nextDay;
}); });
const sessionDurations = daySessions.map(session => { const sessionDurations = daySessions.map((session) => {
const dur = calculateDuration(session.start_time, session.end_time) / 60; const dur =
console.log(` Session: ${session.start_time} to ${session.end_time}, duration: ${dur} hours (${calculateDuration(session.start_time, session.end_time)} minutes)`); calculateDuration(session.start_time, session.end_time) / 60;
console.log(
` Session: ${session.start_time} to ${session.end_time}, duration: ${dur} hours (${calculateDuration(session.start_time, session.end_time)} minutes)`,
);
return dur; return dur;
}); });
console.log(`${dateStr}: ${daySessions.length} sessions, durations:`, sessionDurations); console.log(
`${dateStr}: ${daySessions.length} sessions, durations:`,
sessionDurations,
);
durations.push(sessionDurations); durations.push(sessionDurations);
sessionCounts.push(daySessions.length); sessionCounts.push(daySessions.length);
} }
} }
console.log('Final prepareBarChartData result:', { labels, durations, sessionCounts }); console.log("Final prepareBarChartData result:", {
labels,
durations,
sessionCounts,
});
// Log the actual duration values for debugging // Log the actual duration values for debugging
durations.forEach((periodDurations, idx) => { durations.forEach((periodDurations, idx) => {
if (periodDurations.length > 0) { if (periodDurations.length > 0) {
console.log(` ${labels[idx]}: durations =`, periodDurations); console.log(` ${labels[idx]}: durations =`, periodDurations);
} }
}); });
return { labels, durations, sessionCounts }; return { labels, durations, sessionCounts };
} }
@@ -699,31 +711,36 @@
function renderBarChart(chartData) { function renderBarChart(chartData) {
const ctx = document.getElementById("barChart"); const ctx = document.getElementById("barChart");
// Destroy existing chart if it exists // Destroy existing chart if it exists
if (barChartInstance) { if (barChartInstance) {
barChartInstance.destroy(); barChartInstance.destroy();
} }
// Debug: Log the data // Debug: Log the data
console.log('Bar chart data:', chartData); console.log("Bar chart data:", chartData);
// Find the maximum number of sessions in any period // Find the maximum number of sessions in any period
const maxSessions = Math.max(...chartData.durations.map(d => d.length), 0); const maxSessions = Math.max(
...chartData.durations.map((d) => d.length),
console.log('Max sessions:', maxSessions); 0,
);
console.log("Max sessions:", maxSessions);
// If no sessions at all, show empty chart // If no sessions at all, show empty chart
if (maxSessions === 0) { if (maxSessions === 0) {
barChartInstance = new Chart(ctx, { barChartInstance = new Chart(ctx, {
type: "bar", type: "bar",
data: { data: {
labels: chartData.labels, labels: chartData.labels,
datasets: [{ datasets: [
label: "No Data", {
data: new Array(chartData.labels.length).fill(0), label: "No Data",
backgroundColor: "rgba(79, 172, 254, 0.3)", data: new Array(chartData.labels.length).fill(0),
}], backgroundColor: "rgba(79, 172, 254, 0.3)",
},
],
}, },
options: { options: {
responsive: true, responsive: true,
@@ -747,57 +764,64 @@
}); });
return; return;
} }
// Create color palette for different sessions // Create color palette for different sessions
const colors = [ const colors = [
'rgba(79, 172, 254, 0.8)', "rgba(79, 172, 254, 0.8)",
'rgba(0, 242, 254, 0.8)', "rgba(0, 242, 254, 0.8)",
'rgba(58, 155, 232, 0.8)', "rgba(58, 155, 232, 0.8)",
'rgba(32, 137, 220, 0.8)', "rgba(32, 137, 220, 0.8)",
'rgba(21, 119, 208, 0.8)', "rgba(21, 119, 208, 0.8)",
'rgba(11, 101, 196, 0.8)', "rgba(11, 101, 196, 0.8)",
'rgba(79, 172, 254, 0.6)', "rgba(79, 172, 254, 0.6)",
'rgba(0, 242, 254, 0.6)', "rgba(0, 242, 254, 0.6)",
'rgba(58, 155, 232, 0.6)', "rgba(58, 155, 232, 0.6)",
'rgba(32, 137, 220, 0.6)', "rgba(32, 137, 220, 0.6)",
]; ];
// Create datasets - one for each session position // Create datasets - one for each session position
const datasets = []; const datasets = [];
for (let sessionIndex = 0; sessionIndex < maxSessions; sessionIndex++) { for (let sessionIndex = 0; sessionIndex < maxSessions; sessionIndex++) {
const dataForSession = chartData.durations.map(periodDurations => const dataForSession = chartData.durations.map(
periodDurations[sessionIndex] || 0 (periodDurations) => periodDurations[sessionIndex] || 0,
); );
console.log(`Session ${sessionIndex + 1} data:`, dataForSession); console.log(`Session ${sessionIndex + 1} data:`, dataForSession);
datasets.push({ datasets.push({
label: `Session ${sessionIndex + 1}`, label: `Session ${sessionIndex + 1}`,
data: dataForSession, data: dataForSession,
backgroundColor: colors[sessionIndex % colors.length], backgroundColor: colors[sessionIndex % colors.length],
borderColor: colors[sessionIndex % colors.length].replace('0.8', '1').replace('0.6', '1'), borderColor: colors[sessionIndex % colors.length]
.replace("0.8", "1")
.replace("0.6", "1"),
borderWidth: 1, borderWidth: 1,
}); });
} }
console.log('Datasets:', datasets); console.log("Datasets:", datasets);
// Determine the maximum total duration across all periods (in hours) // Determine the maximum total duration across all periods (in hours)
const maxDuration = Math.max(...chartData.durations.map(periodDurations => const maxDuration = Math.max(
periodDurations.reduce((sum, val) => sum + val, 0) ...chartData.durations.map((periodDurations) =>
), 0); periodDurations.reduce((sum, val) => sum + val, 0),
),
console.log('Max duration (hours):', maxDuration); 0,
);
console.log("Max duration (hours):", maxDuration);
// Decide whether to use minutes or hours based on max duration // Decide whether to use minutes or hours based on max duration
const useMinutes = maxDuration < 2; // Use minutes if max is less than 2 hours const useMinutes = maxDuration < 2; // Use minutes if max is less than 2 hours
// Convert data to minutes if needed // Convert data to minutes if needed
const finalDatasets = useMinutes ? datasets.map(dataset => ({ const finalDatasets = useMinutes
...dataset, ? datasets.map((dataset) => ({
data: dataset.data.map(hours => hours * 60) // Convert hours to minutes ...dataset,
})) : datasets; data: dataset.data.map((hours) => hours * 60), // Convert hours to minutes
}))
: datasets;
barChartInstance = new Chart(ctx, { barChartInstance = new Chart(ctx, {
type: "bar", type: "bar",
data: { data: {
@@ -819,13 +843,13 @@
text: useMinutes ? "Minutes" : "Hours", text: useMinutes ? "Minutes" : "Hours",
}, },
ticks: { ticks: {
callback: function(value) { callback: function (value) {
if (useMinutes) { if (useMinutes) {
return Math.round(value) + 'm'; return Math.round(value) + "m";
} else { } else {
return value.toFixed(1) + 'h'; return value.toFixed(1) + "h";
} }
} },
}, },
}, },
}, },
@@ -835,8 +859,10 @@
}, },
tooltip: { tooltip: {
callbacks: { callbacks: {
label: function(context) { label: function (context) {
const value = useMinutes ? context.parsed.y / 60 : context.parsed.y; const value = useMinutes
? context.parsed.y / 60
: context.parsed.y;
const hours = Math.floor(value); const hours = Math.floor(value);
const minutes = Math.round((value - hours) * 60); const minutes = Math.round((value - hours) * 60);
if (hours === 0) { if (hours === 0) {
@@ -844,18 +870,21 @@
} }
return `${context.dataset.label}: ${hours}h ${minutes}m`; return `${context.dataset.label}: ${hours}h ${minutes}m`;
}, },
footer: function(tooltipItems) { footer: function (tooltipItems) {
const periodIndex = tooltipItems[0].dataIndex; const periodIndex = tooltipItems[0].dataIndex;
const totalHours = chartData.durations[periodIndex].reduce((sum, val) => sum + val, 0); const totalHours = chartData.durations[periodIndex].reduce(
(sum, val) => sum + val,
0,
);
const hours = Math.floor(totalHours); const hours = Math.floor(totalHours);
const minutes = Math.round((totalHours - hours) * 60); const minutes = Math.round((totalHours - hours) * 60);
const sessionCount = chartData.sessionCounts[periodIndex]; const sessionCount = chartData.sessionCounts[periodIndex];
if (hours === 0) { if (hours === 0) {
return `Total: ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`; return `Total: ${minutes}m (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`;
} }
return `Total: ${hours}h ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`; return `Total: ${hours}h ${minutes}m (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`;
} },
} },
}, },
}, },
}, },
@@ -864,12 +893,12 @@
function renderDurationChart(chartData) { function renderDurationChart(chartData) {
const ctx = document.getElementById("durationChart"); const ctx = document.getElementById("durationChart");
// Destroy existing chart if it exists // Destroy existing chart if it exists
if (durationChartInstance) { if (durationChartInstance) {
durationChartInstance.destroy(); durationChartInstance.destroy();
} }
durationChartInstance = new Chart(ctx, { durationChartInstance = new Chart(ctx, {
type: "pie", type: "pie",
data: { data: {
@@ -905,7 +934,7 @@
if (avgDurationChartInstance) { if (avgDurationChartInstance) {
avgDurationChartInstance.destroy(); avgDurationChartInstance.destroy();
} }
const days = {}; const days = {};
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
+1
View File
@@ -1,4 +1,5 @@
"""Utility functions for the baby monitor application.""" """Utility functions for the baby monitor application."""
from .hash_password import hash_password from .hash_password import hash_password
from .verify_child_access import verify_child_access from .verify_child_access import verify_child_access
from .verify_password import verify_password from .verify_password import verify_password