Closes #ISSUE-009 - Implement DEV_MODE Offline-First Protection Shield
This commit is contained in:
14
DEV_LOG.md
14
DEV_LOG.md
@@ -43,3 +43,17 @@ This document tracks all modifications, npm packages, active compilation states,
|
|||||||
* **Active Bugs**: None.
|
* **Active Bugs**: None.
|
||||||
* **Type Checker Status**: Verified clean compilation (`npx tsc --noEmit` returns exit code 0).
|
* **Type Checker Status**: Verified clean compilation (`npx tsc --noEmit` returns exit code 0).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2026-06-12] - Offline-First Architectural Shield (`DEV_MODE`) (#ISSUE-009)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* **API Outbound Interception Layer**: Configured [/api/scanner/route.ts](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/app/api/scanner/route.ts) and [/api/tech/ai/route.ts](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/app/api/tech/ai/route.ts) to intercept fetches if `process.env.DEV_MODE === 'true'`, completely bypassing outbound network calls and short-circuiting to high-fidelity mock/fallback data with `isShieldActive: true`.
|
||||||
|
* **Visual Status Badges**: Mounted highly polished, glassmorphic "API Layer Status" badges inside [ScannerDemo.tsx](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/components/modules/scanner/ScannerDemo.tsx) and [AiSpecialSilo.tsx](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/components/modules/tech/AiSpecialSilo.tsx) upper control bars, displaying "🟡 DEV-ARCHIV AKTIV (0 CALLS)" if the shield is active, and "🟢 LIVE-API ENDPUNKT (FMP CORPO)" if inactive.
|
||||||
|
* **Environment Perimeter Lock**: Appended `DEV_MODE=true` inside [.env](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/.env) to lock the local sandbox securely into a zero-cost offline state.
|
||||||
|
|
||||||
|
### Active Bugs / Compile Status
|
||||||
|
* **Active Bugs**: None.
|
||||||
|
* **Type Checker Status**: Verified clean compilation (`npx tsc --noEmit` returns exit code 0).
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ export async function GET(request: Request) {
|
|||||||
const mode = searchParams.get('mode') || 'day_crash';
|
const mode = searchParams.get('mode') || 'day_crash';
|
||||||
const region = searchParams.get('region') || 'us';
|
const region = searchParams.get('region') || 'us';
|
||||||
const fmpApiKey = process.env.FMP_API_KEY || 'U6lOXaOFPye7oc1D235kyAqJeQaiTAWc';
|
const fmpApiKey = process.env.FMP_API_KEY || 'U6lOXaOFPye7oc1D235kyAqJeQaiTAWc';
|
||||||
|
const isDevMode = process.env.DEV_MODE === 'true';
|
||||||
|
|
||||||
let tickersPool: string[] = [];
|
let tickersPool: string[] = [];
|
||||||
|
|
||||||
@@ -358,38 +359,49 @@ export async function GET(request: Request) {
|
|||||||
// US region: Large/Mid pool + Small Cap pool
|
// US region: Large/Mid pool + Small Cap pool
|
||||||
tickersPool = [...US_MEGA_MID];
|
tickersPool = [...US_MEGA_MID];
|
||||||
|
|
||||||
// Try to load FMP Small Caps or use static Small-Caps fallback list
|
if (isDevMode) {
|
||||||
let fmpSmallCaps: string[] = [];
|
|
||||||
try {
|
|
||||||
fmpSmallCaps = await fetchFmpScreener(fmpApiKey);
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
if (fmpSmallCaps.length > 0) {
|
|
||||||
tickersPool.push(...fmpSmallCaps);
|
|
||||||
} else {
|
|
||||||
tickersPool.push(...US_SMALL_CAPS);
|
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
|
// De-duplicate tickers pool
|
||||||
tickersPool = Array.from(new Set(tickersPool));
|
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[] = [];
|
const parsedResults: TickerDetails[] = [];
|
||||||
|
|
||||||
tickersPool.forEach((ticker, idx) => {
|
if (isDevMode) {
|
||||||
const res = rawCharts[idx];
|
// Bypass Yahoo fetches completely, generate simulated charts directly!
|
||||||
if (res.status === 'fulfilled' && res.value) {
|
tickersPool.forEach((ticker) => {
|
||||||
parsedResults.push(res.value);
|
|
||||||
} else {
|
|
||||||
// Fetch failed, use high-fidelity simulation
|
|
||||||
parsedResults.push(generateSimulatedChart(ticker, mode));
|
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
|
// Fetch fundamentals for top 15 candidates to reduce payload weight
|
||||||
// Sort candidates first based on mode to isolate top 15
|
// Sort candidates first based on mode to isolate top 15
|
||||||
@@ -409,19 +421,26 @@ export async function GET(request: Request) {
|
|||||||
// Overlay fundamentals
|
// Overlay fundamentals
|
||||||
const finalResults = await Promise.all(
|
const finalResults = await Promise.all(
|
||||||
sortedCandidates.map(async (item) => {
|
sortedCandidates.map(async (item) => {
|
||||||
const fund = top15Symbols.has(item.ticker)
|
const useMock = isDevMode || !top15Symbols.has(item.ticker);
|
||||||
? await fetchFMPFundamentalData(item.ticker, fmpApiKey)
|
const fund = useMock
|
||||||
: getMockFundamentals(item.ticker);
|
? getMockFundamentals(item.ticker)
|
||||||
|
: await fetchFMPFundamentalData(item.ticker, fmpApiKey);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
...fund,
|
...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');
|
response.headers.set('Cache-Control', 'no-store, max-age=0, must-revalidate');
|
||||||
|
if (isDevMode) {
|
||||||
|
response.headers.set('X-Shield-Active', 'true');
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,9 +134,10 @@ async function fetchFmpData(ticker: string, apiKey: string): Promise<any> {
|
|||||||
export async function GET() {
|
export async function GET() {
|
||||||
const apiKey = process.env.FMP_API_KEY;
|
const apiKey = process.env.FMP_API_KEY;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
const isDevMode = process.env.DEV_MODE === 'true';
|
||||||
|
|
||||||
// Return cached result if valid
|
// Return cached result if valid (unless in DEV_MODE)
|
||||||
if (cache && (now - cache.timestamp < CACHE_TTL)) {
|
if (!isDevMode && cache && (now - cache.timestamp < CACHE_TTL)) {
|
||||||
return NextResponse.json(cache.data, {
|
return NextResponse.json(cache.data, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Cache-Control': 'public, max-age=3600' }
|
headers: { 'Cache-Control': 'public, max-age=3600' }
|
||||||
@@ -147,7 +148,9 @@ export async function GET() {
|
|||||||
// Deep clone fallback data
|
// Deep clone fallback data
|
||||||
const companyData: CompanyData[] = JSON.parse(JSON.stringify(MOCK_TECH_AI_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 {
|
try {
|
||||||
// Test the API key first with a quick check
|
// 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}`);
|
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 = {
|
const payload = {
|
||||||
dates,
|
dates,
|
||||||
liveDataAvailable,
|
liveDataAvailable,
|
||||||
|
isShieldActive: isDevMode,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
metrics: {
|
metrics: {
|
||||||
monetizationGap: {
|
monetizationGap: {
|
||||||
@@ -436,13 +440,22 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
cache = {
|
if (!isDevMode) {
|
||||||
timestamp: now,
|
cache = {
|
||||||
data: payload
|
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, {
|
return NextResponse.json(payload, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Cache-Control': 'public, max-age=3600' }
|
headers: responseHeaders
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default function ScannerDemo() {
|
|||||||
|
|
||||||
const [showMathAccordion, setShowMathAccordion] = useState(false);
|
const [showMathAccordion, setShowMathAccordion] = useState(false);
|
||||||
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
||||||
|
const [isShieldActive, setIsShieldActive] = useState(false);
|
||||||
|
|
||||||
// Cache for metadata and prices retrieved dynamically
|
// Cache for metadata and prices retrieved dynamically
|
||||||
const [alertsMetadata, setAlertsMetadata] = useState<Record<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }>>({});
|
const [alertsMetadata, setAlertsMetadata] = useState<Record<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }>>({});
|
||||||
@@ -94,6 +95,7 @@ export default function ScannerDemo() {
|
|||||||
throw new Error('Failed to fetch scanner tickers');
|
throw new Error('Failed to fetch scanner tickers');
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
setIsShieldActive(!!data.isShieldActive);
|
||||||
const results = data.results || [];
|
const results = data.results || [];
|
||||||
|
|
||||||
setScanProgress('Berechne GJR-GARCH Volatilitäten...');
|
setScanProgress('Berechne GJR-GARCH Volatilitäten...');
|
||||||
@@ -551,8 +553,20 @@ export default function ScannerDemo() {
|
|||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-amber-400 text-xs font-semibold uppercase tracking-wider">Market Scanner Engine</span>
|
<span className="text-amber-400 text-xs font-semibold uppercase tracking-wider">Market Scanner Engine</span>
|
||||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
<h2 className="text-xl font-bold text-white flex flex-wrap items-center gap-2">
|
||||||
<ShieldAlert className="text-amber-400 w-5 h-5" /> Anomalien-Scanner & Marktverzerrungen
|
<ShieldAlert className="text-amber-400 w-5 h-5" />
|
||||||
|
<span>Anomalien-Scanner & Marktverzerrungen</span>
|
||||||
|
{isShieldActive ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 shadow-[0_0_10px_rgba(245,158,11,0.15)] ml-2 animate-pulse">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
|
||||||
|
DEV-ARCHIV AKTIV (0 CALLS)
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.15)] ml-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-ping" />
|
||||||
|
LIVE-API ENDPUNKT (FMP CORPO)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-slate-400 text-xs max-w-2xl">
|
<p className="text-slate-400 text-xs max-w-2xl">
|
||||||
Isoliert Kursstürze > 5% bei relativem Gesamtmarkt-Stopp (S&P 500 driftet seitwärts oder steigt). Misst die Asymmetrie mittels GJR-GARCH, um Panik von strukturellen Risiken zu separieren.
|
Isoliert Kursstürze > 5% bei relativem Gesamtmarkt-Stopp (S&P 500 driftet seitwärts oder steigt). Misst die Asymmetrie mittels GJR-GARCH, um Panik von strukturellen Risiken zu separieren.
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default function AiSpecialSilo() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [payload, setPayload] = useState<Payload | null>(null);
|
const [payload, setPayload] = useState<Payload | null>(null);
|
||||||
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
||||||
|
const [isShieldActive, setIsShieldActive] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -87,6 +88,7 @@ export default function AiSpecialSilo() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setPayload(data);
|
setPayload(data);
|
||||||
|
setIsShieldActive(!!data.isShieldActive);
|
||||||
} else {
|
} else {
|
||||||
setError('Error fetching AI Tech Hyper-Leverage metrics.');
|
setError('Error fetching AI Tech Hyper-Leverage metrics.');
|
||||||
}
|
}
|
||||||
@@ -186,8 +188,20 @@ export default function AiSpecialSilo() {
|
|||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-teal-400 text-xs font-semibold uppercase tracking-wider">AI & Tech Silo</span>
|
<span className="text-teal-400 text-xs font-semibold uppercase tracking-wider">AI & Tech Silo</span>
|
||||||
<h2 className="text-2xl font-extrabold text-white flex items-center gap-2">
|
<h2 className="text-2xl font-extrabold text-white flex flex-wrap items-center gap-2">
|
||||||
<Zap className="text-teal-400 w-6 h-6" /> AI Hyper-Leverage & CapEx Matrix
|
<Zap className="text-teal-400 w-6 h-6" />
|
||||||
|
<span>AI Hyper-Leverage & CapEx Matrix</span>
|
||||||
|
{isShieldActive ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 shadow-[0_0_10px_rgba(245,158,11,0.15)] ml-2 animate-pulse">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
|
||||||
|
DEV-ARCHIV AKTIV (0 CALLS)
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.15)] ml-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-ping" />
|
||||||
|
LIVE-API ENDPUNKT (FMP CORPO)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-slate-400">
|
||||||
Monitors Big Tech capital expenditures, segment revenues, and inventory velocities to diagnose infrastructure bubbles.
|
Monitors Big Tech capital expenditures, segment revenues, and inventory velocities to diagnose infrastructure bubbles.
|
||||||
|
|||||||
Reference in New Issue
Block a user