Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions MD/PERFORMANCE_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,20 @@

### P2: Medium Impact, Medium Effort

- [ ] #12 — Add pagination to all list endpoints
- [ ] #13 — Remove dead ownerId parameter from frontend fetch
- [ ] #14 — Deduplicate project fetching (useSyncData vs useProjects)
- [ ] #15 — Reuse Puppeteer browser instance OR switch to API
- [ ] #16 — Cache architecture analysis results
- [x] #12 — Add pagination to all list endpoints
- [x] #13 — Remove dead ownerId parameter from frontend fetch
- [x] #14 — Deduplicate project fetching (useSyncData vs useProjects)
- [x] #15 — Reuse Puppeteer browser instance OR switch to API
- [x] #16 — Cache architecture analysis results

### P3: Low-Medium Impact, Low Effort

- [ ] #17 — Add server-side filtering for pinned notes
- [ ] #18 — Add text index for user search
- [ ] #19 — Remove console.log statements, use structured logger
- [ ] #20 — Remove unused Redux Toolkit dependency
- [ ] #21 — Use ETags for GitHub API calls
- [ ] #22 — Support pagination for GitHub repo listing
- [x] #21 — Use ETags for GitHub API calls
- [x] ~~#22 — Support pagination for GitHub repo listing~~

---

Expand Down Expand Up @@ -759,7 +759,7 @@ This is a security issue. If the environment variable is missing, webhook authen
| 19 | Remove console.log statements, use structured logger | Production logs bloat | **Code quality** |
| 20 | Remove unused Redux Toolkit dependency | Dead code | **~20KB bundle ↓** |
| 21 | Use ETags for GitHub API calls | Rate limit conservation | **Rate limit efficiency** |
| 22 | Support pagination for GitHub repo listing | Users with 100+ repos | **UX improvement** |
| 22 | ~~Support pagination for GitHub repo listing~~ | Users with 100+ repos | **UX improvement** |

---

Expand Down
22 changes: 22 additions & 0 deletions PERFORMANCE_AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Performance Audit Todo List

## Frontend (React/Vite)
- [ ] Analyze Vite bundle size (e.g., using `rollup-plugin-visualizer`).
- [ ] Implement lazy loading (`React.lazy`) for heavy routes and components.
- [ ] Audit React component re-renders and apply `useMemo`/`useCallback` where appropriate.
- [ ] Optimize static assets (images, fonts, large icons) for faster loading.
- [ ] Run Lighthouse audits on the frontend production build.
- [ ] Review and clean up unnecessary dependencies in `package.json`.

## Backend (Node/Express)
- [ ] Review MongoDB collection structures and add necessary indexes for slow queries.
- [ ] Optimize database queries (avoid resolving unnecessary large nested documents).
- [x] Implement API pagination and limit dataset sizes returned to the client.
- [ ] Introduce caching strategies (e.g., Redis) for frequently accessed, strictly read-only data.
- [ ] Profile expensive API routes and optimize heavy computational logic.
- [ ] Audit WebSocket (socket.io) event payloads and frequency to prevent network saturation.

## Network & Infrastructure
- [ ] Verify HTTP gzip/brotli compression is enabled on the server.
- [ ] Setup application performance monitoring (APM) to track response times.
- [ ] Review Docker image sizes and optimize `Dockerfile` build processes.
17 changes: 9 additions & 8 deletions backend/controllers/inspirationController.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const fs = require('fs');
const path = require('path');
const { launchBrowser, scrapeDribbble } = require('../services/scraperService');
const { getSharedBrowser, scrapeDribbble } = require('../services/scraperService');
const { paginateArray, setPaginationHeaders } = require('../utils/pagination');

const DATA_FILE = path.join(__dirname, '../data/inspiration.json');

Expand Down Expand Up @@ -48,9 +49,8 @@ async function getInspiration(req, res) {

if (allItems.length < 5 && query) {
console.log(`⚠️ Low cache results for "${query}". Triggering live Dribbble fallback...`);
let browser = null;
try {
browser = await launchBrowser();
const browser = await getSharedBrowser();
const dribbbleItems = await scrapeDribbble(browser, query);

if (dribbbleItems.length > 0) {
Expand All @@ -64,9 +64,6 @@ async function getInspiration(req, res) {
}
} catch (err) {
console.error('Live Fallback Error:', err.message);

} finally {
if (browser) await browser.close();
}
}

Expand All @@ -76,10 +73,14 @@ async function getInspiration(req, res) {
[allItems[i], allItems[j]] = [allItems[j], allItems[i]];
}

const { items, pagination } = paginateArray(allItems, req.query);
setPaginationHeaders(res, pagination);

res.json({
ok: true,
count: allItems.length,
items: allItems
count: items.length,
total: allItems.length,
items
});

} catch (error) {
Expand Down
26 changes: 23 additions & 3 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,29 @@ app.get('/', (req, res) => {
res.send('API is running...');
});

server.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 Server successfully started on port ${PORT}`);
});
const startServer = (port, retriesLeft = 10) => {
const onError = (error) => {
server.off('error', onError);

if (error.code === 'EADDRINUSE' && retriesLeft > 0) {
console.warn(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
setTimeout(() => startServer(port + 1, retriesLeft - 1), 100);
return;
}

console.error('Failed to start backend server:', error);
process.exit(1);
};

server.once('error', onError);

server.listen(port, '0.0.0.0', () => {
server.off('error', onError);
console.log(`🚀 Server successfully started on port ${port}`);
});
};

startServer(PORT);


app.use((err, req, res, next) => {
Expand Down
2 changes: 2 additions & 0 deletions backend/models/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const projectSchema = new mongoose.Schema(

// AI-generated architecture
architecture: { type: mongoose.Schema.Types.Mixed, default: null },
architectureCacheKey: { type: String, default: null },
architectureAnalyzedAt: { type: Date, default: null },

// Google Meet
meetLink: { type: String, default: null },
Expand Down
10 changes: 7 additions & 3 deletions backend/routes/chatRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const router = express.Router();
const mongoose = require('mongoose');
const Message = require('../models/Message');
const verifyToken = require('../middleware/authMiddleware');
const { paginateArray, setPaginationHeaders } = require('../utils/pagination');

// Middleware: reject early if DB not connected
const requireDb = (req, res, next) => {
Expand Down Expand Up @@ -62,11 +63,14 @@ router.get('/conversations', verifyToken, requireDb, async (req, res) => {
doc: { $first: '$$ROOT' }
}},
{ $replaceRoot: { newRoot: '$doc' } },
{ $sort: { createdAt: -1 } },
{ $limit: 100 }
{ $sort: { createdAt: -1 } }
]);

res.json(conversations.map(m => ({ ...m, id: String(m._id) })));
const normalized = conversations.map(m => ({ ...m, id: String(m._id) }));
const { items, pagination } = paginateArray(normalized, req.query, { defaultLimit: 100, maxLimit: 200 });
setPaginationHeaders(res, pagination);

res.json(items);
} catch (error) {
console.error('[ChatRoutes] conversations error:', error);
res.status(500).json({ error: error.message });
Expand Down
133 changes: 99 additions & 34 deletions backend/routes/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const CryptoJS = require('crypto-js');
const User = require('../models/User');
const verifyToken = require('../middleware/authMiddleware');
const { normalizeDoc } = require('../utils/normalize');
const { getInstallationAccessToken } = require('../utils/githubAppAuth');

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY;

Expand All @@ -21,6 +22,28 @@ const decryptToken = (ciphertext) => {
return bytes.toString(CryptoJS.enc.Utf8);
};

const githubCache = new Map();

const fetchWithEtag = async (url, config, cacheKey) => {
const cached = githubCache.get(cacheKey);
const headers = { ...config.headers };
if (cached && cached.etag) {
headers['If-None-Match'] = cached.etag;
}

try {
const res = await axios.get(url, { ...config, headers });
if (res.headers?.etag) {
githubCache.set(cacheKey, { etag: res.headers.etag, data: res.data });
}
return res;
} catch (error) {
if (error.response && error.response.status === 304 && cached) {
return { ...error.response, status: 304, data: cached.data };
}
throw error;
}
};

router.post('/connect', verifyToken, async (req, res) => {
const { accessToken } = req.body;
Expand All @@ -31,12 +54,16 @@ router.post('/connect', verifyToken, async (req, res) => {
}

try {
const githubResponse = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github.v3+json'
}
});
const githubResponse = await fetchWithEtag(
'https://api.github.com/user',
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github.v3+json'
}
},
`connect_user_${uid}`
);

const githubUsername = githubResponse.data.login;
const encryptedToken = encryptToken(accessToken);
Expand Down Expand Up @@ -138,12 +165,16 @@ router.post('/callback', verifyToken, async (req, res) => {
return res.status(400).json({ message: error_description || 'Failed to exchange code for token' });
}

const userResponse = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${access_token}`,
Accept: 'application/vnd.github.v3+json'
}
});
const userResponse = await fetchWithEtag(
'https://api.github.com/user',
{
headers: {
Authorization: `Bearer ${access_token}`,
Accept: 'application/vnd.github.v3+json'
}
},
`callback_user_${uid}`
);

const githubUser = userResponse.data;
const encryptedToken = encryptToken(access_token);
Expand Down Expand Up @@ -197,19 +228,27 @@ router.get('/repos', verifyToken, async (req, res) => {
return res.status(500).json({ message: 'Invalid stored token' });
}

const githubResponse = await axios.get('https://api.github.com/user/repos', {
const page = parseInt(req.query.page) || 1;
const per_page = parseInt(req.query.per_page) || 30;

const cacheKey = `repos_${uid}_${page}_${per_page}`;
const githubResponse = await fetchWithEtag('https://api.github.com/user/repos', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github.v3+json'
},
params: {
sort: 'updated',
visibility: 'all',
per_page: 100
per_page,
page
}
});
}, cacheKey);

const linkHeader = githubResponse.headers.link;
const hasNextPage = !!(linkHeader && linkHeader.includes('rel="next"'));

res.json(githubResponse.data);
res.json({ repos: githubResponse.data, hasNextPage, page });

} catch (error) {
console.error('Error fetching GitHub repos:', error.message);
Expand Down Expand Up @@ -348,7 +387,29 @@ router.get('/user-repos', verifyToken, async (req, res) => {
}

try {
const response = await octokit.request('GET /installation/repositories', { per_page: 100 });
const page = parseInt(req.query.page) || 1;
const per_page = parseInt(req.query.per_page) || 30;

const cacheKey = `user_repos_${uid}_${page}_${per_page}`;
const cached = githubCache.get(cacheKey);
const headers = cached && cached.etag ? { 'If-None-Match': cached.etag } : {};

let response;
try {
response = await octokit.request('GET /installation/repositories', { per_page, page, headers });
if (response.headers?.etag) {
githubCache.set(cacheKey, { etag: response.headers.etag, data: response.data });
}
} catch (err) {
if (err.status === 304 && cached) {
response = { headers: err.response?.headers || {}, data: cached.data };
} else {
throw err;
}
}

const linkHeader = response.headers?.link;
const hasNextPage = !!(linkHeader && linkHeader.includes('rel="next"'));

const repos = response.data.repositories.map(repo => ({
id: repo.id,
Expand All @@ -359,7 +420,7 @@ router.get('/user-repos', verifyToken, async (req, res) => {
html_url: repo.html_url
}));

res.json(repos);
res.json({ repos, hasNextPage, page });
} catch (requestErr) {
console.error("Error fetching repositories from GitHub:", requestErr.message);
await disconnectGithub(uid, { installationId: null });
Expand Down Expand Up @@ -401,12 +462,13 @@ router.get('/stats', verifyToken, async (req, res) => {
const accessToken = decryptToken(github.accessToken);
const username = github.username;

const userResponse = await axios.get(`https://api.github.com/users/${username}`, {
const cacheKey = `stats_${username}`;
const userResponse = await fetchWithEtag(`https://api.github.com/users/${username}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github.v3+json'
}
});
}, cacheKey);

res.json({
login: userResponse.data.login,
Expand Down Expand Up @@ -440,12 +502,13 @@ router.get('/events', verifyToken, async (req, res) => {
const accessToken = decryptToken(github.accessToken);
const username = github.username;

const eventsResponse = await axios.get(`https://api.github.com/users/${username}/events?per_page=30`, {
const cacheKey = `events_${username}`;
const eventsResponse = await fetchWithEtag(`https://api.github.com/users/${username}/events?per_page=30`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github.v3+json'
}
});
}, cacheKey);

const events = eventsResponse.data.map(event => ({
id: event.id,
Expand Down Expand Up @@ -561,22 +624,24 @@ router.get('/readme', verifyToken, async (req, res) => {
}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getInstallationAccessToken() ultimately reads the GitHub App private key from process.env.GITHUB_APP_PRIVATE_KEY, but the repo’s env template/documentation uses GITHUB_PRIVATE_KEY. As-is, this route can crash at runtime when generating the JWT. Align the env var name (or add fallback) and update .env.example accordingly.

Suggested change
const githubAppPrivateKey =
process.env.GITHUB_APP_PRIVATE_KEY || process.env.GITHUB_PRIVATE_KEY;
if (!githubAppPrivateKey) {
console.error('GitHub App private key is not configured. Set GITHUB_APP_PRIVATE_KEY or GITHUB_PRIVATE_KEY.');
return res.status(500).json({ message: 'GitHub App is not configured' });
}

Copilot uses AI. Check for mistakes.
const installationId = github.installationId;
const appId = process.env.GITHUB_APP_ID;
let privateKey = process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, '\n');

const { App } = await import('octokit');
const app = new App({ appId, privateKey });
const octokit = await app.getInstallationOctokit(parseInt(installationId));
const accessToken = await getInstallationAccessToken(installationId);
const readmeCacheKey = `readme_${uid}_${owner}_${repo}_${installationId}`;

Comment on lines +628 to 629
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

githubCache is an unbounded in-memory Map, and this route introduces cache entries keyed by (uid, owner, repo, installationId), which can grow without bound on a long-running server. Consider adding TTL and/or a max-size (LRU) eviction strategy.

Copilot uses AI. Check for mistakes.
try {
const response = await octokit.request('GET /repos/{owner}/{repo}/readme', {
owner,
repo,
mediaType: { format: 'raw' }
});
const response = await fetchWithEtag(
`https://api.github.com/repos/${owner}/${repo}/readme`,
{
Comment on lines +631 to +633
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

owner/repo are interpolated directly into the GitHub URL. Validate they are strings and URL-encode them (e.g., encodeURIComponent) to avoid malformed requests and to prevent cache-key confusion with reserved characters like /, ?, or spaces.

Copilot uses AI. Check for mistakes.
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github.raw+json'
}
},
readmeCacheKey
);

res.send(response.data);
} catch (err) {
if (err.status === 404) {
if (err.response && err.response.status === 404) {
return res.send("# No README found");
}
throw err;
Expand Down
Loading
Loading