Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
Expand All @@ -29,10 +29,10 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Node.js v22
- name: Setup Node.js v24
uses: actions/setup-node@v4
with:
node-version: 22
node-version: '24'
cache: 'npm'
cache-dependency-path: package-lock.json

Expand Down
8 changes: 3 additions & 5 deletions backend/src/__tests__/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('GET /api/emails/search', () => {
const userId = 'test-user-id';
const token = AuthService.generateToken(userId, 'test@example.com');

const countSpy = jest.spyOn(prisma.email, 'count').mockResolvedValue(50);
jest.spyOn(prisma.email, 'count').mockResolvedValue(50);
const findManySpy = jest
.spyOn(prisma.email, 'findMany')
.mockResolvedValue([]);
Expand Down Expand Up @@ -144,10 +144,8 @@ describe('GET /api/emails/search', () => {
const userId = 'test-user-id';
const token = AuthService.generateToken(userId, 'test@example.com');

const countSpy = jest.spyOn(prisma.email, 'count').mockResolvedValue(10);
const findManySpy = jest
.spyOn(prisma.email, 'findMany')
.mockResolvedValue([]);
jest.spyOn(prisma.email, 'count').mockResolvedValue(10);
jest.spyOn(prisma.email, 'findMany').mockResolvedValue([]);

const res = await request(app)
.get('/api/emails/search?q=test&limit=-10&offset=invalid')
Expand Down
1 change: 0 additions & 1 deletion backend/src/middleware/rate-limiter.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Request, Response } from 'express';
import { rateLimit } from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';
import { AuthService } from '../services/auth.service';
import { createRedisClient, RedisHealth } from '../utils/redis-health';

Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/digests.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ digestsRouter.post(
details: validation.error.flatten(),
});
}
const { type, limit } = validation.data;
const { type } = validation.data;

logger.info('[Digests] Generating digest', { userId, type });
const digest = await DigestGeneratorService.generateDigest(userId, type);
Expand Down
1 change: 0 additions & 1 deletion backend/src/routes/integrations.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ImapService } from '../services/imap.service';
import { encrypt } from '../utils/crypto';
import { logger } from '../utils/logger';
import { z } from 'zod';
import crypto from 'crypto';
import { google } from 'googleapis';

export const integrationsRouter = Router();
Expand Down
6 changes: 0 additions & 6 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,8 +800,6 @@ app.get(
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
let newToken: string | undefined;

const cacheKey = `user:profile:${userId}`;

// Try fetching from Redis cache first
Expand Down Expand Up @@ -1026,8 +1024,6 @@ app.get(
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
let newToken: string | undefined;

const settings = await prisma.userSettings.findUnique({
where: { userId },
});
Expand Down Expand Up @@ -1237,8 +1233,6 @@ const getOAuth2Client = (req?: Request) => {
);
};

const oauth2Client = getOAuth2Client();

/**
* GET /api/integrations/gmail/auth
* Generates the Google OAuth URL.
Expand Down
2 changes: 0 additions & 2 deletions backend/src/services/actions/digest-generator.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { PrismaClient, Digest } from '@prisma/client';
import * as Handlebars from 'handlebars';
import { logger } from '../../utils/logger';
import { AIService } from '../ai.service';

const prisma = new PrismaClient();

export interface DigestEmailData {
Expand Down
32 changes: 22 additions & 10 deletions backend/src/services/ai.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,26 @@ export class AIService {
if (this.geminiKeys.length === 0) {
const keysEnv = process.env.GEMINI_API_KEYS;
if (keysEnv) {
this.geminiKeys = keysEnv.split(',').map((k) => k.trim()).filter(Boolean);
this.geminiKeys = keysEnv
.split(',')
.map((k) => k.trim())
.filter(Boolean);
} else {
const key1 = process.env.GEMINI_API_KEY || 'GEMINI_API_KEY_1_PLACEHOLDER';
const key2 = process.env.GEMINI_API_KEY_2 || 'GEMINI_API_KEY_2_PLACEHOLDER';
const key3 = process.env.GEMINI_API_KEY_3 || 'GEMINI_API_KEY_3_PLACEHOLDER';
const key4 = process.env.GEMINI_API_KEY_4 || 'GEMINI_API_KEY_4_PLACEHOLDER';
const key5 = process.env.GEMINI_API_KEY_5 || 'GEMINI_API_KEY_5_PLACEHOLDER';

const key1 =
process.env.GEMINI_API_KEY || 'GEMINI_API_KEY_1_PLACEHOLDER';
const key2 =
process.env.GEMINI_API_KEY_2 || 'GEMINI_API_KEY_2_PLACEHOLDER';
const key3 =
process.env.GEMINI_API_KEY_3 || 'GEMINI_API_KEY_3_PLACEHOLDER';
const key4 =
process.env.GEMINI_API_KEY_4 || 'GEMINI_API_KEY_4_PLACEHOLDER';
const key5 =
process.env.GEMINI_API_KEY_5 || 'GEMINI_API_KEY_5_PLACEHOLDER';

const filtered = [key1, key2, key3, key4, key5].filter(
(k) => !k.includes('PLACEHOLDER')
);

this.geminiKeys = filtered.length > 0 ? filtered : [key1];
}
}
Expand Down Expand Up @@ -1388,7 +1396,9 @@ Provide the result as a JSON object with a single field 'category'.`;
});
const text = response.text;
if (!text) {
throw new Error('Gemini returned an empty link categorization response.');
throw new Error(
'Gemini returned an empty link categorization response.'
);
}
return text;
});
Expand Down Expand Up @@ -1628,7 +1638,9 @@ Do NOT infer or fabricate deadlines. Only return dates explicitly stated.`;

const text = response.text;
if (!text) {
throw new Error('Gemini returned an empty deadline extraction response.');
throw new Error(
'Gemini returned an empty deadline extraction response.'
);
}
return text;
});
Expand Down
2 changes: 1 addition & 1 deletion backend/src/services/imap.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class IMAPService {
});

f.on('message', (msg, seqno) => {
msg.on('body', (stream, info) => {
msg.on('body', (stream, _info) => {
let rawEml = '';
stream.on('data', (chunk) => {
rawEml += chunk.toString('utf8');
Expand Down
2 changes: 1 addition & 1 deletion backend/src/services/rules-engine.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaClient, Rule, RuleCondition, RuleAction } from '@prisma/client';
import { PrismaClient, RuleCondition, RuleAction } from '@prisma/client';
import { WebhookDispatcher } from './webhook-dispatcher.service';
import { EmailSenderService } from './email-sender.service';
import { logger } from '../utils/logger';
Expand Down
2 changes: 1 addition & 1 deletion backend/src/utils/bullmq-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export class Queue {
await this.realQueue.close();
this.realQueue = null;
}
for (const [key, value] of this.localRepeatJobs.entries()) {
for (const [, value] of this.localRepeatJobs.entries()) {
if (value.intervalId) {
clearInterval(value.intervalId);
}
Expand Down
12 changes: 6 additions & 6 deletions backend/src/utils/redis-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,23 @@ export const RedisHealth = {
export class MockRedisClient {
status = 'end';

async get(key: string): Promise<string | null> {
async get(_key: string): Promise<string | null> {
return null;
}

async setex(key: string, seconds: number, value: string): Promise<string> {
async setex(_key: string, _seconds: number, _value: string): Promise<string> {
return 'OK';
}

async del(key: string): Promise<number> {
async del(_key: string): Promise<number> {
return 0;
}

async ping(): Promise<string> {
return 'PONG';
}

async call(cmd: string, ...args: any[]): Promise<any> {
async call(_cmd: string, ..._args: any[]): Promise<any> {
return null;
}

Expand All @@ -87,11 +87,11 @@ export class MockRedisClient {
return this;
}

once(event: string, handler: (...args: any[]) => void): this {
once(_event: string, _handler: (...args: any[]) => void): this {
return this;
}

off(event: string, handler: (...args: any[]) => void): this {
off(_event: string, _handler: (...args: any[]) => void): this {
return this;
}

Expand Down
8 changes: 6 additions & 2 deletions backend/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ export async function registerWorkerHandlers() {

// 4. Extract and save actions
try {
logger.info('[Worker] Extracting action items from email', { emailId });
logger.info('[Worker] Extracting action items from email', {
emailId,
});
const actionItems = await AIService.extractActionItems(
email.subject,
email.body
Expand All @@ -195,7 +197,9 @@ export async function registerWorkerHandlers() {
deadline: item.deadline ? new Date(item.deadline) : null,
})),
});
logger.info('[Worker] Saved action items successfully', { emailId });
logger.info('[Worker] Saved action items successfully', {
emailId,
});
} else {
logger.info('[Worker] No action items extracted from email', {
emailId,
Expand Down
18 changes: 16 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,14 @@ const DashboardContent: React.FC = () => {
const pending = tasksList.filter((t) => !t.isCompleted);
const completed = tasksList.filter((t) => t.isCompleted);
return (
<Layout activeTab={activeTab} setActiveTab={setActiveTab}>
<Layout
activeTab={activeTab}
setActiveTab={setActiveTab}
theme={theme}
onToggleTheme={() =>
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'))
}
>
{toastMessage && (
<div
className="fixed bottom-6 right-6 z-50 flex items-center gap-2.5 px-4 py-3 text-xs font-bold uppercase tracking-wider"
Expand Down Expand Up @@ -1670,7 +1677,14 @@ const DashboardContent: React.FC = () => {

if (activeTab === 'rules') {
return (
<Layout activeTab={activeTab} setActiveTab={setActiveTab}>
<Layout
activeTab={activeTab}
setActiveTab={setActiveTab}
theme={theme}
onToggleTheme={() =>
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'))
}
>
{toastMessage && (
<div
className="fixed bottom-6 right-6 z-50 flex items-center gap-2.5 px-4 py-3 text-xs font-bold uppercase tracking-wider"
Expand Down
Loading