Closes #ISSUE-009 - Implement DEV_MODE Offline-First Protection Shield
This commit is contained in:
@@ -347,6 +347,7 @@ export async function GET(request: Request) {
|
||||
const mode = searchParams.get('mode') || 'day_crash';
|
||||
const region = searchParams.get('region') || 'us';
|
||||
const fmpApiKey = process.env.FMP_API_KEY || 'U6lOXaOFPye7oc1D235kyAqJeQaiTAWc';
|
||||
const isDevMode = process.env.DEV_MODE === 'true';
|
||||
|
||||
let tickersPool: string[] = [];
|
||||
|
||||
@@ -358,38 +359,49 @@ export async function GET(request: Request) {
|
||||
// US region: Large/Mid pool + Small Cap pool
|
||||
tickersPool = [...US_MEGA_MID];
|
||||
|
||||
// Try to load FMP Small Caps or use static Small-Caps fallback list
|
||||
let fmpSmallCaps: string[] = [];
|
||||
try {
|
||||
fmpSmallCaps = await fetchFmpScreener(fmpApiKey);
|
||||
} catch (_) {}
|
||||
|
||||
if (fmpSmallCaps.length > 0) {
|
||||
tickersPool.push(...fmpSmallCaps);
|
||||
} else {
|
||||
if (isDevMode) {
|
||||
tickersPool.push(...US_SMALL_CAPS);
|
||||
} else {
|
||||
// Try to load FMP Small Caps or use static Small-Caps fallback list
|
||||
let fmpSmallCaps: string[] = [];
|
||||
try {
|
||||
fmpSmallCaps = await fetchFmpScreener(fmpApiKey);
|
||||
} catch (_) {}
|
||||
|
||||
if (fmpSmallCaps.length > 0) {
|
||||
tickersPool.push(...fmpSmallCaps);
|
||||
} else {
|
||||
tickersPool.push(...US_SMALL_CAPS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// De-duplicate tickers pool
|
||||
tickersPool = Array.from(new Set(tickersPool));
|
||||
|
||||
// Fetch chart details for all tickers in parallel
|
||||
const rawCharts = await Promise.allSettled(
|
||||
tickersPool.map(t => fetchYahooChart(t))
|
||||
);
|
||||
|
||||
const parsedResults: TickerDetails[] = [];
|
||||
|
||||
tickersPool.forEach((ticker, idx) => {
|
||||
const res = rawCharts[idx];
|
||||
if (res.status === 'fulfilled' && res.value) {
|
||||
parsedResults.push(res.value);
|
||||
} else {
|
||||
// Fetch failed, use high-fidelity simulation
|
||||
if (isDevMode) {
|
||||
// Bypass Yahoo fetches completely, generate simulated charts directly!
|
||||
tickersPool.forEach((ticker) => {
|
||||
parsedResults.push(generateSimulatedChart(ticker, mode));
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Fetch chart details for all tickers in parallel
|
||||
const rawCharts = await Promise.allSettled(
|
||||
tickersPool.map(t => fetchYahooChart(t))
|
||||
);
|
||||
|
||||
tickersPool.forEach((ticker, idx) => {
|
||||
const res = rawCharts[idx];
|
||||
if (res.status === 'fulfilled' && res.value) {
|
||||
parsedResults.push(res.value);
|
||||
} else {
|
||||
// Fetch failed, use high-fidelity simulation
|
||||
parsedResults.push(generateSimulatedChart(ticker, mode));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch fundamentals for top 15 candidates to reduce payload weight
|
||||
// Sort candidates first based on mode to isolate top 15
|
||||
@@ -409,19 +421,26 @@ export async function GET(request: Request) {
|
||||
// Overlay fundamentals
|
||||
const finalResults = await Promise.all(
|
||||
sortedCandidates.map(async (item) => {
|
||||
const fund = top15Symbols.has(item.ticker)
|
||||
? await fetchFMPFundamentalData(item.ticker, fmpApiKey)
|
||||
: getMockFundamentals(item.ticker);
|
||||
const useMock = isDevMode || !top15Symbols.has(item.ticker);
|
||||
const fund = useMock
|
||||
? getMockFundamentals(item.ticker)
|
||||
: await fetchFMPFundamentalData(item.ticker, fmpApiKey);
|
||||
|
||||
return {
|
||||
...item,
|
||||
...fund,
|
||||
dividendYield: fund.dividendYield ? Number((fund.dividendYield * (top15Symbols.has(item.ticker) ? 1 : 100)).toFixed(2)) : 0
|
||||
dividendYield: fund.dividendYield ? Number((fund.dividendYield * (useMock ? 100 : 1)).toFixed(2)) : 0
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const response = NextResponse.json({ results: finalResults });
|
||||
const response = NextResponse.json({
|
||||
results: finalResults,
|
||||
isShieldActive: isDevMode
|
||||
});
|
||||
response.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate');
|
||||
if (isDevMode) {
|
||||
response.headers.set('X-Shield-Active', 'true');
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -134,9 +134,10 @@ async function fetchFmpData(ticker: string, apiKey: string): Promise<any> {
|
||||
export async function GET() {
|
||||
const apiKey = process.env.FMP_API_KEY;
|
||||
const now = Date.now();
|
||||
const isDevMode = process.env.DEV_MODE === 'true';
|
||||
|
||||
// Return cached result if valid
|
||||
if (cache && (now - cache.timestamp < CACHE_TTL)) {
|
||||
// Return cached result if valid (unless in DEV_MODE)
|
||||
if (!isDevMode && cache && (now - cache.timestamp < CACHE_TTL)) {
|
||||
return NextResponse.json(cache.data, {
|
||||
status: 200,
|
||||
headers: { 'Cache-Control': 'public, max-age=3600' }
|
||||
@@ -147,7 +148,9 @@ export async function GET() {
|
||||
// Deep clone fallback data
|
||||
const companyData: CompanyData[] = JSON.parse(JSON.stringify(MOCK_TECH_AI_DATA));
|
||||
|
||||
if (apiKey) {
|
||||
if (isDevMode) {
|
||||
// Short-circuit: completely skip outbound FMP fetches
|
||||
} else if (apiKey) {
|
||||
try {
|
||||
// Test the API key first with a quick check
|
||||
const testRes = await fetchWithTimeout(`https://financialmodelingprep.com/api/v3/income-statement/NVDA?period=quarter&limit=1&apikey=${apiKey}`);
|
||||
@@ -422,6 +425,7 @@ export async function GET() {
|
||||
const payload = {
|
||||
dates,
|
||||
liveDataAvailable,
|
||||
isShieldActive: isDevMode,
|
||||
timestamp: now,
|
||||
metrics: {
|
||||
monetizationGap: {
|
||||
@@ -436,13 +440,22 @@ export async function GET() {
|
||||
}
|
||||
};
|
||||
|
||||
cache = {
|
||||
timestamp: now,
|
||||
data: payload
|
||||
if (!isDevMode) {
|
||||
cache = {
|
||||
timestamp: now,
|
||||
data: payload
|
||||
};
|
||||
}
|
||||
|
||||
const responseHeaders: Record<string, string> = {
|
||||
'Cache-Control': isDevMode ? 'no-store, max-age=0, must-revalidate' : 'public, max-age=3600'
|
||||
};
|
||||
if (isDevMode) {
|
||||
responseHeaders['X-Shield-Active'] = 'true';
|
||||
}
|
||||
|
||||
return NextResponse.json(payload, {
|
||||
status: 200,
|
||||
headers: { 'Cache-Control': 'public, max-age=3600' }
|
||||
headers: responseHeaders
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user