code quality changes
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m3s
Python Code Quality / python-code-quality (pull_request) Successful in 10s
Python Test / python-test (pull_request) Successful in 17s

This commit is contained in:
Brian Bjarke Jensen
2026-01-27 21:21:55 +01:00
parent b2aa71b95b
commit ec0e1f6140
11 changed files with 664 additions and 506 deletions
+2 -1
View File
@@ -31,6 +31,7 @@ def readiness_check() -> dict[str, str]:
if redis_uri: if redis_uri:
try: try:
import redis import redis
redis_client = redis.from_url(redis_uri) redis_client = redis.from_url(redis_uri)
redis_client.ping() redis_client.ping()
except Exception as exc: except Exception as exc:
@@ -51,6 +52,7 @@ def readiness_check() -> dict[str, str]:
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy import text from sqlalchemy import text
engine = create_engine(postgres_uri) engine = create_engine(postgres_uri)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
with SessionLocal() as db: with SessionLocal() as db:
@@ -105,7 +107,6 @@ def service_info() -> dict:
if not features["postgresql"]: if not features["postgresql"]:
config["data_dir"] = os.getenv("DATA_DIR", "/data") config["data_dir"] = os.getenv("DATA_DIR", "/data")
return { return {
"service": "baby-monitor", "service": "baby-monitor",
"version": "0.1.0", "version": "0.1.0",
+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" } }),
);
}
});
}
})(); })();