repo change + daily stat improvements

This commit is contained in:
tdurieux
2026-05-11 11:55:16 +03:00
parent b03c4b437c
commit 03e18fd572
10 changed files with 327 additions and 57 deletions
+43 -3
View File
@@ -5951,10 +5951,45 @@ body {
margin-top: 2px;
}
/* ── Two chart cards side-by-side ─────────────────────────────── */
/* ── Daily highlights ─────────────────────────────────────────── */
.overview-page .ov-daily-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin: -4px 0 22px;
}
.overview-page .ov-daily-card {
background: var(--primary-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px 20px;
box-shadow: var(--card-shadow);
}
.overview-page .ov-daily-label {
font-family: var(--font-mono);
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-muted);
}
.overview-page .ov-daily-value {
font-family: var(--font-serif);
font-size: 2.35rem;
font-weight: 400;
line-height: 1;
color: var(--accent, #3B4AD6);
margin-top: 6px;
}
.overview-page .ov-daily-sub {
font-size: 0.76rem;
color: var(--ink-muted);
margin-top: 5px;
}
/* ── Chart cards ─────────────────────────────────────────────── */
.overview-page .ov-chart-row {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 18px;
}
@@ -5990,7 +6025,9 @@ body {
}
.overview-page .ov-dot-accent { background: var(--accent, #3B4AD6); }
.overview-page .ov-dot-ok-fill { background: #2F7A44; }
.overview-page .ov-dot-user-fill { background: #8B5E2E; }
.dark-mode .overview-page .ov-dot-ok-fill { background: #7DC894; }
.dark-mode .overview-page .ov-dot-user-fill { background: #D0A15F; }
/* Spark bar chart */
.overview-page .ov-spark-bars {
@@ -6013,7 +6050,9 @@ body {
opacity: 0.55;
}
.overview-page .ov-spark-fill-alt { background: #2F7A44; }
.overview-page .ov-spark-fill-user { background: #8B5E2E; }
.dark-mode .overview-page .ov-spark-fill-alt { background: #7DC894; }
.dark-mode .overview-page .ov-spark-fill-user { background: #D0A15F; }
.overview-page .ov-spark-col:hover .ov-spark-fill { opacity: 1; }
.overview-page .ov-spark-x {
display: flex;
@@ -6233,10 +6272,12 @@ body {
/* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 900px) {
.overview-page .ov-kpi-row { grid-template-columns: repeat(3, 1fr); }
.overview-page .ov-daily-row { grid-template-columns: repeat(3, 1fr); }
.overview-page .ov-triple-row { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.overview-page .ov-kpi-row { grid-template-columns: repeat(2, 1fr); }
.overview-page .ov-daily-row { grid-template-columns: 1fr; }
.overview-page .ov-chart-row { grid-template-columns: 1fr; }
.overview-page .ov-header { flex-direction: column; gap: 8px; }
}
@@ -6244,4 +6285,3 @@ body {
.overview-page .ov-kpi-row { grid-template-columns: 1fr 1fr; }
.overview-page .ov-services-grid { grid-template-columns: 1fr 1fr; }
}
+44 -8
View File
@@ -57,17 +57,36 @@
</div>
</section>
<!-- ── Two wide charts ──────────────────────────────────────── -->
<!-- ── Daily activity highlights ───────────────────────────── -->
<section class="ov-daily-row">
<div class="ov-daily-card">
<div class="ov-daily-label">New repos today</div>
<div class="ov-daily-value">+{{data.daily.today.repositories | number}}</div>
<div class="ov-daily-sub">day-over-day total</div>
</div>
<div class="ov-daily-card">
<div class="ov-daily-label">New users today</div>
<div class="ov-daily-value">+{{data.daily.today.users | number}}</div>
<div class="ov-daily-sub">{{data.users.total | number}} total users</div>
</div>
<div class="ov-daily-card">
<div class="ov-daily-label">Page views today</div>
<div class="ov-daily-value">+{{data.daily.today.pageViews | number}}</div>
<div class="ov-daily-sub">since yesterday snapshot</div>
</div>
</section>
<!-- ── Daily charts ─────────────────────────────────────────── -->
<section class="ov-chart-row">
<div class="ov-chart-card">
<div class="ov-chart-head">
<span class="ov-chart-title">Page views &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views</span>
<span class="ov-chart-title">Daily page views &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day</span>
</div>
<div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col" ng-repeat="d in data.history track by $index"
title="{{historyLabel(d)}}: {{d.nbPageViews | number}} views">
<span class="ov-spark-fill" ng-style="{height: historyBarH(d, 'nbPageViews') + 'px'}"></span>
title="{{historyLabel(d)}}: +{{d.dailyPageViews | number}} views">
<span class="ov-spark-fill" ng-style="{height: historyBarH(d, 'dailyPageViews') + 'px'}"></span>
</div>
</div>
<div class="ov-spark-x" ng-if="data.history.length">
@@ -79,12 +98,29 @@
<div class="ov-chart-card">
<div class="ov-chart-head">
<span class="ov-chart-title">New repos &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos/day</span>
</div>
<div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col" ng-repeat="d in data.history track by $index"
title="{{historyLabel(d)}}: {{d.nbRepositories | number}} repos">
<span class="ov-spark-fill ov-spark-fill-alt" ng-style="{height: historyBarH(d, 'nbRepositories') + 'px'}"></span>
title="{{historyLabel(d)}}: +{{d.dailyRepositories | number}} repos">
<span class="ov-spark-fill ov-spark-fill-alt" ng-style="{height: historyBarH(d, 'dailyRepositories') + 'px'}"></span>
</div>
</div>
<div class="ov-spark-x" ng-if="data.history.length">
<span>{{historyLabel(data.history[0])}}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span>
</div>
</div>
<div class="ov-chart-card">
<div class="ov-chart-head">
<span class="ov-chart-title">New users &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>users/day</span>
</div>
<div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col" ng-repeat="d in data.history track by $index"
title="{{historyLabel(d)}}: +{{d.dailyUsers | number}} users">
<span class="ov-spark-fill ov-spark-fill-user" ng-style="{height: historyBarH(d, 'dailyUsers') + 'px'}"></span>
</div>
</div>
<div class="ov-spark-x" ng-if="data.history.length">
+26 -1
View File
@@ -1826,6 +1826,27 @@ angular
return ($scope.data.errors.severity[key] / max) * 100;
};
function computeDailyHistory(history) {
var rows = history || [];
return rows.map(function (d, i) {
var previous = rows[i - 1] || {};
var row = Object.assign({}, d);
row.dailyRepositories = i ? Math.max(0, (d.nbRepositories || 0) - (previous.nbRepositories || 0)) : 0;
row.dailyUsers = i ? Math.max(0, (d.nbUsers || 0) - (previous.nbUsers || 0)) : 0;
row.dailyPageViews = i ? Math.max(0, (d.nbPageViews || 0) - (previous.nbPageViews || 0)) : 0;
return row;
});
}
function todayDailyStats(history) {
var latest = history && history.length ? history[history.length - 1] : {};
return {
repositories: latest.dailyRepositories || 0,
users: latest.dailyUsers || 0,
pageViews: latest.dailyPageViews || 0,
};
}
var historyMaxes = {};
$scope.historyBarH = function (d, field) {
if (!d || !historyMaxes[field]) return 0;
@@ -1839,12 +1860,16 @@ angular
function load() {
$http.get("/api/admin/overview").then(function (r) {
r.data.history = computeDailyHistory(r.data.history);
r.data.daily = {
today: todayDailyStats(r.data.history),
};
$scope.data = r.data;
$scope.loading = false;
$scope.error = null;
historyMaxes = {};
(r.data.history || []).forEach(function (d) {
["nbPageViews", "nbRepositories", "nbUsers"].forEach(function (k) {
["dailyPageViews", "dailyRepositories", "dailyUsers"].forEach(function (k) {
if (!historyMaxes[k] || d[k] > historyMaxes[k]) historyMaxes[k] = d[k];
});
});
+31 -7
View File
@@ -1686,6 +1686,24 @@ angular
}
}
function parseRepoFullName(url) {
try {
const parsed = parseGithubUrl(url);
if (parsed && parsed.owner && parsed.repo) {
return parsed.owner + "/" + parsed.repo;
}
} catch (_) { /* sourceUrl not yet parseable */ }
return null;
}
function sourceRepositoryID() {
if (!$scope.isUpdate || !$scope._originalRepositoryID) return undefined;
const currentFullName = parseRepoFullName($scope.sourceUrl);
return currentFullName === $scope._originalFullName
? $scope._originalRepositoryID
: undefined;
}
getDefault(() => {
// Edit mode: repo
if ($routeParams.repoId && $routeParams.repoId != "") {
@@ -1695,6 +1713,7 @@ angular
$http.get("/api/repo/" + $scope.repoId).then(
async (res) => {
$scope.sourceUrl = "https://github.com/" + res.data.source.fullName;
$scope._originalFullName = res.data.source.fullName;
$scope.terms = res.data.options.terms.filter((f) => f).join("\n");
$scope.source = res.data.source;
$scope.role = res.data.role || "owner";
@@ -1708,6 +1727,7 @@ angular
$scope.options = Object.assign({}, $scope.options, res.data.options);
$scope.conference = res.data.conference;
$scope.repositoryID = res.data.source.repositoryID;
$scope._originalRepositoryID = res.data.source.repositoryID;
if (res.data.options.expirationDate) {
$scope.options.expirationDate = new Date(res.data.options.expirationDate);
}
@@ -1719,7 +1739,6 @@ angular
);
$scope.$watch("anonymize", () => {
if ($scope.anonymize.repoId) $scope.anonymize.repoId.$$element[0].disabled = true;
if ($scope.anonymize.sourceUrl) $scope.anonymize.sourceUrl.$$element[0].disabled = true;
});
}
// Edit mode: PR
@@ -1788,9 +1807,11 @@ angular
// URL change handler - auto-detect type
$scope.urlSelected = async () => {
$scope.terms = $scope.defaultTerms;
$scope.repoId = "";
$scope.pullRequestId = "";
$scope.gistId = "";
if (!$scope.isUpdate) {
$scope.repoId = "";
$scope.pullRequestId = "";
$scope.gistId = "";
}
$scope.details = null;
$scope.branches = [];
$scope.source = { type: "GitHubStream", branch: "", commit: "" };
@@ -1862,7 +1883,7 @@ angular
const o = parseGithubUrl($scope.sourceUrl);
try {
const branches = await $http.get(`/api/repo/${o.owner}/${o.repo}/branches`, {
params: { force: force === true ? "1" : "0", repositoryID: $scope.repositoryID },
params: { force: force === true ? "1" : "0", repositoryID: sourceRepositoryID() },
});
$scope.branches = branches.data;
$scope.sourceUnreachable = false;
@@ -1910,9 +1931,12 @@ angular
// #364) are reflected without waiting for the cached metadata to
// expire. The endpoint hits the GitHub API once.
const res = await $http.get(`/api/repo/${o.owner}/${o.repo}/`, {
params: { repositoryID: $scope.repositoryID, force: "1" },
params: { repositoryID: sourceRepositoryID(), force: "1" },
});
$scope.details = res.data;
if ($scope.details && $scope.details.id) {
$scope.repositoryID = $scope.details.id;
}
if (!$scope.repoId) {
$scope.repoId = $scope.details.repo + "-" + generateRandomId(4);
}
@@ -1935,7 +1959,7 @@ angular
const o = parseGithubUrl($scope.sourceUrl);
try {
const res = await $http.get(`/api/repo/${o.owner}/${o.repo}/readme`, {
params: { force: force === true ? "1" : "0", branch: $scope.source.branch, repositoryID: $scope.repositoryID },
params: { force: force === true ? "1" : "0", branch: $scope.source.branch, repositoryID: sourceRepositoryID() },
});
$scope.readme = res.data;
} catch (error) {