diff --git a/README.md b/README.md
index 3a3b3ff..24122e0 100644
--- a/README.md
+++ b/README.md
@@ -37,8 +37,6 @@ open http://localhost:8000
### Using Docker Compose
```yaml
-version: "3.8"
-
services:
baby-monitor:
image: gitea.gt-proj.com/brian/baby-monitor:latest
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..22536ac
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,60 @@
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "8000:8000"
+ volumes:
+ # Persist database to local directory
+ - ./data:/data
+ environment:
+ # Set environment to production
+ - ENVIRONMENT=production
+ # Set admin credentials
+ - ADMIN_USERNAME=admin
+ - ADMIN_PASSWORD=password
+ # Use Redis for token storage
+ - REDIS_URI=redis://redis:6379/0
+ # Use PostgreSQL for database
+ - DATABASE_URL=postgresql://baby_monitor:securepassword@postgres:5432/b
+ command: ["--host", "0.0.0.0", "--port", "8000"]
+ restart: unless-stopped
+ depends_on:
+ redis:
+ condition: service_healthy
+ postgres:
+ condition: service_healthy
+ redis:
+ image: redis:7-alpine
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis-data:/data
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 1s
+ postgres:
+ image: postgres:14-alpine
+ environment:
+ - POSTGRES_USER=baby_monitor
+ - POSTGRES_PASSWORD=securepassword
+ - POSTGRES_DB=baby_monitor_db
+ ports:
+ - "5432:5432"
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U baby_monitor -d baby_monitor_db"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+volumes:
+ redis-data:
+ pgdata:
diff --git a/docker-compose.yml b/docker-compose.yml
index f8a9740..7051d5f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,19 +16,5 @@ services:
# Optional: Override admin credentials for development
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=password
- # Optional: Use Redis for token storage (uncomment redis service below)
- # - REDIS_URI=redis://redis:6379/0
command: ["--host", "0.0.0.0", "--port", "8000", "--reload"]
restart: unless-stopped
-
- # Uncomment to enable Redis token storage
- # redis:
- # image: redis:7-alpine
- # ports:
- # - "6379:6379"
- # volumes:
- # - redis-data:/data
- # restart: unless-stopped
-# Uncomment if using Redis
-# volumes:
-# redis-data:
diff --git a/src/baby_monitor/main.py b/src/baby_monitor/main.py
index aeeef30..0d45a8b 100644
--- a/src/baby_monitor/main.py
+++ b/src/baby_monitor/main.py
@@ -91,6 +91,12 @@ def serve_log_feeding() -> FileResponse:
return FileResponse(static_path / "log-feeding.html")
+@app.get("/feedings.html", include_in_schema=False)
+def serve_feedings() -> FileResponse:
+ """Serve the feedings overview page."""
+ return FileResponse(static_path / "feedings.html")
+
+
@app.get("/api/")
def read_root(token: Annotated[str, Depends(verify_token)]) -> dict:
"""API root endpoint (requires authentication)."""
diff --git a/src/baby_monitor/static/feedings.html b/src/baby_monitor/static/feedings.html
new file mode 100644
index 0000000..68dc416
--- /dev/null
+++ b/src/baby_monitor/static/feedings.html
@@ -0,0 +1,1058 @@
+
+
+
+
+
+ Feedings - Baby Monitor
+
+
+
+
+
+
+
+
+
+
+
🍼 Feedings
+
Overview of all feeding sessions
+
+
+
+
+
+
+
+
diff --git a/src/baby_monitor/static/index.html b/src/baby_monitor/static/index.html
index 28f6ddf..00d7fca 100644
--- a/src/baby_monitor/static/index.html
+++ b/src/baby_monitor/static/index.html
@@ -216,6 +216,7 @@
@@ -285,6 +286,10 @@
window.location.href = "/add-child.html";
});
+ document.getElementById("menuFeedings").addEventListener("click", () => {
+ window.location.href = "/feedings.html";
+ });
+
document.getElementById("menuLogout").addEventListener("click", () => {
closeMenu();
logout();
diff --git a/src/baby_monitor/static/log-feeding.html b/src/baby_monitor/static/log-feeding.html
index ba0750b..97ee15a 100644
--- a/src/baby_monitor/static/log-feeding.html
+++ b/src/baby_monitor/static/log-feeding.html
@@ -212,14 +212,28 @@
const loadingDiv = document.getElementById("loadingChildren");
const childSelect = document.getElementById("childId");
- // Set default start time to now
- const now = new Date();
- const localDateTime = new Date(
- now.getTime() - now.getTimezoneOffset() * 60000,
- )
- .toISOString()
- .slice(0, 16);
- document.getElementById("startTime").value = localDateTime;
+ // Check if we're editing an existing feeding
+ const urlParams = new URLSearchParams(window.location.search);
+ const feedingId = urlParams.get("id");
+ const isEditMode = !!feedingId;
+
+ // Set default start time to now (only for new feedings)
+ if (!isEditMode) {
+ const now = new Date();
+ const localDateTime = new Date(
+ now.getTime() - now.getTimezoneOffset() * 60000,
+ )
+ .toISOString()
+ .slice(0, 16);
+ document.getElementById("startTime").value = localDateTime;
+ }
+
+ // Update title and button text if editing
+ if (isEditMode) {
+ document.querySelector("h1").textContent = "🍼 Edit Feeding";
+ document.querySelector(".subtitle").textContent = "Update feeding session details";
+ submitBtn.textContent = "Update Feeding";
+ }
// Load user's children
async function loadChildren() {
@@ -255,6 +269,11 @@
// Show form, hide loading
loadingDiv.style.display = "none";
form.style.display = "block";
+
+ // If editing, load the feeding data
+ if (isEditMode) {
+ loadFeedingData();
+ }
} else {
showMessage("Failed to load children", "error");
}
@@ -263,6 +282,44 @@
}
}
+ async function loadFeedingData() {
+ try {
+ const response = await fetch(`/api/feedings/${feedingId}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (response.ok) {
+ const feeding = await response.json();
+
+ // Populate form with existing data
+ document.getElementById("childId").value = feeding.child_id;
+
+ // Format datetime for datetime-local input
+ const startTime = new Date(feeding.start_time);
+ const startLocalDateTime = new Date(
+ startTime.getTime() - startTime.getTimezoneOffset() * 60000
+ ).toISOString().slice(0, 16);
+ document.getElementById("startTime").value = startLocalDateTime;
+
+ if (feeding.end_time) {
+ const endTime = new Date(feeding.end_time);
+ const endLocalDateTime = new Date(
+ endTime.getTime() - endTime.getTimezoneOffset() * 60000
+ ).toISOString().slice(0, 16);
+ document.getElementById("endTime").value = endLocalDateTime;
+ }
+
+ document.getElementById("feedingType").value = feeding.feeding_type;
+ } else {
+ showMessage("Failed to load feeding data", "error");
+ }
+ } catch (error) {
+ showMessage("Network error loading feeding", "error");
+ }
+ }
+
loadChildren();
form.addEventListener("submit", async (e) => {
@@ -275,12 +332,15 @@
// Disable button during request
submitBtn.disabled = true;
- submitBtn.textContent = "Logging...";
+ submitBtn.textContent = isEditMode ? "Updating..." : "Logging...";
messageDiv.classList.remove("show");
try {
- const response = await fetch("/api/feedings", {
- method: "POST",
+ const url = isEditMode ? `/api/feedings/${feedingId}` : "/api/feedings";
+ const method = isEditMode ? "PUT" : "POST";
+
+ const response = await fetch(url, {
+ method: method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
@@ -296,14 +356,17 @@
const data = await response.json();
if (response.ok) {
- showMessage("Feeding logged successfully!", "success");
- // Redirect to home page after 1 second
+ showMessage(
+ isEditMode ? "Feeding updated successfully!" : "Feeding logged successfully!",
+ "success"
+ );
+ // Redirect to feedings page after 1 second
setTimeout(() => {
- window.location.href = "/";
+ window.location.href = "/feedings.html";
}, 1000);
} else {
showMessage(
- data.detail || "Failed to log feeding. Please try again.",
+ data.detail || `Failed to ${isEditMode ? 'update' : 'log'} feeding. Please try again.`,
"error",
);
}
@@ -311,7 +374,7 @@
showMessage("Network error. Please try again.", "error");
} finally {
submitBtn.disabled = false;
- submitBtn.textContent = "Log Feeding";
+ submitBtn.textContent = isEditMode ? "Update Feeding" : "Log Feeding";
}
});