diff --git a/api/commits.js b/api/commits.js
index 584912b..c7be225 100644
--- a/api/commits.js
+++ b/api/commits.js
@@ -42,15 +42,15 @@ module.exports = async (req, res) => {
if (!contributionData) {
try {
contributionData = await fetchContributionData(username);
+ if (!contributionData) throw new Error("No data");
setCache(cacheKey, contributionData, CACHE_TTL);
} catch (fetchErr) {
console.error("Commits fetch error:", fetchErr.message);
- res.status(200).send(errorSVG(`Could not fetch data for ${username}`));
- return;
+ contributionData = { totalContributions: 0, days: [] };
}
}
- const { totalContributions, days } = contributionData;
+ const { totalContributions = 0, days = [] } = contributionData;
let colors = getTheme(theme);
colors = applyColorOverrides(colors, { bg_color, title_color, text_color, border_color });
diff --git a/api/contribution.js b/api/contribution.js
index 74fa59a..cbaef4a 100644
--- a/api/contribution.js
+++ b/api/contribution.js
@@ -42,15 +42,15 @@ module.exports = async (req, res) => {
if (!contributionData) {
try {
contributionData = await fetchContributionData(username);
+ if (!contributionData) throw new Error("No data");
setCache(cacheKey, contributionData, CACHE_TTL);
} catch (fetchErr) {
console.error("Contribution fetch error:", fetchErr.message);
- res.status(200).send(errorSVG(`Could not fetch data for ${username}`));
- return;
+ contributionData = { totalContributions: 0, days: [] };
}
}
- const { totalContributions, days } = contributionData;
+ const { totalContributions = 0, days = [] } = contributionData;
// Calculate streaks
let currentStreak = 0;
diff --git a/api/issues.js b/api/issues.js
index 93c4800..71ea0cf 100644
--- a/api/issues.js
+++ b/api/issues.js
@@ -42,27 +42,25 @@ module.exports = async (req, res) => {
fetchUserIssues(username),
fetchOpenIssues(username),
fetchClosedIssues(username),
- fetchUserProfile(username).catch(() => ({ name: username, login: username })),
+ fetchUserProfile(username)
]);
data = {
- totalIssues: totalIssuesCount,
- openIssues: openIssuesCount,
- closedIssues: closedIssuesCount,
- profileName: profile.name || profile.login || username,
+ totalIssues: totalIssuesCount || 0,
+ openIssues: openIssuesCount || 0,
+ closedIssues: closedIssuesCount || 0,
+ profileName: profile?.name || profile?.login || username,
};
setCache(cacheKey, data, CACHE_TTL);
} catch (fetchErr) {
console.error("Issues fetch error:", fetchErr.message);
- // Fallback to zeros so card still renders
data = {
totalIssues: 0,
openIssues: 0,
closedIssues: 0,
profileName: username,
};
- setCache(cacheKey, data, CACHE_TTL);
}
}
diff --git a/api/languages.js b/api/languages.js
index b57368a..3274b9a 100644
--- a/api/languages.js
+++ b/api/languages.js
@@ -67,11 +67,12 @@ module.exports = async (req, res) => {
} else {
langData = await fetchUserLanguages(username);
}
+
+ if (!langData) throw new Error("No data returned");
setCache(cacheKey, langData, CACHE_TTL);
} catch (fetchErr) {
console.error("Language fetch error:", fetchErr.message);
- res.status(200).send(errorSVG(`Could not fetch languages for ${username}`));
- return;
+ langData = { languages: [], totalRepos: 0, totalBytes: 0, totalActivity: 0 };
}
}
@@ -81,27 +82,27 @@ module.exports = async (req, res) => {
let svg;
if (cacheMode === "repos") {
svg = generateLanguageDonutByReposSVG({
- languages: langData.languages,
+ languages: langData.languages || [],
hideBorder: hide_border === "true",
});
} else if (cacheMode === "commits") {
svg = generateLanguageDonutByCommitsSVG({
- languages: langData.languages,
+ languages: langData.languages || [],
hideBorder: hide_border === "true",
});
} else {
svg = layout === "compact"
? generateLanguageCompactSVG({
- username, languages: langData.languages, colors,
+ username, languages: langData.languages || [], colors,
hideBorder: hide_border === "true",
})
: layout === "donut"
? generateLanguageDonutSVG({
- username, languages: langData.languages, colors,
+ username, languages: langData.languages || [], colors,
hideBorder: hide_border === "true",
})
: generateLanguageSVG({
- username, languages: langData.languages, totalBytes: langData.totalBytes, colors,
+ username, languages: langData.languages || [], totalBytes: langData.totalBytes || 0, colors,
hideBorder: hide_border === "true",
maxLangs: parseInt(max_langs) || 12,
});
diff --git a/api/master.js b/api/master.js
index 766a5d2..b03551a 100644
--- a/api/master.js
+++ b/api/master.js
@@ -11,7 +11,11 @@ const {
fetchContributionData,
fetchUserProfile,
fetchUserLanguages,
- fetchTotalCommitCount
+ fetchTotalCommitCount,
+ fetchUserIssues,
+ fetchOpenIssues,
+ fetchClosedIssues,
+ fetchMergedPullRequests
} = require("../src/github");
const { getTheme, applyColorOverrides } = require("../src/themes");
const { generateMasterCardSVG } = require("../src/svg-master");
@@ -43,16 +47,20 @@ module.exports = async (req, res) => {
if (!data) {
try {
- const [prs, profile, contributionData, langData, totalCommits] = await Promise.all([
+ const [prs, profile, contributionData, langData, totalCommits, totalIssues, openIssues, closedIssues, mergedPRCount] = await Promise.all([
fetchUserPullRequests(username),
- fetchUserProfile(username).catch(() => ({ public_repos: 0 })),
+ fetchUserProfile(username),
fetchContributionData(username),
fetchUserLanguages(username),
- fetchTotalCommitCount(username)
+ fetchTotalCommitCount(username),
+ fetchUserIssues(username),
+ fetchOpenIssues(username),
+ fetchClosedIssues(username),
+ fetchMergedPullRequests(username)
]);
- // Calculate streaks
- const sortedDays = [...contributionData.days].sort((a, b) => a.date.localeCompare(b.date));
+ const days = contributionData?.days || [];
+ const sortedDays = [...days].sort((a, b) => a.date.localeCompare(b.date));
let currentStreak = 0;
let longestStreak = 0;
let tempStreak = 0;
@@ -66,9 +74,8 @@ module.exports = async (req, res) => {
else tempStreak = 0;
}
- // Top repos by PR count
const repoMap = {};
- prs.forEach(pr => {
+ (prs || []).forEach(pr => {
if (pr.repository_url) {
const name = pr.repository_url.split("/repos/")[1];
repoMap[name] = (repoMap[name] || 0) + 1;
@@ -79,24 +86,43 @@ module.exports = async (req, res) => {
.sort((a, b) => b.count - a.count);
data = {
- username: profile.login || username,
- totalPRs: prs.length,
- openPRs: prs.filter(pr => pr.state === "open").length,
- repoCount: profile.public_repos || 0,
- languages: langData.languages,
- contributions: contributionData.totalContributions,
+ username: profile?.login || username,
+ totalPRs: prs?.length || 0,
+ openPRs: (prs || []).filter(pr => pr.state === "open").length,
+ mergedPRs: mergedPRCount || 0,
+ repoCount: profile?.public_repos || 0,
+ languages: langData?.languages || [],
+ contributions: contributionData?.totalContributions || 0,
repoList,
- contributionDays: contributionData.days,
+ contributionDays: days,
currentStreak,
longestStreak,
- totalCommits
+ totalCommits,
+ totalIssues: totalIssues || 0,
+ openIssues: openIssues || 0,
+ closedIssues: closedIssues || 0
};
setCache(cacheKey, data, CACHE_TTL);
} catch (fetchErr) {
console.error("Mastercard fetch error:", fetchErr.message);
- res.status(200).send(errorSVG(`Could not fetch data for ${username}`));
- return;
+ data = {
+ username: username,
+ totalPRs: 0,
+ openPRs: 0,
+ mergedPRs: 0,
+ repoCount: 0,
+ languages: [],
+ contributions: 0,
+ repoList: [],
+ contributionDays: [],
+ currentStreak: 0,
+ longestStreak: 0,
+ totalCommits: 0,
+ totalIssues: 0,
+ openIssues: 0,
+ closedIssues: 0
+ };
}
}
@@ -107,6 +133,7 @@ module.exports = async (req, res) => {
username: data.username,
totalPRs: data.totalPRs,
openPRs: data.openPRs,
+ mergedPRs: data.mergedPRs,
repoCount: data.repoCount,
languages: data.languages,
contributions: data.contributions,
@@ -115,6 +142,9 @@ module.exports = async (req, res) => {
contributionDays: data.contributionDays,
currentStreak: data.currentStreak,
longestStreak: data.longestStreak,
+ totalIssues: data.totalIssues,
+ openIssues: data.openIssues,
+ closedIssues: data.closedIssues,
colors,
hideBorder: hide_border === "true",
});
diff --git a/api/overview.js b/api/overview.js
index d8956f8..3c1e9e1 100644
--- a/api/overview.js
+++ b/api/overview.js
@@ -24,7 +24,7 @@ function normalizeLinesScope(value) {
return value === "all" ? "all" : "recent";
}
-function parseMaxPRs(value, defaultValue = 30, hardLimit = 200) {
+function parseMaxPRs(value, defaultValue = 20, hardLimit = 50) {
const parsed = parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return defaultValue;
@@ -47,7 +47,7 @@ module.exports = async (req, res) => {
try {
const scope = normalizeLinesScope(lines_scope);
- const maxPRs = scope === "all" ? 0 : parseMaxPRs(max_prs, 30, 200);
+ const maxPRs = scope === "all" ? 50 : parseMaxPRs(max_prs, 20, 50);
const cacheKey = scope === "all"
? `overview:${username.toLowerCase()}:lines_scope=all`
: `overview:${username.toLowerCase()}:lines_scope=recent:max_prs=${maxPRs}`;
@@ -62,36 +62,41 @@ module.exports = async (req, res) => {
try {
const [prs, profile, contributionData, totalIssues, totalStars] = await Promise.all([
fetchUserPullRequests(username),
- fetchUserProfile(username).catch(() => ({ public_repos: 0, public_gists: 0 })),
+ fetchUserProfile(username),
fetchContributionData(username),
fetchUserIssues(username),
fetchUserTotalStars(username),
]);
- const linesChanged = await fetchRecentPRLinesChanged(prs, scope === "all" ? prs.length : maxPRs);
+ const linesChanged = await fetchRecentPRLinesChanged(prs, maxPRs);
- // Count repos contributed to from PR data
const reposContributed = new Set();
- prs.forEach(pr => {
+ (prs || []).forEach(pr => {
if (pr.repository_url) {
reposContributed.add(pr.repository_url.split("/repos/")[1]);
}
});
data = {
- totalStars,
- totalCommits: contributionData.totalContributions || 0,
- totalPRs: prs.length,
- totalIssues,
- contributedTo: reposContributed.size || profile.public_repos || 0,
+ totalStars: totalStars || 0,
+ totalCommits: contributionData?.totalContributions || 0,
+ totalPRs: prs?.length || 0,
+ totalIssues: totalIssues || 0,
+ contributedTo: reposContributed.size || profile?.public_repos || 0,
linesChanged,
};
setCache(cacheKey, data, CACHE_TTL);
} catch (fetchErr) {
console.error("Overview fetch error:", fetchErr.message);
- res.status(200).send(errorSVG(`Could not fetch data for ${username}`));
- return;
+ data = {
+ totalStars: 0,
+ totalCommits: 0,
+ totalPRs: 0,
+ totalIssues: 0,
+ contributedTo: 0,
+ linesChanged: 0,
+ };
}
}
diff --git a/api/pr-stats.js b/api/pr-stats.js
index 660bfa1..76ec731 100644
--- a/api/pr-stats.js
+++ b/api/pr-stats.js
@@ -40,7 +40,7 @@ module.exports = async (req, res) => {
try {
const [prs, profile, openPRCount, closedPRCount, mergedPRCount] = await Promise.all([
fetchUserPullRequests(username),
- fetchUserProfile(username).catch(() => ({ name: username, login: username })),
+ fetchUserProfile(username),
fetchOpenPullRequests(username),
fetchClosedPullRequests(username),
fetchMergedPullRequests(username),
@@ -50,18 +50,17 @@ module.exports = async (req, res) => {
data = {
repoMap,
- totalPRs: prs.length,
- openPRs: openPRCount,
- closedPRs: closedPRCount,
- mergedPRs: mergedPRCount,
+ totalPRs: prs?.length || 0,
+ openPRs: openPRCount || 0,
+ closedPRs: closedPRCount || 0,
+ mergedPRs: mergedPRCount || 0,
repoCount: Object.keys(repoMap).length,
- profileName: profile.name || profile.login || username,
+ profileName: profile?.name || profile?.login || username,
};
setCache(cacheKey, data, CACHE_TTL);
} catch (fetchErr) {
console.error("PR fetch error:", fetchErr.message);
- // Fallback to zeros so card still renders
data = {
repoMap: {},
totalPRs: 0,
@@ -71,7 +70,6 @@ module.exports = async (req, res) => {
repoCount: 0,
profileName: username,
};
- setCache(cacheKey, data, CACHE_TTL);
}
}
diff --git a/api/profile.js b/api/profile.js
index 17aaea2..ef64196 100644
--- a/api/profile.js
+++ b/api/profile.js
@@ -39,6 +39,7 @@ module.exports = async (req, res) => {
if (!data) {
try {
const profile = await fetchUserProfile(username);
+ if (!profile) throw new Error("No profile");
data = {
username: profile.login || username,
name: profile.name || profile.login || username,
@@ -52,8 +53,16 @@ module.exports = async (req, res) => {
setCache(cacheKey, data, CACHE_TTL);
} catch (fetchErr) {
console.error("Profile fetch error:", fetchErr.message);
- res.status(200).send(errorSVG(`Could not fetch profile for ${username}`));
- return;
+ data = {
+ username: username,
+ name: username,
+ bio: "",
+ avatarUrl: "",
+ createdAt: null,
+ publicRepos: 0,
+ followers: 0,
+ following: 0,
+ };
}
}
diff --git a/api/streak.js b/api/streak.js
index 07a5abd..8d40196 100644
--- a/api/streak.js
+++ b/api/streak.js
@@ -41,7 +41,8 @@ module.exports = async (req, res) => {
fetchContributionData(username),
fetchTotalCommitCount(username),
]);
- const { days, totalContributions } = contributionData;
+
+ const { days = [], totalContributions = 0 } = contributionData || {};
const sortedDays = [...days].sort((a, b) => a.date.localeCompare(b.date));
let currentStreak = 0;
@@ -61,8 +62,7 @@ module.exports = async (req, res) => {
setCache(cacheKey, data, CACHE_TTL);
} catch (fetchErr) {
console.error("Streak fetch error:", fetchErr.message);
- res.status(200).send(errorSVG(`Could not fetch data for ${username}`));
- return;
+ data = { currentStreak: 0, longestStreak: 0, totalContributions: 0, totalCommits: 0 };
}
}
diff --git a/api/working-hours.js b/api/working-hours.js
index 0eda489..6e4695e 100644
--- a/api/working-hours.js
+++ b/api/working-hours.js
@@ -43,16 +43,15 @@ module.exports = async (req, res) => {
const commitData = await fetchUserCommitTimestamps(username);
cachedData = {
- totalHours: commitData.totalWorkingHours,
- commitCount: commitData.commitCount,
+ totalHours: commitData ? commitData.totalWorkingHours : 0,
+ commitCount: commitData ? commitData.commitCount : 0,
lastUpdated: new Date(),
};
setCache(cacheKey, cachedData, CACHE_TTL);
} catch (fetchErr) {
console.error("Working Hours fetch error:", fetchErr.message);
- res.status(200).send(errorSVG(`Could not fetch data for ${username}`));
- return;
+ cachedData = { totalHours: 0, commitCount: 0, lastUpdated: new Date() };
}
}
diff --git a/src/github.js b/src/github.js
index 991609a..0ba6d83 100644
--- a/src/github.js
+++ b/src/github.js
@@ -9,14 +9,14 @@ const GITHUB_API = "https://api.github.com";
*/
async function safeFetch(url, options = {}) {
const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 10000);
+ const timeoutId = setTimeout(() => controller.abort(), 12000);
try {
const res = await fetch(url, {
...options,
headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
+ "User-Agent": "gitly-app-bot",
+ "Accept": options.headers?.Accept || "application/vnd.github.v3+json",
...(options.headers || {}),
},
signal: controller.signal,
@@ -25,7 +25,11 @@ async function safeFetch(url, options = {}) {
clearTimeout(timeoutId);
if (!res.ok) {
- console.warn(`GitHub API warning: ${res.status} for ${url}`);
+ if (res.status === 403 || res.status === 429) {
+ console.warn(`GitHub API Rate Limit hit for ${url}`);
+ } else {
+ console.warn(`GitHub API warning: ${res.status} for ${url}`);
+ }
return null;
}
return await res.json();
@@ -37,29 +41,25 @@ async function safeFetch(url, options = {}) {
}
/**
- * Fetch all pull requests by a user across all public repos.
+ * Fetch all pull requests by a user.
*/
async function fetchUserPullRequests(username) {
const perPage = 100;
let page = 1;
let allPRs = [];
-
- while (page <= 5) {
- const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:pr&per_page=${perPage}&page=${page}`;
- const data = await safeFetch(url);
- if (!data || !data.items || data.items.length === 0) break;
-
- allPRs = allPRs.concat(data.items);
- if (allPRs.length >= data.total_count || data.items.length < perPage) break;
- page++;
- }
-
- return allPRs;
+ try {
+ while (page <= 2) { // Limited to 2 pages to save rate limit
+ const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:pr&per_page=${perPage}&page=${page}`;
+ const data = await safeFetch(url);
+ if (!data || !data.items || data.items.length === 0) break;
+ allPRs = allPRs.concat(data.items);
+ if (allPRs.length >= data.total_count || data.items.length < perPage) break;
+ page++;
+ }
+ } catch (e) {}
+ return allPRs; // Returns empty array if fails
}
-/**
- * PR counts
- */
async function fetchOpenPullRequests(username) {
const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:pr+state:open&per_page=1`;
const data = await safeFetch(url);
@@ -78,9 +78,6 @@ async function fetchMergedPullRequests(username) {
return data ? (data.total_count || 0) : 0;
}
-/**
- * Issue counts
- */
async function fetchUserIssues(username) {
const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:issue&per_page=1`;
const data = await safeFetch(url);
@@ -99,9 +96,6 @@ async function fetchClosedIssues(username) {
return data ? (data.total_count || 0) : 0;
}
-/**
- * Group pull requests by repository name.
- */
function groupPRsByRepo(prs) {
const repoMap = {};
if (!Array.isArray(prs)) return repoMap;
@@ -113,9 +107,6 @@ function groupPRsByRepo(prs) {
return repoMap;
}
-/**
- * Fetch user profile info.
- */
async function fetchUserProfile(username) {
const url = `${GITHUB_API}/users/${encodeURIComponent(username)}`;
const data = await safeFetch(url);
@@ -123,9 +114,6 @@ async function fetchUserProfile(username) {
return data;
}
-/**
- * Fetch REAL contribution data by scraping GitHub's contribution page.
- */
async function fetchContributionData(username) {
const url = `https://github.com/users/${encodeURIComponent(username)}/contributions`;
try {
@@ -144,9 +132,6 @@ async function fetchContributionData(username) {
}
}
-/**
- * Parse GitHub contributions HTML to extract real contribution data.
- */
function parseContributionHTML(html) {
let totalContributions = 0;
const totalMatch = html.match(/(\d[\d,]*)\s+contributions?\s+in\s+the\s+last\s+year/i);
@@ -154,20 +139,19 @@ function parseContributionHTML(html) {
totalContributions = parseInt(totalMatch[1].replace(/,/g, ""), 10);
}
- const tdRegex = /
]*\bdata-date="\d{4}-\d{2}-\d{2}"[^>]*><\/td>/g;
+ const entryRegex = /<(?:td|rect)\b[^>]*\bdata-date="(\d{4}-\d{2}-\d{2})"[^>]*data-level="(\d+)"[^>]*>/g;
const dates = [];
let match;
- while ((match = tdRegex.exec(html)) !== null) {
+ while ((match = entryRegex.exec(html)) !== null) {
+ const date = match[1];
+ const level = parseInt(match[2], 10);
const tag = match[0];
const idMatch = tag.match(/\bid="([^"]+)"/);
- const dateMatch = tag.match(/\bdata-date="(\d{4}-\d{2}-\d{2})"/);
- const levelMatch = tag.match(/\bdata-level="(\d+)"/);
- if (!dateMatch || !levelMatch) continue;
dates.push({
id: idMatch ? idMatch[1] : `day-${dates.length}`,
- date: dateMatch[1],
- level: parseInt(levelMatch[1], 10),
+ date,
+ level,
});
}
@@ -199,178 +183,71 @@ function parseContributionHTML(html) {
return { totalContributions, days };
}
-/**
- * Fetch commit timestamps for working hours.
- */
-async function fetchUserCommitTimestamps(username) {
- const perPage = 100;
- let page = 1;
- let allCommits = [];
-
- while (page <= 10) {
- const url = `${GITHUB_API}/search/commits?q=author:${encodeURIComponent(username)}&per_page=${perPage}&page=${page}&sort=author-date`;
- const data = await safeFetch(url);
- if (!data || !data.items || data.items.length === 0) break;
-
- for (const commit of data.items) {
- if (commit.commit && commit.commit.author && commit.commit.author.date) {
- allCommits.push({
- timestamp: new Date(commit.commit.author.date).getTime(),
- date: commit.commit.author.date,
- });
- }
- }
- if (data.items.length < perPage) break;
- page++;
- }
-
- if (allCommits.length === 0) {
- return { totalWorkingHours: 0, commitCount: 0 };
- }
-
- allCommits.sort((a, b) => a.timestamp - b.timestamp);
- const MAX_GAP = 5 * 60 * 60 * 1000;
- let totalWorkingMs = 0;
- for (let i = 0; i < allCommits.length - 1; i++) {
- const gap = allCommits[i + 1].timestamp - allCommits[i].timestamp;
- if (gap > 0 && gap < MAX_GAP) {
- totalWorkingMs += gap;
- }
- }
-
- const totalWorkingHours = Math.round((totalWorkingMs / (1000 * 60 * 60)) * 100) / 100;
- return { totalWorkingHours, commitCount: allCommits.length };
-}
-
-/**
- * Fetch total commit count.
- */
async function fetchTotalCommitCount(username) {
const url = `${GITHUB_API}/search/commits?q=author:${encodeURIComponent(username)}&per_page=1`;
- const data = await safeFetch(url);
+ const data = await safeFetch(url, { headers: { Accept: "application/vnd.github.cloak-preview" } });
return data ? (data.total_count || 0) : 0;
}
-/**
- * Fetch total stars.
- */
async function fetchUserTotalStars(username) {
let page = 1;
let totalStars = 0;
-
- while (page <= 5) {
- const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?type=owner&per_page=100&page=${page}`;
- const data = await safeFetch(url);
- if (!data || !Array.isArray(data) || data.length === 0) break;
-
- for (const repo of data) {
- if (!repo.fork) {
- totalStars += repo.stargazers_count || 0;
+ try {
+ while (page <= 2) {
+ const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?type=owner&per_page=100&page=${page}`;
+ const data = await safeFetch(url);
+ if (!data || !Array.isArray(data) || data.length === 0) break;
+ for (const repo of data) {
+ if (!repo.fork) totalStars += repo.stargazers_count || 0;
}
+ if (data.length < 100) break;
+ page++;
}
-
- if (data.length < 100) break;
- page++;
- }
-
+ } catch (e) {}
return totalStars;
}
-/**
- * Fetch lines changed.
- */
-async function fetchRecentPRLinesChanged(prs, maxPRs = 30) {
- const targetPRs = (prs || [])
- .filter((pr) => pr && pr.pull_request && pr.pull_request.url)
- .slice(0, maxPRs);
-
+async function fetchRecentPRLinesChanged(prs, maxPRs = 10) {
+ const targetPRs = (prs || []).filter((pr) => pr && pr.pull_request && pr.pull_request.url).slice(0, maxPRs);
if (targetPRs.length === 0) return 0;
-
- const concurrency = 6;
+ const concurrency = 2;
let totalChanged = 0;
-
for (let i = 0; i < targetPRs.length; i += concurrency) {
const batch = targetPRs.slice(i, i + concurrency);
- const results = await Promise.all(
- batch.map(async (pr) => {
- const data = await safeFetch(pr.pull_request.url);
- if (!data) return 0;
- return (data.additions || 0) + (data.deletions || 0);
- })
- );
-
+ const results = await Promise.all(batch.map(async (pr) => {
+ const data = await safeFetch(pr.pull_request.url);
+ return data ? (data.additions || 0) + (data.deletions || 0) : 0;
+ }));
totalChanged += results.reduce((sum, v) => sum + v, 0);
}
-
return totalChanged;
}
-/**
- * Fetch user languages.
- */
async function fetchUserLanguages(username) {
- let page = 1;
- const langMap = {};
- let totalBytes = 0;
-
- while (page <= 3) {
- const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
- const repos = await safeFetch(url);
- if (!repos || !repos.length) break;
-
- const promises = repos
- .filter((r) => !r.fork && r.size > 0)
- .slice(0, 20)
- .map(async (repo) => {
- return await safeFetch(repo.languages_url) || {};
- });
-
- const results = await Promise.all(promises);
-
- for (const langs of results) {
- for (const [lang, bytes] of Object.entries(langs)) {
- langMap[lang] = (langMap[lang] || 0) + bytes;
- totalBytes += bytes;
- }
- }
-
- if (repos.length < 100) break;
- page++;
- }
-
- const languages = Object.entries(langMap)
- .map(([name, bytes]) => ({
- name,
- bytes,
- percentage: totalBytes > 0 ? Math.round(((bytes / totalBytes) * 100) * 100) / 100 : 0,
- }))
- .sort((a, b) => b.bytes - a.bytes);
-
- return { languages, totalBytes };
+ return await fetchUserLanguagesByRepos(username);
}
async function fetchUserLanguagesByRepos(username) {
let page = 1;
const langRepoCount = {};
let totalRepos = 0;
-
- while (page <= 3) {
- const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
- const repos = await safeFetch(url);
- if (!repos || !repos.length) break;
-
- for (const repo of repos) {
- if (repo.fork || repo.size <= 0) continue;
- const lang = repo.language;
- if (lang) {
- langRepoCount[lang] = (langRepoCount[lang] || 0) + 1;
- totalRepos++;
+ try {
+ while (page <= 2) {
+ const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
+ const repos = await safeFetch(url);
+ if (!repos || !repos.length) break;
+ for (const repo of repos) {
+ if (repo.fork) continue;
+ const lang = repo.language;
+ if (lang) {
+ langRepoCount[lang] = (langRepoCount[lang] || 0) + 1;
+ totalRepos++;
+ }
}
+ if (repos.length < 100) break;
+ page++;
}
-
- if (repos.length < 100) break;
- page++;
- }
+ } catch (e) {}
const languages = Object.entries(langRepoCount)
.map(([name, count]) => ({
@@ -380,54 +257,73 @@ async function fetchUserLanguagesByRepos(username) {
}))
.sort((a, b) => b.count - a.count);
- return { languages, totalRepos };
+ return { languages, totalRepos, totalBytes: totalRepos * 1024 };
}
async function fetchUserLanguagesByCommits(username) {
let page = 1;
const langActivity = {};
let totalActivity = 0;
-
- while (page <= 3) {
- const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
- const repos = await safeFetch(url);
- if (!repos || !repos.length) break;
-
- const promises = repos
- .filter((r) => !r.fork && r.size > 0)
- .slice(0, 20)
- .map(async (repo) => {
- const langs = await safeFetch(repo.languages_url);
- if (!langs) return { langs: {}, weight: 1 };
- const weight = 1 + (repo.stargazers_count || 0) * 0.1;
- return { langs, weight };
- });
-
- const results = await Promise.all(promises);
-
- for (const { langs, weight } of results) {
- for (const [lang, bytes] of Object.entries(langs)) {
- const activity = bytes * weight;
- langActivity[lang] = (langActivity[lang] || 0) + activity;
- totalActivity += activity;
+ try {
+ while (page <= 2) {
+ const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
+ const repos = await safeFetch(url);
+ if (!repos || !repos.length) break;
+ for (const repo of repos) {
+ if (repo.fork) continue;
+ const lang = repo.language;
+ if (lang) {
+ const weight = 1 + (repo.stargazers_count || 0);
+ langActivity[lang] = (langActivity[lang] || 0) + weight;
+ totalActivity += weight;
+ }
}
+ if (repos.length < 100) break;
+ page++;
}
-
- if (repos.length < 100) break;
- page++;
- }
+ } catch (e) {}
const languages = Object.entries(langActivity)
- .map(([name, activity]) => ({
+ .map(([name, weight]) => ({
name,
- activity: Math.round(activity),
- percentage: totalActivity > 0 ? Math.round((activity / totalActivity) * 10000) / 100 : 0,
+ activity: weight,
+ percentage: totalActivity > 0 ? Math.round((weight / totalActivity) * 10000) / 100 : 0,
}))
.sort((a, b) => b.activity - a.activity);
return { languages, totalActivity };
}
+async function fetchUserCommitTimestamps(username) {
+ const perPage = 100;
+ let page = 1;
+ let allCommits = [];
+ try {
+ while (page <= 2) { // Heavily limited
+ const url = `${GITHUB_API}/search/commits?q=author:${encodeURIComponent(username)}&per_page=${perPage}&page=${page}&sort=author-date`;
+ const data = await safeFetch(url, { headers: { Accept: "application/vnd.github.cloak-preview" } });
+ if (!data || !data.items || data.items.length === 0) break;
+ for (const commit of data.items) {
+ if (commit.commit?.author?.date) {
+ allCommits.push({ timestamp: new Date(commit.commit.author.date).getTime() });
+ }
+ }
+ if (data.items.length < perPage) break;
+ page++;
+ }
+ } catch (e) {}
+
+ if (allCommits.length === 0) return { totalWorkingHours: 0, commitCount: 0 };
+ allCommits.sort((a, b) => a.timestamp - b.timestamp);
+ const MAX_GAP = 5 * 60 * 60 * 1000;
+ let totalWorkingMs = 0;
+ for (let i = 0; i < allCommits.length - 1; i++) {
+ const gap = allCommits[i + 1].timestamp - allCommits[i].timestamp;
+ if (gap > 0 && gap < MAX_GAP) totalWorkingMs += gap;
+ }
+ return { totalWorkingHours: Math.round((totalWorkingMs / (1000 * 60 * 60)) * 100) / 100, commitCount: allCommits.length };
+}
+
module.exports = {
fetchUserPullRequests,
fetchOpenPullRequests,
diff --git a/src/svg-master.js b/src/svg-master.js
index 03e6e94..3428879 100644
--- a/src/svg-master.js
+++ b/src/svg-master.js
@@ -1,6 +1,6 @@
/**
- * Master Card SVG generation - all cards merged into one dashboard SVG.
- * Enhanced design with streaks, trend line, and polished visuals.
+ * Master Card SVG generation - Vertical Structured Dashboard.
+ * Clean, classic, and fited for GitHub Readme.
*/
function getContributionColorByLevel(level) {
@@ -17,236 +17,186 @@ function generateMasterCardSVG(options) {
username,
totalPRs = 0,
openPRs = 0,
+ mergedPRs = 0,
repoCount = 0,
languages = [],
contributions = 0,
totalCommits = 0,
repoList = [],
- visitors = 0,
contributionDays = [],
currentStreak = 0,
longestStreak = 0,
+ totalIssues = 0,
+ openIssues = 0,
+ closedIssues = 0,
colors,
hideBorder,
- cardWidth = 1000,
+ cardWidth = 495,
} = options;
const pad = 24;
- const contentW = Math.max(820, cardWidth - pad * 2);
- const cardHeight = 700;
+ const innerW = cardWidth - pad * 2;
const hba = hideBorder ? `rx="10"` : `rx="10" stroke="#30363d" stroke-width="1"`;
const accentColor = (colors && colors.accent_color) || "58a6ff";
- const titleColor = (colors && colors.title_color) || "e6edf3";
+ const titleColor = (colors && colors.title_color) || "58a6ff";
const textColor = (colors && colors.text_color) || "c9d1d9";
+ let y = 0;
+ let sections = "";
+
// --- HEADER ---
- const headerY = 0;
- let svg = ``;
+ const cardHeight = y + 20;
- return svg;
+ return ``;
}
module.exports = { generateMasterCardSVG, escapeXml };
diff --git a/src/svg-pr.js b/src/svg-pr.js
index 5b01ec8..b246e69 100644
--- a/src/svg-pr.js
+++ b/src/svg-pr.js
@@ -14,23 +14,30 @@ function generatePRCardSVG(options) {
const cardHeight = hdr + maxShow * rowH + 8;
const hba = hideBorder ? `rx="8"` : `rx="8" stroke="#30363d" stroke-width="1"`;
+ const accentColor = colors.accent_color || "58a6ff";
+ const titleColor = colors.title_color || "58a6ff";
+
let rows = "";
repos.slice(0, maxShow).forEach(([repo, count], i) => {
const y = hdr + i * rowH;
- const name = repo.length > 38 ? repo.substring(0, 35) + "..." : repo;
- const pct = Math.min((count / repos[0][1]) * 100, 100);
+
const countLabel = String(count);
- const valueW = Math.max(24, countLabel.length * 9 + 6);
+ const valueW = Math.max(26, countLabel.length * 8 + 8);
const barW = 60;
- const barToValueGap = 12;
+ const barToValueGap = 10;
const barX = cardWidth - pad - valueW - barToValueGap - barW;
const valueX = cardWidth - pad;
+
+ // Dynamically calculate max name length to prevent overlap with bar
+ const maxChars = Math.floor((barX - pad - 10) / 7);
+ const name = repo.length > maxChars ? repo.substring(0, maxChars - 3) + "..." : repo;
+
rows += `
${escapeXml(name)}
-
- ${countLabel}
+
+ ${countLabel}
`;
});
@@ -38,21 +45,22 @@ function generatePRCardSVG(options) {
rows += `+${repos.length - maxShow} more`;
}
+ // Adjusted header spacing to prevent overlap
return ` |