Author SHA1 Message Date
Brian Bjarke Jensen a966d0bc8e cleared logs on login, to avoid stale data
Build and Push Docker Image / build-and-push (pull_request) Successful in 59s
Python Code Quality / python-code-quality (pull_request) Successful in 11s
Python Test / python-test (pull_request) Successful in 18s
2025-11-13 22:33:24 +01:00
Brian Bjarke Jensen 753339c28a added check to get user token before loading data 2025-11-13 19:31:36 +01:00
26 changed files with 925 additions and 2081 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ services:
interval: 10s
timeout: 5s
retries: 5
start_period: 3s
start_period: 1s
postgres:
image: postgres:14-alpine
environment:
+1 -1
View File
@@ -41,7 +41,7 @@ dev = [
[[tool.uv.index]]
name = "threadripper-proxpi-cache"
url = "http://10.184.20.2:5001/index/"
url = "http://10.0.0.2:5001/index/"
default = true
[tool.mypy]
+4 -16
View File
@@ -143,22 +143,10 @@ def serve_menu_js() -> FileResponse:
return FileResponse(static_path / "menu.js")
@app.get("/dark-mode.css", include_in_schema=False)
def serve_dark_mode_css() -> FileResponse:
"""Serve the dark mode CSS."""
return FileResponse(static_path / "dark-mode.css")
@app.get("/theme-init.js", include_in_schema=False)
def serve_theme_init_js() -> FileResponse:
"""Serve the theme initialization JavaScript."""
return FileResponse(static_path / "theme-init.js")
@app.get("/chart-theme.js", include_in_schema=False)
def serve_chart_theme_js() -> FileResponse:
"""Serve the chart theming JavaScript."""
return FileResponse(static_path / "chart-theme.js")
@app.get("/auth-utils.js", include_in_schema=False)
def serve_auth_utils_js() -> FileResponse:
"""Serve the shared auth utilities JavaScript."""
return FileResponse(static_path / "auth-utils.js")
@app.get("/api/")
-1
View File
@@ -15,7 +15,6 @@ class CreateSleepRequest(BaseModel):
class UpdateSleepRequest(BaseModel):
"""Request model for updating a sleep log."""
start_time: datetime | None = None
end_time: datetime | None = None
@@ -32,7 +32,6 @@ class SleepRepositoryInterface(ABC):
def update(
self,
sleep_id: int,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> dict | None:
"""Update a sleep log entry."""
@@ -73,7 +73,6 @@ class DatabaseSleepRepository(SleepRepositoryInterface):
def update(
self,
sleep_id: int,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> dict | None:
"""Update a sleep log entry."""
@@ -81,8 +80,6 @@ class DatabaseSleepRepository(SleepRepositoryInterface):
if not sleep:
return None
if start_time is not None:
sleep.start_time = start_time # type: ignore[assignment]
if end_time is not None:
sleep.end_time = end_time # type: ignore[assignment]
+7 -50
View File
@@ -1,7 +1,6 @@
"""Health check router for monitoring endpoints."""
import os
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter
router = APIRouter(tags=["health"])
@@ -21,53 +20,11 @@ def readiness_check() -> dict[str, str]:
"""
Readiness check endpoint for container orchestration.
Returns 200 if the service is ready to accept traffic, else 503.
Returns 200 if the service is ready to accept traffic.
This can include checks for database connectivity, external services, etc.
"""
# Determine if configured to use Redis
redis_uri = os.getenv("REDIS_URI")
# If Redis is used, check connectivity
if redis_uri:
try:
import redis
redis_client = redis.from_url(redis_uri)
redis_client.ping()
except Exception as exc:
raise HTTPException(
status_code=503,
detail={
"status": "not ready",
"reason": "No connection to Redis",
"exception": str(exc),
},
) from exc
# Determine if configured to use PostgreSQL
postgres_uri = os.getenv("POSTGRES_URI")
# If PostgreSQL is used, check connectivity
if postgres_uri:
try:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
engine = create_engine(postgres_uri)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
with SessionLocal() as db:
db.execute(text("SELECT 1"))
except Exception as exc:
raise HTTPException(
status_code=503,
detail={
"status": "not ready",
"reason": "No connection to PostgreSQL",
"exception": str(exc),
},
) from exc
# Unable to prove not ready, so assume ready
# Add dependency checks here if needed (database, cache, etc.)
# For now, simple readiness check
return {"status": "ready"}
@@ -103,9 +60,9 @@ def service_info() -> dict:
# Check current configuration
config = {
"environment": os.getenv("ENVIRONMENT", "production"),
"data_dir": os.getenv("DATA_DIR", "/data"),
"redis_configured": bool(os.getenv("REDIS_URI")),
}
if not features["postgresql"]:
config["data_dir"] = os.getenv("DATA_DIR", "/data")
return {
"service": "baby-monitor",
-1
View File
@@ -112,7 +112,6 @@ def update_sleep(
updated = sleep_repo.update(
sleep_id=sleep_id,
start_time=request.start_time,
end_time=request.end_time,
)
+10 -17
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Add Child - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<style>
* {
margin: 0;
@@ -17,7 +15,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
@@ -35,13 +33,13 @@
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
@@ -53,7 +51,7 @@
label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
color: #333;
font-weight: 600;
font-size: 14px;
}
@@ -61,22 +59,19 @@
input {
width: 100%;
padding: 12px;
border: 2px solid #ccc;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
background-color: var(--input-bg);
color: var(--input-text);
}
input:focus {
outline: none;
border-color: var(--input-focus-border);
border-color: #667eea;
}
.button {
background: var(--gradient-purple);
background-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -140,7 +135,7 @@
.help-text {
font-size: 12px;
color: var(--text-secondary);
color: #666;
margin-top: 4px;
}
</style>
@@ -216,12 +211,10 @@
</div>
</div>
<script src="/auth-utils.js"></script>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
// Get child ID from URL if editing
const urlParams = new URLSearchParams(window.location.search);
+19 -24
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor - Admin</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<style>
* {
margin: 0;
@@ -17,7 +15,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
@@ -35,13 +33,13 @@
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
@@ -64,8 +62,7 @@
}
.button {
background: var(--gradient-purple);
background-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -98,7 +95,7 @@
display: none;
margin-top: 20px;
padding: 15px;
background: var(--bg-tertiary);
background: #f8f9fa;
border-radius: 8px;
border: 2px solid #667eea;
}
@@ -109,7 +106,7 @@
.link-label {
font-size: 12px;
color: var(--text-secondary);
color: #666;
margin-bottom: 8px;
font-weight: 600;
text-transform: uppercase;
@@ -124,13 +121,13 @@
.link-text {
flex: 1;
padding: 10px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
background: white;
border: 1px solid #ddd;
border-radius: 4px;
font-family: "Courier New", monospace;
font-size: 13px;
word-break: break-all;
color: var(--text-primary);
color: #333;
}
.copy-button {
@@ -196,7 +193,7 @@
.search-box {
width: 100%;
padding: 10px;
border: 1px solid var(--border-color);
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
margin-bottom: 15px;
@@ -205,7 +202,7 @@
.user-list {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border-color);
border: 1px solid #ddd;
border-radius: 4px;
}
@@ -214,8 +211,8 @@
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid var(--border-lighter);
background: var(--bg-secondary);
border-bottom: 1px solid #eee;
background: white;
}
.user-item:last-child {
@@ -223,7 +220,7 @@
}
.user-item:hover {
background: var(--bg-tertiary);
background: #f8f9fa;
}
.user-info {
@@ -232,13 +229,13 @@
.user-name {
font-weight: 600;
color: var(--text-primary);
color: #333;
margin-bottom: 4px;
}
.user-meta {
font-size: 12px;
color: var(--text-secondary);
color: #666;
}
.admin-badge {
@@ -427,17 +424,15 @@
</div>
</div>
<script src="/auth-utils.js"></script>
<script>
// Check if user is authenticated and is admin
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
let allUsers = [];
let userToDelete = null;
// Verify user is admin and load data
// Load data
fetch("/api/me", {
headers: {
Authorization: `Bearer ${token}`,
+21
View File
@@ -0,0 +1,21 @@
/**
* Redirect to login page with return URL
* @param {string} returnUrl - The URL to return to after login (defaults to current page)
*/
function redirectToLogin(returnUrl) {
const url = returnUrl || window.location.pathname + window.location.search;
window.location.href = `/login.html?return=${encodeURIComponent(url)}`;
}
/**
* Check if user has valid token, redirect to login if not
* @returns {string|null} - Returns token if valid, redirects to login if not
*/
function getToken() {
const token = localStorage.getItem("access_token");
if (!token) {
redirectToLogin();
return null;
}
return token;
}
-207
View File
@@ -1,207 +0,0 @@
/* Chart.js Dark Mode Helper
* Provides theme-aware color palettes and chart options for Chart.js
* Usage: Include this before your chart initialization code
*/
(function () {
"use strict";
// Helper to get effective theme
function getEffectiveTheme() {
return window.getEffectiveTheme ? window.getEffectiveTheme() : "light";
}
// Get theme-appropriate colors for Chart.js
window.getChartColors = function () {
const isDark = getEffectiveTheme() === "dark";
return {
// Grid lines
grid: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
gridMajor: isDark ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.1)",
// Text colors
text: isDark ? "#b0b0b0" : "#666666",
textPrimary: isDark ? "#e0e0e0" : "#333333",
// Data series colors (optimized for background)
series: isDark
? [
"rgba(139, 158, 255, 0.8)", // Brighter purple
"rgba(110, 197, 255, 0.8)", // Brighter blue
"rgba(255, 179, 255, 0.8)", // Brighter pink
"rgba(129, 199, 132, 0.8)", // Brighter green
"rgba(255, 183, 77, 0.8)", // Brighter orange
"rgba(156, 107, 184, 0.8)", // Brighter deep purple
"rgba(77, 255, 255, 0.8)", // Brighter cyan
"rgba(255, 138, 155, 0.8)", // Brighter red
]
: [
"rgba(102, 126, 234, 0.8)", // Purple
"rgba(118, 75, 162, 0.8)", // Deep purple
"rgba(255, 159, 64, 0.8)", // Orange
"rgba(76, 175, 80, 0.8)", // Green
"rgba(244, 67, 54, 0.8)", // Red
"rgba(156, 39, 176, 0.8)", // Violet
"rgba(102, 126, 234, 0.6)", // Light purple
"rgba(118, 75, 162, 0.6)", // Light deep purple
],
// Solid data colors (for pie charts, etc.)
solidSeries: isDark
? [
"#8b9eff",
"#6ec5ff",
"#ffb3ff",
"#81c784",
"#ffb74d",
"#9c6bb8",
"#4dffff",
"#ff8a9b",
]
: [
"#667eea",
"#764ba2",
"#ff9f40",
"#4caf50",
"#f44336",
"#9c27b0",
"#667eea",
"#764ba2",
],
// Background colors
background: isDark ? "#2d2d2d" : "#f8f9fa",
};
};
// Get default Chart.js options with theme support
window.getChartOptions = function (customOptions = {}) {
const colors = window.getChartColors();
const defaults = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: colors.text,
font: {
size: 12,
},
},
},
tooltip: {
backgroundColor:
getEffectiveTheme() === "dark" ? "#3a3a3a" : "#ffffff",
titleColor: colors.textPrimary,
bodyColor: colors.text,
borderColor: colors.grid,
borderWidth: 1,
},
},
scales: {},
};
// Deep merge customOptions with defaults
return mergeDeep(defaults, customOptions);
};
// Get theme-appropriate scale options
window.getChartScaleOptions = function (axis = "x", customOptions = {}) {
const colors = window.getChartColors();
const defaults = {
grid: {
color: colors.grid,
borderColor: colors.gridMajor,
},
ticks: {
color: colors.text,
font: {
size: 12,
},
},
title: {
color: colors.textPrimary,
font: {
size: 13,
weight: "600",
},
},
};
return mergeDeep(defaults, customOptions);
};
// Helper function to deep merge objects
function mergeDeep(target, source) {
const output = Object.assign({}, target);
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach((key) => {
if (isObject(source[key])) {
if (!(key in target)) {
Object.assign(output, { [key]: source[key] });
} else {
output[key] = mergeDeep(target[key], source[key]);
}
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}
function isObject(item) {
return item && typeof item === "object" && !Array.isArray(item);
}
// Function to update all charts on theme change
window.updateChartsForTheme = function (chartInstances) {
if (!Array.isArray(chartInstances)) {
chartInstances = [chartInstances];
}
const colors = window.getChartColors();
chartInstances.forEach((chart) => {
if (!chart) return;
// Update grid colors
if (chart.options.scales) {
Object.keys(chart.options.scales).forEach((axis) => {
if (chart.options.scales[axis].grid) {
chart.options.scales[axis].grid.color = colors.grid;
chart.options.scales[axis].grid.borderColor = colors.gridMajor;
}
if (chart.options.scales[axis].ticks) {
chart.options.scales[axis].ticks.color = colors.text;
}
if (chart.options.scales[axis].title) {
chart.options.scales[axis].title.color = colors.textPrimary;
}
});
}
// Update legend colors
if (chart.options.plugins && chart.options.plugins.legend) {
if (chart.options.plugins.legend.labels) {
chart.options.plugins.legend.labels.color = colors.text;
}
}
// Update tooltip colors
if (chart.options.plugins && chart.options.plugins.tooltip) {
chart.options.plugins.tooltip.backgroundColor =
getEffectiveTheme() === "dark" ? "#3a3a3a" : "#ffffff";
chart.options.plugins.tooltip.titleColor = colors.textPrimary;
chart.options.plugins.tooltip.bodyColor = colors.text;
chart.options.plugins.tooltip.borderColor = colors.grid;
}
// Update the chart
chart.update();
});
};
})();
-258
View File
@@ -1,258 +0,0 @@
/* Dark Mode CSS Variables and Theming System
* Supports automatic detection via prefers-color-scheme and manual override via .dark-mode class
*/
:root {
/* ===== LIGHT MODE (Default) ===== */
/* Background colors */
--bg-primary: #f0f0f0;
--bg-secondary: #ffffff;
--bg-tertiary: #f8f9fa;
--bg-card: #ffffff;
/* Text colors */
--text-primary: #333333;
--text-secondary: #666666;
--text-muted: #999999;
--text-inverse: #ffffff;
/* Border colors */
--border-color: #dddddd;
--border-light: #e0e0e0;
--border-lighter: #eeeeee;
/* Gradients - Brand */
--gradient-purple: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-blue: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--gradient-pink: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--gradient-green: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--gradient-orange: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%);
/* Action colors */
--color-success: #4caf50;
--color-error: #dc3545;
--color-warning: #ff9800;
--color-info: #2196f3;
--color-danger: #f44336;
/* Action gradients */
--gradient-success: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--gradient-error: linear-gradient(135deg, #dc3545 0%, #c62828 100%);
--gradient-danger: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
/* Interactive elements */
--input-bg: #ffffff;
--input-border: #dddddd;
--input-focus-border: #667eea;
--input-text: #333333;
--input-placeholder: #999999;
/* Shadows */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 2px 10px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.15);
--shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.2);
/* Modal/Overlay */
--overlay-bg: rgba(0, 0, 0, 0.5);
/* Chart colors (vibrant for light backgrounds) */
--chart-grid: rgba(0, 0, 0, 0.1);
--chart-text: #666666;
--chart-color-1: #667eea;
--chart-color-2: #4facfe;
--chart-color-3: #f093fb;
--chart-color-4: #56ab2f;
--chart-color-5: #f2994a;
--chart-color-6: #764ba2;
--chart-color-7: #00f2fe;
--chart-color-8: #f5576c;
}
/* ===== DARK MODE ===== */
/* Applied via @media query (auto-detect) OR .dark-mode class (manual override) */
@media (prefers-color-scheme: dark) {
:root:not(.light-mode) {
/* Background colors - Dark theme */
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3a3a3a;
--bg-card: #2d2d2d;
/* Text colors - Light on dark */
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-muted: #808080;
--text-inverse: #1a1a1a;
/* Border colors - Lighter for visibility */
--border-color: #444444;
--border-light: #4a4a4a;
--border-lighter: #505050;
/* Gradients - Dimmed (~50% brightness reduction) */
--gradient-purple: linear-gradient(135deg, #3d4d8c 0%, #452d61 100%);
--gradient-blue: linear-gradient(135deg, #2f667f 0%, #00919a 100%);
--gradient-pink: linear-gradient(135deg, #905997 0%, #924344 100%);
--gradient-green: linear-gradient(135deg, #33661c 0%, #64873c 100%);
--gradient-orange: linear-gradient(135deg, #915b2d 0%, #917a2e 100%);
/* Action colors - Slightly brighter for visibility */
--color-success: #66bb6a;
--color-error: #ef5350;
--color-warning: #ffa726;
--color-info: #42a5f5;
--color-danger: #ff5252;
/* Action gradients - Dimmed */
--gradient-success: linear-gradient(135deg, #33661c 0%, #64873c 100%);
--gradient-error: linear-gradient(135deg, #a32a2a 0%, #8b2020 100%);
--gradient-danger: linear-gradient(135deg, #a32929 0%, #8b1e1e 100%);
/* Interactive elements */
--input-bg: #2d2d2d;
--input-border: #444444;
--input-focus-border: #667eea;
--input-text: #e0e0e0;
--input-placeholder: #808080;
/* Shadows - Lighter/softer for dark mode */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-md: 0 2px 10px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.5);
--shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.6);
/* Modal/Overlay - Same opacity */
--overlay-bg: rgba(0, 0, 0, 0.5);
/* Chart colors - Brighter and more saturated for dark backgrounds */
--chart-grid: rgba(255, 255, 255, 0.1);
--chart-text: #b0b0b0;
--chart-color-1: #8b9eff;
--chart-color-2: #6ec5ff;
--chart-color-3: #ffb3ff;
--chart-color-4: #81c784;
--chart-color-5: #ffb74d;
--chart-color-6: #9c6bb8;
--chart-color-7: #4dffff;
--chart-color-8: #ff8a9b;
}
}
/* Manual override with .dark-mode class */
body.dark-mode {
/* Background colors - Dark theme */
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3a3a3a;
--bg-card: #2d2d2d;
/* Text colors - Light on dark */
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-muted: #808080;
--text-inverse: #1a1a1a;
/* Border colors - Lighter for visibility */
--border-color: #444444;
--border-light: #4a4a4a;
--border-lighter: #505050;
/* Gradients - Dimmed (~50% brightness reduction) */
--gradient-purple: linear-gradient(135deg, #3d4d8c 0%, #452d61 100%);
--gradient-blue: linear-gradient(135deg, #2f667f 0%, #00919a 100%);
--gradient-pink: linear-gradient(135deg, #905997 0%, #924344 100%);
--gradient-green: linear-gradient(135deg, #33661c 0%, #64873c 100%);
--gradient-orange: linear-gradient(135deg, #915b2d 0%, #917a2e 100%);
/* Action colors - Slightly brighter for visibility */
--color-success: #66bb6a;
--color-error: #ef5350;
--color-warning: #ffa726;
--color-info: #42a5f5;
--color-danger: #ff5252;
/* Action gradients - Dimmed */
--gradient-success: linear-gradient(135deg, #33661c 0%, #64873c 100%);
--gradient-error: linear-gradient(135deg, #a32a2a 0%, #8b2020 100%);
--gradient-danger: linear-gradient(135deg, #a32929 0%, #8b1e1e 100%);
/* Interactive elements */
--input-bg: #2d2d2d;
--input-border: #444444;
--input-focus-border: #667eea;
--input-text: #e0e0e0;
--input-placeholder: #808080;
/* Shadows - Lighter/softer for dark mode */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-md: 0 2px 10px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.5);
--shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.6);
/* Modal/Overlay - Same opacity */
--overlay-bg: rgba(0, 0, 0, 0.5);
/* Chart colors - Brighter and more saturated for dark backgrounds */
--chart-grid: rgba(255, 255, 255, 0.1);
--chart-text: #b0b0b0;
--chart-color-1: #8b9eff;
--chart-color-2: #6ec5ff;
--chart-color-3: #ffb3ff;
--chart-color-4: #81c784;
--chart-color-5: #ffb74d;
--chart-color-6: #9c6bb8;
--chart-color-7: #4dffff;
--chart-color-8: #ff8a9b;
}
/* Manual override with .light-mode class (forces light even when browser prefers dark) */
body.light-mode {
/* Force light mode values - same as root defaults */
--bg-primary: #f0f0f0;
--bg-secondary: #ffffff;
--bg-tertiary: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--text-muted: #999999;
--text-inverse: #ffffff;
--border-color: #dddddd;
--border-light: #e0e0e0;
--border-lighter: #eeeeee;
--gradient-purple: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-blue: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--gradient-pink: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--gradient-green: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--gradient-orange: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%);
--color-success: #4caf50;
--color-error: #dc3545;
--color-warning: #ff9800;
--color-info: #2196f3;
--color-danger: #f44336;
--gradient-success: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--gradient-error: linear-gradient(135deg, #dc3545 0%, #c62828 100%);
--gradient-danger: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
--input-bg: #ffffff;
--input-border: #dddddd;
--input-focus-border: #667eea;
--input-text: #333333;
--input-placeholder: #999999;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 2px 10px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.15);
--shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.2);
--overlay-bg: rgba(0, 0, 0, 0.5);
--chart-grid: rgba(0, 0, 0, 0.1);
--chart-text: #666666;
--chart-color-1: #667eea;
--chart-color-2: #4facfe;
--chart-color-3: #f093fb;
--chart-color-4: #56ab2f;
--chart-color-5: #f2994a;
--chart-color-6: #764ba2;
--chart-color-7: #00f2fe;
--chart-color-8: #f5576c;
}
+34 -73
View File
@@ -4,9 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diapers - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<script src="/chart-theme.js"></script>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
@@ -19,7 +16,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: var(--bg-primary);
background-color: #f0f0f0;
min-height: 100vh;
max-width: 800px;
margin: 2rem auto;
@@ -27,27 +24,26 @@
}
.container {
background: var(--bg-secondary);
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.button {
background: var(--gradient-pink);
background-color: #f093fb;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -83,13 +79,13 @@
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
color: #666;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
color: #666;
}
.empty-state-icon {
@@ -98,7 +94,7 @@
}
.empty-state h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
}
@@ -109,12 +105,12 @@
.chart-container {
margin-bottom: 40px;
padding: 20px;
background: var(--bg-tertiary);
background: #f8f9fa;
border-radius: 8px;
}
.chart-container h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
@@ -154,37 +150,29 @@
.controls label {
font-weight: 600;
color: var(--text-primary);
color: #333;
}
.controls select,
.controls input {
.controls select {
padding: 8px 16px;
border: 2px solid var(--border-color, #ddd);
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
background: var(--bg-secondary);
color: var(--text-primary);
background: white;
}
.controls select:focus,
.controls input:focus {
.controls select:focus {
outline: none;
border-color: #f093fb;
}
.controls input[readonly] {
opacity: 0.6;
cursor: default;
}
.diapers-list {
margin-top: 40px;
}
.diapers-list h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
@@ -192,9 +180,9 @@
.diapers-scroll {
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--border-color);
border: 1px solid #ddd;
border-radius: 8px;
background: var(--bg-tertiary);
background: #f8f9fa;
}
.diaper-item {
@@ -202,13 +190,13 @@
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-lighter);
background: white;
border-bottom: 1px solid #eee;
transition: background 0.2s;
}
.diaper-item:hover {
background: var(--bg-tertiary);
background: #f8f9fa;
}
.diaper-item:last-child {
@@ -221,13 +209,13 @@
.diaper-time {
font-weight: 600;
color: var(--text-primary);
color: #333;
margin-bottom: 5px;
}
.diaper-details {
font-size: 14px;
color: var(--text-secondary);
color: #666;
}
.diaper-badge {
@@ -278,12 +266,10 @@
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="/menu.js"></script>
<script src="/auth-utils.js"></script>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
// Initialize burger menu
initBurgerMenu({ includeHome: true });
@@ -293,9 +279,6 @@
let currentDaysRange =
parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
let selectedChildId = null; // null means show first/only child
let barChartInstance = null; // Track chart instance for proper cleanup
let poopPieChartInstance = null;
let peePieChartInstance = null;
// Restore last selected child from localStorage
const lastChildId = localStorage.getItem("lastDiaperChildFilter");
@@ -368,7 +351,6 @@
<div class="empty-state-icon">🧷</div>
<h2>No Diaper Changes Yet</h2>
<p>Start tracking diaper changes from the home page</p>
<button class="button" onclick="window.location.href='/'">Go Home</button>
</div>
`;
} else {
@@ -396,6 +378,7 @@
id="childDisplay"
value="${allChildren[0].name}"
readonly
style="background-color: #f5f5f5; cursor: default; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
/>
`
: `
@@ -797,12 +780,7 @@
function renderBarChart(chartData) {
const ctx = document.getElementById("diaperChart").getContext("2d");
// Destroy existing chart instance if it exists
if (barChartInstance) {
barChartInstance.destroy();
}
barChartInstance = new Chart(ctx, {
new Chart(ctx, {
type: "bar",
data: {
labels: chartData.labels,
@@ -857,7 +835,7 @@
},
},
scales: {
y: getChartScaleOptions("y", {
y: {
beginAtZero: true,
title: {
display: true,
@@ -873,7 +851,10 @@
size: 12,
},
},
}),
grid: {
color: "rgba(0, 0, 0, 0.05)",
},
},
x: {
ticks: {
font: {
@@ -892,11 +873,6 @@
function renderPoopPieChart(chartData) {
const ctx = document.getElementById("poopPieChart").getContext("2d");
// Destroy existing chart instance if it exists
if (poopPieChartInstance) {
poopPieChartInstance.destroy();
}
const colors = {
Light: {
bg: "rgba(139, 69, 19, 0.5)",
@@ -923,7 +899,7 @@
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
);
poopPieChartInstance = new Chart(ctx, {
new Chart(ctx, {
type: "pie",
data: {
labels: chartData.labels,
@@ -979,11 +955,6 @@
function renderPeePieChart(chartData) {
const ctx = document.getElementById("peePieChart").getContext("2d");
// Destroy existing chart instance if it exists
if (peePieChartInstance) {
peePieChartInstance.destroy();
}
const colors = {
Light: {
bg: "rgba(255, 215, 0, 0.5)",
@@ -1010,7 +981,7 @@
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
);
peePieChartInstance = new Chart(ctx, {
new Chart(ctx, {
type: "pie",
data: {
labels: chartData.labels,
@@ -1096,16 +1067,6 @@
// Load data on page load
loadDiaperChanges();
// Listen for theme changes and update charts
window.addEventListener("themechange", () => {
const charts = [
barChartInstance,
poopPieChartInstance,
peePieChartInstance,
].filter((c) => c);
updateChartsForTheme(charts);
});
</script>
</body>
</html>
+50 -87
View File
@@ -4,9 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Feedings - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<script src="/chart-theme.js"></script>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
@@ -19,7 +16,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: var(--bg-primary);
background-color: #f0f0f0;
min-height: 100vh;
max-width: 800px;
margin: 2rem auto;
@@ -27,27 +24,26 @@
}
.container {
background: var(--bg-secondary);
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.button {
background: var(--gradient-purple);
background-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -83,13 +79,13 @@
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
color: #666;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
color: #666;
}
.empty-state-icon {
@@ -98,7 +94,7 @@
}
.empty-state h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
}
@@ -109,12 +105,12 @@
.chart-container {
margin-bottom: 40px;
padding: 20px;
background: var(--bg-tertiary);
background: #f8f9fa;
border-radius: 8px;
}
.chart-container h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
@@ -152,14 +148,14 @@
}
.stat-card {
background: var(--bg-tertiary);
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card h3 {
color: var(--text-secondary);
color: #666;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
@@ -167,7 +163,7 @@
}
.stat-card .stat-value {
color: var(--text-primary);
color: #333;
font-size: 32px;
font-weight: 700;
}
@@ -186,34 +182,21 @@
.controls label {
font-weight: 600;
color: var(--text-primary);
color: #333;
}
.controls select,
.controls input {
.controls select {
padding: 8px 16px;
border: 2px solid var(--border-color, #ddd);
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
background: var(--bg-secondary);
color: var(--text-primary);
}
.controls select:focus,
.controls input:focus {
outline: none;
border-color: var(--input-focus-border);
}
.controls input[readonly] {
opacity: 0.6;
cursor: default;
background: white;
}
.controls select:focus {
outline: none;
border-color: var(--input-focus-border);
border-color: #667eea;
}
.feedings-list {
@@ -221,7 +204,7 @@
}
.feedings-list h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
@@ -229,9 +212,9 @@
.feedings-scroll {
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--border-color);
border: 1px solid #ddd;
border-radius: 8px;
background: var(--bg-tertiary);
background: #f8f9fa;
}
.feeding-item {
@@ -239,13 +222,13 @@
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-lighter);
background: white;
border-bottom: 1px solid #eee;
transition: background 0.2s;
}
.feeding-item:hover {
background: var(--bg-tertiary);
background: #f8f9fa;
}
.feeding-item:last-child {
@@ -258,13 +241,13 @@
.feeding-time {
font-weight: 600;
color: var(--text-primary);
color: #333;
margin-bottom: 5px;
}
.feeding-details {
font-size: 14px;
color: var(--text-secondary);
color: #666;
}
.feeding-type-badge {
@@ -320,12 +303,10 @@
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="/menu.js"></script>
<script src="/auth-utils.js"></script>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
// Initialize burger menu
initBurgerMenu({ includeHome: true });
@@ -336,8 +317,6 @@
parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
let selectedChildId = null; // null means show first/only child
let barChartInstance = null;
let pieChartInstance = null;
let durationChartInstance = null;
// Restore last selected child from localStorage
const lastChildId = localStorage.getItem("lastFeedingChildFilter");
@@ -404,20 +383,16 @@
function displayFeedings(feedings) {
const content = document.getElementById("content");
// Check if we have children data
if (allChildren.length === 0) {
if (feedings.length === 0) {
content.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">👶</div>
<h2>No Children Added</h2>
<p>Please add a child first before tracking feedings</p>
<button class="button" onclick="window.location.href='/add-child.html'">Add Child</button>
<div class="empty-state-icon">🍼</div>
<h2>No Feedings Yet</h2>
<p>Start tracking feeding sessions from the home page</p>
</div>
`;
return;
}
// Prepare chart data (works with empty arrays too)
} else {
// Prepare chart data
const barChartData = prepareBarChartData(feedings);
const pieChartData = preparePieChartData(feedings);
const durationChartData = prepareDurationChartData(feedings);
@@ -441,6 +416,7 @@
id="childDisplay"
value="${allChildren[0].name}"
readonly
style="background-color: #f5f5f5; cursor: default; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
/>
`
: `
@@ -494,6 +470,7 @@
// Render feeding list
renderFeedingList(feedings);
}
}
function renderFeedingList(feedings) {
// Filter feedings by current time range
@@ -940,7 +917,7 @@
display: false,
},
},
y: getChartScaleOptions("y", {
y: {
stacked: true,
beginAtZero: true,
title: {
@@ -959,7 +936,10 @@
return Math.round(value) + "m";
},
},
}),
grid: {
color: "rgba(0, 0, 0, 0.05)",
},
},
},
plugins: {
legend: {
@@ -997,11 +977,6 @@
function renderPieChart(chartData) {
const ctx = document.getElementById("pieChart").getContext("2d");
// Destroy existing chart instance if it exists
if (pieChartInstance) {
pieChartInstance.destroy();
}
// Color mapping for each type
const colorMap = {
"Left Breast": {
@@ -1026,7 +1001,7 @@
(label) => colorMap[label]?.border || "rgba(200, 200, 200, 1)",
);
pieChartInstance = new Chart(ctx, {
new Chart(ctx, {
type: "pie",
data: {
labels: chartData.labels,
@@ -1082,12 +1057,7 @@
function renderDurationChart(chartData) {
const ctx = document.getElementById("durationChart").getContext("2d");
// Destroy existing chart instance if it exists
if (durationChartInstance) {
durationChartInstance.destroy();
}
durationChartInstance = new Chart(ctx, {
new Chart(ctx, {
type: "bar",
data: {
labels: chartData.labels,
@@ -1127,7 +1097,7 @@
},
},
scales: {
y: getChartScaleOptions("y", {
y: {
beginAtZero: true,
title: {
display: true,
@@ -1143,8 +1113,11 @@
size: 12,
},
},
}),
x: getChartScaleOptions("x", {
grid: {
color: "rgba(0, 0, 0, 0.05)",
},
},
x: {
title: {
display: true,
text: "Duration (minutes)",
@@ -1161,7 +1134,7 @@
grid: {
display: false,
},
}),
},
},
},
});
@@ -1200,16 +1173,6 @@
// Load data on page load
loadFeedings();
// Listen for theme changes and update charts
window.addEventListener("themechange", () => {
const charts = [
barChartInstance,
pieChartInstance,
durationChartInstance,
].filter((c) => c);
updateChartsForTheme(charts);
});
</script>
</body>
</html>
+67 -274
View File
@@ -4,37 +4,27 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<script src="/chart-theme.js"></script>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
background-color: var(--bg-primary);
overflow-x: hidden;
background-color: #f0f0f0;
}
.container {
background: var(--bg-secondary);
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: var(--shadow-md);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 1rem;
}
.greeting {
color: var(--text-secondary);
color: #666;
font-size: 1.1rem;
margin-bottom: 2rem;
}
@@ -69,9 +59,7 @@
padding: 2rem;
font-size: 1.5rem;
margin-bottom: 1.5rem;
background: var(--gradient-purple);
background-color: #667eea;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
transition:
transform 0.2s,
@@ -80,16 +68,13 @@
.feeding-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
background: var(--gradient-purple);
background-color: #667eea;
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.feeding-button.active {
background: linear-gradient(135deg, #f44336 0%, #e91e63 100%);
background-color: #f44336;
}
.feeding-button.active:hover {
background: linear-gradient(135deg, #e91e63 0%, #f44336 100%);
background-color: #e91e63;
}
.feeding-info {
font-size: 0.9rem;
@@ -112,7 +97,7 @@
display: flex;
}
.modal-content {
background: var(--bg-secondary);
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 400px;
@@ -122,7 +107,7 @@
.modal h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: var(--text-primary);
color: #333;
}
.form-group {
margin-bottom: 1rem;
@@ -130,13 +115,13 @@
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-primary);
color: #333;
font-weight: 600;
}
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
@@ -160,9 +145,7 @@
padding: 2rem;
font-size: 1.5rem;
margin-bottom: 1.5rem;
background: var(--gradient-blue);
background-color: #4facfe;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
transition:
transform 0.2s,
@@ -170,69 +153,20 @@
}
.sleep-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(79, 172, 254, 0.4);
background: var(--gradient-blue);
background-color: #4facfe;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.sleep-button.active {
background: var(--gradient-blue);
background-color: #4facfe;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.sleep-button.active:hover {
background: var(--gradient-blue);
background-color: #4facfe;
background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%);
}
.sleep-info {
font-size: 0.9rem;
margin-top: 0.5rem;
opacity: 0.9;
}
.recent-activity {
margin-top: 2rem;
padding: 1.5rem;
background: var(--bg-tertiary);
border-radius: 8px;
}
.recent-activity h3 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--text-primary);
font-size: 1.1rem;
}
.activity-grid {
display: flex;
flex-direction: column;
gap: 1rem;
}
.activity-card {
background: var(--bg-secondary);
padding: 1rem;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.activity-card .icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.activity-card .type {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.activity-card .time {
font-size: 0.85rem;
color: var(--text-secondary);
}
.activity-card .details {
font-size: 0.85rem;
color: #888;
margin-top: 0.5rem;
}
.activity-card.empty {
opacity: 0.5;
font-style: italic;
color: #999;
}
</style>
</head>
<body>
@@ -302,35 +236,26 @@
</div>
<script src="/menu.js"></script>
<script src="/auth-utils.js"></script>
<script>
const token = localStorage.getItem("access_token");
const token = getToken();
const username = localStorage.getItem("username");
// Initialize burger menu
initBurgerMenu({ includeHome: true });
// Check if user is logged in
if (!token) {
window.location.href = "/login.html";
} else {
// Fetch current user info including admin status
fetch("/api/me", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => {
if (response.ok) {
return response.json();
} else {
// Token invalid, redirect to login
localStorage.removeItem("access_token");
localStorage.removeItem("username");
window.location.href = "/login.html";
throw new Error("Authentication failed");
}
})
.then((response) => response.json())
.then((user) => {
if (!user) return; // Early exit if redirected
// Initialize burger menu
initBurgerMenu({
includeHome: true,
});
// Check if user has any children
return fetch("/api/children", {
headers: {
@@ -352,7 +277,6 @@
.catch((error) => {
console.error("Error:", error);
});
}
function showNoChildrenMessage() {
const content = document.getElementById("content");
@@ -385,7 +309,6 @@
let activeSleep = null;
let lastSleep = null;
let sleepUpdateTimerInterval = null;
let lastDiaperChange = null;
async function loadFeedingStatus(children) {
userChildren = children;
@@ -402,8 +325,8 @@
activeFeeding = await activeResponse.json();
}
// Fetch the 10 most recent feedings to calculate the most recent session
const feedingsResponse = await fetch("/api/feedings?limit=10", {
// Fetch all feedings to get the last one
const feedingsResponse = await fetch("/api/feedings", {
headers: {
Authorization: `Bearer ${token}`,
},
@@ -412,68 +335,8 @@
if (feedingsResponse.ok) {
const feedings = await feedingsResponse.json();
if (feedings.length > 0) {
// Calculate the most recent feeding session
let sessionStart = feedings[0].start_time;
let sessionEnd = feedings[0].end_time;
let sessionChildId = feedings[0].child_id;
let sessionEntries = [feedings[0]];
for (let i = 1; i < feedings.length; i++) {
const entry = feedings[i];
// Only group entries for the same child (ignore feeding type)
if (entry.child_id !== sessionChildId) {
break;
}
// If the gap between this entry's end_time and current sessionStart is more than 30 min, stop
const prevEnd = entry.end_time || entry.start_time;
const sessionStartDate = new Date(sessionStart);
const prevEndDate = new Date(prevEnd);
const diffMins = Math.abs(
(sessionStartDate - prevEndDate) / 60000,
);
if (diffMins > 30) {
break;
}
// Extend session to include this entry
sessionStart = entry.start_time;
sessionEntries.push(entry);
}
// Calculate accumulated feeding time for all events in the session
let accumulatedMinutes = 0;
sessionEntries.forEach((entry) => {
if (entry.end_time && entry.start_time) {
const start = new Date(entry.start_time);
const end = new Date(entry.end_time);
const diffMs = end - start;
if (diffMs > 0) {
accumulatedMinutes += Math.floor(diffMs / 60000);
}
}
});
// Build a string showing all feeding types used during the session
const feedingTypesSet = new Set();
sessionEntries.forEach((entry) => {
if (entry.feeding_type) {
feedingTypesSet.add(entry.feeding_type);
}
});
const feedingTypesString = Array.from(feedingTypesSet)
.map(formatFeedingType)
.join(", ");
// Compose a session object
lastFeeding = {
child_id: sessionChildId,
feeding_type: feedings[0].feeding_type,
start_time: sessionStart,
end_time: sessionEnd,
entries: sessionEntries,
child_name: feedings[0].child_name,
accumulated_minutes: accumulatedMinutes,
feeding_types_string: feedingTypesString,
};
// Feedings are ordered by start_time desc, so first is most recent
lastFeeding = feedings[0];
}
}
@@ -501,20 +364,6 @@
lastSleep = sleeps[0];
}
}
// Fetch all diaper changes to get the last one
const diaperResponse = await fetch("/api/diaper-changes", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (diaperResponse.ok) {
const diapers = await diaperResponse.json();
if (diapers.length > 0) {
lastDiaperChange = diapers[0];
}
}
} catch (error) {
console.error("Error loading feeding status:", error);
}
@@ -588,7 +437,7 @@
</button>
`
: `
<button class="feeding-button" onclick="window.location.href='/log-feeding.html'">
<button class="feeding-button" onclick="showStartFeedingModal()">
🍼 Started Feeding
</button>
`;
@@ -617,100 +466,9 @@
</button>
${feedingButtonHtml}
${sleepButtonHtml}
${buildRecentActivitySection()}
`;
}
function buildRecentActivitySection() {
// Build diaper card
const diaperCard = lastDiaperChange
? `
<a href="/diapers.html" class="activity-card" style="text-decoration:none; color:inherit;">
<div class="icon">🧷</div>
<div class="type">Last Diaper</div>
<div class="time">${formatTime(lastDiaperChange.change_time)}</div>
<div class="details">
${lastDiaperChange.poop_amount ? `💩 ${lastDiaperChange.poop_amount}` : ""}
${lastDiaperChange.poop_amount && lastDiaperChange.pee_amount ? " • " : ""}
${lastDiaperChange.pee_amount ? `💧 ${lastDiaperChange.pee_amount}` : ""}
</div>
</a>
`
: `
<a href="/diapers.html" class="activity-card empty" style="text-decoration:none; color:inherit;">
<div class="icon">🧷</div>
<div class="type">No diaper changes yet</div>
</a>
`;
// Build feeding card
const feedingCard = lastFeeding
? `
<a href="/feedings.html" class="activity-card" style="text-decoration:none; color:inherit;">
<div class="icon">🍼</div>
<div class="type">Last Feeding Session</div>
<div class="time">${lastFeeding.end_time ? `stopped ${formatTime(lastFeeding.end_time)}` : `started ${formatTime(lastFeeding.start_time)}`}</div>
<div class="details">
${lastFeeding.feeding_types_string ? `${lastFeeding.feeding_types_string}` : ""}
${lastFeeding.feeding_types_string && lastFeeding.accumulated_minutes ? " • " : ""}
${lastFeeding.accumulated_minutes} minutes total
${lastFeeding.end_time ? "" : " (ongoing)"}
</div>
</a>
`
: `
<a href="/feedings.html" class="activity-card empty" style="text-decoration:none; color:inherit;">
<div class="icon">🍼</div>
<div class="type">No feedings yet</div>
</a>
`;
// Build sleep card
const sleepCard = lastSleep
? `
<a href="/sleep.html" class="activity-card" style="text-decoration:none; color:inherit;">
<div class="icon">😴</div>
<div class="type">Last Sleep</div>
<div class="time">${lastSleep.end_time ? `stopped ${formatTime(lastSleep.end_time)}` : `started ${formatTime(lastSleep.start_time)}`}</div>
<div class="details">
Duration: ${lastSleep.end_time ? calculateDuration(lastSleep.start_time, lastSleep.end_time) : "ongoing"}
</div>
</a>
`
: `
<a href="/sleep.html" class="activity-card empty" style="text-decoration:none; color:inherit;">
<div class="icon">😴</div>
<div class="type">No sleep sessions yet</div>
</a>
`;
return `
<div class="recent-activity">
<h3>📊 Recent Activity</h3>
<div class="activity-grid">
${diaperCard}
${feedingCard}
${sleepCard}
</div>
</div>
`;
}
function calculateDuration(startTime, endTime) {
const start = new Date(startTime);
const end = new Date(endTime);
const diffMs = end - start;
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) {
return `${diffMins} min`;
}
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
return `${hours}h ${mins}m`;
}
function formatFeedingType(type) {
const types = {
left_breast: "Left Breast",
@@ -882,7 +640,42 @@
}
function showStartSleepModal() {
window.location.href = "/log-sleep.html";
const modal = document.getElementById("startSleepModal");
const childSelect = document.getElementById("modalSleepChildId");
// Clear and populate child select or show single child
if (userChildren.length === 1) {
// Single child: replace dropdown with text display
const child = userChildren[0];
const formGroup = childSelect.parentElement;
formGroup.innerHTML = `
<label for="modalSleepChildName">Child</label>
<input
type="text"
id="modalSleepChildName"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default;"
/>
<input type="hidden" id="modalSleepChildId" value="${child.id}" />
`;
} else {
// Multiple children: show dropdown
childSelect.innerHTML = '<option value="">Select a child</option>';
userChildren.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
childSelect.appendChild(option);
});
// Set default based on last sleep
if (lastSleep) {
childSelect.value = lastSleep.child_id;
}
}
modal.classList.add("show");
}
function closeStartSleepModal() {
+18 -52
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Log Diaper Change - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<style>
* {
margin: 0;
@@ -17,7 +15,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: var(--bg-primary);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
@@ -26,22 +24,22 @@
}
.container {
background: var(--bg-secondary);
background: white;
border-radius: 12px;
box-shadow: var(--shadow-md);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
max-width: 500px;
width: 100%;
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
@@ -53,7 +51,7 @@
label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
color: #333;
font-weight: 600;
font-size: 14px;
}
@@ -62,28 +60,20 @@
select {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color, #ddd);
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
background-color: var(--input-bg);
color: var(--input-text);
}
input:focus,
select:focus {
outline: none;
border-color: #f093fb;
}
input[readonly] {
opacity: 0.6;
cursor: default;
border-color: #667eea;
}
.button {
background: var(--gradient-pink);
background-color: #f093fb;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -100,7 +90,7 @@
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(240, 147, 251, 0.4);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.button:active {
@@ -147,25 +137,25 @@
.help-text {
font-size: 12px;
color: var(--text-secondary);
color: #666;
margin-top: 4px;
}
.loading {
text-align: center;
padding: 20px;
color: var(--text-secondary);
color: #666;
}
.section-title {
color: var(--text-secondary);
color: #666;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 25px 0 15px 0;
padding-top: 15px;
border-top: 1px solid var(--border-lighter);
border-top: 1px solid #eee;
}
.section-title:first-of-type {
@@ -245,19 +235,17 @@
<button
type="button"
class="button secondary"
onclick="window.location.href = getReturnUrl()"
onclick="window.location.href=getReturnUrl()"
>
Cancel
</button>
</form>
</div>
<script src="/auth-utils.js"></script>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
// Determine return URL based on referrer
function getReturnUrl() {
@@ -309,6 +297,7 @@
id="childName"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default;"
/>
<input type="hidden" id="childSelect" name="childSelect" value="${child.id}" />
`;
@@ -345,29 +334,6 @@
lastDiaperChange.child_id;
}
}
// Prepopulate poop and pee fields from most recent entry
try {
const response = await fetch("/api/diaper-changes", {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const changes = await response.json();
if (changes.length > 0) {
const last = changes[0];
document.getElementById("poopAmount").value =
last.poop_amount || "";
document.getElementById("poopColor").value =
last.poop_color || "";
document.getElementById("peeAmount").value =
last.pee_amount || "";
document.getElementById("peeColor").value =
last.pee_color || "";
}
}
} catch (error) {
// Ignore error, just use default
}
}
} else {
showMessage("Failed to load children", "error");
+13 -40
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Log Feeding - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<style>
* {
margin: 0;
@@ -17,7 +15,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: var(--bg-primary);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
@@ -26,22 +24,22 @@
}
.container {
background: var(--bg-secondary);
background: white;
border-radius: 12px;
box-shadow: var(--shadow-md);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
max-width: 500px;
width: 100%;
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
@@ -53,7 +51,7 @@
label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
color: #333;
font-weight: 600;
font-size: 14px;
}
@@ -62,12 +60,10 @@
select {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color, #ddd);
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
background-color: var(--input-bg);
color: var(--input-text);
}
input:focus,
@@ -76,14 +72,8 @@
border-color: #667eea;
}
input[readonly] {
opacity: 0.6;
cursor: default;
}
.button {
background: var(--gradient-purple);
background-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -147,14 +137,14 @@
.help-text {
font-size: 12px;
color: var(--text-secondary);
color: #666;
margin-top: 4px;
}
.loading {
text-align: center;
padding: 20px;
color: var(--text-secondary);
color: #666;
}
</style>
</head>
@@ -209,12 +199,10 @@
</form>
</div>
<script src="/auth-utils.js"></script>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
// Determine return URL based on referrer
function getReturnUrl() {
@@ -291,6 +279,7 @@
id="childName"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default;"
/>
<input type="hidden" id="childId" name="childId" value="${child.id}" />
`;
@@ -312,22 +301,6 @@
// If editing, load the feeding data
if (isEditMode) {
loadFeedingData();
} else {
// Set default feeding type to most recent log entry
try {
const response = await fetch("/api/feedings", {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const feedings = await response.json();
if (feedings.length > 0) {
document.getElementById("feedingType").value =
feedings[0].feeding_type;
}
}
} catch (error) {
// Ignore error, just use default
}
}
} else {
showMessage("Failed to load children", "error");
+13 -24
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Log Sleep - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<style>
* {
margin: 0;
@@ -17,7 +15,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: var(--bg-primary);
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
min-height: 100vh;
display: flex;
justify-content: center;
@@ -26,22 +24,22 @@
}
.container {
background: var(--bg-secondary);
background: white;
border-radius: 12px;
box-shadow: var(--shadow-md);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
max-width: 500px;
width: 100%;
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
@@ -53,7 +51,7 @@
label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
color: #333;
font-weight: 600;
font-size: 14px;
}
@@ -62,12 +60,10 @@
select {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color, #ddd);
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
background-color: var(--input-bg);
color: var(--input-text);
}
input:focus,
@@ -76,14 +72,8 @@
border-color: #4facfe;
}
input[readonly] {
opacity: 0.6;
cursor: default;
}
.button {
background: var(--gradient-blue);
background-color: #4facfe;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -147,14 +137,14 @@
.help-text {
font-size: 12px;
color: var(--text-secondary);
color: #666;
margin-top: 4px;
}
.loading {
text-align: center;
padding: 20px;
color: var(--text-secondary);
color: #666;
}
</style>
</head>
@@ -199,12 +189,10 @@
</form>
</div>
<script src="/auth-utils.js"></script>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
// Determine return URL based on referrer
function getReturnUrl() {
@@ -281,6 +269,7 @@
id="childName"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default;"
/>
<input type="hidden" id="childId" name="childId" value="${child.id}" />
`;
+43 -15
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<style>
body {
font-family: Arial, sans-serif;
@@ -14,33 +12,27 @@
align-items: center;
height: 100vh;
margin: 0;
background-color: var(--bg-primary);
background-color: #f0f0f0;
}
.login-container {
background: var(--bg-secondary);
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: var(--shadow-md);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
h1 {
text-align: center;
color: var(--text-primary);
color: #333;
margin-bottom: 1.5rem;
}
input {
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid var(--border-color);
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
background-color: var(--input-bg);
color: var(--input-text);
}
input:focus {
outline: none;
border-color: var(--input-focus-border);
}
button {
width: 100%;
@@ -90,10 +82,36 @@
</div>
<script>
// Check if redirected due to expired session
const params = new URLSearchParams(window.location.search);
const returnUrl = params.get("return");
const form = document.getElementById("loginForm");
const messageDiv = document.getElementById("message");
const loginButton = document.getElementById("loginButton");
// Show session expired message if redirected from another page
if (returnUrl) {
showMessage("Your session has expired. Please log in again.", "error");
}
// Clear user-specific cache data on login
function clearUserCache() {
const keysToRemove = [
'lastDiaperChildFilter',
'lastFeedingChildFilter',
'lastSleepChildFilter',
'lastDiaperTimeRange',
'lastFeedingTimeRange'
];
keysToRemove.forEach(key => {
localStorage.removeItem(key);
});
console.log('Cleared user cache on login');
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
@@ -117,13 +135,23 @@
if (response.ok) {
showMessage(data.message, "success");
// Store token in localStorage
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("username", data.username);
// Redirect based on is_admin from login response
// Clear stale user preferences from previous sessions
clearUserCache();
// Check for return URL parameter
const params = new URLSearchParams(window.location.search);
const returnUrl = params.get("return");
// Redirect based on return URL or is_admin from login response
setTimeout(() => {
if (data.is_admin === true) {
if (returnUrl) {
window.location.href = returnUrl;
} else if (data.is_admin === true) {
window.location.href = "/admin.html";
} else {
window.location.href = "/";
+11 -21
View File
@@ -5,21 +5,18 @@
left: 1rem;
cursor: pointer;
z-index: 1000;
background: var(--bg-secondary, #ffffff);
background: white;
padding: 0.5rem;
border-radius: 4px;
box-shadow: var(--shadow-md, 0 2px 10px rgba(0, 0, 0, 0.1));
transition:
background 0.3s,
box-shadow 0.3s;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.burger-menu div {
width: 25px;
height: 3px;
background-color: var(--text-primary, #333333);
background-color: #333;
margin: 5px 0;
transition: background-color 0.3s;
transition: 0.3s;
}
.menu-backdrop {
@@ -40,17 +37,14 @@
.menu-overlay {
position: fixed;
top: 0;
left: -100%;
left: -250px;
width: 250px;
height: 100vh;
background: var(--bg-secondary, #ffffff);
box-shadow: var(--shadow-md, 2px 0 10px rgba(0, 0, 0, 0.1));
transition:
left 0.3s,
background 0.3s;
background: white;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: left 0.3s;
z-index: 999;
padding: 4rem 1rem 1rem 1rem;
overflow: hidden;
}
.menu-overlay.open {
@@ -59,17 +53,13 @@
.menu-item {
padding: 1rem;
border-bottom: 1px solid var(--border-color, #eeeeee);
border-bottom: 1px solid #eee;
cursor: pointer;
transition:
background 0.2s,
color 0.3s,
border-color 0.3s;
color: var(--text-primary, #333333);
transition: background 0.2s;
}
.menu-item:hover {
background: var(--bg-tertiary, #f5f5f5);
background: #f5f5f5;
}
.menu-item.admin {
+8 -13
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor - Register</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<style>
* {
margin: 0;
@@ -17,7 +15,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
@@ -35,14 +33,14 @@
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
text-align: center;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
text-align: center;
@@ -63,23 +61,20 @@
input {
width: 100%;
padding: 12px;
border: 2px solid #ccc;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
background-color: var(--input-bg);
color: var(--input-text);
}
input:focus {
outline: none;
border-color: var(--input-focus-border);
border-color: #667eea;
}
.button {
width: 100%;
background: var(--gradient-purple);
background-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 14px;
@@ -178,7 +173,7 @@
.password-requirements {
font-size: 12px;
color: var(--text-secondary);
color: #666;
margin-top: 5px;
}
@@ -191,7 +186,7 @@
<div class="container">
<div id="loadingContainer" class="loading-container">
<div class="spinner"></div>
<p style="color: var(--text-secondary)">Validating invitation...</p>
<p style="color: #666">Validating invitation...</p>
</div>
<div id="registerContainer" class="hidden">
+40 -237
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Settings - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
@@ -18,7 +16,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: var(--bg-primary);
background-color: #f0f0f0;
min-height: 100vh;
max-width: 800px;
margin: 2rem auto;
@@ -26,20 +24,20 @@
}
.container {
background: var(--bg-secondary);
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
@@ -47,12 +45,12 @@
.section {
margin-bottom: 40px;
padding: 20px;
background: var(--bg-tertiary);
background: #f8f9fa;
border-radius: 8px;
}
.section h2 {
color: var(--text-primary);
color: #333;
font-size: 20px;
margin-bottom: 20px;
display: flex;
@@ -67,7 +65,7 @@
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
color: #333;
font-weight: 600;
font-size: 14px;
}
@@ -75,27 +73,24 @@
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color, #ddd);
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
background-color: var(--input-bg);
color: var(--input-text);
}
.form-group input:focus {
outline: none;
border-color: var(--input-focus-border);
border-color: #667eea;
}
.form-group input:disabled {
opacity: 0.6;
background-color: #f5f5f5;
cursor: not-allowed;
}
.button {
background: var(--gradient-purple);
background-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -130,7 +125,7 @@
}
.button.success {
background: var(--gradient-success);
background: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
}
.button.success:hover {
@@ -144,20 +139,20 @@
}
.child-card {
background: var(--bg-secondary);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: var(--shadow-md);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.child-card h3 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 18px;
}
.child-info {
color: var(--text-secondary);
color: #666;
font-size: 14px;
margin-bottom: 15px;
}
@@ -174,7 +169,7 @@
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
color: #666;
}
.alert {
@@ -205,7 +200,7 @@
width: 100%;
padding: 15px;
margin-bottom: 15px;
background: var(--bg-secondary);
background: white;
border: 2px solid #667eea;
color: #667eea;
border-radius: 8px;
@@ -238,140 +233,6 @@
</div>
</div>
<div class="section">
<h2>🌓 Theme Preference</h2>
<p
style="
color: var(--text-secondary);
margin-bottom: 20px;
font-size: 14px;
"
>
Choose how Baby Monitor looks to you.
</p>
<div class="form-group">
<div style="display: flex; flex-direction: column; gap: 15px">
<label
style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="light"
>
<input
type="radio"
name="theme"
value="light"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div>
<div
style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
☀️ Light Mode
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Always use light theme
</div>
</div>
</label>
<label
style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="dark"
>
<input
type="radio"
name="theme"
value="dark"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div>
<div
style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
🌙 Dark Mode
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Always use dark theme
</div>
</div>
</label>
<label
style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="auto"
>
<input
type="radio"
name="theme"
value="auto"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div>
<div
style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
🔄 Auto
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Match your browser settings
</div>
</div>
</label>
</div>
</div>
</div>
<div class="section">
<h2>🔑 Change Password</h2>
<form id="userForm">
@@ -400,18 +261,12 @@
"
>
<h3 style="color: #dc3545; margin-bottom: 15px">⚠️ Danger Zone</h3>
<p
style="
color: var(--text-secondary);
margin-bottom: 15px;
font-size: 14px;
"
>
<p style="color: #666; margin-bottom: 15px; font-size: 14px">
Deleting your account is permanent and cannot be undone. This will:
</p>
<ul
style="
color: var(--text-secondary);
color: #666;
margin-bottom: 20px;
margin-left: 20px;
font-size: 14px;
@@ -430,7 +285,9 @@
</ul>
<button
class="button"
style="background: var(--gradient-error)"
style="
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
"
onclick="confirmDeleteAccount()"
>
🗑️ Delete Account
@@ -443,7 +300,7 @@
<button
class="quick-action"
onclick="window.location.href = '/add-child.html'"
onclick="window.location.href='/add-child.html'"
>
Add New Child
</button>
@@ -481,17 +338,15 @@
>
<div
style="
background: var(--bg-secondary);
background: white;
padding: 30px;
border-radius: 12px;
max-width: 400px;
width: 90%;
"
>
<h2 style="margin-top: 0; color: var(--text-primary)">
🔗 Redeem Share Code
</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px">
<h2 style="margin-top: 0">🔗 Redeem Share Code</h2>
<p style="color: #666; margin-bottom: 20px">
Enter the share code to get access to a child.
</p>
<form id="redeemCodeForm">
@@ -505,11 +360,9 @@
style="
width: 100%;
padding: 12px;
border: 2px solid var(--border-color, #ddd);
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
background-color: var(--input-bg);
color: var(--input-text);
"
/>
</div>
@@ -549,21 +402,18 @@
>
<div
style="
background: var(--bg-secondary);
background: white;
padding: 30px;
border-radius: 12px;
max-width: 400px;
width: 90%;
"
>
<h2 style="margin-top: 0; color: var(--text-primary)">🔗 Share Code</h2>
<p
style="color: var(--text-secondary); margin-bottom: 20px"
id="shareCodeChildName"
></p>
<h2 style="margin-top: 0">🔗 Share Code</h2>
<p style="color: #666; margin-bottom: 20px" id="shareCodeChildName"></p>
<div
style="
background: var(--bg-tertiary);
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
@@ -580,7 +430,7 @@
style="
font-size: 24px;
font-weight: bold;
color: var(--text-primary);
color: #333;
letter-spacing: 2px;
"
id="shareCodeValue"
@@ -601,13 +451,7 @@
</button>
</div>
</div>
<p
style="
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 20px;
"
>
<p style="color: #666; font-size: 14px; margin-bottom: 20px">
Share this code with another user. They can use it in Settings >
Redeem Share Code to get access to this child.
<br /><br />
@@ -625,56 +469,13 @@
</div>
<script src="/menu.js"></script>
<script src="/auth-utils.js"></script>
<script>
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
// Initialize burger menu
initBurgerMenu({ includeHome: true });
// Initialize theme selector
function initThemeSelector() {
const currentTheme = window.getThemePreference();
const themeRadio = document.querySelector(
`input[name="theme"][value="${currentTheme}"]`,
);
if (themeRadio) {
themeRadio.checked = true;
}
// Add visual feedback for selected theme option
updateThemeOptionStyles();
// Listen for theme changes
document.querySelectorAll('input[name="theme"]').forEach((radio) => {
radio.addEventListener("change", (e) => {
window.setTheme(e.target.value);
updateThemeOptionStyles();
});
});
}
function updateThemeOptionStyles() {
const selectedValue = document.querySelector(
'input[name="theme"]:checked',
)?.value;
document.querySelectorAll(".theme-option").forEach((option) => {
const isSelected = option.dataset.theme === selectedValue;
if (isSelected) {
option.style.borderColor = "#667eea";
option.style.background = "rgba(102, 126, 234, 0.05)";
} else {
option.style.borderColor = "#e0e0e0";
option.style.background = "transparent";
}
});
}
// Initialize theme selector on load
initThemeSelector();
let currentUser = null;
let children = [];
@@ -692,7 +493,7 @@
currentUser = await userResponse.json();
document.getElementById("username").value = currentUser.username;
} else {
window.location.href = "/login.html";
redirectToLogin();
return;
}
@@ -1049,8 +850,10 @@
}
});
// Load data on page load
// Load data on page load (only if authenticated)
if (token) {
loadData();
}
</script>
</body>
</html>
+43 -59
View File
@@ -4,9 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sleep - Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script>
<script src="/chart-theme.js"></script>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
@@ -19,7 +16,7 @@
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: var(--bg-primary);
background-color: #f0f0f0;
min-height: 100vh;
max-width: 800px;
margin: 2rem auto;
@@ -27,27 +24,26 @@
}
.container {
background: var(--bg-secondary);
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
}
h1 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: var(--text-secondary);
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.button {
background: var(--gradient-blue);
background-color: #4facfe;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
padding: 12px 24px;
@@ -83,13 +79,13 @@
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
color: #666;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
color: #666;
}
.empty-state-icon {
@@ -98,7 +94,7 @@
}
.empty-state h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 10px;
}
@@ -109,12 +105,12 @@
.chart-container {
margin-bottom: 40px;
padding: 20px;
background: var(--bg-tertiary);
background: #f8f9fa;
border-radius: 8px;
}
.chart-container h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
@@ -152,14 +148,14 @@
}
.stat-card {
background: var(--bg-tertiary);
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card h3 {
color: var(--text-secondary);
color: #666;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
@@ -173,13 +169,13 @@
}
.list-container {
background: var(--bg-tertiary);
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.list-container h2 {
color: var(--text-primary);
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
@@ -190,14 +186,14 @@
}
.sleep-item {
background: var(--bg-secondary);
background: white;
padding: 16px;
border-radius: 6px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-sm);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.sleep-item:last-child {
@@ -210,12 +206,12 @@
.sleep-child {
font-weight: 600;
color: var(--text-primary);
color: #333;
margin-bottom: 4px;
}
.sleep-time {
color: var(--text-secondary);
color: #666;
font-size: 14px;
margin-bottom: 4px;
}
@@ -256,31 +252,23 @@
.controls label {
font-weight: 600;
color: var(--text-primary);
color: #333;
}
.controls select,
.controls input {
.controls select {
padding: 8px 16px;
border: 2px solid var(--border-color, #ddd);
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
background: var(--bg-secondary);
color: var(--text-primary);
background: white;
}
.controls select:focus,
.controls input:focus {
.controls select:focus {
outline: none;
border-color: #4facfe;
}
.controls input[readonly] {
opacity: 0.6;
cursor: default;
}
.badge {
display: inline-block;
padding: 4px 8px;
@@ -325,13 +313,9 @@
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="/menu.js"></script>
<script src="/auth-utils.js"></script>
<script>
const token = localStorage.getItem("access_token");
// Check if user is logged in
if (!token) {
window.location.href = "/login.html";
}
const token = getToken();
// Initialize burger menu
initBurgerMenu({ includeHome: true });
@@ -400,6 +384,7 @@
id="childDisplay"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
/>
<label for="timeRangeFilter">Time Range:</label>
@@ -490,7 +475,19 @@
return matchesTimeRange && matchesChild;
});
// Calculate stats (works with empty arrays too)
if (filteredSleeps.length === 0) {
document.getElementById("content").innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">😴</div>
<h2>No Sleep Data</h2>
<p>Start tracking sleep sessions from the home page.</p>
<button class="button" onclick="window.location.href='/'">Go Home</button>
</div>
`;
return;
}
// Calculate stats
const completedSleeps = filteredSleeps.filter(
(sleep) => sleep.end_time,
);
@@ -895,7 +892,7 @@
x: {
stacked: true,
},
y: getChartScaleOptions("y", {
y: {
stacked: true,
beginAtZero: true,
title: {
@@ -911,7 +908,7 @@
}
},
},
}),
},
},
plugins: {
legend: {
@@ -1056,14 +1053,14 @@
responsive: true,
maintainAspectRatio: false,
scales: {
y: getChartScaleOptions("y", {
y: {
beginAtZero: true,
ticks: {
callback: function (value) {
return value + "m";
},
},
}),
},
},
plugins: {
legend: {
@@ -1103,9 +1100,6 @@
</div>
<div class="sleep-duration">${durationText}</div>
</div>
<div class="sleep-actions">
<button class="edit-button" onclick="window.location.href='/log-sleep.html?id=${sleep.id}'">Edit</button>
</div>
</div>
`;
}
@@ -1133,16 +1127,6 @@
// Load data on page load
loadData();
// Listen for theme changes and update charts
window.addEventListener("themechange", () => {
const charts = [
barChartInstance,
durationChartInstance,
avgDurationChartInstance,
].filter((c) => c);
updateChartsForTheme(charts);
});
</script>
</body>
</html>
-87
View File
@@ -1,87 +0,0 @@
/* Theme Initialization Script
* Handles theme detection and application based on localStorage preference or browser settings
* Must be loaded in <head> before page content to prevent flash of unstyled content
*/
(function () {
"use strict";
// Get saved theme preference from localStorage
const savedTheme = localStorage.getItem("theme-preference");
// Apply theme immediately to prevent FOUC (Flash of Unstyled Content)
function applyTheme(theme) {
const body = document.body || document.documentElement;
if (theme === "dark") {
body.classList.remove("light-mode");
body.classList.add("dark-mode");
} else if (theme === "light") {
body.classList.remove("dark-mode");
body.classList.add("light-mode");
} else {
// 'auto' mode - remove manual override classes
body.classList.remove("dark-mode", "light-mode");
}
}
// Apply theme based on preference
if (
savedTheme === "dark" ||
savedTheme === "light" ||
savedTheme === "auto"
) {
applyTheme(savedTheme);
}
// If no preference saved, default to 'auto' (browser preference)
// Make theme switching function available globally
window.setTheme = function (theme) {
if (theme === "light" || theme === "dark" || theme === "auto") {
localStorage.setItem("theme-preference", theme);
applyTheme(theme);
// Trigger custom event for components that need to update (e.g., charts)
window.dispatchEvent(
new CustomEvent("themechange", { detail: { theme } }),
);
}
};
// Get current effective theme (resolves 'auto' to actual theme)
window.getEffectiveTheme = function () {
const savedTheme = localStorage.getItem("theme-preference");
if (savedTheme === "dark") return "dark";
if (savedTheme === "light") return "light";
// 'auto' or no preference - check system preference
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
return "dark";
}
return "light";
};
// Get saved theme preference (returns 'light', 'dark', 'auto', or null)
window.getThemePreference = function () {
return localStorage.getItem("theme-preference") || "auto";
};
// Listen for system theme changes when in auto mode
if (window.matchMedia) {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", function (e) {
const savedTheme = localStorage.getItem("theme-preference");
// Only react to system changes if in auto mode
if (!savedTheme || savedTheme === "auto") {
// Trigger theme change event for components that need to update
window.dispatchEvent(
new CustomEvent("themechange", { detail: { theme: "auto" } }),
);
}
});
}
})();
Generated
+468 -465
View File
File diff suppressed because it is too large Load Diff