Adds a polished release/download dashboard to GitHub repository and releases pages.
// ==UserScript==
// @name GitHub Release Dashboard
// @description Adds a polished release/download dashboard to GitHub repository and releases pages.
// @version 1.0.0
// @namespace lafa2k
// @author L2K
// @license MIT
// @match https://github.com/*/*
// @match https://github.com/*/*/releases*
// @grant none
// ==/UserScript==
(() => {
'use strict';
if (window.__l2kGithubReleaseDashboardLoaded) {
return;
}
window.__l2kGithubReleaseDashboardLoaded = true;
const SCRIPT_ID = 'l2k-github-release-dashboard';
const STYLE_ID = `${SCRIPT_ID}-style`;
const CACHE_TTL_MS = 5 * 60 * 1000;
const MAX_RELEASE_PAGES = 10;
let currentRepoKey = null;
let bootToken = 0;
function parseRepoContext() {
const match = location.pathname.match(/^\/([^/]+)\/([^/]+)(\/.*)?$/);
if (!match) return null;
const owner = match[1];
const repo = match[2];
const tail = match[3] || '';
if (!owner || !repo) return null;
return {
owner,
repo,
fullName: `${owner}/${repo}`,
isRepoHome: tail === '' || tail === '/',
isReleasesPage: tail === '/releases' || tail.startsWith('/releases/')
};
}
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
@keyframes l2k-rd-slow-pan {
0% {
background-position: 0% 0%, 100% 100%, 0% 50%;
}
50% {
background-position: 8% 4%, 92% 96%, 100% 50%;
}
100% {
background-position: 0% 0%, 100% 100%, 0% 50%;
}
}
.l2k-rd-card {
position: relative;
overflow: hidden;
border: 1px solid rgba(86, 187, 255, 0.22);
border-radius: 18px;
padding: 16px;
background:
radial-gradient(circle at top left, rgba(34, 197, 94, 0.18), transparent 30%),
radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.2), transparent 36%),
linear-gradient(145deg, rgba(11, 18, 32, 0.98), rgba(15, 23, 42, 0.98));
background-size: 120% 120%, 120% 120%, 200% 200%;
box-shadow:
0 14px 30px rgba(0, 0, 0, 0.20),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
color: #e5eefc;
margin-bottom: 16px;
backdrop-filter: blur(8px);
animation: l2k-rd-slow-pan 18s ease-in-out infinite;
}
.l2k-rd-card::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(120deg, transparent 0%, rgba(255,255,255,0.04) 20%, transparent 40%);
}
.l2k-rd-title {
font-size: 14px;
font-weight: 800;
letter-spacing: 0.03em;
color: #f8fbff;
margin-bottom: 4px;
}
.l2k-rd-subtitle {
font-size: 12px;
color: #93c5fd;
margin-bottom: 14px;
}
.l2k-rd-badge-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.l2k-rd-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 7px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
color: #d9fff2;
border: 1px solid rgba(110, 231, 183, 0.24);
background: rgba(16, 185, 129, 0.12);
}
.l2k-rd-section {
margin-top: 12px;
}
.l2k-rd-section-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #9fb4d4;
margin-bottom: 8px;
}
.l2k-rd-release-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 4px;
margin-bottom: 12px;
}
.l2k-rd-release-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
border-radius: 14px;
border: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(255, 255, 255, 0.04);
}
.l2k-rd-release-body {
min-width: 0;
flex: 1 1 auto;
}
.l2k-rd-release-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #9fb4d4;
margin-bottom: 6px;
}
.l2k-rd-release-name {
font-size: 13px;
font-weight: 700;
color: #f8fbff;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.l2k-rd-release-name a {
color: #f8fbff;
text-decoration: none;
}
.l2k-rd-release-name a:hover {
text-decoration: underline;
}
.l2k-rd-release-meta {
font-size: 12px;
color: #9ec5ff;
}
.l2k-rd-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.l2k-rd-list-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 9px 10px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(148, 163, 184, 0.12);
}
.l2k-rd-list-item a {
color: #dbeafe;
text-decoration: none;
font-size: 12px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.l2k-rd-list-item a:hover {
text-decoration: underline;
}
.l2k-rd-count {
font-size: 12px;
color: #86efac;
font-weight: 700;
white-space: nowrap;
}
.l2k-rd-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 12px;
margin-top: 14px;
font-size: 12px;
}
.l2k-rd-link {
color: #86efac !important;
text-decoration: none !important;
font-weight: 700;
}
.l2k-rd-link:hover {
text-decoration: underline !important;
}
.l2k-rd-muted {
color: #a9bbd5;
}
.l2k-rd-asset-badge {
display: inline-flex;
margin-left: 8px;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
color: #d1fae5;
border: 1px solid rgba(110, 231, 183, 0.24);
background: rgba(16, 185, 129, 0.12);
vertical-align: middle;
}
.l2k-rd-inline-summary {
margin-top: 16px;
}
.l2k-rd-error {
color: #fecaca;
border-color: rgba(248, 113, 113, 0.25);
background:
radial-gradient(circle at top left, rgba(239, 68, 68, 0.18), transparent 30%),
linear-gradient(145deg, rgba(30, 10, 16, 0.98), rgba(42, 15, 23, 0.98));
}
@media (max-width: 768px) {
.l2k-rd-release-row {
grid-template-columns: 1fr;
}
}
`;
document.head.appendChild(style);
}
function formatNumber(value) {
return Number(value || 0).toLocaleString();
}
function formatDate(value) {
if (!value) return 'Unknown';
const date = new Date(value);
return date.toLocaleString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
function getCacheKey(repoFull) {
return `${SCRIPT_ID}:${repoFull}`;
}
function loadCache(repoFull) {
try {
const raw = sessionStorage.getItem(getCacheKey(repoFull));
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || (Date.now() - parsed.cachedAt) > CACHE_TTL_MS) {
sessionStorage.removeItem(getCacheKey(repoFull));
return null;
}
return parsed.payload;
} catch {
return null;
}
}
function saveCache(repoFull, payload) {
try {
sessionStorage.setItem(getCacheKey(repoFull), JSON.stringify({
cachedAt: Date.now(),
payload
}));
} catch {
}
}
async function fetchJson(url) {
const res = await fetch(url, {
headers: {
'Accept': 'application/vnd.github+json'
}
});
if (!res.ok) {
throw new Error(`GitHub API error ${res.status}`);
}
return res.json();
}
async function fetchRepoPayload(repoFull) {
const cached = loadCache(repoFull);
if (cached) return cached;
const [repoInfo, releases] = await Promise.all([
fetchJson(`https://api.github.com/repos/${repoFull}`),
fetchAllReleases(repoFull)
]);
const payload = { repoInfo, releases };
saveCache(repoFull, payload);
return payload;
}
async function fetchAllReleases(repoFull) {
const releases = [];
for (let page = 1; page <= MAX_RELEASE_PAGES; page++) {
const url = `https://api.github.com/repos/${repoFull}/releases?per_page=100&page=${page}`;
const batch = await fetchJson(url);
if (!Array.isArray(batch) || batch.length === 0) break;
releases.push(...batch);
if (batch.length < 100) break;
}
return releases;
}
function computeStats(repoInfo, releases) {
let totalDownloads = 0;
let totalAssets = 0;
const topAssets = [];
const releaseSummaries = [];
for (const release of releases) {
const assets = Array.isArray(release.assets) ? release.assets : [];
let releaseDownloads = 0;
for (const asset of assets) {
const count = Number(asset.download_count || 0);
releaseDownloads += count;
totalDownloads += count;
totalAssets += 1;
topAssets.push({
name: asset.name,
count,
url: asset.browser_download_url,
releaseName: release.name || release.tag_name || 'Release'
});
}
releaseSummaries.push({
id: release.id,
name: release.name || release.tag_name || 'Untitled release',
tag: release.tag_name || '',
url: release.html_url,
assetsCount: assets.length,
downloads: releaseDownloads,
publishedAt: release.published_at || release.created_at
});
}
topAssets.sort((a, b) => b.count - a.count);
releaseSummaries.sort((a, b) => b.downloads - a.downloads);
const latestRelease = releases[0] || null;
const firstRelease = releases.length > 0 ? releases[releases.length - 1] : null;
const latestReleaseDownloads = latestRelease
? releaseSummaries.find(r => r.id === latestRelease.id)?.downloads || 0
: 0;
const firstReleaseDownloads = firstRelease
? releaseSummaries.find(r => r.id === firstRelease.id)?.downloads || 0
: 0;
return {
repoInfo,
releases,
totalDownloads,
totalAssets,
latestRelease,
latestReleaseDownloads,
firstRelease,
firstReleaseDownloads,
topAssets: topAssets.slice(0, 5),
topReleases: releaseSummaries.slice(0, 5),
releaseCount: releases.length
};
}
function createEl(tag, className, text) {
const el = document.createElement(tag);
if (className) el.className = className;
if (typeof text === 'string') el.textContent = text;
return el;
}
function makeBadge(text) {
return createEl('span', 'l2k-rd-badge', text);
}
function makeListItem(label, count, url) {
const item = createEl('div', 'l2k-rd-list-item');
const link = createEl('a');
link.href = url;
link.textContent = label;
link.target = '_blank';
link.rel = 'noopener noreferrer';
item.appendChild(link);
item.appendChild(createEl('div', 'l2k-rd-count', `${formatNumber(count)} downloads`));
return item;
}
function makeReleasePanel(label, release, downloads) {
const panel = createEl('div', 'l2k-rd-release-panel');
const body = createEl('div', 'l2k-rd-release-body');
body.appendChild(createEl('div', 'l2k-rd-release-label', label));
const name = createEl('div', 'l2k-rd-release-name');
if (release?.html_url) {
const link = createEl('a');
link.href = release.html_url;
link.textContent = release.name || release.tag_name || 'Untitled release';
link.target = '_blank';
link.rel = 'noopener noreferrer';
name.appendChild(link);
} else {
name.textContent = 'No release';
}
body.appendChild(name);
const parts = [];
if (release?.published_at || release?.created_at) {
parts.push(formatDate(release.published_at || release.created_at));
}
if (release?.assets?.length) {
parts.push(`${formatNumber(release.assets.length)} assets`);
}
body.appendChild(createEl('div', 'l2k-rd-release-meta', parts.join(' • ')));
panel.appendChild(body);
panel.appendChild(createEl('div', 'l2k-rd-count', `${formatNumber(downloads)} downloads`));
return panel;
}
function buildDashboardCard(stats, repoFull) {
const card = createEl('section', 'l2k-rd-card');
card.appendChild(createEl('div', 'l2k-rd-title', 'Release Dashboard'));
card.appendChild(createEl('div', 'l2k-rd-subtitle', repoFull));
const badges = createEl('div', 'l2k-rd-badge-row');
badges.appendChild(makeBadge(`Total Downloads: ${formatNumber(stats.totalDownloads)}`));
badges.appendChild(makeBadge(`Releases: ${formatNumber(stats.releaseCount)}`));
badges.appendChild(makeBadge(`Assets: ${formatNumber(stats.totalAssets)}`));
badges.appendChild(makeBadge(`Stars: ${formatNumber(stats.repoInfo.stargazers_count)}`));
badges.appendChild(makeBadge(`Forks: ${formatNumber(stats.repoInfo.forks_count)}`));
badges.appendChild(makeBadge(`Issues: ${formatNumber(stats.repoInfo.open_issues_count)}`));
card.appendChild(badges);
const releaseRow = createEl('div', 'l2k-rd-release-row');
releaseRow.appendChild(makeReleasePanel('Last Release', stats.latestRelease, stats.latestReleaseDownloads));
releaseRow.appendChild(makeReleasePanel('First Release', stats.firstRelease, stats.firstReleaseDownloads));
card.appendChild(releaseRow);
const footer = createEl('div', 'l2k-rd-footer');
const latestLink = createEl('a', 'l2k-rd-link', 'Open latest release');
latestLink.href = `https://github.com/${repoFull}/releases/latest`;
latestLink.target = '_blank';
latestLink.rel = 'noopener noreferrer';
footer.appendChild(latestLink);
const pushed = createEl('span', 'l2k-rd-muted', `Last push: ${formatDate(stats.repoInfo.pushed_at)}`);
footer.appendChild(pushed);
card.appendChild(footer);
return card;
}
function buildReleasesSummaryCard(stats, repoFull) {
return buildDashboardCard(stats, repoFull);
}
function mountOnRepoHome(stats, repoFull) {
const host =
document.querySelector('main .Layout-sidebar .BorderGrid') ||
document.querySelector('main .Layout-sidebar') ||
document.querySelector('main');
if (!host) return false;
if (document.getElementById(`${SCRIPT_ID}-repo-home`)) return true;
const card = buildDashboardCard(stats, repoFull);
card.id = `${SCRIPT_ID}-repo-home`;
if (host.classList.contains('BorderGrid')) {
const row = document.createElement('div');
row.className = 'BorderGrid-row';
const cell = document.createElement('div');
cell.className = 'BorderGrid-cell';
cell.appendChild(card);
row.appendChild(cell);
host.prepend(row);
} else {
host.prepend(card);
}
return true;
}
function annotateReleaseAssets(stats) {
const assetCounts = new Map();
for (const release of stats.releases) {
for (const asset of release.assets || []) {
assetCounts.set(asset.name, Number(asset.download_count || 0));
}
}
const links = document.querySelectorAll('a[href*="/releases/download/"]');
for (const link of links) {
const fileName = link.textContent.trim();
if (!fileName || link.dataset.l2kAssetAnnotated === '1') continue;
const count = assetCounts.get(fileName);
if (count === undefined) continue;
const badge = createEl('span', 'l2k-rd-asset-badge', `${formatNumber(count)} downloads`);
link.after(badge);
link.dataset.l2kAssetAnnotated = '1';
}
}
function mountOnReleasesPage(stats, repoFull) {
const host =
document.querySelector('main .container-xl') ||
document.querySelector('main [data-testid="release-list"]') ||
document.querySelector('main');
if (!host) return false;
if (!document.getElementById(`${SCRIPT_ID}-releases-page`)) {
const wrapper = createEl('div', 'l2k-rd-inline-summary');
wrapper.id = `${SCRIPT_ID}-releases-page`;
wrapper.appendChild(buildReleasesSummaryCard(stats, repoFull));
const anchor =
host.querySelector('[data-testid="release-list"]') ||
host.firstElementChild ||
host;
if (anchor && anchor.parentElement) {
anchor.parentElement.insertBefore(wrapper, anchor);
} else {
host.prepend(wrapper);
}
}
annotateReleaseAssets(stats);
return true;
}
function mountError(message, context) {
const existing = document.getElementById(`${SCRIPT_ID}-error`);
if (existing) return;
const card = createEl('section', 'l2k-rd-card l2k-rd-error');
card.id = `${SCRIPT_ID}-error`;
card.appendChild(createEl('div', 'l2k-rd-title', 'Release Dashboard'));
card.appendChild(createEl('div', 'l2k-rd-subtitle', context.fullName));
card.appendChild(createEl('div', 'l2k-rd-muted', message));
const host = document.querySelector('main .Layout-sidebar') || document.querySelector('main');
if (host) host.prepend(card);
}
function clearMountedUi() {
const ids = [
`${SCRIPT_ID}-repo-home`,
`${SCRIPT_ID}-releases-page`,
`${SCRIPT_ID}-error`
];
for (const id of ids) {
const node = document.getElementById(id);
if (node) {
node.remove();
}
}
}
async function boot() {
const context = parseRepoContext();
if (!context || (!context.isRepoHome && !context.isReleasesPage)) return;
injectStyles();
clearMountedUi();
const thisBoot = ++bootToken;
currentRepoKey = context.fullName;
try {
const payload = await fetchRepoPayload(context.fullName);
if (thisBoot !== bootToken || currentRepoKey !== context.fullName) return;
const stats = computeStats(payload.repoInfo, payload.releases);
if (context.isRepoHome) mountOnRepoHome(stats, context.fullName);
if (context.isReleasesPage) mountOnReleasesPage(stats, context.fullName);
} catch (error) {
if (thisBoot !== bootToken) return;
mountError(`Unable to load release stats right now. ${error.message || error}`, context);
console.error(`${SCRIPT_ID}:`, error);
}
}
function scheduleBoot() {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
boot();
});
});
}
window.addEventListener('turbo:load', scheduleBoot, { passive: true });
window.addEventListener('turbo:render', scheduleBoot, { passive: true });
scheduleBoot();
})();