Files
anonymous_github/scripts/dev-mock.js
T
tdurieuxandClaude Opus 5.5 be2f979296 fix: keep redaction marks correct with custom masks, Prism and Mermaid
- The mask from /api/options is stored once loaded; code markers and
  rendered Markdown are refreshed when it arrives instead of keeping the
  XXXX default.
- Mermaid sources are left untouched so diagrams still parse.
- Fenced code blocks are marked from a Prism complete hook, after Prism
  has rewritten their HTML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:46:13 +02:00

785 lines
44 KiB
JavaScript

/**
* Mock server for local UI work on authenticated pages (dashboard, etc.).
*
* Serves the local `public/` folder exactly like scripts/dev-proxy.js, but
* instead of proxying the API to the live site it answers the user-facing
* endpoints with fixture data. No database, no GitHub login required.
*
* node scripts/dev-mock.js # default port 4002
* PORT=5000 node scripts/dev-mock.js
*
* The fixtures deliberately include awkward records (expired, error,
* stuck download, legacy record without an id, conference tag, unlimited
* quota) so the dashboard's edge cases can be checked visually.
*/
const path = require("path");
const fs = require("fs");
const express = require("express");
const PORT = parseInt(process.env.PORT || "4002", 10);
const PUBLIC_DIR = path.resolve(__dirname, "..", "public");
const manifestPath = path.join(PUBLIC_DIR, "asset-manifest.json");
function asset(name) {
try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
return manifest[name] || name;
} catch {
return name;
}
}
function indexHtml() {
return fs
.readFileSync(path.join(PUBLIC_DIR, "index.html"), "utf-8")
.replace("__CORE_JS__", asset("core.min.js"))
.replace("__VENDOR_JS__", asset("vendor.min.js"))
.replace("__MERMAID_JS__", asset("mermaid.min.js"))
.replace("__ALL_CSS__", asset("all.min.css"));
}
// ---- Fixtures --------------------------------------------------------------
const DAY = 24 * 60 * 60 * 1000;
const ago = (days) => new Date(Date.now() - days * DAY).toISOString();
const ahead = (days) => new Date(Date.now() + days * DAY).toISOString();
const opts = (extra) =>
Object.assign(
{ terms: [], expirationMode: "never", update: false, page: false },
extra || {}
);
const repositories = [
{
repoId: "anonymous_github-C72C",
status: "ready",
anonymizeDate: ago(3),
lastView: ago(1),
pageView: 15,
conference: "FSE25",
source: { fullName: "tdurieux/anonymous_github", branch: "main", commit: "c40da900ab12" },
options: opts({ expirationMode: "redirect", expirationDate: ahead(420), update: true, page: true }),
},
{
repoId: "840c8c57-3c32-451e-bf12-0e20be300389",
status: "ready",
anonymizeDate: ago(120),
lastView: ago(0.2),
pageView: 596460,
source: { fullName: "tdurieux/anonymous_github", branch: "main", commit: "f817a29a0000" },
options: opts({ update: true }),
},
{
repoId: "JarSift",
status: "expired",
anonymizeDate: ago(560),
lastView: ago(30),
pageView: 72,
conference: "FSE25",
source: { fullName: "Cornul11/JarSift", commit: "035adc67ffff" },
options: opts({ expirationMode: "remove", expirationDate: ago(9) }),
},
{
repoId: "b4751b8e-6139-4a94-84f8-646106435809",
status: "error",
statusMessage: "branch_not_found",
anonymizeDate: ago(1990),
pageView: 0,
source: { fullName: "tdurieux/anonymous_github", branch: "master", commit: "a5b0c7c3aaaa" },
options: opts({ update: true }),
},
{
repoId: "runtime-repair-experiments-very-long-identifier-2ff4",
status: "ready",
anonymizeDate: ago(40),
pageView: 5,
conference: "International Conference on Software Engineering 2026 Artifact Track",
source: { fullName: "Spirals-Team/runtime-repair-experiments", branch: "master", commit: "2ff4cdef1234" },
options: opts({ expirationMode: "remove", expirationDate: ahead(1) , update: true }),
},
{
repoId: "coauthored-repo-91AB",
status: "ready",
role: "coauthor",
anonymizeDate: ago(12),
pageView: 41,
source: { fullName: "someone-else/paper-artifact", commit: "b5ab2e21dddd" },
options: opts(),
},
];
const pullRequests = [
{
pullRequestId: "5774",
status: "ready",
anonymizeDate: ago(2),
lastView: ago(2),
pageView: 9,
source: { repositoryFullName: "vyperlang/vyper", pullRequestId: 3224 },
options: opts(),
},
{
pullRequestId: "436B",
status: "download",
anonymizeDate: ago(126),
pageView: 12,
source: { repositoryFullName: "lncapital/torq", pullRequestId: 307 },
options: opts(),
},
{
pullRequestId: "7CF7",
status: "preparing",
anonymizeDate: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
pageView: 0,
source: { repositoryFullName: "Tiledesk/tiledesk-server", pullRequestId: 89 },
options: opts(),
},
{
// Legacy record without an identifier or options.
status: "ready",
pageView: 0,
source: { repositoryFullName: "atmoz/sftp", pullRequestId: 357 },
},
{
pullRequestId: "demo-pr",
status: "removed",
anonymizeDate: ago(1030),
pageView: 319,
source: { repositoryFullName: "tdurieux/anonymous_github", pullRequestId: 156 },
options: opts(),
},
];
const gists = [
{
gistId: "g-4f2a",
status: "ready",
anonymizeDate: ago(6),
pageView: 3,
source: { gistId: "a1b2c3d4e5f6a7b8c9d0" },
options: opts({ expirationMode: "remove", expirationDate: ahead(90) }),
},
];
const quota = {
storage: { used: 5.6 * 1024 * 1024, total: 2 * 1024 * 1024 * 1024 },
file: { used: 1381, total: 0 },
repository: { used: 17, total: 20 },
};
// ---- Server ----------------------------------------------------------------
const app = express();
app.get(/^\/(script|css)\/(.+)\.([a-f0-9]{10})\.(min\.\w+|\w+)$/, (req, res, next) => {
const filePath = path.join(PUBLIC_DIR, req.params[0], `${req.params[1]}.${req.params[3]}`);
if (!fs.existsSync(filePath)) return next();
res.setHeader("Cache-Control", "no-store");
res.sendFile(filePath);
});
const conferences = [
{
conferenceID: "FSE25",
name: "Foundations of Software Engineering 2025",
url: "https://conf.researchr.org/home/fse-2025",
startDate: ago(400),
endDate: ahead(30),
status: "ready",
options: opts({ page: true }),
plan: { planID: "premium_conference", name: "Premium", pricePerRepository: 0.5 },
price: 12.5,
nbRepositories: 2,
repositories: repositories.slice(0, 4).map((r) => Object.assign({}, r, { addDate: ago(60) })),
},
{
conferenceID: "ICSE26-AE",
name: "ICSE 2026 Artifact Evaluation",
url: "https://conf.researchr.org/home/icse-2026",
startDate: ahead(10),
endDate: ahead(120),
status: "ready",
options: opts(),
plan: { planID: "free_conference", name: "Free", pricePerRepository: 0 },
price: 0,
nbRepositories: 0,
repositories: [],
},
{
conferenceID: "FSE23",
name: "Foundations of Software Engineering 2023",
url: "",
startDate: ago(1200),
endDate: ago(900),
status: "expired",
options: opts(),
plan: { planID: "free_conference", name: "Free", pricePerRepository: 0 },
price: 0,
nbRepositories: 3,
repositories: [],
},
];
const plans = [
{ id: "free_conference", name: "Free", pricePerRepo: 0, storagePerRepo: -1, description: "<li><strong>Quota is deducted from user account</strong></li><li>No download</li><li>Conference dashboard</li>" },
{ id: "premium_conference", name: "Premium", pricePerRepo: 0.5, storagePerRepo: 500 * 8 * 1024, description: "<li>500 MB per repository</li><li>Repository download</li><li>Conference dashboard</li>" },
{ id: "unlimited_conference", name: "Unlimited", pricePerRepo: 3, storagePerRepo: 0, description: "<li><strong>Unlimited</strong> repository size</li><li>Repository download</li><li>Conference dashboard</li>" },
];
const stat = { nbRepositories: 41230, nbUsers: 9870, nbPageViews: 3120000, nbPullRequests: 1480 };
// Cumulative totals with uneven daily growth (weekday peaks), so the landing
// sparklines show real variation instead of identical bars.
const wave = (i, base) => Math.round(base * (1 + 0.6 * Math.sin(i / 1.3) + 0.3 * Math.cos(i / 4)));
const cumulative = (start, base) => {
let total = start;
return Array.from({ length: 60 }, (_, i) => (total += Math.max(0, wave(i, base))));
};
const repoSeries = cumulative(40000, 20);
const userSeries = cumulative(9500, 6);
const viewSeries = cumulative(3000000, 2000);
const prSeries = cumulative(1400, 1);
const history = Array.from({ length: 60 }, (_, i) => ({
date: ago(59 - i),
nbRepositories: repoSeries[i],
nbUsers: userSeries[i],
nbPageViews: viewSeries[i],
nbPullRequests: prSeries[i],
}));
// ANON=1 serves the signed-out experience (landing page, FAQ) instead;
// ADMIN=1 signs in as an administrator so /admin pages render.
app.get("/api/user", (req, res) =>
process.env.ANON
? res.status(401).json({ error: "not_connected" })
: res.json({ username: "tdurieux", photo: "https://avatars.githubusercontent.com/u/5577568?v=4", isAdmin: !!process.env.ADMIN })
);
// Local connection fixtures keep the account screens usable without GitHub.
app.get("/github/connections", (req, res) => res.json({
appEnabled: true, appConnected: true, oauthEnabled: true, oauthConnected: true,
installations: [], gistCount: 0, resources: [], csrf: "local-preview-only",
}));
app.get("/api/options", (req, res) => res.json({ MAX_REPO_SIZE: 8 * 1024, ANONYMIZATION_MASK: "XXXX", GITHUB_APP_ENABLED: true, GITHUB_OAUTH_ENABLED: true }));
app.get("/api/message", (req, res) => res.status(404).end());
app.get("/api/user/quota", (req, res) => res.json(quota));
app.get("/api/user/anonymized_repositories", (req, res) => res.json(repositories));
app.get("/api/user/anonymized_pull_requests", (req, res) => res.json(pullRequests));
app.get("/api/user/anonymized_gists", (req, res) => res.json(gists));
app.get("/api/user/default", (req, res) =>
res.json({ terms: ["Jane Smith", "MIT CSAIL"], options: { link: true, image: true, pdf: true, notebook: true, loc: true, page: false, update: false, mode: "GitHubStream", expirationMode: "remove" } })
);
app.post("/api/user/default", (req, res) => res.json({}));
app.get("/api/conferences/plans", (req, res) => res.json(plans));
app.get("/api/conferences/", (req, res) => res.json(conferences));
app.get("/api/conferences/:id", (req, res) => {
const c = conferences.find((x) => x.conferenceID === req.params.id);
return c ? res.json(c) : res.status(404).json({ error: "conf_not_found" });
});
// Explorer fixture: anonymous_github-C72C has a tiny file tree; "missing-repo"
// returns the same error the live site gives for a deleted repository.
const explorerFiles = {
"": [
{ name: "src", path: "" },
{ name: "README.md", path: "", size: 640, sha: "a1" },
{ name: "LICENSE", path: "", size: 1070, sha: "a2" },
{ name: "requirements.txt", path: "", size: 40, sha: "a3" },
],
src: [
{ name: "utils", path: "src" },
{ name: "train.py", path: "src", size: 900, sha: "b1" },
],
"src/utils": [{ name: "data.py", path: "src/utils", size: 300, sha: "c1" }],
};
const explorerContent = {
"README.md": "# DeepLearnUtils\n\nDeveloped by XXXX-1 at XXXX-2.\n\nUtilities for reproducing the paper's experiments. Every script in `src/` reads its configuration from `configs/` and writes results to `out/`, so a full run of the evaluation only needs the two commands below. The long sentence here checks the reading width of rendered Markdown.\n\n## Quick start\n\n```bash\n# maintained by XXXX-1\npip install -r requirements.txt\npython src/train.py --config configs/base.yaml\n```\n\n```mermaid\ngraph LR; XXXX-2 --> Paper\n```\n\n| Model | Accuracy |\n| --- | --- |\n| Baseline | 71.2 |\n| Ours | 78.9 |\n\nContact: XXXX-3\n",
LICENSE: "MIT License\n\nCopyright (c) 2026 XXXX-1\n",
"requirements.txt": "torch>=2.3\nnumpy\npyyaml\n",
"src/train.py": '# Copyright 2026 XXXX-1, XXXX-2\nimport argparse\nimport yaml\n\nfrom utils.data import load_dataset\n\n\ndef main():\n parser = argparse.ArgumentParser(description="Train the model")\n parser.add_argument("--config", required=True)\n args = parser.parse_args()\n with open(args.config) as f:\n config = yaml.safe_load(f)\n dataset = load_dataset(config["data"])\n print(f"Loaded {len(dataset)} examples")\n\n\nif __name__ == "__main__":\n main()\n',
"src/utils/data.py": "def load_dataset(path):\n with open(path) as f:\n return [line.strip() for line in f]\n",
};
const EXPLORER_ID = "anonymous_github-C72C";
app.get("/api/repo/missing-repo/{*rest}", (req, res) =>
res.status(404).json({ error: "repo_not_found" })
);
app.get(`/api/repo/${EXPLORER_ID}/options`, (req, res) =>
res.json({
url: null,
download: true,
lastUpdateDate: ago(3),
anonymizedAt: ago(3),
sourceCommitDate: ago(4),
expirationDate: ahead(60),
isAdmin: false,
isOwner: true,
hasWebsite: false,
truncatedFolders: [],
hasSubmodules: false,
})
);
app.get(`/api/repo/${EXPLORER_ID}/files/counts`, (req, res) =>
res.json({ "": 6, src: 2, "src/utils": 1 })
);
app.get(`/api/repo/${EXPLORER_ID}/files`, (req, res) =>
res.json(explorerFiles[req.query.path || ""] || [])
);
app.get(`/api/repo/${EXPLORER_ID}/file/{*rest}`, (req, res) => {
const file = req.params.rest.join("/");
if (!(file in explorerContent)) return res.status(404).json({ error: "file_not_found" });
res.type("text/plain").send(explorerContent[file]);
});
app.get("/api/repo/:id", (req, res) => {
const r = repositories.find((x) => x.repoId === req.params.id) || repositories[0];
res.json(r);
});
app.get("/api/stat", (req, res) => res.json(stat));
app.get("/api/stat/history", (req, res) => res.json(history));
// ---- Admin fixtures (ADMIN=1) ------------------------------------------------
// Shapes follow src/server/routes/admin.ts: list endpoints return raw Mongo
// documents, the user detail returns User.toJSON() with populated GitHub
// repositories, and queue jobs follow the /api/admin/queues response.
const HOUR = 60 * 60 * 1000;
const MB = 1024 * 1024;
const oid = (n) => "65f0c0de" + String(n).padStart(16, "0");
const adminUsers = [
{ n: 1, username: "tdurieux", email: "thomas@durieux.me", isAdmin: true, days: 2400, photo: "https://avatars.githubusercontent.com/u/5577568?v=4", repos: 17 },
{ n: 2, username: "alice-research", email: "alice@cs.example.edu", days: 30, repos: 3 },
{ n: 3, username: "bob-lab", email: "bob.lab@uni-example.de", days: 410, repos: 142 },
{ n: 4, username: "spammer-9000", email: "free.crypto.giveaway@mail.example", status: "banned", days: 12, repos: 0 },
{ n: 5, username: "noemail-user", email: null, days: 800, repos: 1 },
{ n: 6, username: "carol", email: "carol@example.org", status: "removed", days: 1500, repos: 0 },
{ n: 7, username: "a-very-long-github-username-for-layout-tests", email: "someone.with.a.really.long.address@department.faculty.university.example.ac.uk", days: 90, repos: 6 },
{ n: 8, username: "dmitri", email: "dmitri@example.ru", isAdmin: true, days: 5, repos: 2 },
].map((u) => ({
_id: oid(u.n),
username: u.username,
emails: u.email ? [{ email: u.email, default: true, _id: oid(100 + u.n) }] : [],
isAdmin: !!u.isAdmin,
photo: u.photo || "https://avatars.githubusercontent.com/u/" + (1000 + u.n) + "?v=4",
repositories: Array.from({ length: u.repos }, (_, i) => String(70000000 + u.n * 1000 + i)),
status: u.status || "active",
dateOfEntry: ago(u.days),
accessTokenDates: u.status === "removed" ? {} : { github: ago(Math.min(u.days, 3)) },
externalIDs: { github: String(1000 + u.n) },
default: { terms: [], options: { expirationMode: "remove", update: false, image: true, pdf: true, notebook: true, link: true } },
__v: 0,
}));
// Anonymized repositories as stored in Mongo (source.repositoryName, size.storage).
const adminRepo = (r) => ({
_id: oid(200 + r.n),
repoId: r.repoId,
status: r.status,
statusDate: r.statusDate || ago(r.days),
statusMessage: r.statusMessage,
anonymizeDate: ago(r.days),
lastView: r.lastView,
pageView: r.pageView || 0,
owner: oid(r.owner || 1),
coauthors: [],
conference: r.conference,
source: { type: "GitHubStream", branch: r.branch || "main", commit: r.commit || "c40da900ab12ef34", repositoryId: String(500000 + r.n), repositoryName: r.fullName },
truncatedFolders: [],
options: opts(Object.assign({ terms: r.terms || ["Jane Smith", "MIT CSAIL"] }, r.options)),
dateOfEntry: ago(r.days),
size: { storage: (r.storage || 0) * 8, file: r.files || 0 }, // stored in bits, like prod
isReseted: false,
__v: 0,
});
const adminRepos = [
{ n: 1, repoId: "anonymous_github-C72C", status: "ready", days: 3, lastView: ago(0.1), pageView: 1520, conference: "FSE25", fullName: "tdurieux/anonymous_github", storage: 48 * MB, files: 1381, options: { update: true, page: true } },
{ n: 2, repoId: "dl-benchmark-2026", status: "error", days: 0.5, owner: 2, fullName: "alice-research/dl-benchmark", statusMessage: "repository_too_big: The repository exceeds the maximum size of 8 GB (measured 11.4 GB across 214,331 files). Remove large assets (datasets, model checkpoints, LFS objects) from the branch or anonymize a smaller subfolder, then retry.", terms: ["Alice Martin"] },
{ n: 3, repoId: "JarSift", status: "expired", days: 560, lastView: ago(30), pageView: 72, conference: "FSE25", fullName: "Cornul11/JarSift", options: { expirationMode: "remove", expirationDate: ago(9) } },
{ n: 4, repoId: "fuzz-harness-7F21", status: "download", days: 0.01, owner: 3, fullName: "bob-lab/fuzz-harness", branch: "artifact-eval" },
{ n: 5, repoId: "old-paper-artifact", status: "removed", days: 1200, owner: 6, lastView: ago(900), pageView: 33, fullName: "carol/old-paper-artifact" },
{ n: 6, repoId: "runtime-repair-experiments-very-long-identifier-for-the-icse-artifact-track-2ff4", status: "ready", days: 40, owner: 7, lastView: ago(2), pageView: 5, conference: "ICSE26-AE", fullName: "a-very-long-github-username-for-layout-tests/runtime-repair-experiments", branch: "camera-ready-final-v2", storage: 3.2 * 1024 * MB, files: 48211, options: { update: true } },
{ n: 7, repoId: "crypto-airdrop", status: "removing", days: 12, owner: 4, fullName: "spammer-9000/crypto-airdrop", pageView: 4 },
{ n: 8, repoId: "graph-mining-A91B", status: "ready", days: 220, owner: 3, lastView: ago(14), pageView: 596460, fullName: "bob-lab/graph-mining", storage: 210 * MB, files: 3102, terms: [] },
{ n: 9, repoId: "b4751b8e-6139-4a94-84f8-646106435809", status: "error", days: 1990, owner: 5, fullName: "noemail-user/thesis-code", branch: "master", statusMessage: "branch_not_found" },
{ n: 10, repoId: "llm-eval-suite", status: "preparing", days: 0.002, owner: 8, fullName: "dmitri/llm-eval-suite" },
{ n: 11, repoId: "smart-contract-audit", status: "expiring", days: 365, owner: 2, lastView: ago(1), pageView: 210, conference: "FSE25", fullName: "alice-research/smart-contract-audit", storage: 12 * MB, files: 240, options: { expirationMode: "redirect", expirationDate: ago(0.1) } },
].map(adminRepo);
// Repository.toJSON() as returned by /api/admin/users/:username/repos.
const repoView = (r) => ({
repoId: r.repoId,
options: r.options,
coauthors: r.coauthors,
conference: r.conference,
anonymizeDate: r.anonymizeDate,
status: r.status,
statusMessage: r.statusMessage,
lastView: r.lastView,
pageView: r.pageView,
size: r.status === "ready" ? r.size : { storage: 0, file: 0 },
source: { repositoryID: r.source.repositoryId, fullName: r.source.repositoryName, commit: r.source.commit, branch: r.source.branch, type: r.source.type },
});
// Populated GitHub repositories on the user detail (Repository model).
const githubRepos = (u) =>
u.repositories.slice(0, 40).map((id, i) => ({
_id: oid(9000 + i),
externalId: "gh_" + id,
name: u.username + "/" + ["paper-artifact", "dataset-tools", "experiments", "website", "dotfiles", "replication-package-with-a-long-name"][i % 6] + (i >= 6 ? "-" + i : ""),
url: "https://github.com/" + u.username,
source: "github",
hasPage: i % 4 === 0,
branches: [],
defaultBranch: "main",
size: [120, 48213, 3, 980000, 0, 15400][i % 6],
status: "ready",
dateOfEntry: ago(30 + i),
__v: 0,
}));
const statusCounts = (rows, withStorage) => {
const map = {};
rows.forEach((r) => {
const row = (map[r.status] = map[r.status] || { _id: r.status, count: 0 });
row.count++;
if (withStorage) row.storage = (row.storage || 0) + (r.size ? r.size.storage : 0);
});
return Object.values(map);
};
const page = (req, rows, withStorage) => {
const counts = statusCounts(rows, withStorage);
const sort = { [req.query.sort || "_id"]: req.query.direction === "asc" ? 1 : -1 };
const out = { query: {}, page: parseInt(req.query.page, 10) || 1, total: rows.length, sort, results: rows, statusCounts: counts };
if (withStorage) out.totalSize = counts.reduce((t, c) => t + (c.storage || 0), 0);
return out;
};
// Conferences as stored in Mongo (repositories are {id, addDate} refs).
const adminConferences = [
{ conferenceID: "FSE25", name: "Foundations of Software Engineering 2025", url: "https://conf.researchr.org/home/fse-2025", startDate: ago(400), endDate: ahead(30), status: "ready", plan: { planID: "premium_conference", pricePerRepository: 0.5, quota: { repository: 0, size: 500 * 8 * 1024, file: 0 } }, price: 12.5, repos: 25 },
{ conferenceID: "ICSE26-AE", name: "ICSE 2026 Artifact Evaluation", url: "https://conf.researchr.org/home/icse-2026", startDate: ahead(10), endDate: ahead(120), status: "preparing", plan: { planID: "unlimited_conference", pricePerRepository: 3, quota: { repository: 0, size: 0, file: 0 } }, price: 1260, repos: 420 },
{ conferenceID: "FSE23", name: "Foundations of Software Engineering 2023", url: "", startDate: ago(1200), endDate: ago(900), status: "expired", plan: { planID: "free_conference", pricePerRepository: 0 }, repos: 3 },
{ conferenceID: "MSR-WORKSHOP-ON-REPRODUCIBILITY-AND-OPEN-SCIENCE-2026", name: "International Workshop on Reproducibility, Replicability and Open Science in Mining Software Repositories", url: "https://example.org/msr-ros", startDate: ago(20), endDate: ahead(5), status: "error", plan: { planID: "free_conference", pricePerRepository: 0 }, repos: 0 },
].map((c, i) => ({
_id: oid(300 + i),
name: c.name,
conferenceID: c.conferenceID,
url: c.url,
startDate: c.startDate,
endDate: c.endDate,
status: c.status,
owners: [oid(1)],
repositories: Array.from({ length: c.repos }, (_, j) => ({ id: oid(5000 + j), addDate: ago(j % 60), _id: oid(6000 + j) })),
options: { expirationMode: "remove", expirationDate: c.endDate, update: false, image: true, pdf: true, notebook: true, link: true, page: false },
dateOfEntry: c.startDate,
plan: c.plan,
price: c.price,
billing: c.price ? { name: "Program Chairs", email: "chairs@example.org", country: "DE" } : undefined,
__v: 0,
}));
// ---- Overview & performance
const overview = {
system: {
platform: "linux", arch: "x64", nodeVersion: "v22.11.0", uptime: 3 * 86400 + 7 * 3600 + 120,
cpuCount: 8, cpuPercent: 37, loadAvg: [2.96, 2.41, 2.2],
memTotal: 32 * 1024 * MB, memFree: 9.5 * 1024 * MB, memUsed: 22.5 * 1024 * MB, memPercent: 70,
processRss: 612 * MB, processHeapUsed: 280 * MB, processHeapTotal: 350 * MB,
diskTotal: 1000 * 1024 * MB, diskUsed: 912 * 1024 * MB, diskFree: 88 * 1024 * MB, diskPercent: 91, diskMount: "/",
},
repos: {
total: 41230,
statusBreakdown: [
{ _id: "ready", count: 30112, storage: 1.9e12 },
{ _id: "expired", count: 6200, storage: 0 },
{ _id: "removed", count: 3810, storage: 0 },
{ _id: "error", count: 920, storage: 0 },
{ _id: "download", count: 14, storage: 0 },
{ _id: "preparing", count: 6, storage: 0 },
{ _id: "expiring", count: 120, storage: 2e9 },
{ _id: "removing", count: 48, storage: 0 },
],
totalStorage: 1.902e12,
recentErrors24h: 17,
activeRepos24h: 2310,
newRepos24h: 64,
},
users: { total: 9870, newUsers24h: 23 },
conferences: { total: adminConferences.length },
queues: {
download: { waiting: 12, active: 4, completed: 1840, failed: 7, delayed: 2 },
remove: { waiting: 0, active: 1, completed: 310, failed: 0, delayed: 0 },
cache: { waiting: 230, active: 2, completed: 9120, failed: 41, delayed: 0 },
},
errors: { last24h: 312, severity: { error: 41, warn: 118, info: 153 } },
// Same daily rows as the landing stats, last 30 days only.
history: history.slice(-30),
};
const LATENCY_BUCKETS = [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000];
const metricRow = (service, metric, route, method, count, avgMs, p95, p99, extra) =>
Object.assign({
metric, route, method, count, sumMs: count * avgMs, bytes: 0, aborted: 0, errors: 0, slow: 0,
buckets: LATENCY_BUCKETS.map((b, i) => (b >= avgMs && i < 6 ? Math.round(count / 6) : 0)).concat([0]),
service, avgMs, p95UpperMs: p95, p99UpperMs: p99,
}, extra || {});
const instance = (name, service, extra) => Object.assign({
instance: name, service, sampledAt: Date.now() - 8000, uptime: 86400, cpuPercent: 12.4,
memoryLimitBytes: 2048 * MB, memory: { rss: 410 * MB, heapTotal: 300 * MB, heapUsed: 220 * MB, external: 30 * MB, arrayBuffers: 4 * MB },
activeRequests: 3, sockets: { descriptors: 42, established: 38, closeWait: 0, listening: 1, truncated: false },
workers: null, eventLoop: { utilization: 0.21, p95Ms: 18.2, maxMs: 64 }, droppedMetrics: 0, droppedBatches: 0, lastFlushAt: Date.now() - 8000,
}, extra || {});
const performanceFor = (minutes) => {
const windowMinutes = minutes === 60 ? 60 : 15;
const scale = windowMinutes / 15;
const n = (v) => Math.round(v * scale);
const now = Math.floor(Date.now() / 60000) * 60000;
const instances = [
instance("api:anon-api-7c9d4", "api"),
instance("api:anon-api-b21fe", "api", { cpuPercent: 88.6, eventLoop: { utilization: 0.83, p95Ms: 142.7, maxMs: 910 }, activeRequests: 57 }),
instance("streamer:anon-streamer-0", "streamer", { memory: { rss: 1780 * MB, heapTotal: 900 * MB, heapUsed: 700 * MB, external: 600 * MB, arrayBuffers: 500 * MB },
sockets: { descriptors: 210, established: 180, closeWait: 6, listening: 1, truncated: false }, workers: { running: 4, waiting: 11, reservedBytes: 640 * MB } }),
instance("streamer:anon-streamer-1", "streamer", { sampledAt: Date.now() - 70000, sockets: null, workers: { running: 0, waiting: 0, reservedBytes: 0 }, droppedBatches: 2, droppedMetrics: 130 }),
];
return {
available: true,
generatedAt: Date.now(),
windowMinutes,
instances,
routes: [
metricRow("streamer", "request", "/api/repo/:repoId/file/:path", "GET", n(18230), 240, 1000, 5000, { bytes: n(9.1e9), aborted: n(41), errors: n(3), slow: n(88), firstByteCount: n(18100), firstByteP95UpperMs: 500 }),
metricRow("api", "request", "/api/repo/:repoId/files", "GET", n(9120), 85, 250, 1000, { firstByteCount: n(9120), firstByteP95UpperMs: 250 }),
metricRow("api", "request", "/api/repo/:repoId/zip", "GET", n(37), 18400, 30000, null, { bytes: n(4.2e9), aborted: n(9), slow: n(31), firstByteCount: n(37), firstByteP95UpperMs: 2500 }),
metricRow("api", "request", "/api/admin/overview", "GET", n(30), 410, 1000, 1000, { firstByteCount: n(30), firstByteP95UpperMs: 1000 }),
metricRow("api", "request", "/api/repo/:repoId/refresh", "POST", n(12), 3100, 10000, 10000, { errors: n(4), firstByteCount: 0, firstByteP95UpperMs: null }),
metricRow("api", "request", "/api/user", "GET", n(5400), 6, 10, 50, { firstByteCount: n(5400), firstByteP95UpperMs: 10 }),
].sort((a, b) => b.sumMs - a.sumMs),
stages: [
metricRow("streamer", "repository", "all", "all", n(18230), 4, 10, 50),
metricRow("streamer", "authorization", "all", "all", n(18230), 2, 10, 10),
metricRow("streamer", "upstream", "all", "all", n(2100), 620, 2500, 10000, { errors: n(12) }),
metricRow("streamer", "cache_lookup", "all", "all", n(18230), 3, 10, 50),
metricRow("streamer", "cache_fill", "all", "all", n(2100), 140, 500, 1000),
metricRow("streamer", "worker_wait", "all", "all", n(1600), 380, 2500, 5000),
metricRow("streamer", "anonymize", "all", "all", n(2100), 55, 250, 500),
metricRow("streamer", "source_hit", "all", "all", n(16130), 0, 10, 10),
metricRow("streamer", "source_miss", "all", "all", n(2100), 0, 10, 10),
metricRow("streamer", "first_byte", "/api/repo/:repoId/file/:path", "GET", n(18100), 90, 500, 1000),
],
series: Array.from({ length: windowMinutes }, (_, i) => ({
minute: now - (windowMinutes - 1 - i) * 60000,
requests: 900 + Math.round(400 * Math.sin(i / 2)), aborted: i % 5 === 0 ? 3 : 0, errors: i === 7 ? 4 : 0, sumMs: 90000 + i * 1200,
})),
runtimeSeries: instances.flatMap((s) => Array.from({ length: windowMinutes * 4 }, (_, i) => ({
instance: s.instance, service: s.service, sampledAt: Date.now() - i * 15000,
rss: s.memory.rss * (1 + 0.2 * Math.sin(i / 5)), heapUsed: s.memory.heapUsed, external: s.memory.external,
cpuPercent: s.cpuPercent, loopP95Ms: s.eventLoop.p95Ms, sockets: s.sockets ? s.sockets.descriptors : null,
closeWait: s.sockets ? s.sockets.closeWait : null, waiting: s.workers ? s.workers.waiting : 0, reservedBytes: s.workers ? s.workers.reservedBytes : 0,
}))),
latencyBucketsMs: LATENCY_BUCKETS,
percentileNote: "Percentiles are histogram bucket upper bounds; null means no samples or above 60s.",
};
};
// ---- Queues
// Jobs mirror /api/admin/queues: BullMQ job.asJSON() with data, failedReason,
// stacktrace and returnvalue restored to their parsed values.
const QUEUES = [
{ key: "download", label: "Download", counts: { waiting: 12, active: 4, completed: 1840, failed: 7, delayed: 2 }, paused: false, workers: 2, concurrency: 4, completed24h: 1712, failed24h: 9 },
{ key: "remove", label: "Remove", counts: { waiting: 0, active: 1, completed: 310, failed: 0, delayed: 0 }, paused: false, workers: 1, concurrency: 1, completed24h: 44, failed24h: 0 },
{ key: "cache", label: "Cache cleanup", counts: { waiting: 230, active: 2, completed: 9120, failed: 41, delayed: 0 }, paused: true, workers: 0, concurrency: null, completed24h: 0, failed24h: 0 },
];
const job = (state, repoId, j) => {
const ts = Date.now() - (j.ageMin || 1) * 60000;
const out = {
id: j.id || repoId,
name: repoId,
data: { repoId },
opts: { attempts: 3, backoff: { type: "exponential", delay: 5000 }, removeOnComplete: 1000, removeOnFail: 1000 },
progress: j.progress === undefined ? 0 : j.progress,
attemptsMade: j.attemptsMade || 0,
attemptsStarted: j.attemptsMade || 0,
stalledCounter: 0,
timestamp: ts,
processedOn: state === "waiting" || state === "delayed" ? undefined : ts + 2000,
finishedOn: state === "completed" || state === "failed" ? ts + 2000 + (j.durationMs || 4200) : undefined,
failedReason: j.failedReason,
stacktrace: j.stacktrace || [],
returnvalue: state === "completed" ? null : undefined,
_state: state,
};
if (state === "delayed") out.delayUntil = ts + 5 * 60000;
return out;
};
const queueJobs = {
download: [
job("active", "fuzz-harness-7F21", { ageMin: 2, progress: { status: "get_files", percent: 42 } }),
job("active", "llm-eval-suite", { ageMin: 0.5, progress: { status: "anonymize" } }),
job("active", "runtime-repair-experiments-very-long-identifier-for-the-icse-artifact-track-2ff4", { ageMin: 9, progress: 87 }),
job("waiting", "graph-mining-A91B", { ageMin: 1 }),
job("waiting", "smart-contract-audit", { ageMin: 3 }),
job("delayed", "dl-benchmark-2026", { ageMin: 4, attemptsMade: 1 }),
job("failed", "b4751b8e-6139-4a94-84f8-646106435809", { ageMin: 50, attemptsMade: 3, durationMs: 31000,
failedReason: "branch_not_found: the branch 'master' does not exist in noemail-user/thesis-code (GitHub returned 404 Not Found for GET /repos/noemail-user/thesis-code/branches/master)",
stacktrace: [
"AnonymousError: branch_not_found\n at GitHubStream.getBranchCommit (/app/build/core/source/GitHubStream.js:212:19)\n at async Repository.updateIfNeeded (/app/build/core/Repository.js:488:27)\n at async Repository.anonymize (/app/build/core/Repository.js:531:9)\n at async default (/app/build/queue/processes/downloadRepository.js:41:9)\n at async Worker.processJob (/app/node_modules/bullmq/dist/cjs/classes/worker.js:463:28)",
"AnonymousError: branch_not_found\n at GitHubStream.getBranchCommit (/app/build/core/source/GitHubStream.js:212:19)",
] }),
job("failed", "dl-benchmark-2026-retry", { ageMin: 300, attemptsMade: 1, failedReason: "repository_too_big" }),
job("completed", "anonymous_github-C72C", { ageMin: 15, progress: { status: "ready", percent: 100 }, durationMs: 12800 }),
job("completed", "JarSift", { ageMin: 120, durationMs: 640 }),
],
remove: [
job("active", "crypto-airdrop", { ageMin: 1 }),
job("completed", "old-paper-artifact", { ageMin: 2000, durationMs: 230 }),
],
cache: [],
};
const metricsPoints = (queue, range) => {
const minutes = { "1h": 60, "6h": 360, "24h": 1440, "7d": 10080 }[range] || 60;
const now = Math.floor(Date.now() / 60000) * 60000;
const quiet = queue === "cache"; // paused queue: flat zero line
return Array.from({ length: minutes }, (_, i) => {
const t = now - (minutes - 1 - i) * 60000;
const completed = quiet ? 0 : Math.max(0, Math.round(2 + 2 * Math.sin(i / 17) + (i % 37 === 0 ? 9 : 0)));
const failed = quiet ? 0 : i % 53 === 0 ? 2 : 0;
return { ts: t, completed, failed, avgMs: completed + failed ? 1800 + (i % 13) * 250 : 0 };
});
};
// ---- Errors (logger ring entries: { ts, level, module, message, raw })
const errorEntries = [
[0.02, "error", "streamer", "anonymous error", { name: "AnonymousError", message: "file_not_found", httpStatus: 500, detail: "dl-benchmark-2026", url: "/api/repo/dl-benchmark-2026/file/data/train/part-00031.parquet", cause: { name: "Error", message: "ENOENT: no such file or directory, open '/data/repos/dl-benchmark-2026/data/train/part-00031.parquet'", stack: "Error: ENOENT: no such file or directory, open '/data/repos/dl-benchmark-2026/data/train/part-00031.parquet'\n at async open (node:internal/fs/promises:639:25)\n at async FileSystem.read (/app/build/core/storage/FileSystem.js:58:22)\n at async AnonymizedFile.content (/app/build/core/AnonymizedFile.js:301:24)" } }],
[0.05, "warn", "github", "GitHub API rate limit low", { name: "HttpError", message: "API rate limit exceeded for installation ID 41234567.", status: 403, url: "https://api.github.com/repos/bob-lab/graph-mining/git/trees/c40da900ab12?recursive=1", method: "GET" }],
[0.1, "error", "download", "anonymous error", { name: "AnonymousError", message: "repository_too_big", httpStatus: 500, detail: JSON.stringify({ repoId: "dl-benchmark-2026", size: 12241827840, limit: 8589934592, fullName: "alice-research/dl-benchmark" }) }],
[0.3, "error", "streamer", "anonymous error", { name: "AnonymousError", message: "file_not_found", httpStatus: 500, detail: "dl-benchmark-2026", url: "/api/repo/dl-benchmark-2026/file/data/train/part-00032.parquet" }],
[0.4, "error", "api", "unhandled error", { name: "TypeError", message: "Cannot read properties of undefined (reading 'repositoryName')", stack: "TypeError: Cannot read properties of undefined (reading 'repositoryName')\n at Repository.toJSON (/app/build/core/Repository.js:1127:38)\n at JSON.stringify (<anonymous>)\n at ServerResponse.json (/app/node_modules/express/lib/response.js:263:14)\n at /app/build/server/routes/admin.js:912:17\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)" }],
[0.8, "warn", "api", "anonymous error", { name: "AnonymousError", message: "invalid_terms_format", httpStatus: 400, url: "/api/repo/new-paper/", method: "POST", detail: "Terms must be an array of non-empty strings; received a single string of 2,048 characters containing newlines, which usually means the terms were pasted into the wrong field." }],
[1.5, "error", "streamer", "anonymous error", { name: "AnonymousError", message: "file_not_found", httpStatus: 500, detail: "dl-benchmark-2026", url: "/api/repo/dl-benchmark-2026/file/README.md" }],
[2, "error", "queue", "job failed", { name: "AnonymousError", message: "branch_not_found", httpStatus: 404, detail: "b4751b8e-6139-4a94-84f8-646106435809" }],
[3.4, "warn", "mongo", "slow query", { name: "MongoServerSelectionError", message: "Server selection timed out after 30000 ms", code: "ETIMEDOUT" }],
[5, "error", "storage", "s3 put failed", { name: "S3ServiceException", message: "SlowDown: Please reduce your request rate.", status: 503, url: "https://s3.eu-west-1.amazonaws.com/anon-cache/repos/graph-mining-A91B/src/main.py" }],
[6, "warn", "github", "GitHub API rate limit low", { name: "HttpError", message: "API rate limit exceeded for installation ID 41234567.", status: 403, url: "https://api.github.com/repos/bob-lab/fuzz-harness", method: "GET" }],
[9, "error", "api", "anonymous error", { name: "AnonymousError", message: "repository_expired", httpStatus: 410, detail: "JarSift", url: "/api/repo/JarSift/files" }],
[14, "warn", "streamer", "client aborted", { name: "Error", message: "aborted", code: "ECONNRESET", url: "/api/repo/runtime-repair-experiments-very-long-identifier-for-the-icse-artifact-track-2ff4/zip" }],
[20, "error", "api", "unhandled error", { name: "RangeError", message: "Maximum call stack size exceeded", stack: "RangeError: Maximum call stack size exceeded\n at anonymizeContent (/app/build/core/anonymize-utils.js:88:18)\n at anonymizeContent (/app/build/core/anonymize-utils.js:102:16)\n at anonymizeContent (/app/build/core/anonymize-utils.js:102:16)\n at anonymizeContent (/app/build/core/anonymize-utils.js:102:16)" }],
[23, "warn", "conference", "billing webhook", { message: "Stripe webhook signature mismatch for event evt_1Q2w3E4r5T6y7U8i (conference ICSE26-AE)" }],
[23.5, "error", "streamer", "anonymous error", { name: "AnonymousError", message: "file_not_found", httpStatus: 500, detail: "graph-mining-A91B", url: "/api/repo/graph-mining-A91B/file/docs/very/deeply/nested/folder/structure/that/keeps/going/and/going/index.html" }],
].map(([hoursAgo, level, module, message, detail]) => ({
ts: new Date(Date.now() - hoursAgo * HOUR).toISOString(),
level,
module,
message,
raw: [message, detail],
}));
const errorStats = (() => {
const hourNow = Math.floor(Date.now() / HOUR) * HOUR;
const buckets = Array.from({ length: 24 }, (_, i) => {
const spike = i === 20 ? 6 : 1; // an incident a few hours ago
return { hour: hourNow - (23 - i) * HOUR + HOUR, error: Math.round((1 + (i % 4)) * spike), warn: 3 + (i % 6), info: i % 3 === 0 ? 0 : 8 + (i % 5) };
});
const severity = { error: 0, warn: 0, info: 0 };
buckets.forEach((b) => { severity.error += b.error; severity.warn += b.warn; severity.info += b.info; });
const last24h = severity.error + severity.warn + severity.info;
return { available: true, last24h, prev24h: Math.round(last24h * 0.74), severity, unique: { error: 7, warn: 4, info: 3 }, buckets, dropped: 12 };
})();
const adminTokens = [
{ id: oid(700), name: "ci-deploy", createdAt: ago(200), lastUsedAt: ago(0.01) },
{ id: oid(701), name: "local laptop (never used)", createdAt: ago(3) },
];
const ok = (req, res) => res.json({ ok: true });
app.get("/api/admin/overview", (req, res) => res.json(overview));
app.get("/api/admin/performance", (req, res) => res.json(performanceFor(Number(req.query.minutes))));
app.get("/api/admin/stats", (req, res) => res.json({
statusBreakdown: overview.repos.statusBreakdown, totalStorage: overview.repos.totalStorage,
recentErrors24h: overview.repos.recentErrors24h, totalUsers: overview.users.total, totalConferences: adminConferences.length,
}));
app.get("/api/admin/repos", (req, res) => {
let rows = adminRepos;
if (req.query.owner) {
const u = adminUsers.find((x) => x.username === req.query.owner);
rows = u ? rows.filter((r) => r.owner === u._id) : [];
}
if (req.query.conference) rows = rows.filter((r) => r.conference === req.query.conference);
res.json(page(req, rows, true));
});
app.get("/api/admin/repos/:repoId/github", (req, res) => {
const r = adminRepos.find((x) => x.repoId === req.params.repoId) || adminRepos[0];
const [owner, repo] = r.source.repositoryName.split("/");
res.json({
source: { owner, repo, branch: r.source.branch, commit: r.source.commit },
repository: { fullName: r.source.repositoryName, private: true, archived: false, disabled: false, defaultBranch: "main", description: "Replication package", stargazers: 3, watchers: 3, forks: 0, openIssues: 1, size: 48213, language: "Python", license: "MIT", createdAt: ago(400), updatedAt: ago(2), pushedAt: ago(2), htmlUrl: "https://github.com/" + r.source.repositoryName, topics: [] },
branchError: r.status === "error" ? "Branch not found" : undefined,
rateLimit: { remaining: 4123, limit: 5000, reset: ahead(0.03) },
});
});
app.delete("/api/admin/repos/:repoId", (req, res) => res.json({ status: "ready" }));
app.get("/api/admin/users", (req, res) => {
const rows = adminUsers.map((u) => {
const { repositories, ...rest } = u;
return Object.assign(rest, { repositories, repoCount: repositories.length });
});
res.json(page(req, rows, false));
});
app.get("/api/admin/users/:username/repos", (req, res) => {
const u = adminUsers.find((x) => x.username === req.params.username);
if (!u) return res.status(404).json({ error: "user_not_found" });
res.json(adminRepos.filter((r) => r.owner === u._id).map(repoView));
});
app.get("/api/admin/users/:username", (req, res) => {
const u = adminUsers.find((x) => x.username === req.params.username);
if (!u) return res.status(404).json({ error: "user_not_found" });
res.json(Object.assign({}, u, { repositories: githubRepos(u) }));
});
app.post("/api/admin/users/:username/ban", (req, res) => res.json({ ok: true, reposQueued: 2 }));
app.post("/api/admin/users/:username/:action", ok);
app.get("/api/user/:username/all_repositories", (req, res) => {
const u = adminUsers.find((x) => x.username === req.params.username) || adminUsers[0];
res.json(githubRepos(u));
});
app.get("/api/admin/tokens", (req, res) => res.json(adminTokens));
app.post("/api/admin/tokens", (req, res) => res.json({ id: oid(799), name: "new token", createdAt: new Date().toISOString(), token: "agh_mock_0123456789abcdef0123456789abcdef" }));
app.delete("/api/admin/tokens/:id", (req, res) => res.json({ removed: 1 }));
app.get("/api/admin/conferences", (req, res) => res.json(page(req, adminConferences, false)));
app.delete("/api/admin/conferences/:id", ok);
app.get("/api/admin/queues/metrics", (req, res) => {
const queue = String(req.query.queue || "download");
const range = String(req.query.range || "1h");
res.json({ queue, range, points: metricsPoints(queue, range) });
});
app.get("/api/admin/queues", (req, res) => {
const selected = QUEUES.some((q) => q.key === req.query.queue) ? req.query.queue : "download";
const search = String(req.query.search || "").toLowerCase();
const jobs = queueJobs[selected].filter((j) => !search || j.id.toLowerCase().includes(search) || j.name.toLowerCase().includes(search));
res.json({ queues: QUEUES, selectedQueue: selected, jobs });
});
app.post("/api/admin/queues/pause-all", ok);
app.post("/api/admin/queue/:name/retry-failed", (req, res) => res.json({ retried: 2, total: 2 }));
app.post("/api/admin/queue/:name/:action", ok);
app.delete("/api/admin/queue/:name/:id", ok);
app.get("/api/admin/errors", (req, res) => {
const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
const limit = Math.min(5000, Math.max(1, parseInt(req.query.limit, 10) || 250));
res.json({ entries: errorEntries.slice(offset, offset + limit), offset, limit, total: errorEntries.length, max: 5000, available: true });
});
app.get("/api/admin/errors/stats", (req, res) => res.json(errorStats));
app.delete("/api/admin/errors", (req, res) => res.json({ ok: true, cleared: errorEntries.length, hourlyCleared: 48 }));
app.all("/api/{*rest}", (req, res) => res.status(404).json({ error: "not mocked: " + req.path }));
app.use((req, res, next) => {
res.setHeader("Cache-Control", "no-store, max-age=0");
next();
});
// index: false so "/" falls through to the placeholder-filled index.html below.
app.use(express.static(PUBLIC_DIR, { etag: false, cacheControl: false, index: false }));
app.get("/{*rest}", (req, res) => res.type("html").send(indexHtml()));
app.listen(PORT, () => {
console.log(`mock ui http://localhost:${PORT}/dashboard`);
});