FossilRepo

Add contribution heatmap, technotes, custom error pages, keyboard nav Contribution heatmap: - GitHub-style daily activity grid on user profile page - 365 days of data, color-coded by commit count - Tooltips showing date and count on hover Technotes: - New /fossil/technotes/ view for Fossil's blog-like entries - Lists all technotes with timestamps, users, descriptions Custom error pages: - Branded 403, 404, 500 pages with navigation buttons - Extends base.html for consistent dark theme Keyboard navigation: - j/k to move between timeline entries - Enter to open the focused checkin - / to open global search (added earlier)

lmata 2026-04-07 00:28 trunk
Commit 8586c2d9dd835ecc5c14a086fb0e546c43f18dc630654333d48fce3c85a746ff
--- fossil/reader.py
+++ fossil/reader.py
@@ -328,13 +328,59 @@
328328
result["forum_count"] = row[0] if row else 0
329329
330330
# Ticket-related event count
331331
row = self.conn.execute("SELECT count(*) FROM event WHERE user=? AND type='t'", (username,)).fetchone()
332332
result["ticket_count"] = row[0] if row else 0
333
+ # Daily activity heatmap (last 365 days)
334
+ result["daily_activity"] = {}
335
+ try:
336
+ rows = self.conn.execute(
337
+ """
338
+ SELECT date(event.mtime - 0.5) as day, count(*) as cnt
339
+ FROM event
340
+ WHERE event.user = ? AND event.type = 'ci'
341
+ AND event.mtime > julianday('now') - 365
342
+ GROUP BY day ORDER BY day
343
+ """,
344
+ (username,),
345
+ ).fetchall()
346
+ for r in rows:
347
+ if r["day"]:
348
+ result["daily_activity"][r["day"]] = r["cnt"]
349
+ except sqlite3.OperationalError:
350
+ pass
333351
except sqlite3.OperationalError:
334352
pass
335353
return result
354
+
355
+ def get_technotes(self, limit: int = 50) -> list[dict]:
356
+ """Get technotes (timestamped blog-like entries)."""
357
+ notes = []
358
+ try:
359
+ rows = self.conn.execute(
360
+ """
361
+ SELECT blob.uuid, event.mtime, event.user, event.comment
362
+ FROM event
363
+ JOIN blob ON event.objid = blob.rid
364
+ WHERE event.type = 'e'
365
+ ORDER BY event.mtime DESC
366
+ LIMIT ?
367
+ """,
368
+ (limit,),
369
+ ).fetchall()
370
+ for r in rows:
371
+ notes.append(
372
+ {
373
+ "uuid": r["uuid"],
374
+ "timestamp": _julian_to_datetime(r["mtime"]),
375
+ "user": r["user"] or "",
376
+ "comment": r["comment"] or "",
377
+ }
378
+ )
379
+ except sqlite3.OperationalError:
380
+ pass
381
+ return notes
336382
337383
def get_commit_activity(self, weeks: int = 52) -> list[dict]:
338384
"""Get weekly commit counts for the last N weeks. Returns [{week, count}]."""
339385
activity = []
340386
try:
341387
--- fossil/reader.py
+++ fossil/reader.py
@@ -328,13 +328,59 @@
328 result["forum_count"] = row[0] if row else 0
329
330 # Ticket-related event count
331 row = self.conn.execute("SELECT count(*) FROM event WHERE user=? AND type='t'", (username,)).fetchone()
332 result["ticket_count"] = row[0] if row else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333 except sqlite3.OperationalError:
334 pass
335 return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
337 def get_commit_activity(self, weeks: int = 52) -> list[dict]:
338 """Get weekly commit counts for the last N weeks. Returns [{week, count}]."""
339 activity = []
340 try:
341
--- fossil/reader.py
+++ fossil/reader.py
@@ -328,13 +328,59 @@
328 result["forum_count"] = row[0] if row else 0
329
330 # Ticket-related event count
331 row = self.conn.execute("SELECT count(*) FROM event WHERE user=? AND type='t'", (username,)).fetchone()
332 result["ticket_count"] = row[0] if row else 0
333 # Daily activity heatmap (last 365 days)
334 result["daily_activity"] = {}
335 try:
336 rows = self.conn.execute(
337 """
338 SELECT date(event.mtime - 0.5) as day, count(*) as cnt
339 FROM event
340 WHERE event.user = ? AND event.type = 'ci'
341 AND event.mtime > julianday('now') - 365
342 GROUP BY day ORDER BY day
343 """,
344 (username,),
345 ).fetchall()
346 for r in rows:
347 if r["day"]:
348 result["daily_activity"][r["day"]] = r["cnt"]
349 except sqlite3.OperationalError:
350 pass
351 except sqlite3.OperationalError:
352 pass
353 return result
354
355 def get_technotes(self, limit: int = 50) -> list[dict]:
356 """Get technotes (timestamped blog-like entries)."""
357 notes = []
358 try:
359 rows = self.conn.execute(
360 """
361 SELECT blob.uuid, event.mtime, event.user, event.comment
362 FROM event
363 JOIN blob ON event.objid = blob.rid
364 WHERE event.type = 'e'
365 ORDER BY event.mtime DESC
366 LIMIT ?
367 """,
368 (limit,),
369 ).fetchall()
370 for r in rows:
371 notes.append(
372 {
373 "uuid": r["uuid"],
374 "timestamp": _julian_to_datetime(r["mtime"]),
375 "user": r["user"] or "",
376 "comment": r["comment"] or "",
377 }
378 )
379 except sqlite3.OperationalError:
380 pass
381 return notes
382
383 def get_commit_activity(self, weeks: int = 52) -> list[dict]:
384 """Get weekly commit counts for the last N weeks. Returns [{week, count}]."""
385 activity = []
386 try:
387
--- fossil/urls.py
+++ fossil/urls.py
@@ -20,10 +20,11 @@
2020
path("forum/", views.forum_list, name="forum"),
2121
path("forum/<str:thread_uuid>/", views.forum_thread, name="forum_thread"),
2222
path("user/<str:username>/", views.user_activity, name="user_activity"),
2323
path("branches/", views.branch_list, name="branches"),
2424
path("tags/", views.tag_list, name="tags"),
25
+ path("technotes/", views.technote_list, name="technotes"),
2526
path("search/", views.search, name="search"),
2627
path("stats/", views.repo_stats, name="stats"),
2728
path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
2829
path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
2930
path("code/history/<path:filepath>", views.file_history, name="file_history"),
3031
--- fossil/urls.py
+++ fossil/urls.py
@@ -20,10 +20,11 @@
20 path("forum/", views.forum_list, name="forum"),
21 path("forum/<str:thread_uuid>/", views.forum_thread, name="forum_thread"),
22 path("user/<str:username>/", views.user_activity, name="user_activity"),
23 path("branches/", views.branch_list, name="branches"),
24 path("tags/", views.tag_list, name="tags"),
 
25 path("search/", views.search, name="search"),
26 path("stats/", views.repo_stats, name="stats"),
27 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
28 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
29 path("code/history/<path:filepath>", views.file_history, name="file_history"),
30
--- fossil/urls.py
+++ fossil/urls.py
@@ -20,10 +20,11 @@
20 path("forum/", views.forum_list, name="forum"),
21 path("forum/<str:thread_uuid>/", views.forum_thread, name="forum_thread"),
22 path("user/<str:username>/", views.user_activity, name="user_activity"),
23 path("branches/", views.branch_list, name="branches"),
24 path("tags/", views.tag_list, name="tags"),
25 path("technotes/", views.technote_list, name="technotes"),
26 path("search/", views.search, name="search"),
27 path("stats/", views.repo_stats, name="stats"),
28 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
29 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
30 path("code/history/<path:filepath>", views.file_history, name="file_history"),
31
--- fossil/views.py
+++ fossil/views.py
@@ -850,22 +850,45 @@
850850
project, fossil_repo, reader = _get_repo_and_reader(slug)
851851
852852
with reader:
853853
activity = reader.get_user_activity(username)
854854
855
+ import json
856
+
857
+ heatmap_json = json.dumps(activity.get("daily_activity", {}))
858
+
855859
return render(
856860
request,
857861
"fossil/user_activity.html",
858862
{
859863
"project": project,
860864
"fossil_repo": fossil_repo,
861865
"username": username,
862866
"activity": activity,
867
+ "heatmap_json": heatmap_json,
863868
"active_tab": "timeline",
864869
},
865870
)
866871
872
+
873
+# --- Technotes ---
874
+
875
+
876
+@login_required
877
+def technote_list(request, slug):
878
+ P.PROJECT_VIEW.check(request.user)
879
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
880
+
881
+ with reader:
882
+ notes = reader.get_technotes()
883
+
884
+ return render(
885
+ request,
886
+ "fossil/technote_list.html",
887
+ {"project": project, "notes": notes, "active_tab": "wiki"},
888
+ )
889
+
867890
868891
# --- Search ---
869892
870893
871894
@login_required
872895
873896
ADDED templates/403.html
874897
ADDED templates/404.html
875898
ADDED templates/500.html
876899
ADDED templates/fossil/technote_list.html
--- fossil/views.py
+++ fossil/views.py
@@ -850,22 +850,45 @@
850 project, fossil_repo, reader = _get_repo_and_reader(slug)
851
852 with reader:
853 activity = reader.get_user_activity(username)
854
 
 
 
 
855 return render(
856 request,
857 "fossil/user_activity.html",
858 {
859 "project": project,
860 "fossil_repo": fossil_repo,
861 "username": username,
862 "activity": activity,
 
863 "active_tab": "timeline",
864 },
865 )
866
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
867
868 # --- Search ---
869
870
871 @login_required
872
873 DDED templates/403.html
874 DDED templates/404.html
875 DDED templates/500.html
876 DDED templates/fossil/technote_list.html
--- fossil/views.py
+++ fossil/views.py
@@ -850,22 +850,45 @@
850 project, fossil_repo, reader = _get_repo_and_reader(slug)
851
852 with reader:
853 activity = reader.get_user_activity(username)
854
855 import json
856
857 heatmap_json = json.dumps(activity.get("daily_activity", {}))
858
859 return render(
860 request,
861 "fossil/user_activity.html",
862 {
863 "project": project,
864 "fossil_repo": fossil_repo,
865 "username": username,
866 "activity": activity,
867 "heatmap_json": heatmap_json,
868 "active_tab": "timeline",
869 },
870 )
871
872
873 # --- Technotes ---
874
875
876 @login_required
877 def technote_list(request, slug):
878 P.PROJECT_VIEW.check(request.user)
879 project, fossil_repo, reader = _get_repo_and_reader(slug)
880
881 with reader:
882 notes = reader.get_technotes()
883
884 return render(
885 request,
886 "fossil/technote_list.html",
887 {"project": project, "notes": notes, "active_tab": "wiki"},
888 )
889
890
891 # --- Search ---
892
893
894 @login_required
895
896 DDED templates/403.html
897 DDED templates/404.html
898 DDED templates/500.html
899 DDED templates/fossil/technote_list.html
--- a/templates/403.html
+++ b/templates/403.html
@@ -0,0 +1,19 @@
1
+{% extends "base.html" %}
2
+{% block title %}Access Denied — Fossilrepo{% endblock %}
3
+
4
+{% block content %}
5
+<div class="flex flex-col items-center justify-center py-20">
6
+ <div class="text-6/div>
7
+ <h1 clasbrand mb-4">403</div>
8
+ <h1 class="text-2xl font-bold text-gray-100 mb-2">Access Denied</h1>
9
+ -2">Access Denied</h1>
10
+ 6 text-center max-w-md">
11
+ You don't have perm
12
+ </p>
13
+ <div class="flex gap-3">
14
+x gap-3 jubrand px-4 py-2-[var(--brandbg-brand-hover">Go to Dashboard</a>
15
+ <button onclick="history.back()l lang="en">
16
+<head>
17
+ <<!DOCTYPE html>
18
+<h-[var(--brand)] px-5 py-2.5 text-sm font-semibold text-white hover:opacity-90 transition">Go Home</a>
19
+ <a href="/auth/login/" class="rounded-md bg-graygray-100 ring-1 ring-inset ring-gray-600 hover:bg-gray-600">G
--- a/templates/403.html
+++ b/templates/403.html
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/403.html
+++ b/templates/403.html
@@ -0,0 +1,19 @@
1 {% extends "base.html" %}
2 {% block title %}Access Denied — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="flex flex-col items-center justify-center py-20">
6 <div class="text-6/div>
7 <h1 clasbrand mb-4">403</div>
8 <h1 class="text-2xl font-bold text-gray-100 mb-2">Access Denied</h1>
9 -2">Access Denied</h1>
10 6 text-center max-w-md">
11 You don't have perm
12 </p>
13 <div class="flex gap-3">
14 x gap-3 jubrand px-4 py-2-[var(--brandbg-brand-hover">Go to Dashboard</a>
15 <button onclick="history.back()l lang="en">
16 <head>
17 <<!DOCTYPE html>
18 <h-[var(--brand)] px-5 py-2.5 text-sm font-semibold text-white hover:opacity-90 transition">Go Home</a>
19 <a href="/auth/login/" class="rounded-md bg-graygray-100 ring-1 ring-inset ring-gray-600 hover:bg-gray-600">G
--- a/templates/404.html
+++ b/templates/404.html
@@ -0,0 +1,15 @@
1
+{% extends "base.html" %}
2
+{% block title %}Page Not Found — Fossilrepo{% endblock %}
3
+
4
+{% block content %}
5
+<div class="flex flex-col items-center justify-center py-20">
6
+ <div class="text-6/div>
7
+ <h1 clasbrand mb-4">404</div>
8
+ <h1 class="text-2xl font-bold text-gray-100 mb-2">Page Not Found</h1>
9
+ 2">Page Not Found</h1>
10
+ 6 text-center max-w-md">
11
+ The page you're looking for doesn't exist or has been moved.
12
+ </p>
13
+ <div class="flex gap-3">
14
+ <a hrebrand px-4 py-2-md bg-gray-800 px-5 py-2.5 white hover:bg-brand-700 px-4 py-2-md bg-gray-800 px-5 py-2.5 text-100 ring-1 ring-inset ring-gray-600 hover:bg-gray-600">Go Back</button>
15
+ </
--- a/templates/404.html
+++ b/templates/404.html
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/404.html
+++ b/templates/404.html
@@ -0,0 +1,15 @@
1 {% extends "base.html" %}
2 {% block title %}Page Not Found — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="flex flex-col items-center justify-center py-20">
6 <div class="text-6/div>
7 <h1 clasbrand mb-4">404</div>
8 <h1 class="text-2xl font-bold text-gray-100 mb-2">Page Not Found</h1>
9 2">Page Not Found</h1>
10 6 text-center max-w-md">
11 The page you're looking for doesn't exist or has been moved.
12 </p>
13 <div class="flex gap-3">
14 <a hrebrand px-4 py-2-md bg-gray-800 px-5 py-2.5 white hover:bg-brand-700 px-4 py-2-md bg-gray-800 px-5 py-2.5 text-100 ring-1 ring-inset ring-gray-600 hover:bg-gray-600">Go Back</button>
15 </
--- a/templates/500.html
+++ b/templates/500.html
@@ -0,0 +1,15 @@
1
+{% extends "base.html" %}
2
+{% block title %}Server Error — Fossilrepo{% endblock %}
3
+
4
+{% block content %}
5
+<div class="flex flex-col items-center justify-center py-20">
6
+ <div class="text-6/div>
7
+ <h1 clasbrand mb-4">500</div>
8
+ <h1 class="text-2xl font-bold text-gray-100 mb-2">Something Went Wrong</h1>
9
+ ething Went Wrong</h1>
10
+ 6 text-center max-w-md">
11
+ An unexpected error occurred. The team has been notified.
12
+ </p>
13
+ <div class="flex gap-3">
14
+x gap-3 jubrand px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700 px-4 py-2 text-sm font-semibold text-gray-100 ring-1 ring-inset ring-gray-600 hover:bg-gray-600">Go Back</button>
15
+ </
--- a/templates/500.html
+++ b/templates/500.html
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/500.html
+++ b/templates/500.html
@@ -0,0 +1,15 @@
1 {% extends "base.html" %}
2 {% block title %}Server Error — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="flex flex-col items-center justify-center py-20">
6 <div class="text-6/div>
7 <h1 clasbrand mb-4">500</div>
8 <h1 class="text-2xl font-bold text-gray-100 mb-2">Something Went Wrong</h1>
9 ething Went Wrong</h1>
10 6 text-center max-w-md">
11 An unexpected error occurred. The team has been notified.
12 </p>
13 <div class="flex gap-3">
14 x gap-3 jubrand px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700 px-4 py-2 text-sm font-semibold text-gray-100 ring-1 ring-inset ring-gray-600 hover:bg-gray-600">Go Back</button>
15 </
--- a/templates/fossil/technote_list.html
+++ b/templates/fossil/technote_list.html
@@ -0,0 +1,12 @@
1
+{% extends "base.html" %}
2
+{% load fossil_filters %}
3
+{% block title %}Technotes — {{ project.name }} — Fossilrepo{% endblock %}
4
+
5
+{% block content %}
6
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.naiv class="flex ite mb-4">Technotes</h2>
7
+x-5 py-4ml" %}
8
+{% load fossil_{% extends "base.html" %}
9
+{%a>
10
+ checkincheckin_uuid=note.uuid %}" hover:text-brandadiv>
11
+ {% empty %}
12
+500 py-8 tendblock %}
--- a/templates/fossil/technote_list.html
+++ b/templates/fossil/technote_list.html
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/technote_list.html
+++ b/templates/fossil/technote_list.html
@@ -0,0 +1,12 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Technotes — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.naiv class="flex ite mb-4">Technotes</h2>
7 x-5 py-4ml" %}
8 {% load fossil_{% extends "base.html" %}
9 {%a>
10 checkincheckin_uuid=note.uuid %}" hover:text-brandadiv>
11 {% empty %}
12 500 py-8 tendblock %}
--- templates/fossil/timeline.html
+++ templates/fossil/timeline.html
@@ -25,6 +25,30 @@
2525
class="inline-flex items-center rounded-md bg-gray-800 px-4 py-2 text-sm text-gray-400 hover:text-white border border-gray-700">
2626
Load more
2727
</a>
2828
</div>
2929
{% endif %}
30
+
31
+<script>
32
+ // Keyboard navigation: j/k to move between timeline entries
33
+ (function() {
34
+ let current = -1;
35
+ const rows = document.querySelectorAll('.tl-row');
36
+ function highlight(idx) {
37
+ rows.forEach(r => r.style.background = '');
38
+ if (idx >= 0 && idx < rows.length) {
39
+ rows[idx].style.background = 'rgba(220,57,76,0.06)';
40
+ rows[idx].scrollIntoView({ behavior: 'smooth', block: 'center' });
41
+ }
42
+ }
43
+ document.addEventListener('keydown', function(e) {
44
+ if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;
45
+ if (e.key === 'j') { current = Math.min(current + 1, rows.length - 1); highlight(current); }
46
+ else if (e.key === 'k') { current = Math.max(current - 1, 0); highlight(current); }
47
+ else if (e.key === 'Enter' && current >= 0) {
48
+ const link = rows[current].querySelector('a[href*="/checkin/"]');
49
+ if (link) window.location = link.href;
50
+ }
51
+ });
52
+ })();
53
+</script>
3054
{% endblock %}
3155
--- templates/fossil/timeline.html
+++ templates/fossil/timeline.html
@@ -25,6 +25,30 @@
25 class="inline-flex items-center rounded-md bg-gray-800 px-4 py-2 text-sm text-gray-400 hover:text-white border border-gray-700">
26 Load more
27 </a>
28 </div>
29 {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30 {% endblock %}
31
--- templates/fossil/timeline.html
+++ templates/fossil/timeline.html
@@ -25,6 +25,30 @@
25 class="inline-flex items-center rounded-md bg-gray-800 px-4 py-2 text-sm text-gray-400 hover:text-white border border-gray-700">
26 Load more
27 </a>
28 </div>
29 {% endif %}
30
31 <script>
32 // Keyboard navigation: j/k to move between timeline entries
33 (function() {
34 let current = -1;
35 const rows = document.querySelectorAll('.tl-row');
36 function highlight(idx) {
37 rows.forEach(r => r.style.background = '');
38 if (idx >= 0 && idx < rows.length) {
39 rows[idx].style.background = 'rgba(220,57,76,0.06)';
40 rows[idx].scrollIntoView({ behavior: 'smooth', block: 'center' });
41 }
42 }
43 document.addEventListener('keydown', function(e) {
44 if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;
45 if (e.key === 'j') { current = Math.min(current + 1, rows.length - 1); highlight(current); }
46 else if (e.key === 'k') { current = Math.max(current - 1, 0); highlight(current); }
47 else if (e.key === 'Enter' && current >= 0) {
48 const link = rows[current].querySelector('a[href*="/checkin/"]');
49 if (link) window.location = link.href;
50 }
51 });
52 })();
53 </script>
54 {% endblock %}
55
--- templates/fossil/user_activity.html
+++ templates/fossil/user_activity.html
@@ -2,10 +2,43 @@
22
{% block title %}{{ username }} — {{ project.name }} — Fossilrepo{% endblock %}
33
44
{% block content %}
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
7
+
8
+{% if heatmap_json and heatmap_json != "{}" %}
9
+<div class="rounded-lg bg-gray-800 border border-gray-700 p-4 mb-6">
10
+ <h3 class="text-sm font-medium text-gray-300 mb-3">Contribution Activity</h3>
11
+ <div class="overflow-x-auto">
12
+ <div id="heatmap" style="display:flex; gap:2px; flex-wrap:wrap;"></div>
13
+ </div>
14
+</div>
15
+<script>
16
+ (function() {
17
+ const data = {{ heatmap_json|safe }};
18
+ const container = document.getElementById('heatmap');
19
+ const today = new Date();
20
+ for (let i = 364; i >= 0; i--) {
21
+ const d = new Date(today);
22
+ d.setDate(d.getDate() - i);
23
+ const key = d.toISOString().split('T')[0];
24
+ const count = data[key] || 0;
25
+ const el = document.createElement('div');
26
+ el.style.width = '10px';
27
+ el.style.height = '10px';
28
+ el.style.borderRadius = '2px';
29
+ el.title = key + ': ' + count + ' commit' + (count !== 1 ? 's' : '');
30
+ if (count === 0) el.style.background = '#1f2937';
31
+ else if (count <= 2) el.style.background = '#5b2130';
32
+ else if (count <= 5) el.style.background = '#8B3138';
33
+ else if (count <= 10) el.style.background = '#DC394C';
34
+ else el.style.background = '#e8677a';
35
+ container.appendChild(el);
36
+ }
37
+ })();
38
+</script>
39
+{% endif %}
740
841
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
942
<!-- Main content -->
1043
<div class="lg:col-span-2">
1144
<div class="rounded-lg bg-gray-800 border border-gray-700">
1245
--- templates/fossil/user_activity.html
+++ templates/fossil/user_activity.html
@@ -2,10 +2,43 @@
2 {% block title %}{{ username }} — {{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
8 <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
9 <!-- Main content -->
10 <div class="lg:col-span-2">
11 <div class="rounded-lg bg-gray-800 border border-gray-700">
12
--- templates/fossil/user_activity.html
+++ templates/fossil/user_activity.html
@@ -2,10 +2,43 @@
2 {% block title %}{{ username }} — {{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 {% if heatmap_json and heatmap_json != "{}" %}
9 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4 mb-6">
10 <h3 class="text-sm font-medium text-gray-300 mb-3">Contribution Activity</h3>
11 <div class="overflow-x-auto">
12 <div id="heatmap" style="display:flex; gap:2px; flex-wrap:wrap;"></div>
13 </div>
14 </div>
15 <script>
16 (function() {
17 const data = {{ heatmap_json|safe }};
18 const container = document.getElementById('heatmap');
19 const today = new Date();
20 for (let i = 364; i >= 0; i--) {
21 const d = new Date(today);
22 d.setDate(d.getDate() - i);
23 const key = d.toISOString().split('T')[0];
24 const count = data[key] || 0;
25 const el = document.createElement('div');
26 el.style.width = '10px';
27 el.style.height = '10px';
28 el.style.borderRadius = '2px';
29 el.title = key + ': ' + count + ' commit' + (count !== 1 ? 's' : '');
30 if (count === 0) el.style.background = '#1f2937';
31 else if (count <= 2) el.style.background = '#5b2130';
32 else if (count <= 5) el.style.background = '#8B3138';
33 else if (count <= 10) el.style.background = '#DC394C';
34 else el.style.background = '#e8677a';
35 container.appendChild(el);
36 }
37 })();
38 </script>
39 {% endif %}
40
41 <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
42 <!-- Main content -->
43 <div class="lg:col-span-2">
44 <div class="rounded-lg bg-gray-800 border border-gray-700">
45

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button