FossilRepo

fossilrepo / templates / dashboard.html
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

Keyboard Shortcuts

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