Fix bugs, data integrity, and cache busting
- models.py: add UniqueConstraint(user_id, date) to flock_history so duplicate flock entries for the same day are rejected at the DB level - main.py: v2.3 migration applies the new unique constraint to existing installs at startup - login.html: update register form minlength and placeholder from 6 to 10 characters to match backend; add specific 429 error message so rate- limited users see "Too many attempts — please wait a minute" instead of a generic failure - auth.js: update settings modal password input minlength from 6 to 10 - summary.js: fix CSV export truncation — pass limit=10000 so users with more than 500 days of data get a complete export; read chart border color from --green CSS variable instead of hardcoded hex - All HTML files: bump JS version params to ?v=4 so browsers discard cached copies of files changed across recent sessions (api.js, auth.js, dashboard.js, history.js, log.js, flock.js, budget.js, summary.js, admin.js) - .env.example: add password strength guidance for MySQL and admin vars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
# cp .env.example .env
|
# cp .env.example .env
|
||||||
|
|
||||||
# ── MySQL ─────────────────────────────────────────────────────────────────────
|
# ── MySQL ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Use strong random passwords — generate with: openssl rand -hex 16
|
||||||
MYSQL_ROOT_PASSWORD=change_me
|
MYSQL_ROOT_PASSWORD=change_me
|
||||||
MYSQL_DATABASE=eggtracker
|
MYSQL_DATABASE=eggtracker
|
||||||
MYSQL_USER=eggtracker
|
MYSQL_USER=eggtracker
|
||||||
@@ -9,6 +10,7 @@ MYSQL_PASSWORD=change_me
|
|||||||
|
|
||||||
# ── Super admin ───────────────────────────────────────────────────────────────
|
# ── Super admin ───────────────────────────────────────────────────────────────
|
||||||
# This account is created (and its password synced) automatically on every startup.
|
# 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_USERNAME=admin
|
||||||
ADMIN_PASSWORD=change_me
|
ADMIN_PASSWORD=change_me
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,15 @@ def _run_migrations():
|
|||||||
except Exception:
|
except Exception:
|
||||||
db.rollback() # index already exists — safe to ignore
|
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
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ class EggCollection(Base):
|
|||||||
|
|
||||||
class FlockHistory(Base):
|
class FlockHistory(Base):
|
||||||
__tablename__ = "flock_history"
|
__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)
|
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)
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
|||||||
@@ -30,6 +30,6 @@
|
|||||||
<a href="/" class="btn btn-primary">Go to Dashboard</a>
|
<a href="/" class="btn btn-primary">Go to Dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js?v=4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/js/api.js?v=3"></script>
|
<script src="/js/api.js?v=4"></script>
|
||||||
<script src="/js/auth.js?v=3"></script>
|
<script src="/js/auth.js?v=4"></script>
|
||||||
<script src="/js/admin.js?v=3"></script>
|
<script src="/js/admin.js?v=4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -122,8 +122,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/js/api.js?v=3"></script>
|
<script src="/js/api.js?v=4"></script>
|
||||||
<script src="/js/auth.js?v=3"></script>
|
<script src="/js/auth.js?v=4"></script>
|
||||||
<script src="/js/budget.js?v=3"></script>
|
<script src="/js/budget.js?v=4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -79,8 +79,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/js/api.js?v=3"></script>
|
<script src="/js/api.js?v=4"></script>
|
||||||
<script src="/js/auth.js?v=3"></script>
|
<script src="/js/auth.js?v=4"></script>
|
||||||
<script src="/js/flock.js"></script>
|
<script src="/js/flock.js?v=4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -68,12 +68,11 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-muted mt-1"><a href="/history">View full history →</a></p>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<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="/js/api.js?v=3"></script>
|
<script src="/js/api.js?v=4"></script>
|
||||||
<script src="/js/auth.js?v=3"></script>
|
<script src="/js/auth.js?v=4"></script>
|
||||||
<script src="/js/dashboard.js?v=2"></script>
|
<script src="/js/dashboard.js?v=4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ function initNav() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:0.75rem">
|
<div class="form-group" style="margin-bottom:0.75rem">
|
||||||
<label>New Password</label>
|
<label>New Password</label>
|
||||||
<input type="password" id="pw-new" autocomplete="new-password" minlength="6">
|
<input type="password" id="pw-new" autocomplete="new-password" minlength="10">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:1rem">
|
<div class="form-group" style="margin-bottom:1rem">
|
||||||
<label>Confirm New Password</label>
|
<label>Confirm New Password</label>
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ function buildChart(rows) {
|
|||||||
const labels = display.map(r => r.month_label);
|
const labels = display.map(r => r.month_label);
|
||||||
const data = display.map(r => r.total_eggs);
|
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();
|
if (monthlyChart) monthlyChart.destroy();
|
||||||
|
|
||||||
monthlyChart = new Chart(ctx, {
|
monthlyChart = new Chart(ctx, {
|
||||||
@@ -28,7 +29,7 @@ function buildChart(rows) {
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
data,
|
data,
|
||||||
backgroundColor: 'rgba(61,107,79,0.75)',
|
backgroundColor: 'rgba(61,107,79,0.75)',
|
||||||
borderColor: '#3d6b4f',
|
borderColor: green,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
}],
|
}],
|
||||||
@@ -100,7 +101,7 @@ async function exportCSV() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [eggsData, flockAll, feedData] = await Promise.all([
|
const [eggsData, flockAll, feedData] = await Promise.all([
|
||||||
API.get('/api/eggs'),
|
API.get('/api/eggs?limit=10000'),
|
||||||
API.get('/api/flock'),
|
API.get('/api/flock'),
|
||||||
API.get('/api/feed'),
|
API.get('/api/feed'),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -83,9 +83,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/js/api.js?v=3"></script>
|
<script src="/js/api.js?v=4"></script>
|
||||||
<script src="/js/auth.js?v=3"></script>
|
<script src="/js/auth.js?v=4"></script>
|
||||||
<script src="/js/log.js"></script>
|
<script src="/js/log.js?v=4"></script>
|
||||||
<script src="/js/history.js"></script>
|
<script src="/js/history.js?v=4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:1rem">
|
<div class="form-group" style="margin-bottom:1rem">
|
||||||
<label for="reg-password">Password</label>
|
<label for="reg-password">Password</label>
|
||||||
<input type="password" id="reg-password" autocomplete="new-password" required minlength="6" placeholder="min 6 characters">
|
<input type="password" id="reg-password" autocomplete="new-password" required minlength="10" placeholder="min 10 characters">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:1.5rem">
|
<div class="form-group" style="margin-bottom:1.5rem">
|
||||||
<label for="reg-confirm">Confirm Password</label>
|
<label for="reg-confirm">Confirm Password</label>
|
||||||
@@ -114,6 +114,7 @@
|
|||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
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; }
|
if (!res.ok) { showError('login-msg', data.detail || 'Login failed'); return; }
|
||||||
localStorage.setItem('token', data.access_token);
|
localStorage.setItem('token', data.access_token);
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
|
|||||||
@@ -67,8 +67,8 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<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="/js/api.js?v=3"></script>
|
<script src="/js/api.js?v=4"></script>
|
||||||
<script src="/js/auth.js?v=3"></script>
|
<script src="/js/auth.js?v=4"></script>
|
||||||
<script src="/js/summary.js?v=2"></script>
|
<script src="/js/summary.js?v=4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user