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 @@
{% for entry in recent_entries %}
-
+
{{ entry.category.name }}
{{ entry.entry_date|date:"M j" }}
@@ -253,6 +277,13 @@
Energy {{ entry.energy_score }}/10
{{ entry.deep_work_minutes }} min
+
{% 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;
+ }
+}