diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 1df2733..6569460 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/core/entry_list.html b/core/templates/core/entry_list.html index 9e171ff..3a21dcc 100644 --- a/core/templates/core/entry_list.html +++ b/core/templates/core/entry_list.html @@ -18,7 +18,7 @@
History

{% if is_demo_mode %}Demo momentum entries{% else %}Your momentum entries{% endif %}

-

{% if is_demo_mode %}Browse the seeded sample history, then create an account when you want private tracking.{% else %}Filter your private check-ins by category and open any one for its detail view.{% endif %}

+

{% if is_demo_mode %}Browse the seeded sample history, then create an account when you want private tracking.{% else %}Filter your private check-ins, spot patterns faster, and open any entry for the full detail view.{% endif %}

Back to dashboard @@ -35,6 +35,37 @@
+
+
+
+ Total check-ins + {{ history_overview.total_entries }} +

{% if history_overview.latest_entry %}Latest: {{ history_overview.latest_entry.entry_date|date:"M j, Y" }}{% else %}No entries yet{% endif %}

+
+
+
+
+ Avg. momentum + {{ history_overview.avg_momentum }}/10 +

Focus {{ history_overview.avg_focus }}/10 · Energy {{ history_overview.avg_energy }}/10

+
+
+
+
+ Current streak + {{ history_overview.streak }} day{{ history_overview.streak|pluralize }} +

Top lane: {{ history_overview.top_category }}

+
+
+
+
+ Deep work total + {{ history_overview.total_minutes }}m +

{% if is_demo_mode %}Sample time invested{% else %}Time protected for focused work{% endif %}

+
+
+
+
All categories {% for category in categories %} @@ -43,10 +74,85 @@
{% if entries %} +
+
+
+
+
+ Recent pattern +

Last {{ recent_activity|length }} check-ins at a glance

+

Quick comparison bars make it easier to notice whether focus, energy, or deep work is drifting.

+
+
+
+ {% for item in recent_activity %} +
+
+
+ {{ item.entry.category.name }} +

{{ item.entry.title }}

+
+ +
+
+
+ Focus +
+ {{ item.entry.focus_score }}/10 +
+
+ Energy +
+ {{ item.entry.energy_score }}/10 +
+
+ Deep work +
+ {{ item.entry.deep_work_minutes }}m +
+
+
+ {% endfor %} +
+
+
+
+ +
+
+
{% for entry in entries %}
-
+
{{ entry.category.name }} @@ -58,6 +164,13 @@ Energy {{ entry.energy_score }}/10 {{ entry.deep_work_minutes }} min
+
+
+ Momentum score + {{ entry.momentum_score }}/10 +
+ View details +
{% endfor %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 81b349f..89ad168 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -211,6 +211,16 @@ {% if is_demo_mode %}Open demo history{% else %}Open your history{% endif %}
+
+
+ Compare your week +

Two bars per day show whether focus and energy are moving together or drifting apart.

+
+
+ Focus + Energy +
+
{% for day in weekly_trend %}
@@ -224,6 +234,20 @@
{% endfor %}
+
+
+ Check-in days + {{ weekly_summary.check_in_days }}/7 +
+
+ Deep work logged + {{ weekly_summary.total_minutes }}m +
+
+ Strongest day + {{ weekly_summary.strongest_label }} · {{ weekly_summary.strongest_score }}/10 +
+
@@ -241,7 +265,7 @@
{% for entry in recent_entries %}
-
+
{{ entry.category.name }} @@ -253,6 +277,13 @@ Energy {{ entry.energy_score }}/10 {{ entry.deep_work_minutes }} min
+
+
+ Momentum score + {{ entry.momentum_score }}/10 +
+ View details +
{% endfor %} diff --git a/core/views.py b/core/views.py index 992803c..d025d5b 100644 --- a/core/views.py +++ b/core/views.py @@ -71,6 +71,133 @@ def _build_weekly_trend(entries): return trend +def _build_weekly_summary(weekly_trend): + check_in_days = sum(1 for day in weekly_trend if day["focus"] or day["energy"] or day["minutes"]) + total_minutes = sum(day["minutes"] for day in weekly_trend) + strongest_day = max( + weekly_trend, + key=lambda day: (day["focus"] + day["energy"], day["minutes"]), + default=None, + ) + strongest_has_data = bool( + strongest_day and (strongest_day["focus"] or strongest_day["energy"] or strongest_day["minutes"]) + ) + strongest_score = round(((strongest_day["focus"] + strongest_day["energy"]) / 2), 1) if strongest_has_data else 0 + return { + "check_in_days": check_in_days, + "total_minutes": total_minutes, + "strongest_label": strongest_day["label"] if strongest_has_data else "No data yet", + "strongest_score": strongest_score, + } + + +def _build_history_overview(entries): + totals = entries.aggregate( + total_entries=Count("id"), + avg_focus=Coalesce(Avg("focus_score"), 0.0), + avg_energy=Coalesce(Avg("energy_score"), 0.0), + total_minutes=Coalesce(Sum("deep_work_minutes"), 0), + ) + avg_focus = float(totals["avg_focus"] or 0) + avg_energy = float(totals["avg_energy"] or 0) + avg_momentum = round((avg_focus + avg_energy) / 2, 1) if totals["total_entries"] else 0 + + ordered_dates = [] + seen_dates = set() + for entry_date in entries.values_list("entry_date", flat=True): + if entry_date not in seen_dates: + ordered_dates.append(entry_date) + seen_dates.add(entry_date) + + streak = 0 + previous_date = None + for entry_date in ordered_dates: + if previous_date is None: + streak = 1 + previous_date = entry_date + continue + if previous_date - timedelta(days=1) == entry_date: + streak += 1 + previous_date = entry_date + continue + break + + top_category = ( + entries.values("category__name") + .annotate(total=Count("id")) + .order_by("-total", "category__name") + .first() + ) + + latest_entry = entries.first() + return { + "total_entries": totals["total_entries"], + "avg_focus": round(avg_focus, 1), + "avg_energy": round(avg_energy, 1), + "avg_momentum": avg_momentum, + "total_minutes": int(totals["total_minutes"] or 0), + "streak": streak if totals["total_entries"] else 0, + "latest_entry": latest_entry, + "top_category": top_category["category__name"] if top_category else "No category yet", + } + + +def _build_recent_activity(entries, limit=7): + recent_entries = list(entries[:limit]) + if not recent_entries: + return [] + + recent_entries.reverse() + max_minutes = max((entry.deep_work_minutes for entry in recent_entries), default=0) + activity = [] + for entry in recent_entries: + momentum = float(entry.momentum_score) + minutes_width = 0 + if entry.deep_work_minutes and max_minutes: + minutes_width = max(14, int(round((entry.deep_work_minutes / max_minutes) * 100))) + activity.append( + { + "entry": entry, + "focus_width": entry.focus_score * 10, + "energy_width": entry.energy_score * 10, + "momentum_width": int(round(momentum * 10)), + "minutes_width": minutes_width, + } + ) + return activity + + +def _build_category_breakdown(entries): + grouped = list( + entries.values("category__name", "category__slug", "category__accent_color") + .annotate( + entry_total=Count("id"), + avg_focus=Coalesce(Avg("focus_score"), 0.0), + avg_energy=Coalesce(Avg("energy_score"), 0.0), + total_minutes=Coalesce(Sum("deep_work_minutes"), 0), + ) + .order_by("-entry_total", "category__name")[:4] + ) + total_entries = sum(item["entry_total"] for item in grouped) or 1 + + breakdown = [] + for item in grouped: + avg_momentum = round((float(item["avg_focus"] or 0) + float(item["avg_energy"] or 0)) / 2, 1) + breakdown.append( + { + "name": item["category__name"], + "slug": item["category__slug"], + "accent_color": item["category__accent_color"] or "#0F766E", + "entry_total": item["entry_total"], + "total_minutes": int(item["total_minutes"] or 0), + "avg_momentum": avg_momentum, + "share_percent": max(8, int(round((item["entry_total"] / total_entries) * 100))), + "momentum_width": max(8, int(round(avg_momentum * 10))) if avg_momentum else 0, + } + ) + return breakdown + + def _dashboard_context(request): entries = _entries_for_request(request) recent_entries = entries[:6] @@ -106,6 +233,7 @@ def _dashboard_context(request): "recent_entries": recent_entries, "categories": _categories_for_request(request), "weekly_trend": weekly_trend, + "weekly_summary": _build_weekly_summary(weekly_trend), "is_demo_mode": not request.user.is_authenticated, "stats": { "total_entries": totals["total_entries"], @@ -170,6 +298,9 @@ def entry_list(request): "categories": _categories_for_request(request), "selected_slug": selected_slug, "is_demo_mode": not request.user.is_authenticated, + "history_overview": _build_history_overview(entries), + "recent_activity": _build_recent_activity(entries), + "category_breakdown": _build_category_breakdown(entries), } return render(request, "core/entry_list.html", context) diff --git a/static/css/custom.css b/static/css/custom.css index 951a829..59b781d 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -589,3 +589,228 @@ p { width: fit-content; } } + + +/* Step 3 history + chart polish */ +.chart-panel-top { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + margin-bottom: 1.5rem; +} + +.chart-panel-top-inline { + align-items: center; +} + +.chart-intro { + max-width: 620px; + color: var(--brand-muted); +} + +.chart-legend { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; +} + +.legend-pill { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.5rem 0.8rem; + border-radius: 999px; + background: rgba(248, 250, 252, 0.92); + border: 1px solid var(--brand-border); + font-size: 0.88rem; + color: var(--brand-ink); +} + +.legend-dot { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; +} + +.legend-dot.focus { + background: linear-gradient(180deg, var(--brand-primary), var(--brand-highlight)); +} + +.legend-dot.energy { + background: linear-gradient(180deg, var(--brand-secondary), var(--brand-accent)); +} + +.trend-summary-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +.trend-summary-card, +.summary-stat-card { + background: rgba(255, 255, 255, 0.72); + border: 1px solid var(--brand-border); + border-radius: 24px; + padding: 1.2rem 1.25rem; +} + +.trend-summary-card span, +.summary-stat-card span, +.mini-meter span, +.lane-meta span, +.score-track-item span { + color: var(--brand-muted); + font-size: 0.88rem; +} + +.trend-summary-card strong, +.summary-stat-card strong { + display: block; + font-size: 1.4rem; + margin-top: 0.35rem; +} + +.summary-stat-card p { + margin: 0.65rem 0 0; + color: var(--brand-muted); +} + +.activity-list, +.lane-list { + display: grid; + gap: 1rem; +} + +.activity-row, +.lane-card { + padding: 1.15rem; + border-radius: 20px; + background: rgba(255, 255, 255, 0.7); + border: 1px solid var(--brand-border); +} + +.activity-row-head, +.lane-card-top, +.entry-card-footer, +.lane-meta { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.activity-row-head h3 { + margin: 0.45rem 0 0; + font-size: 1.05rem; +} + +.score-track-group { + display: grid; + gap: 0.7rem; + margin-top: 1rem; +} + +.score-track-item { + display: grid; + grid-template-columns: 72px minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; +} + +.score-track { + width: 100%; + height: 10px; + border-radius: 999px; + background: rgba(203, 213, 225, 0.5); + overflow: hidden; +} + +.score-fill { + height: 100%; + border-radius: inherit; +} + +.score-fill.focus { + background: linear-gradient(90deg, var(--brand-primary), var(--brand-highlight)); +} + +.score-fill.energy { + background: linear-gradient(90deg, var(--brand-secondary), var(--brand-accent)); +} + +.score-fill.minutes { + background: linear-gradient(90deg, var(--brand-ink), #334155); +} + +.lane-card-top strong { + display: block; +} + +.lane-card-top span, +.lane-meta a { + color: var(--brand-muted); + text-decoration: none; +} + +.lane-track { + height: 12px; + border-radius: 999px; + background: rgba(203, 213, 225, 0.45); + overflow: hidden; + margin: 0.95rem 0 0.8rem; +} + +.lane-fill { + height: 100%; + border-radius: inherit; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12); +} + +.entry-card-enhanced { + position: relative; + overflow: hidden; +} + +.entry-card-enhanced::after { + content: ""; + position: absolute; + inset: auto 0 0 0; + height: 4px; + background: linear-gradient(90deg, var(--entry-accent, var(--brand-primary)), var(--brand-highlight)); +} + +.entry-card-footer { + margin-top: 1.2rem; + padding-top: 1rem; + border-top: 1px solid rgba(148, 163, 184, 0.2); +} + +.mini-meter strong { + display: block; + font-size: 1rem; + margin-top: 0.25rem; +} + +@media (max-width: 991.98px) { + .trend-summary-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 767.98px) { + .chart-panel-top, + .activity-row-head, + .lane-card-top, + .entry-card-footer, + .lane-meta { + flex-direction: column; + align-items: flex-start; + } + + .score-track-item { + grid-template-columns: 1fr; + } +} diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 951a829..59b781d 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -589,3 +589,228 @@ p { width: fit-content; } } + + +/* Step 3 history + chart polish */ +.chart-panel-top { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + margin-bottom: 1.5rem; +} + +.chart-panel-top-inline { + align-items: center; +} + +.chart-intro { + max-width: 620px; + color: var(--brand-muted); +} + +.chart-legend { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; +} + +.legend-pill { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.5rem 0.8rem; + border-radius: 999px; + background: rgba(248, 250, 252, 0.92); + border: 1px solid var(--brand-border); + font-size: 0.88rem; + color: var(--brand-ink); +} + +.legend-dot { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; +} + +.legend-dot.focus { + background: linear-gradient(180deg, var(--brand-primary), var(--brand-highlight)); +} + +.legend-dot.energy { + background: linear-gradient(180deg, var(--brand-secondary), var(--brand-accent)); +} + +.trend-summary-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +.trend-summary-card, +.summary-stat-card { + background: rgba(255, 255, 255, 0.72); + border: 1px solid var(--brand-border); + border-radius: 24px; + padding: 1.2rem 1.25rem; +} + +.trend-summary-card span, +.summary-stat-card span, +.mini-meter span, +.lane-meta span, +.score-track-item span { + color: var(--brand-muted); + font-size: 0.88rem; +} + +.trend-summary-card strong, +.summary-stat-card strong { + display: block; + font-size: 1.4rem; + margin-top: 0.35rem; +} + +.summary-stat-card p { + margin: 0.65rem 0 0; + color: var(--brand-muted); +} + +.activity-list, +.lane-list { + display: grid; + gap: 1rem; +} + +.activity-row, +.lane-card { + padding: 1.15rem; + border-radius: 20px; + background: rgba(255, 255, 255, 0.7); + border: 1px solid var(--brand-border); +} + +.activity-row-head, +.lane-card-top, +.entry-card-footer, +.lane-meta { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.activity-row-head h3 { + margin: 0.45rem 0 0; + font-size: 1.05rem; +} + +.score-track-group { + display: grid; + gap: 0.7rem; + margin-top: 1rem; +} + +.score-track-item { + display: grid; + grid-template-columns: 72px minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; +} + +.score-track { + width: 100%; + height: 10px; + border-radius: 999px; + background: rgba(203, 213, 225, 0.5); + overflow: hidden; +} + +.score-fill { + height: 100%; + border-radius: inherit; +} + +.score-fill.focus { + background: linear-gradient(90deg, var(--brand-primary), var(--brand-highlight)); +} + +.score-fill.energy { + background: linear-gradient(90deg, var(--brand-secondary), var(--brand-accent)); +} + +.score-fill.minutes { + background: linear-gradient(90deg, var(--brand-ink), #334155); +} + +.lane-card-top strong { + display: block; +} + +.lane-card-top span, +.lane-meta a { + color: var(--brand-muted); + text-decoration: none; +} + +.lane-track { + height: 12px; + border-radius: 999px; + background: rgba(203, 213, 225, 0.45); + overflow: hidden; + margin: 0.95rem 0 0.8rem; +} + +.lane-fill { + height: 100%; + border-radius: inherit; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12); +} + +.entry-card-enhanced { + position: relative; + overflow: hidden; +} + +.entry-card-enhanced::after { + content: ""; + position: absolute; + inset: auto 0 0 0; + height: 4px; + background: linear-gradient(90deg, var(--entry-accent, var(--brand-primary)), var(--brand-highlight)); +} + +.entry-card-footer { + margin-top: 1.2rem; + padding-top: 1rem; + border-top: 1px solid rgba(148, 163, 184, 0.2); +} + +.mini-meter strong { + display: block; + font-size: 1rem; + margin-top: 0.25rem; +} + +@media (max-width: 991.98px) { + .trend-summary-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 767.98px) { + .chart-panel-top, + .activity-row-head, + .lane-card-top, + .entry-card-footer, + .lane-meta { + flex-direction: column; + align-items: flex-start; + } + + .score-track-item { + grid-template-columns: 1fr; + } +}