mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-09-12 13:48:58 +02:00
The dashboard showed the anonymize date under "Expired", a dash in the
Expires column for 54 of 60 rows, raw status codes such as
branch_not_found, and a chip reading "REMOVED" that actually meant
"removed items are hidden". Muted text sat at 3.5:1 contrast, keyboard
focus was invisible, numbers were left-aligned, and a legacy record
without an identifier rendered as an empty link to /pr/undefined/.
This rewrites the dashboard template and controller and tightens the
theme tokens:
- Status sub-line is labelled ("anonymized on Sep 6, 2026"); Expires
shows Never, the date, or "Expired on <date>".
- Conference folded into the name cell as a tag; Views right-aligned;
every column header is a keyboard-sortable button with aria-sort.
- Sort/Status buttons show their state; chips read "Hiding Removed";
result count and Clear filters; empty state with a way out.
- Status filter covers Ready, In progress, Error, Expired, Removed.
Stuck downloads say so; codes map to sentences; statuses to labels.
- Lists load in parallel and merge once behind a skeleton; broken
records are flagged instead of linking nowhere.
- Actions menu: View, View page, Edit, Force update, divider, Remove,
all as buttons rather than href="#" anchors.
- Theme: warm dark canvas, muted text >= 4.5:1 in both modes, semantic
status tokens, 11/13/14/16px type scale, mono only for identifiers,
6px/10px/pill radii, :focus-visible ring, quota bars in ink until a
limit is near, unlimited shown as such.
- Mobile: no 9px text, filter row wraps in two lines, meta on one line.
Adds scripts/dev-mock.js to serve the dashboard with fixture data and
test/dashboard-ui.test.js covering the filters, template invariants and
token contrast.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
203 lines
6.5 KiB
JavaScript
203 lines
6.5 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);
|
|
});
|
|
|
|
app.get("/api/user", (req, res) => res.json({ username: "tdurieux", photo: "", isAdmin: false }));
|
|
app.get("/api/options", (req, res) => res.json({}));
|
|
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({}));
|
|
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();
|
|
});
|
|
app.use(express.static(PUBLIC_DIR, { etag: false, cacheControl: false }));
|
|
app.get("/{*rest}", (req, res) => res.type("html").send(indexHtml()));
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`mock ui http://localhost:${PORT}/dashboard`);
|
|
});
|