diff --git a/.env.example b/.env.example index ebda3b0..9a305b5 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ # cp .env.example .env # ── MySQL ───────────────────────────────────────────────────────────────────── +# Use strong random passwords — generate with: openssl rand -hex 16 MYSQL_ROOT_PASSWORD=change_me MYSQL_DATABASE=eggtracker MYSQL_USER=eggtracker @@ -9,6 +10,7 @@ MYSQL_PASSWORD=change_me # ── Super admin ─────────────────────────────────────────────────────────────── # This account is created (and its password synced) automatically on every startup. +# Use a strong password of at least 10 characters. ADMIN_USERNAME=admin ADMIN_PASSWORD=change_me diff --git a/backend/main.py b/backend/main.py index 60615bf..252291c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -91,6 +91,15 @@ def _run_migrations(): except Exception: db.rollback() # index already exists — safe to ignore + # v2.3 — unique constraint on flock_history (user_id, date) + try: + db.execute(text( + "ALTER TABLE flock_history ADD CONSTRAINT uq_flock_user_date UNIQUE (user_id, date)" + )) + db.commit() + except Exception: + db.rollback() # constraint already exists — safe to ignore + @asynccontextmanager async def lifespan(app: FastAPI): diff --git a/backend/models.py b/backend/models.py index 48020b1..7f40f42 100644 --- a/backend/models.py +++ b/backend/models.py @@ -30,7 +30,10 @@ class EggCollection(Base): class FlockHistory(Base): __tablename__ = "flock_history" - __table_args__ = (Index("ix_flock_history_user_date", "user_id", "date"),) + __table_args__ = ( + UniqueConstraint("user_id", "date", name="uq_flock_user_date"), + Index("ix_flock_history_user_date", "user_id", "date"), + ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) diff --git a/nginx/html/404.html b/nginx/html/404.html index caf2a51..fe3bbb0 100644 --- a/nginx/html/404.html +++ b/nginx/html/404.html @@ -30,6 +30,6 @@ Go to Dashboard - + diff --git a/nginx/html/admin.html b/nginx/html/admin.html index 022730f..ef2ad0d 100644 --- a/nginx/html/admin.html +++ b/nginx/html/admin.html @@ -77,8 +77,8 @@ - - - + + + diff --git a/nginx/html/budget.html b/nginx/html/budget.html index 2176229..c490712 100644 --- a/nginx/html/budget.html +++ b/nginx/html/budget.html @@ -122,8 +122,8 @@ - - - + + + diff --git a/nginx/html/flock.html b/nginx/html/flock.html index ba3083f..8dd546f 100644 --- a/nginx/html/flock.html +++ b/nginx/html/flock.html @@ -79,8 +79,8 @@ - - - + + + diff --git a/nginx/html/index.html b/nginx/html/index.html index f2dd98f..ef720d2 100644 --- a/nginx/html/index.html +++ b/nginx/html/index.html @@ -68,12 +68,11 @@ -

View full history →

- - - + + + diff --git a/nginx/html/js/auth.js b/nginx/html/js/auth.js index 93da8f9..6dc6947 100644 --- a/nginx/html/js/auth.js +++ b/nginx/html/js/auth.js @@ -157,7 +157,7 @@ function initNav() {
- +
diff --git a/nginx/html/js/summary.js b/nginx/html/js/summary.js index 34278e1..1221951 100644 --- a/nginx/html/js/summary.js +++ b/nginx/html/js/summary.js @@ -18,7 +18,8 @@ function buildChart(rows) { const labels = display.map(r => r.month_label); const data = display.map(r => r.total_eggs); - const ctx = document.getElementById('monthly-chart').getContext('2d'); + const ctx = document.getElementById('monthly-chart').getContext('2d'); + const green = getComputedStyle(document.documentElement).getPropertyValue('--green').trim(); if (monthlyChart) monthlyChart.destroy(); monthlyChart = new Chart(ctx, { @@ -28,7 +29,7 @@ function buildChart(rows) { datasets: [{ data, backgroundColor: 'rgba(61,107,79,0.75)', - borderColor: '#3d6b4f', + borderColor: green, borderWidth: 1, borderRadius: 4, }], @@ -100,7 +101,7 @@ async function exportCSV() { try { const [eggsData, flockAll, feedData] = await Promise.all([ - API.get('/api/eggs'), + API.get('/api/eggs?limit=10000'), API.get('/api/flock'), API.get('/api/feed'), ]); diff --git a/nginx/html/log.html b/nginx/html/log.html index 5d1c101..6a87091 100644 --- a/nginx/html/log.html +++ b/nginx/html/log.html @@ -83,9 +83,9 @@
- - - - + + + + diff --git a/nginx/html/login.html b/nginx/html/login.html index 7f95219..1a14423 100644 --- a/nginx/html/login.html +++ b/nginx/html/login.html @@ -42,7 +42,7 @@
- +
@@ -114,6 +114,7 @@ body: JSON.stringify({ username, password }), }); const data = await res.json(); + if (res.status === 429) { showError('login-msg', 'Too many attempts — please wait a minute and try again.'); return; } if (!res.ok) { showError('login-msg', data.detail || 'Login failed'); return; } localStorage.setItem('token', data.access_token); window.location.href = '/'; diff --git a/nginx/html/summary.html b/nginx/html/summary.html index 7430427..bf53a47 100644 --- a/nginx/html/summary.html +++ b/nginx/html/summary.html @@ -67,8 +67,8 @@ - - - + + +