Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add caching mechanism with Vercel deployment support #9

Closed
wants to merge 7 commits into from
Closed
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# GitHub API token for fetching activity data
VITE_GITHUB_TOKEN=

# Set to 'true' to use the cache, 'false' to always fetch from GitHub
VITE_USE_CACHE=true
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Cache
.cache/

# Environment variables
.env
.env.local
58 changes: 54 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,35 @@ Create a `.env` file in the root directory with the following variables:

```env
# Required: GitHub Personal Access Token
GITHUB_TOKEN=your_github_token_here
VITE_GITHUB_TOKEN=your_github_token_here

# Optional: Use cache to reduce GitHub API calls (default: true)
VITE_USE_CACHE=true
```

Note: Never commit your `.env` file to version control. The `.gitignore` file already includes it.

## Caching Mechanism

To reduce the number of GitHub API calls and improve loading times, the application includes a caching system:

1. **Building the Cache**
```bash
npm run build:cache
```
This creates a cache of GitHub API responses in the `.cache` directory. The cache is valid for 24 hours.

2. **Using the Cache**
- Set `VITE_USE_CACHE=true` in your `.env` file (default setting)
- When enabled, the app will:
- Check for a valid cache first
- Use cached data if available and less than 24 hours old
- Fall back to live GitHub API calls if cache is missing or expired

3. **Disabling the Cache**
- Set `VITE_USE_CACHE=false` in your `.env` file
- This will always use live GitHub API calls

## Installation

1. Clone the repository:
Expand Down Expand Up @@ -94,7 +118,29 @@ The production build will be in the `dist` directory.

## Deployment

### Static Hosting (e.g., GitHub Pages, Netlify)
### Vercel Deployment (Recommended)

1. Install the Vercel CLI:
```bash
npm install -g vercel
```

2. Deploy to Vercel:
```bash
vercel
```

3. Set up the required environment variables in your Vercel project settings:
- `VITE_GITHUB_TOKEN`: Your GitHub Personal Access Token
- `CRON_SECRET`: A secure random string for authenticating cron jobs
- `VITE_USE_CACHE`: Set to "true" to enable caching (recommended)

4. The cache will be built automatically:
- During deployment (as part of the build process)
- Every 6 hours via Vercel Cron Jobs
- On-demand through the `/api/rebuild-cache` endpoint (requires CRON_SECRET)

### Other Static Hosting (e.g., GitHub Pages, Netlify)

1. Build the application:
```bash
Expand All @@ -103,7 +149,9 @@ npm run build

2. Deploy the contents of the `dist` directory to your hosting provider.

3. Make sure to set the `GITHUB_TOKEN` environment variable in your hosting provider's configuration.
3. Make sure to set the `VITE_GITHUB_TOKEN` environment variable in your hosting provider's configuration.

Note: Cache rebuilding is not available with static hosting. You'll need to either disable caching or rebuild manually.

### Docker Deployment

Expand All @@ -114,11 +162,13 @@ docker build -t openhands-monitor .

2. Run the container:
```bash
docker run -p 8080:80 -e GITHUB_TOKEN=your_token_here openhands-monitor
docker run -p 8080:80 -e VITE_GITHUB_TOKEN=your_token_here -e VITE_USE_CACHE=true openhands-monitor
```

The app will be available at `http://localhost:8080`.

Note: Cache rebuilding is not available with Docker deployment. You'll need to either disable caching or rebuild manually.

## Configuration

The application is configured to monitor the OpenHands repository by default. To monitor a different repository, modify the following constants in `src/services/github.ts`:
Expand Down
97 changes: 97 additions & 0 deletions api/rebuild-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { VercelRequest, VercelResponse } from '@vercel/node';
import handler from './rebuild-cache';

// Mock buildCache function
vi.mock('../scripts/build-cache', () => ({
buildCache: vi.fn()
}));

describe('Rebuild Cache API', () => {
let mockJson: ReturnType<typeof vi.fn>;
let mockStatus: ReturnType<typeof vi.fn>;
let mockRes: VercelResponse;

beforeEach(() => {
vi.resetAllMocks();
process.env.CRON_SECRET = 'test-secret';
mockJson = vi.fn();
mockStatus = vi.fn().mockReturnValue({ json: mockJson });
mockRes = { status: mockStatus } as unknown as VercelResponse;
});

it('should return 401 if no authorization header is provided', async () => {
const mockReq = {
headers: {}
} as unknown as VercelRequest;

await handler(mockReq, mockRes);

expect(mockStatus).toHaveBeenCalledWith(401);
expect(mockJson).toHaveBeenCalledWith({ error: 'Unauthorized' });
});

it('should return 401 if authorization header is invalid', async () => {
const mockReq = {
headers: {
authorization: 'Bearer wrong-secret'
}
} as unknown as VercelRequest;

await handler(mockReq, mockRes);

expect(mockStatus).toHaveBeenCalledWith(401);
expect(mockJson).toHaveBeenCalledWith({ error: 'Unauthorized' });
});

it('should rebuild cache successfully with valid authorization', async () => {
const mockReq = {
headers: {
authorization: 'Bearer test-secret'
}
} as unknown as VercelRequest;

const { buildCache } = await import('../scripts/build-cache');
vi.mocked(buildCache).mockResolvedValueOnce(true);

await handler(mockReq, mockRes);

expect(buildCache).toHaveBeenCalled();
expect(mockStatus).toHaveBeenCalledWith(200);
expect(mockJson).toHaveBeenCalledWith({ message: 'Cache rebuilt successfully' });
});

it('should handle cache rebuild failure', async () => {
const mockReq = {
headers: {
authorization: 'Bearer test-secret'
}
} as unknown as VercelRequest;

const { buildCache } = await import('../scripts/build-cache');
vi.mocked(buildCache).mockResolvedValueOnce(false);

await handler(mockReq, mockRes);

expect(buildCache).toHaveBeenCalled();
expect(mockStatus).toHaveBeenCalledWith(500);
expect(mockJson).toHaveBeenCalledWith({ error: 'Failed to rebuild cache' });
});

it('should handle errors during cache rebuild', async () => {
const mockReq = {
headers: {
authorization: 'Bearer test-secret'
}
} as unknown as VercelRequest;

const { buildCache } = await import('../scripts/build-cache');
vi.mocked(buildCache).mockRejectedValueOnce(new Error('Test error'));

await handler(mockReq, mockRes);

expect(buildCache).toHaveBeenCalled();
expect(mockStatus).toHaveBeenCalledWith(500);
expect(mockJson).toHaveBeenCalledWith({ error: 'Internal server error' });
});
});
27 changes: 27 additions & 0 deletions api/rebuild-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { VercelRequest, VercelResponse } from '@vercel/node';
import { buildCache } from '../scripts/build-cache';

export default async function handler(
req: VercelRequest,
res: VercelResponse,
) {
// Verify cron secret to ensure only authorized calls can rebuild cache
const cronSecret = process.env.CRON_SECRET;
const authHeader = req.headers.authorization;

if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
return res.status(401).json({ error: 'Unauthorized' });
}

try {
const success = await buildCache();
if (success) {
return res.status(200).json({ message: 'Cache rebuilt successfully' });
} else {
return res.status(500).json({ error: 'Failed to rebuild cache' });
}
} catch (error) {
console.error('Error rebuilding cache:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
10 changes: 10 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_GITHUB_TOKEN: string;
readonly VITE_USE_CACHE: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default tseslint.config(
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
project: ['./tsconfig.app.json', './tsconfig.node.json'],
project: ['./tsconfig.app.json', './tsconfig.node.json', './tsconfig.test.json'],
tsconfigRootDir: import.meta.dirname,
},
},
Expand Down
Loading
Loading