For the 90-minute prototype sprint, use mock data to simulate real API responses. This lets you:
- ✅ Build & test frontend components without API delays
- ✅ Avoid rate limiting issues
- ✅ Have complete control over test data
- ✅ Demo with realistic, consistent data
- ⏰ Save 30+ minutes vs. real integration
Real API integration is post-sprint work.
// src/services/mock/jira-mock.ts
import { JiraItem } from '@/types/jira';
export const mockJiraItems: JiraItem[] = [
{
id: 'jira-1',
issueKey: 'DAIS-123',
summary: 'API authentication not working in staging',
priority: 'BLOCKER',
projectKey: 'DAIS',
projectName: 'Data Integration Service',
currentStatus: 'In Progress',
previousStatus: 'Open',
statusChangedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
changeType: 'STATUS_CHANGE',
changedBy: { id: 'user-1', name: 'Alex Chen', avatarUrl: '...' },
relevanceReason: 'ASSIGNEE',
url: 'https://jira.company.com/browse/DAIS-123',
updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
blocks: ['DAIS-456', 'DAIS-789']
},
{
id: 'jira-2',
issueKey: 'DAIS-987',
summary: 'Database migration rollback needed',
priority: 'P1',
projectKey: 'DAIS',
projectName: 'Data Integration Service',
currentStatus: 'Resolved',
previousStatus: 'In Review',
statusChangedAt: new Date(Date.now() - 4 * 60 * 60 * 1000),
changeType: 'RESOLVED',
changedBy: { id: 'user-2', name: 'Charlie Wong', avatarUrl: '...' },
relevanceReason: 'WATCHER',
url: 'https://jira.company.com/browse/DAIS-987',
updatedAt: new Date(Date.now() - 4 * 60 * 60 * 1000)
},
// ... more items
];
export const mockJiraService = {
getIssuesForUser: async (userId: string) => {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 200));
return mockJiraItems;
}
};// src/services/mock/outlook-mock.ts
import { EmailItem, CalendarItem } from '@/types/outlook';
export const mockEmails: EmailItem[] = [
{
id: 'email-1',
messageId: '<msg@company.com>',
subject: 'ACTION REQUIRED: Urgent approval needed for Q3 roadmap',
sender: {
name: 'Jennifer Wong',
email: 'jennifer.wong@company.com',
avatarUrl: '...'
},
body: 'We need your approval on the Q3 roadmap update. Can you review and sign off by EOD today?',
isRead: false,
isFlagged: true,
receivedAt: new Date(Date.now() - 1.5 * 60 * 60 * 1000),
sentAt: new Date(Date.now() - 1.5 * 60 * 60 * 1000),
category: 'CRITICAL',
senderPriority: 'LEADERSHIP',
responseStatus: 'AWAITING_RESPONSE',
isAutoGenerated: false,
hasAttachments: true,
url: 'https://outlook.office.com/mail/inbox/id/123'
},
// ... more emails
];
export const mockCalendarItems: CalendarItem[] = [
{
id: 'event-1',
title: 'Architecture Review - Q3 Planning',
startTime: new Date(Date.now() + 1 * 60 * 60 * 1000), // 1 hour from now
endTime: new Date(Date.now() + 2 * 60 * 60 * 1000),
duration: 60,
isAllDay: false,
conflictType: 'UNACCEPTED',
userResponseStatus: 'TENTATIVE',
organizer: { name: 'Michael Johnson', email: 'michael.johnson@company.com' },
attendeeCount: 12,
requiredAttendees: 8,
calendarName: 'Team Calendar',
isTeamsEvent: true,
teamsMeetingUrl: 'https://teams.microsoft.com/l/meetup-join/...',
url: 'https://outlook.office.com/calendar/item/123',
updatedAt: new Date()
},
// ... more events
];
export const mockOutlookService = {
getEmails: async () => {
await new Promise(resolve => setTimeout(resolve, 150));
return mockEmails;
},
getCalendarEvents: async () => {
await new Promise(resolve => setTimeout(resolve, 150));
return mockCalendarItems;
}
};// src/services/aggregation-service.ts
import { mockJiraService } from '@/services/mock/jira-mock';
import { mockOutlookService } from '@/services/mock/outlook-mock';
class AggregationService {
async generateOvernightSummary(userId: string, date?: Date): Promise<OvernightSummary> {
const user = { id: userId, timezone: 'America/New_York', businessHoursStart: 8, businessHoursEnd: 18 };
const { start, end } = this.getOvernightWindow(user, date);
// Use mock services
const [jiraItems, emailItems, calendarItems] = await Promise.all([
mockJiraService.getIssuesForUser(userId), // Mock data
mockOutlookService.getEmails(), // Mock data
mockOutlookService.getCalendarEvents() // Mock data
]);
// Rest of aggregation logic works the same
return {
// ... build summary from mock data
};
}
}// src/services/mock/mock-utils.ts
export const createMockOverview = (scenario: 'busy' | 'normal' | 'quiet') => {
switch (scenario) {
case 'busy':
// 5+ Jira items, 3+ emails, 2+ conflicts
return { jiraItems: [...], emailItems: [...], calendarItems: [...] };
case 'normal':
// 2-3 Jira items, 1-2 emails, 0-1 conflicts
return { jiraItems: [...], emailItems: [...], calendarItems: [...] };
case 'quiet':
// 0-1 Jira items, 0 emails, 0 conflicts
return { jiraItems: [], emailItems: [], calendarItems: [] };
}
};
// Allows frontend to test different states without changing code- Node.js 18+
- PostgreSQL 13+
- Git
- No API credentials needed!
This is fully optional during sprint. Build with mock data first.
1.1 Create OAuth App in Jira (POST-SPRINT)
-
Login to Jira Cloud (https://dev.atlassian.com/)
-
Go to Apps → OAuth 2.0 integrations
-
Create new app with:
- App name: Morning Command Center
- Redirect URL:
http://localhost:3000/api/auth/callback/jira - Scopes:
read:jira-workread:jira-user
-
Save Client ID and Secret
1.2 Environment Variables
# .env.local
JIRA_CLIENT_ID=xxxxx
JIRA_CLIENT_SECRET=xxxxx
JIRA_INSTANCE_URL=https://company.atlassian.net1.3 API Client Implementation
// src/services/jira-service.ts
import { JiraClient } from '@jira/cloud-sdk';
class JiraService {
private client: JiraClient;
constructor(accessToken: string) {
this.client = new JiraClient({
host: process.env.JIRA_INSTANCE_URL,
authentication: {
bearer: accessToken
}
});
}
async getIssuesForUser(userId: string, options?: { days: number }): Promise<JiraItem[]> {
const jql = `
assignee = ${userId}
OR reporter = ${userId}
OR watchers in (${userId})
OR text ~ "${userId}"
`;
const results = await this.client.issueSearch.searchForIssuesUsingJQL({
jql,
maxResults: 50,
fields: [
'key', 'summary', 'priority', 'status', 'updated', 'created',
'assignee', 'reporter', 'labels', 'issuelinks'
]
});
return results.issues.map(issue => ({
id: issue.id,
issueKey: issue.key,
summary: issue.fields.summary,
priority: this.mapPriority(issue.fields.priority),
// ... map other fields
}));
}
private mapPriority(priority: any): 'BLOCKER' | 'P1' | 'P2' | 'P3' | 'P4' | 'P5' {
const nameMap = {
'Blocker': 'BLOCKER',
'Highest': 'P1',
'High': 'P2',
'Medium': 'P3',
'Low': 'P4',
'Lowest': 'P5'
};
return nameMap[priority.name] || 'P3';
}
}
export default JiraService;1.4 Testing
npm test -- src/services/jira-service.test.ts2.1 Register Application in Azure AD
-
Go to Azure Portal (https://portal.azure.com/)
-
Azure AD → App registrations → New registration
-
Name: Morning Command Center
-
Redirect URI:
http://localhost:3000/api/auth/callback/outlook -
In Certificates & secrets → New client secret
-
In API permissions, add:
Mail.Read(Delegated)Calendars.Read(Delegated)User.Read(Delegated)
-
Save Application ID and Client Secret
2.2 Environment Variables
# .env.local
OUTLOOK_CLIENT_ID=xxxxx
OUTLOOK_CLIENT_SECRET=xxxxx
OUTLOOK_TENANT_ID=xxxxx
OUTLOOK_REDIRECT_URI=http://localhost:3000/api/auth/callback/outlook2.3 API Client Implementation
// src/services/outlook-service.ts
import { Client } from '@microsoft/microsoft-graph-client';
import 'isomorphic-fetch';
class OutlookService {
private graphClient: Client;
constructor(accessToken: string) {
this.graphClient = Client.init({
authProvider: (done) => {
done(null, accessToken);
}
});
}
async getEmails(options?: { days: number }): Promise<EmailItem[]> {
const sinceDate = new Date();
sinceDate.setDate(sinceDate.getDate() - (options?.days || 1));
const messages = await this.graphClient
.api('/me/messages')
.filter(`receivedDateTime ge ${sinceDate.toISOString()}`)
.orderby('receivedDateTime desc')
.top(50)
.get();
return messages.value.map(msg => ({
id: msg.id,
subject: msg.subject,
sender: {
name: msg.sender.emailAddress.name,
email: msg.sender.emailAddress.address
},
// ... map other fields
body: msg.bodyPreview,
isRead: msg.isRead,
isFlagged: msg.flag?.flagStatus === 'flagged',
receivedAt: new Date(msg.receivedDateTime)
}));
}
async getCalendarEvents(options?: { days: number }): Promise<CalendarItem[]> {
const now = new Date();
const future = new Date();
future.setDate(future.getDate() + (options?.days || 7));
const events = await this.graphClient
.api('/me/events')
.filter(`start/dateTime ge '${now.toISOString()}'`)
.orderby('start/dateTime')
.top(100)
.get();
return events.value.map(event => ({
id: event.id,
title: event.subject,
startTime: new Date(event.start.dateTime),
endTime: new Date(event.end.dateTime),
// ... map other fields
}));
}
}
export default OutlookService;2.4 Testing
npm test -- src/services/outlook-service.test.ts// src/services/auth-service.ts
import jwt from 'jsonwebtoken';
class AuthService {
async exchangeCodeForToken(provider: string, code: string) {
let tokenEndpoint = '';
let clientId = '';
let clientSecret = '';
if (provider === 'jira') {
tokenEndpoint = 'https://auth.atlassian.com/oauth/token';
clientId = process.env.JIRA_CLIENT_ID;
clientSecret = process.env.JIRA_CLIENT_SECRET;
} else if (provider === 'outlook') {
tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
clientId = process.env.OUTLOOK_CLIENT_ID;
clientSecret = process.env.OUTLOOK_CLIENT_SECRET;
}
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: process.env.REDIRECT_URI
})
});
const data = await response.json();
// Store token in database
await this.storeToken(provider, data.access_token, data.refresh_token, data.expires_in);
return data;
}
async storeToken(provider: string, accessToken: string, refreshToken: string, expiresIn: number) {
const expiresAt = new Date(Date.now() + expiresIn * 1000);
// INSERT or UPDATE integration_tokens table
await db.query(
`INSERT INTO integration_tokens (user_id, provider, access_token, refresh_token, expires_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_id, provider) DO UPDATE SET
access_token = $3,
refresh_token = $4,
expires_at = $5`,
[userId, provider, accessToken, refreshToken, expiresAt]
);
}
}
export default AuthService;// src/services/aggregation-service.ts
class AggregationService {
async generateOvernightSummary(userId: string, date?: Date): Promise<OvernightSummary> {
const user = await this.getUser(userId);
const { start, end } = this.getOvernightWindow(user, date);
// Fetch data from all sources in parallel
const [jiraItems, emailItems, calendarItems] = await Promise.all([
this.getJiraItems(userId, { start, end }),
this.getEmailItems(userId, { start, end }),
this.getCalendarItems(userId, { start, end })
]);
// Synthesize into summary
const summary = {
id: uuid(),
userId,
generatedAt: new Date(),
generatedFromData: end,
metrics: {
jiraItemsRequiringAction: jiraItems.filter(i => i.changeType !== 'CLOSED').length,
escalationsAssigned: jiraItems.filter(i => i.changeType === 'ESCALATION').length,
itemsResolved: jiraItems.filter(i => i.currentStatus === 'Resolved').length,
itemsClosed: jiraItems.filter(i => i.currentStatus === 'Closed').length,
criticalEmails: emailItems.filter(i => i.category === 'CRITICAL').length,
agingEmails: emailItems.filter(i => i.category === 'AGING').length,
meetingConflicts: calendarItems.filter(i => i.conflictType === 'OVERLAP').length,
unacceptedMeetings: calendarItems.filter(i => i.userResponseStatus === 'TENTATIVE').length
},
biggestRisk: this.identifyBiggestRisk(jiraItems, emailItems, calendarItems),
recommendedActions: this.generateRecommendedActions(jiraItems, emailItems, calendarItems),
jiraItems,
emailItems,
calendarItems,
attentionScore: this.calculateAttentionScore({ jiraItems, emailItems, calendarItems }),
sourcesQueried: ['JIRA', 'OUTLOOK', 'CALENDAR'],
generationTimeMs: Date.now() - startTime
};
// Cache result
await this.cacheResult(summary);
return summary;
}
private identifyBiggestRisk(jiraItems, emailItems, calendarItems): BiggestRisk {
// Find most severe blockers or critical items
const blockers = jiraItems.filter(i => i.changeType === 'BLOCKER');
const critical = emailItems.filter(i => i.category === 'CRITICAL');
if (blockers.length > 0) {
return {
title: blockers[0].summary,
source: 'JIRA',
sourceId: blockers[0].issueKey,
severity: 'CRITICAL'
};
}
// ... else check emails, etc.
}
private generateRecommendedActions(jiraItems, emailItems, calendarItems): RecommendedAction[] {
const actions: RecommendedAction[] = [];
// Action 1: Review critical Jira item
const criticalJira = jiraItems.find(i => i.priority === 'BLOCKER');
if (criticalJira) {
actions.push({
order: 1,
description: `Review ${criticalJira.issueKey}`,
sourceType: 'JIRA',
sourceId: criticalJira.issueKey,
actionType: 'REVIEW',
estimatedTime: 15
});
}
// Action 2: Respond to critical email
const criticalEmail = emailItems.find(i => i.category === 'CRITICAL' && i.responseStatus === 'AWAITING_RESPONSE');
if (criticalEmail) {
actions.push({
order: 2,
description: `Respond to "${criticalEmail.subject}"`,
sourceType: 'EMAIL',
sourceId: criticalEmail.id,
actionType: 'RESPOND',
estimatedTime: 10
});
}
// Action 3: Accept meeting
const unaccepted = calendarItems.find(i => i.userResponseStatus === 'TENTATIVE');
if (unaccepted) {
actions.push({
order: 3,
description: `Accept "${unaccepted.title}" meeting`,
sourceType: 'CALENDAR',
sourceId: unaccepted.id,
actionType: 'ACCEPT',
estimatedTime: 2
});
}
return actions.slice(0, 5); // Top 5 actions
}
}
export default AggregationService;// src/routes/overnight-summary.ts
import { Router } from 'express';
import { authMiddleware } from '../middleware/auth';
import AggregationService from '../services/aggregation-service';
const router = Router();
const aggregationService = new AggregationService();
router.get('/api/overnight-summary', authMiddleware, async (req, res) => {
try {
const userId = req.user.id;
const date = req.query.date ? new Date(req.query.date as string) : new Date();
const summary = await aggregationService.generateOvernightSummary(userId, date);
return res.status(200).json({
status: 'SUCCESS',
data: summary,
metadata: {
queriedSources: summary.sourcesQueried,
generationTime: summary.generationTimeMs,
cacheHit: false
}
});
} catch (error) {
return res.status(500).json({
status: 'ERROR',
error: {
code: 'AGGREGATION_FAILED',
message: 'Failed to generate overnight summary',
details: error.message
}
});
}
});
export default router;-- migrations/001_initial_schema.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
timezone VARCHAR(50) DEFAULT 'UTC',
business_hours_start SMALLINT DEFAULT 8,
business_hours_end SMALLINT DEFAULT 18,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE integration_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
access_token TEXT NOT NULL,
refresh_token TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, provider)
);
CREATE TABLE overnight_summaries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
generated_from_date TIMESTAMP,
attention_score SMALLINT,
biggest_risk_json JSONB,
metrics_json JSONB,
jira_items_json JSONB,
email_items_json JSONB,
calendar_items_json JSONB,
generation_time_ms INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, DATE(generated_from_date))
);
CREATE INDEX idx_overnight_summaries_user_date
ON overnight_summaries(user_id, generated_from_date DESC);// src/services/aggregation-service.test.ts
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import AggregationService from './aggregation-service';
describe('AggregationService', () => {
let service: AggregationService;
beforeEach(() => {
service = new AggregationService();
});
it('should generate overnight summary', async () => {
const userId = 'test-user-123';
const summary = await service.generateOvernightSummary(userId);
expect(summary).toBeDefined();
expect(summary.userId).toBe(userId);
expect(summary.metrics).toBeDefined();
expect(summary.recommendedActions.length).toBeGreaterThan(0);
});
it('should identify biggest risk', () => {
const jiraItems = [
{ changeType: 'BLOCKER', summary: 'API is down', issueKey: 'TEST-1' }
];
const risk = service.identifyBiggestRisk(jiraItems, [], []);
expect(risk.severity).toBe('CRITICAL');
expect(risk.source).toBe('JIRA');
});
});| Issue | Solution |
|---|---|
| Jira API 401 Unauthorized | Check token expiration, refresh if needed |
| Outlook API 429 Rate Limited | Implement exponential backoff, use cache |
| Database connection failing | Verify PostgreSQL is running, check connection string |
| CORS errors | Add frontend URL to CORS whitelist in API server |
| Slow aggregation (>5s) | Check Jira/Outlook API response times, add caching |
- Choose tech stack: Node.js + Express, Next.js, or other backend framework
- Set up databases: PostgreSQL local + staging + production
- Implement auth: Use Passport.js or similar for OAuth flows
- Build core services: Start with Jira, then Outlook, then Calendar
- Connect API routes: Wire up aggregation pipeline
- Frontend components: Build React components consuming API
- Testing & deployment: Add CI/CD pipeline
✅ Use mock data (see Mock Data Strategy section at top)
✅ Build all components with realistic test data
✅ Test edge cases (empty state, errors, loading)
✅ Demo with consistent, predictable data
- Implement real Jira OAuth flow
- Implement real Outlook OAuth flow
- Test with live API data
- Handle API errors gracefully
- Implement caching strategy
- Set up rate limiting protection
- Load test with real API rates
| Benefit | Why It Matters |
|---|---|
| No API rate limits | Build fast without throttling |
| Consistent test data | Same output every test run |
| Complete control | Test edge cases easily |
| No credentials needed | Faster sprint setup |
| Swap later | Drop-in replacement post-sprint |
| Demo safely | No risk of API outages during demo |
When you're ready to wire real APIs (after sprint):
// src/services/jira-service-real.ts
// (Copy code from sections 1.3 above)
// In aggregation-service.ts, swap:
- import { mockJiraService } from '@/services/mock/jira-mock';
+ import { jiraService } from '@/services/jira-service-real';
// That's it! Everything else stays the same.- Jira API Docs: https://developer.atlassian.com/cloud/jira/rest/v3/
- Microsoft Graph API: https://learn.microsoft.com/en-us/graph/
- OAuth 2.0 Guide: https://oauth.net/2/
- Passport.js Documentation: https://www.passportjs.org/
For the sprint: Use mock data, focus on UI/UX.
Post-sprint: Refer back to sections 1-3 for real API implementation.