from datetime import date, datetime, timedelta from flask import Blueprint, render_template, redirect, request, url_for from flask_login import login_required, current_user from pytz import utc from app.models import Reading from app.insights import DateRange, user_timezone, parse_range, readings_query, summary, annotate, chart_data main = Blueprint('main', __name__) PAGE_SIZE = 25 @main.route('/') def landing(): return redirect(url_for('main.dashboard')) if current_user.is_authenticated else render_template('landing.html') @main.route('/health') def health(): return 'OK' @main.route('/dashboard') @login_required def dashboard(): view = request.args.get('view', 'overview') if view not in ('overview', 'history', 'trends'): view = 'overview' tz = user_timezone() mode = request.args.get('mode', 'readings') if view == 'history' else 'readings' calendar = None try: selected = parse_range(request.args, tz, 7 if view == 'overview' else 30) if mode == 'calendar': unit = request.args.get('unit', 'month') if unit not in ('week', 'month'): raise ValueError('Choose a week or month calendar.') anchor = date.fromisoformat(request.args.get('anchor', datetime.now(tz).date().isoformat())) if not 1900 <= anchor.year <= 2099: raise ValueError('Choose a calendar date between 1900 and 2099.') if unit == 'week': start = anchor - timedelta(days=anchor.weekday()) end = start + timedelta(days=6) prev, following = start - timedelta(days=7), start + timedelta(days=7) else: start = anchor.replace(day=1) following = (start + timedelta(days=32)).replace(day=1) end = following - timedelta(days=1) prev = (start - timedelta(days=1)).replace(day=1) selected = DateRange(start, end, tz) calendar = dict(unit=unit, anchor=anchor.isoformat(), prev=prev.isoformat(), next=following.isoformat()) except ValueError as error: return render_template('error.html', message=str(error)), 400 query = readings_query(current_user.id, selected) stats = summary(query) pagination = None readings = [] latest = None chart = None if view == 'overview': latest = readings_query(current_user.id).order_by(Reading.timestamp.desc(), Reading.id.desc()).first() if latest: annotate([latest], tz) readings = annotate(query.order_by(Reading.timestamp.desc(), Reading.id.desc()).limit(5).all(), tz) elif mode != 'calendar': if view == 'history': page = max(1, request.args.get('page', 1, type=int)) pagination = query.order_by(Reading.timestamp.desc(), Reading.id.desc()).paginate(page=page, per_page=PAGE_SIZE, error_out=False) readings = annotate(pagination.items, tz) if view in ('overview', 'trends') or calendar: chart = chart_data(query, selected) if calendar: by_day = {p['date']: p for p in chart['points']} calendar['days'] = [dict(date=(selected.start + timedelta(days=i)), data=by_day.get((selected.start + timedelta(days=i)).isoformat())) for i in range((selected.end - selected.start).days + 1)] profile = current_user.profile active_days = (selected.end - selected.start).days + 1 if selected.end == datetime.now(tz).date() else None return render_template('dashboard.html', view=view, mode=mode, selected=selected, stats=stats, readings=readings, latest=latest, chart=chart, pagination=pagination, calendar=calendar, timezone_name=str(tz), active_days=active_days, sys_threshold=(profile.systolic_threshold or 140) if profile else 140, dia_threshold=(profile.diastolic_threshold or 90) if profile else 90) @main.route('/dashboard/report') @login_required def report(): tz = user_timezone() try: selected = parse_range(request.args, tz, 30) except ValueError as error: return render_template('error.html', message=str(error)), 400 query = readings_query(current_user.id, selected) readings = annotate(query.order_by(Reading.timestamp, Reading.id).all(), tz) return render_template('report.html', selected=selected, stats=summary(query), readings=readings, timezone_name=str(tz)) # Preserve bookmarks to the former separate views. @main.route('/dashboard/list') @main.route('/dashboard/table') @login_required def dashboard_table(): params = request.args.to_dict() params['view'] = 'history' return redirect(url_for('main.dashboard', **params)) @main.route('/dashboard/graph') @login_required def dashboard_graph(): params = request.args.to_dict() params['view'] = 'trends' return redirect(url_for('main.dashboard', **params)) @main.route('/dashboard/weekly') @main.route('/dashboard/monthly') @login_required def dashboard_calendar(): unit = 'week' if request.path.endswith('weekly') else 'month' anchor = datetime.now(user_timezone()).date() offset = request.args.get('week_offset' if unit == 'week' else 'month_offset', 0, type=int) try: if unit == 'week': anchor += timedelta(weeks=offset) else: month_index = anchor.year * 12 + anchor.month - 1 + offset anchor = date(month_index // 12, month_index % 12 + 1, 1) except (OverflowError, ValueError): return render_template('error.html', message='Choose a valid calendar date.'), 400 return redirect(url_for('main.dashboard', view='history', mode='calendar', unit=unit, anchor=anchor.isoformat())) def _compute_badges(total_readings, timestamps, user_tz, now_local=None): if now_local is None: now_local = datetime.now(user_tz).date() badges = [] if total_readings == 0: return badges streak_count = 0 if timestamps: distinct_dates = [] last_date = None for (ts,) in timestamps: local_date = utc.localize(ts).astimezone(user_tz).date() if local_date != last_date: distinct_dates.append(local_date) last_date = local_date if distinct_dates: most_recent_date = distinct_dates[0] if (now_local - most_recent_date).days <= 1: streak_count = 1 current_check_date = most_recent_date for d in distinct_dates[1:]: if (current_check_date - d).days == 1: streak_count += 1 current_check_date = d else: break if streak_count >= 1: badges.append(f"Current Streak: {streak_count} Days") if streak_count >= 7: badges.append("Logged Every Day for a Week") if streak_count >= 30: badges.append("Monthly Streak") last_7_readings = timestamps[:7] if len(last_7_readings) == 7: if all(5 <= utc.localize(ts).astimezone(user_tz).hour < 12 for (ts,) in last_7_readings): badges.append("Morning Riser: Logged Readings Every Morning for a Week") if all(18 <= utc.localize(ts).astimezone(user_tz).hour <= 23 for (ts,) in last_7_readings): badges.append("Night Owl: Logged Readings Every Night for a Week") milestones = [10, 50, 100, 500, 1000, 5000, 10000] highest_milestone = max((m for m in milestones if total_readings >= m), default=None) if highest_milestone: badges.append(f"{highest_milestone} Readings Logged") return badges