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