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",
+66 -62
View File
@@ -4,70 +4,74 @@
*/ */
(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",
}; };
}; };
@@ -83,19 +87,20 @@
labels: { labels: {
color: colors.text, color: colors.text,
font: { font: {
size: 12 size: 12,
} },
} },
}, },
tooltip: { tooltip: {
backgroundColor: getEffectiveTheme() === 'dark' ? '#3a3a3a' : '#ffffff', backgroundColor:
getEffectiveTheme() === "dark" ? "#3a3a3a" : "#ffffff",
titleColor: colors.textPrimary, titleColor: colors.textPrimary,
bodyColor: colors.text, bodyColor: colors.text,
borderColor: colors.grid, borderColor: colors.grid,
borderWidth: 1, borderWidth: 1,
}
}, },
scales: {} },
scales: {},
}; };
// Deep merge customOptions with defaults // Deep merge customOptions with defaults
@@ -103,7 +108,7 @@
}; };
// Get theme-appropriate scale options // Get theme-appropriate scale options
window.getChartScaleOptions = function(axis = 'x', customOptions = {}) { window.getChartScaleOptions = function (axis = "x", customOptions = {}) {
const colors = window.getChartColors(); const colors = window.getChartColors();
const defaults = { const defaults = {
@@ -114,16 +119,16 @@
ticks: { ticks: {
color: colors.text, color: colors.text,
font: { font: {
size: 12 size: 12,
} },
}, },
title: { title: {
color: colors.textPrimary, color: colors.textPrimary,
font: { font: {
size: 13, size: 13,
weight: '600' weight: "600",
} },
} },
}; };
return mergeDeep(defaults, customOptions); return mergeDeep(defaults, customOptions);
@@ -133,7 +138,7 @@
function mergeDeep(target, source) { function mergeDeep(target, source) {
const output = Object.assign({}, target); const output = Object.assign({}, target);
if (isObject(target) && isObject(source)) { if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => { Object.keys(source).forEach((key) => {
if (isObject(source[key])) { if (isObject(source[key])) {
if (!(key in target)) { if (!(key in target)) {
Object.assign(output, { [key]: source[key] }); Object.assign(output, { [key]: source[key] });
@@ -149,7 +154,7 @@
} }
function isObject(item) { function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item); return item && typeof item === "object" && !Array.isArray(item);
} }
// Function to update all charts on theme change // Function to update all charts on theme change
@@ -160,12 +165,12 @@
const colors = window.getChartColors(); const colors = window.getChartColors();
chartInstances.forEach(chart => { chartInstances.forEach((chart) => {
if (!chart) return; if (!chart) return;
// Update grid colors // Update grid colors
if (chart.options.scales) { if (chart.options.scales) {
Object.keys(chart.options.scales).forEach(axis => { Object.keys(chart.options.scales).forEach((axis) => {
if (chart.options.scales[axis].grid) { if (chart.options.scales[axis].grid) {
chart.options.scales[axis].grid.color = colors.grid; chart.options.scales[axis].grid.color = colors.grid;
chart.options.scales[axis].grid.borderColor = colors.gridMajor; chart.options.scales[axis].grid.borderColor = colors.gridMajor;
@@ -189,7 +194,7 @@
// Update tooltip colors // Update tooltip colors
if (chart.options.plugins && chart.options.plugins.tooltip) { if (chart.options.plugins && chart.options.plugins.tooltip) {
chart.options.plugins.tooltip.backgroundColor = chart.options.plugins.tooltip.backgroundColor =
getEffectiveTheme() === 'dark' ? '#3a3a3a' : '#ffffff'; getEffectiveTheme() === "dark" ? "#3a3a3a" : "#ffffff";
chart.options.plugins.tooltip.titleColor = colors.textPrimary; chart.options.plugins.tooltip.titleColor = colors.textPrimary;
chart.options.plugins.tooltip.bodyColor = colors.text; chart.options.plugins.tooltip.bodyColor = colors.text;
chart.options.plugins.tooltip.borderColor = colors.grid; chart.options.plugins.tooltip.borderColor = colors.grid;
@@ -199,5 +204,4 @@
chart.update(); chart.update();
}); });
}; };
})(); })();
+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>
+9 -5
View File
@@ -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;
+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);
} }
+151 -30
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
@@ -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>
+36 -25
View File
@@ -4,72 +4,83 @@
*/ */
(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 (
savedTheme === "dark" ||
savedTheme === "light" ||
savedTheme === "auto"
) {
applyTheme(savedTheme); applyTheme(savedTheme);
} }
// If no preference saved, default to 'auto' (browser preference) // If no preference saved, default to 'auto' (browser preference)
// Make theme switching function available globally // Make theme switching function available globally
window.setTheme = function (theme) { window.setTheme = function (theme) {
if (theme === 'light' || theme === 'dark' || theme === 'auto') { if (theme === "light" || theme === "dark" || theme === "auto") {
localStorage.setItem('theme-preference', theme); localStorage.setItem("theme-preference", theme);
applyTheme(theme); applyTheme(theme);
// Trigger custom event for components that need to update (e.g., charts) // Trigger custom event for components that need to update (e.g., charts)
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme } })); window.dispatchEvent(
new CustomEvent("themechange", { detail: { theme } }),
);
} }
}; };
// Get current effective theme (resolves 'auto' to actual theme) // Get current effective theme (resolves 'auto' to actual theme)
window.getEffectiveTheme = function () { window.getEffectiveTheme = function () {
const savedTheme = localStorage.getItem('theme-preference'); const savedTheme = localStorage.getItem("theme-preference");
if (savedTheme === 'dark') return 'dark'; if (savedTheme === "dark") return "dark";
if (savedTheme === 'light') return 'light'; if (savedTheme === "light") return "light";
// 'auto' or no preference - check system preference // 'auto' or no preference - check system preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { if (
return 'dark'; window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
return "dark";
} }
return 'light'; return "light";
}; };
// Get saved theme preference (returns 'light', 'dark', 'auto', or null) // Get saved theme preference (returns 'light', 'dark', 'auto', or null)
window.getThemePreference = function () { window.getThemePreference = function () {
return localStorage.getItem('theme-preference') || 'auto'; return localStorage.getItem("theme-preference") || "auto";
}; };
// Listen for system theme changes when in auto mode // Listen for system theme changes when in auto mode
if (window.matchMedia) { if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener('change', function(e) { mediaQuery.addEventListener("change", function (e) {
const savedTheme = localStorage.getItem('theme-preference'); const savedTheme = localStorage.getItem("theme-preference");
// Only react to system changes if in auto mode // Only react to system changes if in auto mode
if (!savedTheme || savedTheme === 'auto') { if (!savedTheme || savedTheme === "auto") {
// Trigger theme change event for components that need to update // Trigger theme change event for components that need to update
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme: 'auto' } })); window.dispatchEvent(
new CustomEvent("themechange", { detail: { theme: "auto" } }),
);
} }
}); });
} }
Generated
+465 -468
View File
File diff suppressed because it is too large Load Diff