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; 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 { .overview-page .ov-chart-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: repeat(3, 1fr);
gap: 16px; gap: 16px;
margin-bottom: 18px; margin-bottom: 18px;
} }
@@ -5990,7 +6025,9 @@ body {
} }
.overview-page .ov-dot-accent { background: var(--accent, #3B4AD6); } .overview-page .ov-dot-accent { background: var(--accent, #3B4AD6); }
.overview-page .ov-dot-ok-fill { background: #2F7A44; } .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-ok-fill { background: #7DC894; }
.dark-mode .overview-page .ov-dot-user-fill { background: #D0A15F; }
/* Spark bar chart */ /* Spark bar chart */
.overview-page .ov-spark-bars { .overview-page .ov-spark-bars {
@@ -6013,7 +6050,9 @@ body {
opacity: 0.55; opacity: 0.55;
} }
.overview-page .ov-spark-fill-alt { background: #2F7A44; } .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-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-col:hover .ov-spark-fill { opacity: 1; }
.overview-page .ov-spark-x { .overview-page .ov-spark-x {
display: flex; display: flex;
@@ -6233,10 +6272,12 @@ body {
/* ── Responsive ───────────────────────────────────────────────── */ /* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 900px) { @media (max-width: 900px) {
.overview-page .ov-kpi-row { grid-template-columns: repeat(3, 1fr); } .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; } .overview-page .ov-triple-row { grid-template-columns: 1fr; }
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.overview-page .ov-kpi-row { grid-template-columns: repeat(2, 1fr); } .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-chart-row { grid-template-columns: 1fr; }
.overview-page .ov-header { flex-direction: column; gap: 8px; } .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-kpi-row { grid-template-columns: 1fr 1fr; }
.overview-page .ov-services-grid { grid-template-columns: 1fr 1fr; } .overview-page .ov-services-grid { grid-template-columns: 1fr 1fr; }
} }
+44 -8
View File
@@ -57,17 +57,36 @@
</div> </div>
</section> </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"> <section class="ov-chart-row">
<div class="ov-chart-card"> <div class="ov-chart-card">
<div class="ov-chart-head"> <div class="ov-chart-head">
<span class="ov-chart-title">Page views &middot; 30d</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</span> <span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day</span>
</div> </div>
<div class="ov-spark-bars" ng-if="data.history.length"> <div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col" ng-repeat="d in data.history track by $index" <div class="ov-spark-col" ng-repeat="d in data.history track by $index"
title="{{historyLabel(d)}}: {{d.nbPageViews | number}} views"> title="{{historyLabel(d)}}: +{{d.dailyPageViews | number}} views">
<span class="ov-spark-fill" ng-style="{height: historyBarH(d, 'nbPageViews') + 'px'}"></span> <span class="ov-spark-fill" ng-style="{height: historyBarH(d, 'dailyPageViews') + 'px'}"></span>
</div> </div>
</div> </div>
<div class="ov-spark-x" ng-if="data.history.length"> <div class="ov-spark-x" ng-if="data.history.length">
@@ -79,12 +98,29 @@
<div class="ov-chart-card"> <div class="ov-chart-card">
<div class="ov-chart-head"> <div class="ov-chart-head">
<span class="ov-chart-title">New repos &middot; 30d</span> <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>
<div class="ov-spark-bars" ng-if="data.history.length"> <div class="ov-spark-bars" ng-if="data.history.length">
<div class="ov-spark-col" ng-repeat="d in data.history track by $index" <div class="ov-spark-col" ng-repeat="d in data.history track by $index"
title="{{historyLabel(d)}}: {{d.nbRepositories | number}} repos"> title="{{historyLabel(d)}}: +{{d.dailyRepositories | number}} repos">
<span class="ov-spark-fill ov-spark-fill-alt" ng-style="{height: historyBarH(d, 'nbRepositories') + 'px'}"></span> <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> </div>
<div class="ov-spark-x" ng-if="data.history.length"> <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; 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 = {}; var historyMaxes = {};
$scope.historyBarH = function (d, field) { $scope.historyBarH = function (d, field) {
if (!d || !historyMaxes[field]) return 0; if (!d || !historyMaxes[field]) return 0;
@@ -1839,12 +1860,16 @@ angular
function load() { function load() {
$http.get("/api/admin/overview").then(function (r) { $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.data = r.data;
$scope.loading = false; $scope.loading = false;
$scope.error = null; $scope.error = null;
historyMaxes = {}; historyMaxes = {};
(r.data.history || []).forEach(function (d) { (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]; 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(() => { getDefault(() => {
// Edit mode: repo // Edit mode: repo
if ($routeParams.repoId && $routeParams.repoId != "") { if ($routeParams.repoId && $routeParams.repoId != "") {
@@ -1695,6 +1713,7 @@ angular
$http.get("/api/repo/" + $scope.repoId).then( $http.get("/api/repo/" + $scope.repoId).then(
async (res) => { async (res) => {
$scope.sourceUrl = "https://github.com/" + res.data.source.fullName; $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.terms = res.data.options.terms.filter((f) => f).join("\n");
$scope.source = res.data.source; $scope.source = res.data.source;
$scope.role = res.data.role || "owner"; $scope.role = res.data.role || "owner";
@@ -1708,6 +1727,7 @@ angular
$scope.options = Object.assign({}, $scope.options, res.data.options); $scope.options = Object.assign({}, $scope.options, res.data.options);
$scope.conference = res.data.conference; $scope.conference = res.data.conference;
$scope.repositoryID = res.data.source.repositoryID; $scope.repositoryID = res.data.source.repositoryID;
$scope._originalRepositoryID = res.data.source.repositoryID;
if (res.data.options.expirationDate) { if (res.data.options.expirationDate) {
$scope.options.expirationDate = new Date(res.data.options.expirationDate); $scope.options.expirationDate = new Date(res.data.options.expirationDate);
} }
@@ -1719,7 +1739,6 @@ angular
); );
$scope.$watch("anonymize", () => { $scope.$watch("anonymize", () => {
if ($scope.anonymize.repoId) $scope.anonymize.repoId.$$element[0].disabled = true; 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 // Edit mode: PR
@@ -1788,9 +1807,11 @@ angular
// URL change handler - auto-detect type // URL change handler - auto-detect type
$scope.urlSelected = async () => { $scope.urlSelected = async () => {
$scope.terms = $scope.defaultTerms; $scope.terms = $scope.defaultTerms;
$scope.repoId = ""; if (!$scope.isUpdate) {
$scope.pullRequestId = ""; $scope.repoId = "";
$scope.gistId = ""; $scope.pullRequestId = "";
$scope.gistId = "";
}
$scope.details = null; $scope.details = null;
$scope.branches = []; $scope.branches = [];
$scope.source = { type: "GitHubStream", branch: "", commit: "" }; $scope.source = { type: "GitHubStream", branch: "", commit: "" };
@@ -1862,7 +1883,7 @@ angular
const o = parseGithubUrl($scope.sourceUrl); const o = parseGithubUrl($scope.sourceUrl);
try { try {
const branches = await $http.get(`/api/repo/${o.owner}/${o.repo}/branches`, { 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.branches = branches.data;
$scope.sourceUnreachable = false; $scope.sourceUnreachable = false;
@@ -1910,9 +1931,12 @@ angular
// #364) are reflected without waiting for the cached metadata to // #364) are reflected without waiting for the cached metadata to
// expire. The endpoint hits the GitHub API once. // expire. The endpoint hits the GitHub API once.
const res = await $http.get(`/api/repo/${o.owner}/${o.repo}/`, { 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; $scope.details = res.data;
if ($scope.details && $scope.details.id) {
$scope.repositoryID = $scope.details.id;
}
if (!$scope.repoId) { if (!$scope.repoId) {
$scope.repoId = $scope.details.repo + "-" + generateRandomId(4); $scope.repoId = $scope.details.repo + "-" + generateRandomId(4);
} }
@@ -1935,7 +1959,7 @@ angular
const o = parseGithubUrl($scope.sourceUrl); const o = parseGithubUrl($scope.sourceUrl);
try { try {
const res = await $http.get(`/api/repo/${o.owner}/${o.repo}/readme`, { 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; $scope.readme = res.data;
} catch (error) { } catch (error) {
+56 -15
View File
@@ -457,25 +457,66 @@ export default class GitHubStream extends GitHubBase {
}); });
} }
} }
const fetchSubtree = async (
entry: { sha: string; parentPath: string }
) => {
const data = await this.getGHTree(oct, token, entry.sha, count, {
recursive: true,
callback: () => {
if (progress) {
progress("List file: " + count.file);
}
},
});
if (!data.truncated) {
return this.tree2Tree(data.tree, entry.parentPath);
}
// Subtree was truncated — break it down by fetching non-recursively
// and then recursing into each child subtree individually.
logger.info(
`Tree truncated for ${entry.parentPath}, breaking down into subtrees`
);
const shallow = await this.getGHTree(oct, token, entry.sha, count, {
recursive: false,
callback: () => {
if (progress) {
progress("List file: " + count.file);
}
},
});
if (shallow.truncated) {
this._truncatedFolders.push(entry.parentPath);
}
const files = this.tree2Tree(shallow.tree, entry.parentPath);
const childSubtrees = shallow.tree
.filter(
(f): f is typeof f & { sha: string; path: string } =>
f.type === "tree" && !!f.path && !!f.sha
)
.map((f) => ({
sha: f.sha,
parentPath: path.join(entry.parentPath, f.path),
}));
const childResults = await pMap(
childSubtrees,
(child) => fetchSubtree(child),
GH_API_CONCURRENCY
);
for (const childFiles of childResults) {
files.push(...childFiles);
}
return files;
};
const results = await pMap( const results = await pMap(
subtrees, subtrees,
async (entry) => (entry) => fetchSubtree(entry),
this.getGHTree(oct, token, entry.sha, count, {
recursive: true,
callback: () => {
if (progress) {
progress("List file: " + count.file);
}
},
}),
GH_API_CONCURRENCY GH_API_CONCURRENCY
); );
results.forEach((data, i) => { for (const files of results) {
if (data.truncated) { output.push(...files);
this._truncatedFolders.push(subtrees[i].parentPath); }
}
output.push(...this.tree2Tree(data.tree, subtrees[i].parentPath));
});
return output; return output;
} }
+29
View File
@@ -12,6 +12,10 @@ export interface HomeStats {
nbPullRequests: number; nbPullRequests: number;
} }
export interface HomeStatsHistoryRow extends HomeStats {
date: Date;
}
export async function computeStats(): Promise<HomeStats> { export async function computeStats(): Promise<HomeStats> {
const [nbRepositories, nbUsersAgg, nbPageViews, nbPullRequests] = const [nbRepositories, nbUsersAgg, nbPageViews, nbPullRequests] =
await Promise.all([ await Promise.all([
@@ -40,6 +44,31 @@ function utcMidnight(d: Date = new Date()): Date {
); );
} }
export function mergeCurrentStatsIntoHistory(
rows: HomeStatsHistoryRow[],
currentStats: HomeStats,
now: Date = new Date()
): HomeStatsHistoryRow[] {
const today = utcMidnight(now);
const history = rows.map((row) => ({
...row,
date: new Date(row.date),
}));
const currentRow = { date: today, ...currentStats };
const todayTime = today.getTime();
const todayIndex = history.findIndex(
(row) => utcMidnight(row.date).getTime() === todayTime
);
if (todayIndex >= 0) {
history[todayIndex] = currentRow;
} else {
history.push(currentRow);
}
return history.sort((a, b) => a.date.getTime() - b.date.getTime());
}
export async function computeAndStoreDailyStats(): Promise<void> { export async function computeAndStoreDailyStats(): Promise<void> {
try { try {
const stats = await computeStats(); const stats = await computeStats();
+5 -2
View File
@@ -23,6 +23,8 @@ import { startWorker, recoverStuckPreparing } from "../queue";
import { import {
computeStats, computeStats,
ensureTodaySnapshot, ensureTodaySnapshot,
HomeStatsHistoryRow,
mergeCurrentStatsIntoHistory,
} from "./dailyStatsSnapshot"; } from "./dailyStatsSnapshot";
import DailyStatsModel from "../core/model/dailyStats/dailyStats.model"; import DailyStatsModel from "../core/model/dailyStats/dailyStats.model";
import { getUser } from "./routes/route-utils"; import { getUser } from "./routes/route-utils";
@@ -238,7 +240,7 @@ export default async function start() {
}); });
let stat: Record<string, unknown> = {}; let stat: Record<string, unknown> = {};
let history: Array<Record<string, unknown>> | null = null; let history: HomeStatsHistoryRow[] | null = null;
let historyKey: number | null = null; let historyKey: number | null = null;
setInterval(() => { setInterval(() => {
@@ -274,13 +276,14 @@ export default async function start() {
const docs = await DailyStatsModel.find({ date: { $gte: since } }) const docs = await DailyStatsModel.find({ date: { $gte: since } })
.sort({ date: 1 }) .sort({ date: 1 })
.lean(); .lean();
history = docs.map((d) => ({ const rows = docs.map((d) => ({
date: d.date, date: d.date,
nbRepositories: d.nbRepositories, nbRepositories: d.nbRepositories,
nbUsers: d.nbUsers, nbUsers: d.nbUsers,
nbPageViews: d.nbPageViews, nbPageViews: d.nbPageViews,
nbPullRequests: d.nbPullRequests, nbPullRequests: d.nbPullRequests,
})); }));
history = mergeCurrentStatsIntoHistory(rows, await computeStats());
historyKey = days; historyKey = days;
res.json(history); res.json(history);
}); });
+9 -2
View File
@@ -8,6 +8,11 @@ import ConferenceModel from "../../core/model/conference/conferences.model";
import UserModel from "../../core/model/users/users.model"; import UserModel from "../../core/model/users/users.model";
import { cacheQueue, downloadQueue, removeQueue } from "../../queue"; import { cacheQueue, downloadQueue, removeQueue } from "../../queue";
import { queryMetrics } from "../../queue/queueMetrics"; import { queryMetrics } from "../../queue/queueMetrics";
import {
computeStats,
HomeStatsHistoryRow,
mergeCurrentStatsIntoHistory,
} from "../dailyStatsSnapshot";
import User from "../../core/User"; import User from "../../core/User";
import { ensureAuthenticated } from "./connection"; import { ensureAuthenticated } from "./connection";
import { handleError, getUser, isOwnerOrAdmin, getRepo } from "./route-utils"; import { handleError, getUser, isOwnerOrAdmin, getRepo } from "./route-utils";
@@ -712,7 +717,7 @@ router.get("/overview", async (req, res) => {
} }
// Daily history (last 30 days) from DailyStatsModel // Daily history (last 30 days) from DailyStatsModel
let history: Array<Record<string, unknown>> = []; let history: HomeStatsHistoryRow[] = [];
try { try {
const { default: DailyStatsModel } = await import( const { default: DailyStatsModel } = await import(
"../../core/model/dailyStats/dailyStats.model" "../../core/model/dailyStats/dailyStats.model"
@@ -723,12 +728,14 @@ router.get("/overview", async (req, res) => {
const docs = await DailyStatsModel.find({ date: { $gte: since } }) const docs = await DailyStatsModel.find({ date: { $gte: since } })
.sort({ date: 1 }) .sort({ date: 1 })
.lean(); .lean();
history = docs.map((d) => ({ const rows = docs.map((d) => ({
date: d.date, date: d.date,
nbRepositories: d.nbRepositories, nbRepositories: d.nbRepositories,
nbUsers: d.nbUsers, nbUsers: d.nbUsers,
nbPageViews: d.nbPageViews, nbPageViews: d.nbPageViews,
nbPullRequests: d.nbPullRequests,
})); }));
history = mergeCurrentStatsIntoHistory(rows, await computeStats());
} catch { } catch {
// DailyStats collection might not exist yet // DailyStats collection might not exist yet
} }
+20 -19
View File
@@ -396,24 +396,7 @@ router.post(
validateNewRepo(repoUpdate); validateNewRepo(repoUpdate);
// Only the commit/branch backs the cached FileModel — anonymization const r = gh(repoUpdate.fullName);
// options (terms, image/link toggles, etc.) are applied on the fly per
// request. Re-running the download queue is therefore only needed when
// the underlying snapshot moves. Other edits (e.g. turning off
// auto-update — see #360) just persist and return.
const sourceChanged =
repoUpdate.source.commit != repo.model.source.commit ||
repoUpdate.source.branch != repo.model.source.branch;
if (sourceChanged) {
repo.model.anonymizeDate = new Date();
repo.model.source.commit = repoUpdate.source.commit;
await repo.remove();
}
updateRepoModel(repo.model, repoUpdate);
const r = gh(repo.model.source.repositoryName || repoUpdate.fullName);
if (!r?.owner || !r?.name) { if (!r?.owner || !r?.name) {
await repo.resetSate(RepositoryStatus.ERROR, "repo_not_found"); await repo.resetSate(RepositoryStatus.ERROR, "repo_not_found");
throw new AnonymousError("repo_not_found", { throw new AnonymousError("repo_not_found", {
@@ -421,11 +404,22 @@ router.post(
httpStatus: 404, httpStatus: 404,
}); });
} }
// Only the source repository/commit/branch backs the cached FileModel —
// anonymization options (terms, image/link toggles, etc.) are applied on
// the fly per request. Re-running the download queue is therefore only
// needed when the underlying snapshot moves. Other edits (e.g. turning
// off auto-update — see #360) just persist and return.
const sourceChanged =
repoUpdate.source.commit != repo.model.source.commit ||
repoUpdate.source.branch != repo.model.source.branch ||
repoUpdate.fullName != repo.model.source.repositoryName;
updateRepoModel(repo.model, repoUpdate);
const repository = await getRepositoryFromGitHub({ const repository = await getRepositoryFromGitHub({
accessToken: user.accessToken, accessToken: user.accessToken,
owner: r.owner, owner: r.owner,
repo: r.name, repo: r.name,
repositoryID: repo.model.source.repositoryId,
}); });
if (!repository) { if (!repository) {
@@ -439,6 +433,13 @@ router.post(
await repository.getCommitInfo(repoUpdate.source.commit, { await repository.getCommitInfo(repoUpdate.source.commit, {
accessToken: user.accessToken, accessToken: user.accessToken,
}); });
repo.model.source.repositoryId = repository.model.id;
repo.model.source.repositoryName = repository.fullName || repoUpdate.fullName;
if (sourceChanged) {
repo.model.anonymizeDate = new Date();
await repo.remove();
}
const removeRepoFromConference = async (conferenceID: string) => { const removeRepoFromConference = async (conferenceID: string) => {
const conf = await ConferenceModel.findOne({ const conf = await ConferenceModel.findOne({
+64
View File
@@ -0,0 +1,64 @@
require("ts-node/register/transpile-only");
const { expect } = require("chai");
const {
mergeCurrentStatsIntoHistory,
} = require("../src/server/dailyStatsSnapshot");
describe("daily stats history", function () {
const liveStats = {
nbRepositories: 15,
nbUsers: 7,
nbPageViews: 120,
nbPullRequests: 4,
};
it("replaces today's stored snapshot with live totals", function () {
const rows = [
{
date: new Date("2026-05-10T00:00:00.000Z"),
nbRepositories: 10,
nbUsers: 5,
nbPageViews: 100,
nbPullRequests: 2,
},
{
date: new Date("2026-05-11T00:05:00.000Z"),
nbRepositories: 11,
nbUsers: 5,
nbPageViews: 101,
nbPullRequests: 2,
},
];
const history = mergeCurrentStatsIntoHistory(
rows,
liveStats,
new Date("2026-05-11T14:30:00.000Z")
);
expect(history).to.have.length(2);
expect(history[1]).to.deep.include(liveStats);
expect(history[1].date.toISOString()).to.equal("2026-05-11T00:00:00.000Z");
});
it("appends today's live totals when no snapshot exists", function () {
const history = mergeCurrentStatsIntoHistory(
[
{
date: new Date("2026-05-10T00:00:00.000Z"),
nbRepositories: 10,
nbUsers: 5,
nbPageViews: 100,
nbPullRequests: 2,
},
],
liveStats,
new Date("2026-05-11T14:30:00.000Z")
);
expect(history).to.have.length(2);
expect(history[1]).to.deep.include(liveStats);
expect(history[1].date.toISOString()).to.equal("2026-05-11T00:00:00.000Z");
});
});