Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a966d0bc8e | ||
|
|
753339c28a | ||
|
|
1d641288e5 | ||
|
|
e58a58e766 | ||
|
|
e0bcda6e7c | ||
|
|
f4419f6a72 | ||
|
|
6de91fa6a9 | ||
|
|
a1a914ee4d |
@@ -28,6 +28,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Run showcase data seeding script
|
- name: Run showcase data seeding script
|
||||||
env:
|
env:
|
||||||
|
ADMIN_USERNAME: ${{ secrets.ADMIN_USERNAME }}
|
||||||
|
ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
|
||||||
POSTGRES_URI: ${{ secrets.POSTGRES_URI }}
|
POSTGRES_URI: ${{ secrets.POSTGRES_URI }}
|
||||||
run: |
|
run: |
|
||||||
uv run python scripts/seed_showcase_data.py
|
uv run python scripts/seed_showcase_data.py
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ def serve_menu_js() -> FileResponse:
|
|||||||
return FileResponse(static_path / "menu.js")
|
return FileResponse(static_path / "menu.js")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/auth-utils.js", include_in_schema=False)
|
||||||
|
def serve_auth_utils_js() -> FileResponse:
|
||||||
|
"""Serve the shared auth utilities JavaScript."""
|
||||||
|
return FileResponse(static_path / "auth-utils.js")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/")
|
@app.get("/api/")
|
||||||
def read_root(token: Annotated[str, Depends(verify_token)]) -> dict:
|
def read_root(token: Annotated[str, Depends(verify_token)]) -> dict:
|
||||||
"""API root endpoint (requires authentication)."""
|
"""API root endpoint (requires authentication)."""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import logging
|
|||||||
from baby_monitor.repositories.interfaces import (
|
from baby_monitor.repositories.interfaces import (
|
||||||
TokenRepositoryInterface,
|
TokenRepositoryInterface,
|
||||||
)
|
)
|
||||||
from baby_monitor.repositories.token.memory_token import (
|
from baby_monitor.repositories.token import (
|
||||||
InMemoryTokenRepository,
|
InMemoryTokenRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ def _initialize_token_repository() -> TokenRepositoryInterface:
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from baby_monitor.repositories.token.redis_token import (
|
from baby_monitor.repositories.token import (
|
||||||
RedisTokenRepository,
|
RedisTokenRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"""Token repository implementations and configurations."""
|
||||||
|
|
||||||
|
from .default_ttl import DEFAULT_TTL
|
||||||
|
from .memory_token import InMemoryTokenRepository
|
||||||
|
from .redis_token import RedisTokenRepository
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DEFAULT_TTL",
|
||||||
|
"InMemoryTokenRepository",
|
||||||
|
"RedisTokenRepository",
|
||||||
|
]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Definition of Token default TTL."""
|
||||||
|
|
||||||
|
DEFAULT_TTL = 28800 # seconds -> 8 hours
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
from datetime import datetime, timedelta, UTC
|
from datetime import datetime, timedelta, UTC
|
||||||
|
|
||||||
from baby_monitor.repositories.interfaces import TokenRepositoryInterface
|
from baby_monitor.repositories.interfaces import TokenRepositoryInterface
|
||||||
|
from baby_monitor.repositories.token import DEFAULT_TTL
|
||||||
|
|
||||||
|
|
||||||
class InMemoryTokenRepository(TokenRepositoryInterface):
|
class InMemoryTokenRepository(TokenRepositoryInterface):
|
||||||
@@ -16,9 +17,8 @@ class InMemoryTokenRepository(TokenRepositoryInterface):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# token -> (user_id, expiry_time)
|
# token -> (user_id, expiry_time)
|
||||||
self._tokens: dict[str, tuple[int, datetime]] = {}
|
self._tokens: dict[str, tuple[int, datetime]] = {}
|
||||||
self._default_ttl = 3600 # Store default TTL for sliding expiration
|
|
||||||
|
|
||||||
def store(self, token: str, user_id: int, ttl: int = 3600) -> None:
|
def store(self, token: str, user_id: int, ttl: int = DEFAULT_TTL) -> None:
|
||||||
"""Store a token with TTL (time to live in seconds)."""
|
"""Store a token with TTL (time to live in seconds)."""
|
||||||
expiry = datetime.now(UTC) + timedelta(seconds=ttl)
|
expiry = datetime.now(UTC) + timedelta(seconds=ttl)
|
||||||
self._tokens[token] = (user_id, expiry)
|
self._tokens[token] = (user_id, expiry)
|
||||||
@@ -39,7 +39,7 @@ class InMemoryTokenRepository(TokenRepositoryInterface):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Sliding expiration: extend the token lifetime
|
# Sliding expiration: extend the token lifetime
|
||||||
new_expiry = datetime.now(UTC) + timedelta(seconds=self._default_ttl)
|
new_expiry = datetime.now(UTC) + timedelta(seconds=DEFAULT_TTL)
|
||||||
self._tokens[token] = (user_id, new_expiry)
|
self._tokens[token] = (user_id, new_expiry)
|
||||||
|
|
||||||
return user_id
|
return user_id
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from baby_monitor.repositories.interfaces import TokenRepositoryInterface
|
from baby_monitor.repositories.interfaces import TokenRepositoryInterface
|
||||||
|
from baby_monitor.repositories.token import DEFAULT_TTL
|
||||||
|
|
||||||
|
|
||||||
class RedisTokenRepository(TokenRepositoryInterface):
|
class RedisTokenRepository(TokenRepositoryInterface):
|
||||||
"""
|
"""
|
||||||
Redis-based token storage.
|
Redis-based token storage.
|
||||||
|
|
||||||
Requires redis package: pip install redis
|
This implementation uses Redis to store tokens with TTL. Implements sliding expiration.
|
||||||
Set REDIS_URI environment variable (e.g., redis://localhost:6379/0)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, redis_client: Any) -> None:
|
def __init__(self, redis_client: Any) -> None:
|
||||||
@@ -21,9 +21,8 @@ class RedisTokenRepository(TokenRepositoryInterface):
|
|||||||
redis_client: Redis client instance from redis.from_url()
|
redis_client: Redis client instance from redis.from_url()
|
||||||
"""
|
"""
|
||||||
self.redis = redis_client
|
self.redis = redis_client
|
||||||
self.default_ttl = 3600 # Store default TTL for sliding expiration
|
|
||||||
|
|
||||||
def store(self, token: str, user_id: int, ttl: int = 3600) -> None:
|
def store(self, token: str, user_id: int, ttl: int = DEFAULT_TTL) -> None:
|
||||||
"""Store a token with TTL (time to live in seconds)."""
|
"""Store a token with TTL (time to live in seconds)."""
|
||||||
key = f"token:{token}"
|
key = f"token:{token}"
|
||||||
self.redis.setex(key, ttl, str(user_id))
|
self.redis.setex(key, ttl, str(user_id))
|
||||||
@@ -37,7 +36,7 @@ class RedisTokenRepository(TokenRepositoryInterface):
|
|||||||
user_id_str = self.redis.get(key)
|
user_id_str = self.redis.get(key)
|
||||||
if user_id_str:
|
if user_id_str:
|
||||||
# Sliding expiration: extend the token lifetime
|
# Sliding expiration: extend the token lifetime
|
||||||
self.redis.expire(key, self.default_ttl)
|
self.redis.expire(key, DEFAULT_TTL)
|
||||||
return int(user_id_str)
|
return int(user_id_str)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -211,12 +211,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get child ID from URL if editing
|
// Get child ID from URL if editing
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|||||||
@@ -424,35 +424,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated and is admin
|
// Check if user is authenticated and is admin
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
let allUsers = [];
|
let allUsers = [];
|
||||||
let userToDelete = null;
|
let userToDelete = null;
|
||||||
|
|
||||||
// Verify user is admin and load data
|
// Load data
|
||||||
fetch("/api/me", {
|
fetch("/api/me", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (!user.is_admin) {
|
if (!user.is_admin) {
|
||||||
alert("Access denied. Admin privileges required.");
|
alert("Access denied. Admin privileges required.");
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
// Load users after verifying admin status
|
// Load users after verifying admin status
|
||||||
loadUsers();
|
loadUsers();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error verifying admin status:", error);
|
console.error("Error verifying admin status:", error);
|
||||||
window.location.href = "/login.html";
|
window.location.href = "/login.html";
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const container = document.getElementById("userListContainer");
|
const container = document.getElementById("userListContainer");
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Redirect to login page with return URL
|
||||||
|
* @param {string} returnUrl - The URL to return to after login (defaults to current page)
|
||||||
|
*/
|
||||||
|
function redirectToLogin(returnUrl) {
|
||||||
|
const url = returnUrl || window.location.pathname + window.location.search;
|
||||||
|
window.location.href = `/login.html?return=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has valid token, redirect to login if not
|
||||||
|
* @returns {string|null} - Returns token if valid, redirects to login if not
|
||||||
|
*/
|
||||||
|
function getToken() {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) {
|
||||||
|
redirectToLogin();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
@@ -266,12 +266,10 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize burger menu
|
// Initialize burger menu
|
||||||
initBurgerMenu({ includeHome: true });
|
initBurgerMenu({ includeHome: true });
|
||||||
|
|||||||
@@ -303,12 +303,10 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize burger menu
|
// Initialize burger menu
|
||||||
initBurgerMenu({ includeHome: true });
|
initBurgerMenu({ includeHome: true });
|
||||||
|
|||||||
@@ -236,59 +236,47 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
const username = localStorage.getItem("username");
|
const username = localStorage.getItem("username");
|
||||||
|
|
||||||
// Check if user is logged in
|
// Fetch current user info including admin status
|
||||||
if (!token) {
|
fetch("/api/me", {
|
||||||
window.location.href = "/login.html";
|
headers: {
|
||||||
} else {
|
Authorization: `Bearer ${token}`,
|
||||||
// Fetch current user info including admin status
|
},
|
||||||
fetch("/api/me", {
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((user) => {
|
||||||
|
if (!user) return; // Early exit if redirected
|
||||||
|
|
||||||
|
// Initialize burger menu
|
||||||
|
initBurgerMenu({
|
||||||
|
includeHome: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if user has any children
|
||||||
|
return fetch("/api/children", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => response.json())
|
||||||
if (response.ok) {
|
.then((children) => {
|
||||||
return response.json();
|
// If no children, show welcome message with option to add
|
||||||
} else {
|
if (children.length === 0) {
|
||||||
// Token invalid, redirect to login
|
showNoChildrenMessage();
|
||||||
localStorage.removeItem("access_token");
|
return;
|
||||||
localStorage.removeItem("username");
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
throw new Error("Authentication failed");
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.then((user) => {
|
|
||||||
// Initialize burger menu
|
|
||||||
initBurgerMenu({
|
|
||||||
includeHome: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if user has any children
|
// Load feeding status and show content
|
||||||
return fetch("/api/children", {
|
loadFeedingStatus(children);
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((children) => {
|
|
||||||
// If no children, show welcome message with option to add
|
|
||||||
if (children.length === 0) {
|
|
||||||
showNoChildrenMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load feeding status and show content
|
|
||||||
loadFeedingStatus(children);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error:", error);
|
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
function showNoChildrenMessage() {
|
function showNoChildrenMessage() {
|
||||||
const content = document.getElementById("content");
|
const content = document.getElementById("content");
|
||||||
|
|||||||
@@ -242,12 +242,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
|
|||||||
@@ -199,12 +199,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
|
|||||||
@@ -189,12 +189,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
|
|||||||
@@ -82,10 +82,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Check if redirected due to expired session
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const returnUrl = params.get("return");
|
||||||
|
|
||||||
const form = document.getElementById("loginForm");
|
const form = document.getElementById("loginForm");
|
||||||
const messageDiv = document.getElementById("message");
|
const messageDiv = document.getElementById("message");
|
||||||
const loginButton = document.getElementById("loginButton");
|
const loginButton = document.getElementById("loginButton");
|
||||||
|
|
||||||
|
// Show session expired message if redirected from another page
|
||||||
|
if (returnUrl) {
|
||||||
|
showMessage("Your session has expired. Please log in again.", "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear user-specific cache data on login
|
||||||
|
function clearUserCache() {
|
||||||
|
const keysToRemove = [
|
||||||
|
'lastDiaperChildFilter',
|
||||||
|
'lastFeedingChildFilter',
|
||||||
|
'lastSleepChildFilter',
|
||||||
|
'lastDiaperTimeRange',
|
||||||
|
'lastFeedingTimeRange'
|
||||||
|
];
|
||||||
|
|
||||||
|
keysToRemove.forEach(key => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Cleared user cache on login');
|
||||||
|
}
|
||||||
|
|
||||||
form.addEventListener("submit", async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -109,13 +135,23 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showMessage(data.message, "success");
|
showMessage(data.message, "success");
|
||||||
|
|
||||||
// Store token in localStorage
|
// Store token in localStorage
|
||||||
localStorage.setItem("access_token", data.access_token);
|
localStorage.setItem("access_token", data.access_token);
|
||||||
localStorage.setItem("username", data.username);
|
localStorage.setItem("username", data.username);
|
||||||
|
|
||||||
|
// Clear stale user preferences from previous sessions
|
||||||
|
clearUserCache();
|
||||||
|
|
||||||
// Redirect based on is_admin from login response
|
// Check for return URL parameter
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const returnUrl = params.get("return");
|
||||||
|
|
||||||
|
// Redirect based on return URL or is_admin from login response
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (data.is_admin === true) {
|
if (returnUrl) {
|
||||||
|
window.location.href = returnUrl;
|
||||||
|
} else if (data.is_admin === true) {
|
||||||
window.location.href = "/admin.html";
|
window.location.href = "/admin.html";
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
|||||||
@@ -469,11 +469,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize burger menu
|
// Initialize burger menu
|
||||||
initBurgerMenu({ includeHome: true });
|
initBurgerMenu({ includeHome: true });
|
||||||
@@ -495,7 +493,7 @@
|
|||||||
currentUser = await userResponse.json();
|
currentUser = await userResponse.json();
|
||||||
document.getElementById("username").value = currentUser.username;
|
document.getElementById("username").value = currentUser.username;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/login.html";
|
redirectToLogin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,8 +850,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load data on page load
|
// Load data on page load (only if authenticated)
|
||||||
loadData();
|
if (token) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -313,13 +313,9 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
|
|
||||||
// Check if user is logged in
|
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize burger menu
|
// Initialize burger menu
|
||||||
initBurgerMenu({ includeHome: true });
|
initBurgerMenu({ includeHome: true });
|
||||||
@@ -345,13 +341,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document
|
function attachTimeRangeListener() {
|
||||||
.getElementById("timeRangeFilter")
|
const timeRangeFilter = document.getElementById("timeRangeFilter");
|
||||||
.addEventListener("change", function () {
|
if (timeRangeFilter) {
|
||||||
// Save selection to localStorage
|
timeRangeFilter.addEventListener("change", function () {
|
||||||
localStorage.setItem("lastSleepTimeRange", this.value);
|
// Save selection to localStorage
|
||||||
loadData();
|
localStorage.setItem("lastSleepTimeRange", this.value);
|
||||||
});
|
renderCharts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
@@ -443,6 +442,9 @@
|
|||||||
attachChildFilterListener();
|
attachChildFilterListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always attach time range listener after recreating the controls
|
||||||
|
attachTimeRangeListener();
|
||||||
|
|
||||||
renderCharts();
|
renderCharts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading data:", error);
|
console.error("Error loading data:", error);
|
||||||
|
|||||||
Reference in New Issue
Block a user