Auto commit: 2026-04-16T13:25:51.679Z

This commit is contained in:
Flatlogic Bot 2026-04-16 13:25:51 +00:00
parent 3a44f34cf9
commit 9b2308aeac
6 changed files with 728 additions and 3 deletions

View File

@ -18,7 +18,7 @@
<div>
<span class="eyebrow">History</span>
<h1>{% if is_demo_mode %}Demo momentum entries{% else %}Your momentum entries{% endif %}</h1>
<p>{% 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 %}</p>
<p>{% 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 %}</p>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<a class="btn btn-ghost" href="{% url 'home' %}">Back to dashboard</a>
@ -35,6 +35,37 @@
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-sm-6 col-xl-3">
<article class="glass-panel summary-stat-card h-100">
<span>Total check-ins</span>
<strong>{{ history_overview.total_entries }}</strong>
<p>{% if history_overview.latest_entry %}Latest: {{ history_overview.latest_entry.entry_date|date:"M j, Y" }}{% else %}No entries yet{% endif %}</p>
</article>
</div>
<div class="col-sm-6 col-xl-3">
<article class="glass-panel summary-stat-card h-100">
<span>Avg. momentum</span>
<strong>{{ history_overview.avg_momentum }}/10</strong>
<p>Focus {{ history_overview.avg_focus }}/10 · Energy {{ history_overview.avg_energy }}/10</p>
</article>
</div>
<div class="col-sm-6 col-xl-3">
<article class="glass-panel summary-stat-card h-100">
<span>Current streak</span>
<strong>{{ history_overview.streak }} day{{ history_overview.streak|pluralize }}</strong>
<p>Top lane: {{ history_overview.top_category }}</p>
</article>
</div>
<div class="col-sm-6 col-xl-3">
<article class="glass-panel summary-stat-card h-100">
<span>Deep work total</span>
<strong>{{ history_overview.total_minutes }}m</strong>
<p>{% if is_demo_mode %}Sample time invested{% else %}Time protected for focused work{% endif %}</p>
</article>
</div>
</div>
<div class="filter-row mb-4">
<a class="filter-chip {% if not selected_slug %}active{% endif %}" href="{% url 'entry_list' %}">All categories</a>
{% for category in categories %}
@ -43,10 +74,85 @@
</div>
{% if entries %}
<div class="row g-4 mb-4">
<div class="col-xl-7">
<section class="glass-panel chart-panel h-100">
<div class="chart-panel-top">
<div>
<span class="panel-label">Recent pattern</span>
<h2 class="h3 mb-2">Last {{ recent_activity|length }} check-ins at a glance</h2>
<p class="mb-0">Quick comparison bars make it easier to notice whether focus, energy, or deep work is drifting.</p>
</div>
</div>
<div class="activity-list">
{% for item in recent_activity %}
<article class="activity-row">
<div class="activity-row-head">
<div>
<span class="category-badge">{{ item.entry.category.name }}</span>
<h3><a href="{% url 'entry_detail' item.entry.pk %}">{{ item.entry.title }}</a></h3>
</div>
<span class="entry-date">{{ item.entry.entry_date|date:"M j" }}</span>
</div>
<div class="score-track-group">
<div class="score-track-item">
<span>Focus</span>
<div class="score-track"><div class="score-fill focus" style="width: {{ item.focus_width }}%"></div></div>
<strong>{{ item.entry.focus_score }}/10</strong>
</div>
<div class="score-track-item">
<span>Energy</span>
<div class="score-track"><div class="score-fill energy" style="width: {{ item.energy_width }}%"></div></div>
<strong>{{ item.entry.energy_score }}/10</strong>
</div>
<div class="score-track-item minutes">
<span>Deep work</span>
<div class="score-track"><div class="score-fill minutes" style="width: {{ item.minutes_width }}%"></div></div>
<strong>{{ item.entry.deep_work_minutes }}m</strong>
</div>
</div>
</article>
{% endfor %}
</div>
</section>
</div>
<div class="col-xl-5">
<aside class="glass-panel chart-panel h-100">
<div class="chart-panel-top">
<div>
<span class="panel-label">Category lanes</span>
<h2 class="h3 mb-2">Where your momentum shows up most</h2>
<p class="mb-0">These lanes rank the strongest categories in the current filtered history.</p>
</div>
</div>
<div class="lane-list">
{% for lane in category_breakdown %}
<article class="lane-card">
<div class="lane-card-top">
<div>
<strong>{{ lane.name }}</strong>
<span>{{ lane.entry_total }} entry{{ lane.entry_total|pluralize }}</span>
</div>
<span>{{ lane.avg_momentum }}/10 avg</span>
</div>
<div class="lane-track" aria-hidden="true">
<div class="lane-fill" style="width: {{ lane.share_percent }}%; background: linear-gradient(135deg, {{ lane.accent_color }}, var(--brand-highlight));"></div>
</div>
<div class="lane-meta">
<span>{{ lane.total_minutes }} minutes tracked</span>
<a href="{% url 'entry_list' %}?category={{ lane.slug }}">Open lane</a>
</div>
</article>
{% endfor %}
</div>
</aside>
</div>
</div>
<div class="row g-4">
{% for entry in entries %}
<div class="col-lg-4 col-md-6">
<article class="glass-panel entry-card h-100">
<article class="glass-panel entry-card entry-card-enhanced h-100" style="--entry-accent: {{ entry.category.accent_color }};">
<div class="entry-card-top">
<span class="category-badge">{{ entry.category.name }}</span>
<span class="entry-date">{{ entry.entry_date|date:"M j, Y" }}</span>
@ -58,6 +164,13 @@
<span>Energy {{ entry.energy_score }}/10</span>
<span>{{ entry.deep_work_minutes }} min</span>
</div>
<div class="entry-card-footer">
<div class="mini-meter">
<span>Momentum score</span>
<strong>{{ entry.momentum_score }}/10</strong>
</div>
<a class="text-link" href="{% url 'entry_detail' entry.pk %}">View details</a>
</div>
</article>
</div>
{% endfor %}

View File

@ -211,6 +211,16 @@
<a class="text-link" href="{% url 'entry_list' %}">{% if is_demo_mode %}Open demo history{% else %}Open your history{% endif %}</a>
</div>
<div class="glass-panel chart-panel">
<div class="chart-panel-top chart-panel-top-inline">
<div>
<span class="panel-label">Compare your week</span>
<p class="chart-intro mb-0">Two bars per day show whether focus and energy are moving together or drifting apart.</p>
</div>
<div class="chart-legend" aria-label="Chart legend">
<span class="legend-pill"><span class="legend-dot focus"></span>Focus</span>
<span class="legend-pill"><span class="legend-dot energy"></span>Energy</span>
</div>
</div>
<div class="trend-chart">
{% for day in weekly_trend %}
<article class="trend-day">
@ -224,6 +234,20 @@
</article>
{% endfor %}
</div>
<div class="trend-summary-grid">
<article class="trend-summary-card">
<span>Check-in days</span>
<strong>{{ weekly_summary.check_in_days }}/7</strong>
</article>
<article class="trend-summary-card">
<span>Deep work logged</span>
<strong>{{ weekly_summary.total_minutes }}m</strong>
</article>
<article class="trend-summary-card">
<span>Strongest day</span>
<strong>{{ weekly_summary.strongest_label }} · {{ weekly_summary.strongest_score }}/10</strong>
</article>
</div>
</div>
</div>
</section>
@ -241,7 +265,7 @@
<div class="row g-4">
{% for entry in recent_entries %}
<div class="col-lg-4 col-md-6">
<article class="glass-panel entry-card h-100">
<article class="glass-panel entry-card entry-card-enhanced h-100" style="--entry-accent: {{ entry.category.accent_color }};">
<div class="entry-card-top">
<span class="category-badge">{{ entry.category.name }}</span>
<span class="entry-date">{{ entry.entry_date|date:"M j" }}</span>
@ -253,6 +277,13 @@
<span>Energy {{ entry.energy_score }}/10</span>
<span>{{ entry.deep_work_minutes }} min</span>
</div>
<div class="entry-card-footer">
<div class="mini-meter">
<span>Momentum score</span>
<strong>{{ entry.momentum_score }}/10</strong>
</div>
<a class="text-link" href="{% url 'entry_detail' entry.pk %}">View details</a>
</div>
</article>
</div>
{% endfor %}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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;
}
}