|
4ce269c…
|
ragelink
|
1 |
{% extends "base.html" %} |
|
4ce269c…
|
ragelink
|
2 |
{% load static %} |
|
4ce269c…
|
ragelink
|
3 |
{% block title %}Dashboard — Fossilrepo{% endblock %} |
|
4ce269c…
|
ragelink
|
4 |
|
|
4ce269c…
|
ragelink
|
5 |
{% block extra_head %} |
|
4ce269c…
|
ragelink
|
6 |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script> |
|
4ce269c…
|
ragelink
|
7 |
{% endblock %} |
|
4ce269c…
|
ragelink
|
8 |
|
|
4ce269c…
|
ragelink
|
9 |
{% block content %} |
|
4ce269c…
|
ragelink
|
10 |
<div class="mb-6"> |
|
4ce269c…
|
ragelink
|
11 |
<h1 class="text-2xl font-bold text-gray-100">Dashboard</h1> |
|
4ce269c…
|
ragelink
|
12 |
<p class="mt-1 text-sm text-gray-400">Welcome back, {{ user.get_full_name|default:user.username }}</p> |
|
4ce269c…
|
ragelink
|
13 |
</div> |
|
4ce269c…
|
ragelink
|
14 |
|
|
4ce269c…
|
ragelink
|
15 |
<!-- Stats cards --> |
|
4ce269c…
|
ragelink
|
16 |
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-6"> |
|
2f13242…
|
ragelink
|
17 |
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-gray-600 transition-colors"> |
|
4ce269c…
|
ragelink
|
18 |
<div class="text-2xl font-bold text-gray-100">{{ total_projects }}</div> |
|
4ce269c…
|
ragelink
|
19 |
<div class="text-xs text-gray-500 mt-1">Projects</div> |
|
4ce269c…
|
ragelink
|
20 |
</div> |
|
2f13242…
|
ragelink
|
21 |
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-gray-600 transition-colors"> |
|
4ce269c…
|
ragelink
|
22 |
<div class="text-2xl font-bold text-gray-100">{{ total_checkins|default:"0" }}</div> |
|
4ce269c…
|
ragelink
|
23 |
<div class="text-xs text-gray-500 mt-1">Total Checkins</div> |
|
4ce269c…
|
ragelink
|
24 |
</div> |
|
2f13242…
|
ragelink
|
25 |
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-gray-600 transition-colors"> |
|
4ce269c…
|
ragelink
|
26 |
<div class="text-2xl font-bold text-gray-100">{{ total_tickets|default:"0" }}</div> |
|
4ce269c…
|
ragelink
|
27 |
<div class="text-xs text-gray-500 mt-1">Tickets</div> |
|
4ce269c…
|
ragelink
|
28 |
</div> |
|
2f13242…
|
ragelink
|
29 |
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-gray-600 transition-colors"> |
|
4ce269c…
|
ragelink
|
30 |
<div class="text-2xl font-bold text-gray-100">{{ total_wiki|default:"0" }}</div> |
|
4ce269c…
|
ragelink
|
31 |
<div class="text-xs text-gray-500 mt-1">Wiki Pages</div> |
|
4ce269c…
|
ragelink
|
32 |
</div> |
|
4ce269c…
|
ragelink
|
33 |
</div> |
|
c588255…
|
ragelink
|
34 |
|
|
c588255…
|
ragelink
|
35 |
<!-- Activity heatmap (all projects, last year) --> |
|
c588255…
|
ragelink
|
36 |
{% if heatmap_json %} |
|
2f13242…
|
ragelink
|
37 |
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4 mb-6 shadow-sm"> |
|
c588255…
|
ragelink
|
38 |
<h3 class="text-sm font-medium text-gray-300 mb-3">Activity (last year)</h3> |
|
c588255…
|
ragelink
|
39 |
<div id="heatmap" class="overflow-x-auto"></div> |
|
c588255…
|
ragelink
|
40 |
<div class="flex items-center justify-end gap-1 mt-2 text-xs text-gray-500"> |
|
c588255…
|
ragelink
|
41 |
<span>Less</span> |
|
c588255…
|
ragelink
|
42 |
<span class="inline-block w-3 h-3 rounded-sm bg-gray-700"></span> |
|
c588255…
|
ragelink
|
43 |
<span class="inline-block w-3 h-3 rounded-sm" style="background:#14532d"></span> |
|
c588255…
|
ragelink
|
44 |
<span class="inline-block w-3 h-3 rounded-sm" style="background:#166534"></span> |
|
c588255…
|
ragelink
|
45 |
<span class="inline-block w-3 h-3 rounded-sm" style="background:#22c55e"></span> |
|
c588255…
|
ragelink
|
46 |
<span class="inline-block w-3 h-3 rounded-sm" style="background:#4ade80"></span> |
|
c588255…
|
ragelink
|
47 |
<span>More</span> |
|
c588255…
|
ragelink
|
48 |
</div> |
|
c588255…
|
ragelink
|
49 |
</div> |
|
c588255…
|
ragelink
|
50 |
{% endif %} |
|
4ce269c…
|
ragelink
|
51 |
|
|
4ce269c…
|
ragelink
|
52 |
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3"> |
|
4ce269c…
|
ragelink
|
53 |
<!-- Main column --> |
|
4ce269c…
|
ragelink
|
54 |
<div class="lg:col-span-2 space-y-6"> |
|
4ce269c…
|
ragelink
|
55 |
{% if system_activity_json and system_activity_json != "[]" %} |
|
4ce269c…
|
ragelink
|
56 |
<!-- System-wide activity chart --> |
|
2f13242…
|
ragelink
|
57 |
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm"> |
|
4ce269c…
|
ragelink
|
58 |
<h3 class="text-sm font-medium text-gray-300 mb-3">System Activity (26 weeks)</h3> |
|
4ce269c…
|
ragelink
|
59 |
<div style="height: 140px;"> |
|
4ce269c…
|
ragelink
|
60 |
<canvas id="systemChart"></canvas> |
|
4ce269c…
|
ragelink
|
61 |
</div> |
|
4ce269c…
|
ragelink
|
62 |
</div> |
|
4ce269c…
|
ragelink
|
63 |
{% endif %} |
|
4ce269c…
|
ragelink
|
64 |
|
|
4ce269c…
|
ragelink
|
65 |
{% if recent_across_all %} |
|
4ce269c…
|
ragelink
|
66 |
<!-- Recent activity across all projects --> |
|
2f13242…
|
ragelink
|
67 |
<div class="rounded-lg bg-gray-800 border border-gray-700 shadow-sm"> |
|
4ce269c…
|
ragelink
|
68 |
<div class="px-4 py-3 border-b border-gray-700"> |
|
4ce269c…
|
ragelink
|
69 |
<h3 class="text-sm font-medium text-gray-300">Recent Activity</h3> |
|
4ce269c…
|
ragelink
|
70 |
</div> |
|
4ce269c…
|
ragelink
|
71 |
<div class="divide-y divide-gray-700"> |
|
4ce269c…
|
ragelink
|
72 |
{% for item in recent_across_all %} |
|
2f13242…
|
ragelink
|
73 |
<div class="px-4 py-3 flex items-start gap-3 hover:bg-gray-700/30 transition-colors"> |
|
4ce269c…
|
ragelink
|
74 |
<div class="flex-shrink-0 mt-1"> |
|
4ce269c…
|
ragelink
|
75 |
<div class="w-2.5 h-2.5 rounded-full bg-brand"></div> |
|
4ce269c…
|
ragelink
|
76 |
</div> |
|
4ce269c…
|
ragelink
|
77 |
<div class="flex-1 min-w-0"> |
|
4ce269c…
|
ragelink
|
78 |
<a href="{% url 'fossil:checkin_detail' slug=item.project.slug checkin_uuid=item.entry.uuid %}" |
|
4ce269c…
|
ragelink
|
79 |
class="text-sm text-gray-200 hover:text-brand-light">{{ item.entry.comment|truncatechars:70 }}</a> |
|
313537c…
|
ragelink
|
80 |
<div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-gray-500"> |
|
4ce269c…
|
ragelink
|
81 |
<a href="{% url 'projects:detail' slug=item.project.slug %}" class="text-brand-light hover:text-brand">{{ item.project.name }}</a> |
|
4ce269c…
|
ragelink
|
82 |
<a href="{% url 'fossil:user_activity' slug=item.project.slug username=item.entry.user %}" class="hover:text-gray-300">{{ item.entry.user }}</a> |
|
4ce269c…
|
ragelink
|
83 |
<a href="{% url 'fossil:checkin_detail' slug=item.project.slug checkin_uuid=item.entry.uuid %}" class="font-mono text-brand-light hover:text-brand">{{ item.entry.uuid|truncatechars:10 }}</a> |
|
4ce269c…
|
ragelink
|
84 |
<span>{{ item.entry.timestamp|timesince }} ago</span> |
|
4ce269c…
|
ragelink
|
85 |
</div> |
|
4ce269c…
|
ragelink
|
86 |
</div> |
|
4ce269c…
|
ragelink
|
87 |
</div> |
|
4ce269c…
|
ragelink
|
88 |
{% endfor %} |
|
4ce269c…
|
ragelink
|
89 |
</div> |
|
4ce269c…
|
ragelink
|
90 |
</div> |
|
0e40dc2…
|
ragelink
|
91 |
{% elif not system_activity_json or system_activity_json == "[]" %} |
|
0e40dc2…
|
ragelink
|
92 |
<!-- Empty state when no activity exists --> |
|
0e40dc2…
|
ragelink
|
93 |
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center"> |
|
0e40dc2…
|
ragelink
|
94 |
<svg class="mx-auto h-12 w-12 text-gray-600" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor"> |
|
0e40dc2…
|
ragelink
|
95 |
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|
0e40dc2…
|
ragelink
|
96 |
</svg> |
|
0e40dc2…
|
ragelink
|
97 |
<h3 class="mt-2 text-sm font-semibold text-gray-300">No recent activity</h3> |
|
0e40dc2…
|
ragelink
|
98 |
<p class="mt-1 text-sm text-gray-500">Activity from your projects will appear here once repositories have checkins.</p> |
|
0e40dc2…
|
ragelink
|
99 |
</div> |
|
4ce269c…
|
ragelink
|
100 |
{% endif %} |
|
4ce269c…
|
ragelink
|
101 |
</div> |
|
4ce269c…
|
ragelink
|
102 |
|
|
4ce269c…
|
ragelink
|
103 |
<!-- Sidebar --> |
|
4ce269c…
|
ragelink
|
104 |
<div class="space-y-4"> |
|
4ce269c…
|
ragelink
|
105 |
<!-- Quick links --> |
|
4ce269c…
|
ragelink
|
106 |
{% if perms.projects.view_project %} |
|
2f13242…
|
ragelink
|
107 |
<a href="{% url 'projects:list' %}" class="block rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-brand hover:shadow-md transition-all"> |
|
4ce269c…
|
ragelink
|
108 |
<h3 class="text-sm font-semibold text-gray-100">Projects</h3> |
|
4ce269c…
|
ragelink
|
109 |
<p class="mt-1 text-xs text-gray-500">Manage projects and team access</p> |
|
4ce269c…
|
ragelink
|
110 |
</a> |
|
4ce269c…
|
ragelink
|
111 |
{% endif %} |
|
4ce269c…
|
ragelink
|
112 |
{% if perms.organization.view_team %} |
|
2f13242…
|
ragelink
|
113 |
<a href="{% url 'organization:team_list' %}" class="block rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-brand hover:shadow-md transition-all"> |
|
4ce269c…
|
ragelink
|
114 |
<h3 class="text-sm font-semibold text-gray-100">Teams</h3> |
|
4ce269c…
|
ragelink
|
115 |
<p class="mt-1 text-xs text-gray-500">Organize members into teams</p> |
|
4ce269c…
|
ragelink
|
116 |
</a> |
|
4ce269c…
|
ragelink
|
117 |
{% endif %} |
|
4ce269c…
|
ragelink
|
118 |
{% if perms.pages.view_page %} |
|
2f13242…
|
ragelink
|
119 |
<a href="{% url 'pages:list' %}" class="block rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-brand hover:shadow-md transition-all"> |
|
45192ef…
|
ragelink
|
120 |
<h3 class="text-sm font-semibold text-gray-100">FossilRepo Docs</h3> |
|
4ce269c…
|
ragelink
|
121 |
<p class="mt-1 text-xs text-gray-500">Guides, runbooks, documentation</p> |
|
4ce269c…
|
ragelink
|
122 |
</a> |
|
4ce269c…
|
ragelink
|
123 |
{% endif %} |
|
4ce269c…
|
ragelink
|
124 |
{% if perms.organization.view_organization %} |
|
2f13242…
|
ragelink
|
125 |
<a href="{% url 'organization:settings' %}" class="block rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-brand hover:shadow-md transition-all"> |
|
4ce269c…
|
ragelink
|
126 |
<h3 class="text-sm font-semibold text-gray-100">Settings</h3> |
|
4ce269c…
|
ragelink
|
127 |
<p class="mt-1 text-xs text-gray-500">Organization configuration</p> |
|
4ce269c…
|
ragelink
|
128 |
</a> |
|
4ce269c…
|
ragelink
|
129 |
{% endif %} |
|
4ce269c…
|
ragelink
|
130 |
{% if user.is_staff %} |
|
2f13242…
|
ragelink
|
131 |
<a href="{% url 'admin:index' %}" class="block rounded-lg bg-gray-800 border border-gray-700 p-4 shadow-sm hover:border-brand hover:shadow-md transition-all"> |
|
4ce269c…
|
ragelink
|
132 |
<h3 class="text-sm font-semibold text-gray-100">Admin</h3> |
|
4ce269c…
|
ragelink
|
133 |
<p class="mt-1 text-xs text-gray-500">Users, groups, permissions</p> |
|
4ce269c…
|
ragelink
|
134 |
</a> |
|
4ce269c…
|
ragelink
|
135 |
{% endif %} |
|
4ce269c…
|
ragelink
|
136 |
</div> |
|
4ce269c…
|
ragelink
|
137 |
</div> |
|
4ce269c…
|
ragelink
|
138 |
|
|
4ce269c…
|
ragelink
|
139 |
{% if system_activity_json and system_activity_json != "[]" %} |
|
4ce269c…
|
ragelink
|
140 |
<script> |
|
4ce269c…
|
ragelink
|
141 |
new Chart(document.getElementById('systemChart').getContext('2d'), { |
|
4ce269c…
|
ragelink
|
142 |
type: 'bar', |
|
4ce269c…
|
ragelink
|
143 |
data: { |
|
4ce269c…
|
ragelink
|
144 |
labels: {{ system_activity_json|safe }}.map((_, i) => ''), |
|
4ce269c…
|
ragelink
|
145 |
datasets: [{ |
|
4ce269c…
|
ragelink
|
146 |
data: {{ system_activity_json|safe }}, |
|
4ce269c…
|
ragelink
|
147 |
backgroundColor: '#DC394C', |
|
4ce269c…
|
ragelink
|
148 |
borderRadius: 2, |
|
4ce269c…
|
ragelink
|
149 |
barPercentage: 0.8, |
|
4ce269c…
|
ragelink
|
150 |
categoryPercentage: 0.9, |
|
4ce269c…
|
ragelink
|
151 |
}] |
|
4ce269c…
|
ragelink
|
152 |
}, |
|
4ce269c…
|
ragelink
|
153 |
options: { |
|
4ce269c…
|
ragelink
|
154 |
responsive: true, |
|
4ce269c…
|
ragelink
|
155 |
maintainAspectRatio: false, |
|
4ce269c…
|
ragelink
|
156 |
plugins: { legend: { display: false }, tooltip: { |
|
4ce269c…
|
ragelink
|
157 |
callbacks: { title: (items) => { const w = 25 - items[0].dataIndex; return w === 0 ? 'This week' : w + ' week' + (w > 1 ? 's' : '') + ' ago'; } } |
|
4ce269c…
|
ragelink
|
158 |
}}, |
|
4ce269c…
|
ragelink
|
159 |
scales: { |
|
4ce269c…
|
ragelink
|
160 |
x: { display: false, grid: { display: false } }, |
|
4ce269c…
|
ragelink
|
161 |
y: { display: false, grid: { display: false }, beginAtZero: true } |
|
4ce269c…
|
ragelink
|
162 |
} |
|
4ce269c…
|
ragelink
|
163 |
} |
|
4ce269c…
|
ragelink
|
164 |
}); |
|
c588255…
|
ragelink
|
165 |
</script> |
|
c588255…
|
ragelink
|
166 |
{% endif %} |
|
c588255…
|
ragelink
|
167 |
|
|
c588255…
|
ragelink
|
168 |
{% if heatmap_json %} |
|
c588255…
|
ragelink
|
169 |
<script> |
|
c588255…
|
ragelink
|
170 |
(function() { |
|
c588255…
|
ragelink
|
171 |
var data = {{ heatmap_json|safe }}; |
|
c588255…
|
ragelink
|
172 |
var counts = {}; |
|
c588255…
|
ragelink
|
173 |
data.forEach(function(d) { counts[d.date] = d.count; }); |
|
c588255…
|
ragelink
|
174 |
|
|
c588255…
|
ragelink
|
175 |
// Generate 365 days ending today |
|
c588255…
|
ragelink
|
176 |
var today = new Date(); |
|
c588255…
|
ragelink
|
177 |
var days = []; |
|
c588255…
|
ragelink
|
178 |
for (var i = 364; i >= 0; i--) { |
|
c588255…
|
ragelink
|
179 |
var d = new Date(today); |
|
c588255…
|
ragelink
|
180 |
d.setDate(d.getDate() - i); |
|
c588255…
|
ragelink
|
181 |
var key = d.toISOString().slice(0, 10); |
|
c588255…
|
ragelink
|
182 |
days.push({ date: key, count: counts[key] || 0, dow: d.getDay() }); |
|
c588255…
|
ragelink
|
183 |
} |
|
c588255…
|
ragelink
|
184 |
|
|
c588255…
|
ragelink
|
185 |
var cellSize = 12; |
|
c588255…
|
ragelink
|
186 |
var cellGap = 2; |
|
c588255…
|
ragelink
|
187 |
var step = cellSize + cellGap; |
|
c588255…
|
ragelink
|
188 |
var labelWidth = 28; |
|
c588255…
|
ragelink
|
189 |
var monthHeight = 16; |
|
c588255…
|
ragelink
|
190 |
|
|
c588255…
|
ragelink
|
191 |
// The first day may not be Sunday (dow=0). We need to offset the first column. |
|
c588255…
|
ragelink
|
192 |
var startDow = days[0].dow; |
|
c588255…
|
ragelink
|
193 |
var totalSlots = days.length + startDow; |
|
c588255…
|
ragelink
|
194 |
var weeks = Math.ceil(totalSlots / 7); |
|
c588255…
|
ragelink
|
195 |
var svgWidth = labelWidth + weeks * step; |
|
c588255…
|
ragelink
|
196 |
var svgHeight = monthHeight + 7 * step; |
|
c588255…
|
ragelink
|
197 |
|
|
c588255…
|
ragelink
|
198 |
var svg = '<svg width="' + svgWidth + '" height="' + svgHeight + '" class="text-gray-500">'; |
|
c588255…
|
ragelink
|
199 |
|
|
c588255…
|
ragelink
|
200 |
// Day-of-week labels (Mon, Wed, Fri) |
|
c588255…
|
ragelink
|
201 |
var dayLabels = ['', 'Mon', '', 'Wed', '', 'Fri', '']; |
|
c588255…
|
ragelink
|
202 |
for (var di = 0; di < dayLabels.length; di++) { |
|
c588255…
|
ragelink
|
203 |
if (dayLabels[di]) { |
|
c588255…
|
ragelink
|
204 |
svg += '<text x="0" y="' + (monthHeight + di * step + cellSize - 2) + '" fill="currentColor" font-size="9" font-family="sans-serif">' + dayLabels[di] + '</text>'; |
|
c588255…
|
ragelink
|
205 |
} |
|
c588255…
|
ragelink
|
206 |
} |
|
c588255…
|
ragelink
|
207 |
|
|
c588255…
|
ragelink
|
208 |
// Month labels -- find the first occurrence of each month in the grid |
|
c588255…
|
ragelink
|
209 |
var monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; |
|
c588255…
|
ragelink
|
210 |
var lastMonth = -1; |
|
c588255…
|
ragelink
|
211 |
for (var mi = 0; mi < days.length; mi++) { |
|
c588255…
|
ragelink
|
212 |
var monthNum = parseInt(days[mi].date.slice(5, 7), 10) - 1; |
|
c588255…
|
ragelink
|
213 |
if (monthNum !== lastMonth) { |
|
c588255…
|
ragelink
|
214 |
lastMonth = monthNum; |
|
c588255…
|
ragelink
|
215 |
var weekIdx = Math.floor((mi + startDow) / 7); |
|
c588255…
|
ragelink
|
216 |
var x = labelWidth + weekIdx * step; |
|
c588255…
|
ragelink
|
217 |
svg += '<text x="' + x + '" y="10" fill="currentColor" font-size="9" font-family="sans-serif">' + monthNames[monthNum] + '</text>'; |
|
c588255…
|
ragelink
|
218 |
} |
|
c588255…
|
ragelink
|
219 |
} |
|
c588255…
|
ragelink
|
220 |
|
|
c588255…
|
ragelink
|
221 |
// Color scale |
|
c588255…
|
ragelink
|
222 |
function getColor(count) { |
|
c588255…
|
ragelink
|
223 |
if (count === 0) return '#1f2937'; |
|
c588255…
|
ragelink
|
224 |
if (count <= 2) return '#14532d'; |
|
c588255…
|
ragelink
|
225 |
if (count <= 5) return '#166534'; |
|
c588255…
|
ragelink
|
226 |
if (count <= 10) return '#22c55e'; |
|
c588255…
|
ragelink
|
227 |
return '#4ade80'; |
|
c588255…
|
ragelink
|
228 |
} |
|
c588255…
|
ragelink
|
229 |
|
|
c588255…
|
ragelink
|
230 |
// Render cells |
|
c588255…
|
ragelink
|
231 |
for (var ci = 0; ci < days.length; ci++) { |
|
c588255…
|
ragelink
|
232 |
var day = days[ci]; |
|
c588255…
|
ragelink
|
233 |
var wk = Math.floor((ci + startDow) / 7); |
|
c588255…
|
ragelink
|
234 |
var dow = (ci + startDow) % 7; |
|
c588255…
|
ragelink
|
235 |
var cx = labelWidth + wk * step; |
|
c588255…
|
ragelink
|
236 |
var cy = monthHeight + dow * step; |
|
c588255…
|
ragelink
|
237 |
var color = getColor(day.count); |
|
c588255…
|
ragelink
|
238 |
svg += '<rect x="' + cx + '" y="' + cy + '" width="' + cellSize + '" height="' + cellSize + '" rx="2" fill="' + color + '">'; |
|
c588255…
|
ragelink
|
239 |
svg += '<title>' + day.date + ': ' + day.count + ' commit' + (day.count !== 1 ? 's' : '') + '</title>'; |
|
c588255…
|
ragelink
|
240 |
svg += '</rect>'; |
|
c588255…
|
ragelink
|
241 |
} |
|
c588255…
|
ragelink
|
242 |
|
|
c588255…
|
ragelink
|
243 |
svg += '</svg>'; |
|
c588255…
|
ragelink
|
244 |
document.getElementById('heatmap').innerHTML = svg; |
|
c588255…
|
ragelink
|
245 |
})(); |
|
4ce269c…
|
ragelink
|
246 |
</script> |
|
4ce269c…
|
ragelink
|
247 |
{% endif %} |
|
4ce269c…
|
ragelink
|
248 |
{% endblock %} |