FossilRepo

fossilrepo / templates / dashboard.html
Source Blame History 248 lines
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 %}

Keyboard Shortcuts

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