Merge pull request 'implement-ready-endpoint-logic' (#42) from implement-ready-endpoint-logic into main
Build and Push Docker Image / build-and-push (push) Successful in 27s
Python Code Quality / python-code-quality (push) Successful in 10s
Python Test / python-test (push) Successful in 19s
Refresh Showcase Data / refresh-data (push) Successful in 47s

Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
2026-01-27 21:26:06 +01:00
12 changed files with 1177 additions and 980 deletions
+50 -7
View File
@@ -1,6 +1,7 @@
"""Health check router for monitoring endpoints.""" """Health check router for monitoring endpoints."""
from fastapi import APIRouter import os
from fastapi import APIRouter, HTTPException
router = APIRouter(tags=["health"]) router = APIRouter(tags=["health"])
@@ -20,11 +21,53 @@ def readiness_check() -> dict[str, str]:
""" """
Readiness check endpoint for container orchestration. Readiness check endpoint for container orchestration.
Returns 200 if the service is ready to accept traffic. Returns 200 if the service is ready to accept traffic, else 503.
This can include checks for database connectivity, external services, etc.
""" """
# Add dependency checks here if needed (database, cache, etc.)
# For now, simple readiness check # 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
return {"status": "ready"} return {"status": "ready"}
@@ -60,9 +103,9 @@ def service_info() -> dict:
# Check current configuration # Check current configuration
config = { config = {
"environment": os.getenv("ENVIRONMENT", "production"), "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 { return {
"service": "baby-monitor", "service": "baby-monitor",
+180 -176
View File
@@ -3,201 +3,205 @@
* Usage: Include this before your chart initialization code * Usage: Include this before your chart initialization code
*/ */
(function() { (function () {
'use strict'; "use strict";
// Helper to get effective theme // Helper to get effective theme
function getEffectiveTheme() { function getEffectiveTheme() {
return window.getEffectiveTheme ? window.getEffectiveTheme() : 'light'; return window.getEffectiveTheme ? window.getEffectiveTheme() : "light";
} }
// Get theme-appropriate colors for Chart.js // Get theme-appropriate colors for Chart.js
window.getChartColors = function() { window.getChartColors = function () {
const isDark = getEffectiveTheme() === 'dark'; const isDark = getEffectiveTheme() === "dark";
return { return {
// Grid lines // Grid lines
grid: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)', 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)', gridMajor: isDark ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.1)",
// Text colors // Text colors
text: isDark ? '#b0b0b0' : '#666666', text: isDark ? "#b0b0b0" : "#666666",
textPrimary: isDark ? '#e0e0e0' : '#333333', textPrimary: isDark ? "#e0e0e0" : "#333333",
// Data series colors (optimized for background) // Data series colors (optimized for background)
series: isDark ? [ series: isDark
'rgba(139, 158, 255, 0.8)', // Brighter purple ? [
'rgba(110, 197, 255, 0.8)', // Brighter blue "rgba(139, 158, 255, 0.8)", // Brighter purple
'rgba(255, 179, 255, 0.8)', // Brighter pink "rgba(110, 197, 255, 0.8)", // Brighter blue
'rgba(129, 199, 132, 0.8)', // Brighter green "rgba(255, 179, 255, 0.8)", // Brighter pink
'rgba(255, 183, 77, 0.8)', // Brighter orange "rgba(129, 199, 132, 0.8)", // Brighter green
'rgba(156, 107, 184, 0.8)', // Brighter deep purple "rgba(255, 183, 77, 0.8)", // Brighter orange
'rgba(77, 255, 255, 0.8)', // Brighter cyan "rgba(156, 107, 184, 0.8)", // Brighter deep purple
'rgba(255, 138, 155, 0.8)', // Brighter red "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(102, 126, 234, 0.8)", // Purple
'rgba(76, 175, 80, 0.8)', // Green "rgba(118, 75, 162, 0.8)", // Deep purple
'rgba(244, 67, 54, 0.8)', // Red "rgba(255, 159, 64, 0.8)", // Orange
'rgba(156, 39, 176, 0.8)', // Violet "rgba(76, 175, 80, 0.8)", // Green
'rgba(102, 126, 234, 0.6)', // Light purple "rgba(244, 67, 54, 0.8)", // Red
'rgba(118, 75, 162, 0.6)', // Light deep purple "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.) // Solid data colors (for pie charts, etc.)
solidSeries: isDark ? [ solidSeries: isDark
'#8b9eff', ? [
'#6ec5ff', "#8b9eff",
'#ffb3ff', "#6ec5ff",
'#81c784', "#ffb3ff",
'#ffb74d', "#81c784",
'#9c6bb8', "#ffb74d",
'#4dffff', "#9c6bb8",
'#ff8a9b', "#4dffff",
] : [ "#ff8a9b",
'#667eea', ]
'#764ba2', : [
'#ff9f40', "#667eea",
'#4caf50', "#764ba2",
'#f44336', "#ff9f40",
'#9c27b0', "#4caf50",
'#667eea', "#f44336",
'#764ba2', "#9c27b0",
], "#667eea",
"#764ba2",
],
// Background colors // Background colors
background: isDark ? '#2d2d2d' : '#f8f9fa', 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: {},
}; };
// Get default Chart.js options with theme support // Deep merge customOptions with defaults
window.getChartOptions = function(customOptions = {}) { return mergeDeep(defaults, customOptions);
const colors = window.getChartColors(); };
const defaults = { // Get theme-appropriate scale options
responsive: true, window.getChartScaleOptions = function (axis = "x", customOptions = {}) {
maintainAspectRatio: false, const colors = window.getChartColors();
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 const defaults = {
return mergeDeep(defaults, customOptions); grid: {
color: colors.grid,
borderColor: colors.gridMajor,
},
ticks: {
color: colors.text,
font: {
size: 12,
},
},
title: {
color: colors.textPrimary,
font: {
size: 13,
weight: "600",
},
},
}; };
// Get theme-appropriate scale options return mergeDeep(defaults, customOptions);
window.getChartScaleOptions = function(axis = 'x', customOptions = {}) { };
const colors = window.getChartColors();
const defaults = { // Helper function to deep merge objects
grid: { function mergeDeep(target, source) {
color: colors.grid, const output = Object.assign({}, target);
borderColor: colors.gridMajor, if (isObject(target) && isObject(source)) {
}, Object.keys(source).forEach((key) => {
ticks: { if (isObject(source[key])) {
color: colors.text, if (!(key in target)) {
font: { Object.assign(output, { [key]: source[key] });
size: 12 } else {
} output[key] = mergeDeep(target[key], source[key]);
}, }
title: { } else {
color: colors.textPrimary, Object.assign(output, { [key]: source[key] });
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; });
}
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];
} }
function isObject(item) { const colors = window.getChartColors();
return item && typeof item === 'object' && !Array.isArray(item);
}
// Function to update all charts on theme change chartInstances.forEach((chart) => {
window.updateChartsForTheme = function(chartInstances) { if (!chart) return;
if (!Array.isArray(chartInstances)) {
chartInstances = [chartInstances];
}
const colors = window.getChartColors(); // Update grid colors
if (chart.options.scales) {
chartInstances.forEach(chart => { Object.keys(chart.options.scales).forEach((axis) => {
if (!chart) return; if (chart.options.scales[axis].grid) {
chart.options.scales[axis].grid.color = colors.grid;
// Update grid colors chart.options.scales[axis].grid.borderColor = colors.gridMajor;
if (chart.options.scales) { }
Object.keys(chart.options.scales).forEach(axis => { if (chart.options.scales[axis].ticks) {
if (chart.options.scales[axis].grid) { chart.options.scales[axis].ticks.color = colors.text;
chart.options.scales[axis].grid.color = colors.grid; }
chart.options.scales[axis].grid.borderColor = colors.gridMajor; if (chart.options.scales[axis].title) {
} chart.options.scales[axis].title.color = colors.textPrimary;
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();
}); });
}; }
// 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();
});
};
})(); })();
+169 -169
View File
@@ -3,146 +3,78 @@
*/ */
:root { :root {
/* ===== LIGHT MODE (Default) ===== */ /* ===== LIGHT MODE (Default) ===== */
/* Background colors */ /* Background colors */
--bg-primary: #f0f0f0; --bg-primary: #f0f0f0;
--bg-secondary: #ffffff; --bg-secondary: #ffffff;
--bg-tertiary: #f8f9fa; --bg-tertiary: #f8f9fa;
--bg-card: #ffffff; --bg-card: #ffffff;
/* Text colors */ /* Text colors */
--text-primary: #333333; --text-primary: #333333;
--text-secondary: #666666; --text-secondary: #666666;
--text-muted: #999999; --text-muted: #999999;
--text-inverse: #ffffff; --text-inverse: #ffffff;
/* Border colors */ /* Border colors */
--border-color: #dddddd; --border-color: #dddddd;
--border-light: #e0e0e0; --border-light: #e0e0e0;
--border-lighter: #eeeeee; --border-lighter: #eeeeee;
/* Gradients - Brand */ /* Gradients - Brand */
--gradient-purple: linear-gradient(135deg, #667eea 0%, #764ba2 100%); --gradient-purple: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-blue: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); --gradient-blue: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--gradient-pink: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); --gradient-pink: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--gradient-green: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%); --gradient-green: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--gradient-orange: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%); --gradient-orange: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%);
/* Action colors */ /* Action colors */
--color-success: #4caf50; --color-success: #4caf50;
--color-error: #dc3545; --color-error: #dc3545;
--color-warning: #ff9800; --color-warning: #ff9800;
--color-info: #2196f3; --color-info: #2196f3;
--color-danger: #f44336; --color-danger: #f44336;
/* Action gradients */ /* Action gradients */
--gradient-success: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%); --gradient-success: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--gradient-error: linear-gradient(135deg, #dc3545 0%, #c62828 100%); --gradient-error: linear-gradient(135deg, #dc3545 0%, #c62828 100%);
--gradient-danger: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); --gradient-danger: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
/* Interactive elements */ /* Interactive elements */
--input-bg: #ffffff; --input-bg: #ffffff;
--input-border: #dddddd; --input-border: #dddddd;
--input-focus-border: #667eea; --input-focus-border: #667eea;
--input-text: #333333; --input-text: #333333;
--input-placeholder: #999999; --input-placeholder: #999999;
/* Shadows */ /* Shadows */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1); --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 2px 10px 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-lg: 0 4px 20px rgba(0, 0, 0, 0.15);
--shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.2); --shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.2);
/* Modal/Overlay */ /* Modal/Overlay */
--overlay-bg: rgba(0, 0, 0, 0.5); --overlay-bg: rgba(0, 0, 0, 0.5);
/* Chart colors (vibrant for light backgrounds) */ /* Chart colors (vibrant for light backgrounds) */
--chart-grid: rgba(0, 0, 0, 0.1); --chart-grid: rgba(0, 0, 0, 0.1);
--chart-text: #666666; --chart-text: #666666;
--chart-color-1: #667eea; --chart-color-1: #667eea;
--chart-color-2: #4facfe; --chart-color-2: #4facfe;
--chart-color-3: #f093fb; --chart-color-3: #f093fb;
--chart-color-4: #56ab2f; --chart-color-4: #56ab2f;
--chart-color-5: #f2994a; --chart-color-5: #f2994a;
--chart-color-6: #764ba2; --chart-color-6: #764ba2;
--chart-color-7: #00f2fe; --chart-color-7: #00f2fe;
--chart-color-8: #f5576c; --chart-color-8: #f5576c;
} }
/* ===== DARK MODE ===== */ /* ===== DARK MODE ===== */
/* Applied via @media query (auto-detect) OR .dark-mode class (manual override) */ /* Applied via @media query (auto-detect) OR .dark-mode class (manual override) */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root:not(.light-mode) { :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 */ /* Background colors - Dark theme */
--bg-primary: #1a1a1a; --bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d; --bg-secondary: #2d2d2d;
@@ -206,53 +138,121 @@ body.dark-mode {
--chart-color-6: #9c6bb8; --chart-color-6: #9c6bb8;
--chart-color-7: #4dffff; --chart-color-7: #4dffff;
--chart-color-8: #ff8a9b; --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) */ /* Manual override with .light-mode class (forces light even when browser prefers dark) */
body.light-mode { body.light-mode {
/* Force light mode values - same as root defaults */ /* Force light mode values - same as root defaults */
--bg-primary: #f0f0f0; --bg-primary: #f0f0f0;
--bg-secondary: #ffffff; --bg-secondary: #ffffff;
--bg-tertiary: #f8f9fa; --bg-tertiary: #f8f9fa;
--bg-card: #ffffff; --bg-card: #ffffff;
--text-primary: #333333; --text-primary: #333333;
--text-secondary: #666666; --text-secondary: #666666;
--text-muted: #999999; --text-muted: #999999;
--text-inverse: #ffffff; --text-inverse: #ffffff;
--border-color: #dddddd; --border-color: #dddddd;
--border-light: #e0e0e0; --border-light: #e0e0e0;
--border-lighter: #eeeeee; --border-lighter: #eeeeee;
--gradient-purple: linear-gradient(135deg, #667eea 0%, #764ba2 100%); --gradient-purple: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-blue: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); --gradient-blue: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--gradient-pink: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); --gradient-pink: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--gradient-green: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%); --gradient-green: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--gradient-orange: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%); --gradient-orange: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%);
--color-success: #4caf50; --color-success: #4caf50;
--color-error: #dc3545; --color-error: #dc3545;
--color-warning: #ff9800; --color-warning: #ff9800;
--color-info: #2196f3; --color-info: #2196f3;
--color-danger: #f44336; --color-danger: #f44336;
--gradient-success: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%); --gradient-success: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--gradient-error: linear-gradient(135deg, #dc3545 0%, #c62828 100%); --gradient-error: linear-gradient(135deg, #dc3545 0%, #c62828 100%);
--gradient-danger: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); --gradient-danger: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
--input-bg: #ffffff; --input-bg: #ffffff;
--input-border: #dddddd; --input-border: #dddddd;
--input-focus-border: #667eea; --input-focus-border: #667eea;
--input-text: #333333; --input-text: #333333;
--input-placeholder: #999999; --input-placeholder: #999999;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1); --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 2px 10px 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-lg: 0 4px 20px rgba(0, 0, 0, 0.15);
--shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.2); --shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.2);
--overlay-bg: rgba(0, 0, 0, 0.5); --overlay-bg: rgba(0, 0, 0, 0.5);
--chart-grid: rgba(0, 0, 0, 0.1); --chart-grid: rgba(0, 0, 0, 0.1);
--chart-text: #666666; --chart-text: #666666;
--chart-color-1: #667eea; --chart-color-1: #667eea;
--chart-color-2: #4facfe; --chart-color-2: #4facfe;
--chart-color-3: #f093fb; --chart-color-3: #f093fb;
--chart-color-4: #56ab2f; --chart-color-4: #56ab2f;
--chart-color-5: #f2994a; --chart-color-5: #f2994a;
--chart-color-6: #764ba2; --chart-color-6: #764ba2;
--chart-color-7: #00f2fe; --chart-color-7: #00f2fe;
--chart-color-8: #f5576c; --chart-color-8: #f5576c;
} }
+7 -3
View File
@@ -857,7 +857,7 @@
}, },
}, },
scales: { scales: {
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
beginAtZero: true, beginAtZero: true,
title: { title: {
display: true, display: true,
@@ -1098,8 +1098,12 @@
loadDiaperChanges(); loadDiaperChanges();
// Listen for theme changes and update charts // Listen for theme changes and update charts
window.addEventListener('themechange', () => { window.addEventListener("themechange", () => {
const charts = [barChartInstance, poopPieChartInstance, peePieChartInstance].filter(c => c); const charts = [
barChartInstance,
poopPieChartInstance,
peePieChartInstance,
].filter((c) => c);
updateChartsForTheme(charts); updateChartsForTheme(charts);
}); });
</script> </script>
+21 -17
View File
@@ -424,13 +424,13 @@
// Build child dropdown options // Build child dropdown options
const childOptions = allChildren const childOptions = allChildren
.map( .map(
(child) => (child) =>
`<option value="${child.id}" ${selectedChildId === child.id ? "selected" : ""}>${child.name}</option>`, `<option value="${child.id}" ${selectedChildId === child.id ? "selected" : ""}>${child.name}</option>`,
) )
.join(""); .join("");
content.innerHTML = ` content.innerHTML = `
<div class="controls"> <div class="controls">
${ ${
allChildren.length === 1 allChildren.length === 1
@@ -486,13 +486,13 @@
</div> </div>
`; `;
// Render charts // Render charts
renderBarChart(barChartData); renderBarChart(barChartData);
renderPieChart(pieChartData); renderPieChart(pieChartData);
renderDurationChart(durationChartData); renderDurationChart(durationChartData);
// Render feeding list // Render feeding list
renderFeedingList(feedings); renderFeedingList(feedings);
} }
function renderFeedingList(feedings) { function renderFeedingList(feedings) {
@@ -940,7 +940,7 @@
display: false, display: false,
}, },
}, },
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
stacked: true, stacked: true,
beginAtZero: true, beginAtZero: true,
title: { title: {
@@ -1127,7 +1127,7 @@
}, },
}, },
scales: { scales: {
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
beginAtZero: true, beginAtZero: true,
title: { title: {
display: true, display: true,
@@ -1144,7 +1144,7 @@
}, },
}, },
}), }),
x: getChartScaleOptions('x', { x: getChartScaleOptions("x", {
title: { title: {
display: true, display: true,
text: "Duration (minutes)", text: "Duration (minutes)",
@@ -1202,8 +1202,12 @@
loadFeedings(); loadFeedings();
// Listen for theme changes and update charts // Listen for theme changes and update charts
window.addEventListener('themechange', () => { window.addEventListener("themechange", () => {
const charts = [barChartInstance, pieChartInstance, durationChartInstance].filter(c => c); const charts = [
barChartInstance,
pieChartInstance,
durationChartInstance,
].filter((c) => c);
updateChartsForTheme(charts); updateChartsForTheme(charts);
}); });
</script> </script>
+4 -2
View File
@@ -3,9 +3,11 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor</title> <link rel="stylesheet" href="/dark-mode.css" /> <title>Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script> <script src="/theme-init.js"></script>
<script src="/chart-theme.js"></script> <link rel="stylesheet" href="/menu.css" /> <script src="/chart-theme.js"></script>
<link rel="stylesheet" href="/menu.css" />
<style> <style>
* { * {
margin: 0; margin: 0;
+1 -1
View File
@@ -245,7 +245,7 @@
<button <button
type="button" type="button"
class="button secondary" class="button secondary"
onclick="window.location.href=getReturnUrl()" onclick="window.location.href = getReturnUrl()"
> >
Cancel Cancel
</button> </button>
+10 -3
View File
@@ -9,7 +9,9 @@
padding: 0.5rem; padding: 0.5rem;
border-radius: 4px; border-radius: 4px;
box-shadow: var(--shadow-md, 0 2px 10px rgba(0, 0, 0, 0.1)); box-shadow: var(--shadow-md, 0 2px 10px rgba(0, 0, 0, 0.1));
transition: background 0.3s, box-shadow 0.3s; transition:
background 0.3s,
box-shadow 0.3s;
} }
.burger-menu div { .burger-menu div {
@@ -43,7 +45,9 @@
height: 100vh; height: 100vh;
background: var(--bg-secondary, #ffffff); background: var(--bg-secondary, #ffffff);
box-shadow: var(--shadow-md, 2px 0 10px rgba(0, 0, 0, 0.1)); box-shadow: var(--shadow-md, 2px 0 10px rgba(0, 0, 0, 0.1));
transition: left 0.3s, background 0.3s; transition:
left 0.3s,
background 0.3s;
z-index: 999; z-index: 999;
padding: 4rem 1rem 1rem 1rem; padding: 4rem 1rem 1rem 1rem;
overflow: hidden; overflow: hidden;
@@ -57,7 +61,10 @@
padding: 1rem; padding: 1rem;
border-bottom: 1px solid var(--border-color, #eeeeee); border-bottom: 1px solid var(--border-color, #eeeeee);
cursor: pointer; cursor: pointer;
transition: background 0.2s, color 0.3s, border-color 0.3s; transition:
background 0.2s,
color 0.3s,
border-color 0.3s;
color: var(--text-primary, #333333); color: var(--text-primary, #333333);
} }
+152 -31
View File
@@ -240,30 +240,132 @@
<div class="section"> <div class="section">
<h2>🌓 Theme Preference</h2> <h2>🌓 Theme Preference</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px; font-size: 14px"> <p
style="
color: var(--text-secondary);
margin-bottom: 20px;
font-size: 14px;
"
>
Choose how Baby Monitor looks to you. Choose how Baby Monitor looks to you.
</p> </p>
<div class="form-group"> <div class="form-group">
<div style="display: flex; flex-direction: column; gap: 15px"> <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"> <label
<input type="radio" name="theme" value="light" style="margin-right: 12px; width: 20px; height: 20px; cursor: pointer;" /> 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>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">☀️ Light Mode</div> <div
<div style="font-size: 12px; color: var(--text-secondary);">Always use light theme</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> </div>
</label> </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"> <label
<input type="radio" name="theme" value="dark" style="margin-right: 12px; width: 20px; height: 20px; cursor: pointer;" /> 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>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">🌙 Dark Mode</div> <div
<div style="font-size: 12px; color: var(--text-secondary);">Always use dark theme</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> </div>
</label> </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"> <label
<input type="radio" name="theme" value="auto" style="margin-right: 12px; width: 20px; height: 20px; cursor: pointer;" /> 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>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">🔄 Auto</div> <div
<div style="font-size: 12px; color: var(--text-secondary);">Match your browser settings</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> </div>
</label> </label>
</div> </div>
@@ -298,7 +400,13 @@
" "
> >
<h3 style="color: #dc3545; margin-bottom: 15px">⚠️ Danger Zone</h3> <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: var(--text-secondary);
margin-bottom: 15px;
font-size: 14px;
"
>
Deleting your account is permanent and cannot be undone. This will: Deleting your account is permanent and cannot be undone. This will:
</p> </p>
<ul <ul
@@ -322,9 +430,7 @@
</ul> </ul>
<button <button
class="button" class="button"
style=" style="background: var(--gradient-error)"
background: var(--gradient-error);
"
onclick="confirmDeleteAccount()" onclick="confirmDeleteAccount()"
> >
🗑️ Delete Account 🗑️ Delete Account
@@ -337,7 +443,7 @@
<button <button
class="quick-action" class="quick-action"
onclick="window.location.href='/add-child.html'" onclick="window.location.href = '/add-child.html'"
> >
Add New Child Add New Child
</button> </button>
@@ -382,7 +488,9 @@
width: 90%; width: 90%;
" "
> >
<h2 style="margin-top: 0; color: var(--text-primary);">🔗 Redeem Share Code</h2> <h2 style="margin-top: 0; color: var(--text-primary)">
🔗 Redeem Share Code
</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px"> <p style="color: var(--text-secondary); margin-bottom: 20px">
Enter the share code to get access to a child. Enter the share code to get access to a child.
</p> </p>
@@ -448,8 +556,11 @@
width: 90%; width: 90%;
" "
> >
<h2 style="margin-top: 0; color: var(--text-primary);">🔗 Share Code</h2> <h2 style="margin-top: 0; color: var(--text-primary)">🔗 Share Code</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px" id="shareCodeChildName"></p> <p
style="color: var(--text-secondary); margin-bottom: 20px"
id="shareCodeChildName"
></p>
<div <div
style=" style="
background: var(--bg-tertiary); background: var(--bg-tertiary);
@@ -490,7 +601,13 @@
</button> </button>
</div> </div>
</div> </div>
<p style="color: var(--text-secondary); font-size: 14px; margin-bottom: 20px"> <p
style="
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 20px;
"
>
Share this code with another user. They can use it in Settings > Share this code with another user. They can use it in Settings >
Redeem Share Code to get access to this child. Redeem Share Code to get access to this child.
<br /><br /> <br /><br />
@@ -520,7 +637,9 @@
// Initialize theme selector // Initialize theme selector
function initThemeSelector() { function initThemeSelector() {
const currentTheme = window.getThemePreference(); const currentTheme = window.getThemePreference();
const themeRadio = document.querySelector(`input[name="theme"][value="${currentTheme}"]`); const themeRadio = document.querySelector(
`input[name="theme"][value="${currentTheme}"]`,
);
if (themeRadio) { if (themeRadio) {
themeRadio.checked = true; themeRadio.checked = true;
} }
@@ -529,8 +648,8 @@
updateThemeOptionStyles(); updateThemeOptionStyles();
// Listen for theme changes // Listen for theme changes
document.querySelectorAll('input[name="theme"]').forEach(radio => { document.querySelectorAll('input[name="theme"]').forEach((radio) => {
radio.addEventListener('change', (e) => { radio.addEventListener("change", (e) => {
window.setTheme(e.target.value); window.setTheme(e.target.value);
updateThemeOptionStyles(); updateThemeOptionStyles();
}); });
@@ -538,15 +657,17 @@
} }
function updateThemeOptionStyles() { function updateThemeOptionStyles() {
const selectedValue = document.querySelector('input[name="theme"]:checked')?.value; const selectedValue = document.querySelector(
document.querySelectorAll('.theme-option').forEach(option => { 'input[name="theme"]:checked',
)?.value;
document.querySelectorAll(".theme-option").forEach((option) => {
const isSelected = option.dataset.theme === selectedValue; const isSelected = option.dataset.theme === selectedValue;
if (isSelected) { if (isSelected) {
option.style.borderColor = '#667eea'; option.style.borderColor = "#667eea";
option.style.background = 'rgba(102, 126, 234, 0.05)'; option.style.background = "rgba(102, 126, 234, 0.05)";
} else { } else {
option.style.borderColor = '#e0e0e0'; option.style.borderColor = "#e0e0e0";
option.style.background = 'transparent'; option.style.background = "transparent";
} }
}); });
} }
+8 -4
View File
@@ -895,7 +895,7 @@
x: { x: {
stacked: true, stacked: true,
}, },
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
stacked: true, stacked: true,
beginAtZero: true, beginAtZero: true,
title: { title: {
@@ -1056,7 +1056,7 @@
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
scales: { scales: {
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
beginAtZero: true, beginAtZero: true,
ticks: { ticks: {
callback: function (value) { callback: function (value) {
@@ -1135,8 +1135,12 @@
loadData(); loadData();
// Listen for theme changes and update charts // Listen for theme changes and update charts
window.addEventListener('themechange', () => { window.addEventListener("themechange", () => {
const charts = [barChartInstance, durationChartInstance, avgDurationChartInstance].filter(c => c); const charts = [
barChartInstance,
durationChartInstance,
avgDurationChartInstance,
].filter((c) => c);
updateChartsForTheme(charts); updateChartsForTheme(charts);
}); });
</script> </script>
+71 -60
View File
@@ -3,74 +3,85 @@
* Must be loaded in <head> before page content to prevent flash of unstyled content * Must be loaded in <head> before page content to prevent flash of unstyled content
*/ */
(function() { (function () {
'use strict'; "use strict";
// Get saved theme preference from localStorage // Get saved theme preference from localStorage
const savedTheme = localStorage.getItem('theme-preference'); const savedTheme = localStorage.getItem("theme-preference");
// Apply theme immediately to prevent FOUC (Flash of Unstyled Content) // Apply theme immediately to prevent FOUC (Flash of Unstyled Content)
function applyTheme(theme) { function applyTheme(theme) {
const body = document.body || document.documentElement; const body = document.body || document.documentElement;
if (theme === 'dark') { if (theme === "dark") {
body.classList.remove('light-mode'); body.classList.remove("light-mode");
body.classList.add('dark-mode'); body.classList.add("dark-mode");
} else if (theme === 'light') { } else if (theme === "light") {
body.classList.remove('dark-mode'); body.classList.remove("dark-mode");
body.classList.add('light-mode'); body.classList.add("light-mode");
} else { } else {
// 'auto' mode - remove manual override classes // 'auto' mode - remove manual override classes
body.classList.remove('dark-mode', 'light-mode'); body.classList.remove("dark-mode", "light-mode");
}
} }
}
// Apply theme based on preference // Apply theme based on preference
if (savedTheme === 'dark' || savedTheme === 'light' || savedTheme === 'auto') { if (
applyTheme(savedTheme); 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 } }),
);
} }
// If no preference saved, default to 'auto' (browser preference) };
// Make theme switching function available globally // Get current effective theme (resolves 'auto' to actual theme)
window.setTheme = function(theme) { window.getEffectiveTheme = function () {
if (theme === 'light' || theme === 'dark' || theme === 'auto') { const savedTheme = localStorage.getItem("theme-preference");
localStorage.setItem('theme-preference', theme);
applyTheme(theme);
// Trigger custom event for components that need to update (e.g., charts) if (savedTheme === "dark") return "dark";
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme } })); if (savedTheme === "light") return "light";
}
};
// Get current effective theme (resolves 'auto' to actual theme) // 'auto' or no preference - check system preference
window.getEffectiveTheme = function() { if (
const savedTheme = localStorage.getItem('theme-preference'); window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
if (savedTheme === 'dark') return 'dark'; ) {
if (savedTheme === 'light') return 'light'; return "dark";
// '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' } }));
}
});
} }
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
+465 -468
View File
File diff suppressed because it is too large Load Diff