GoldScrape_Api / src /server.js
Aurum
Initialize clean codebase without binary files
dcc4f27
Raw
History Blame Contribute Delete
28.3 kB
/**
* GoldRate Engine β€” Express Server
*
* Serves Gold 24K and Silver 999 prices from two sources:
* - SLN Bullion Chennai (local Chennai rates)
* - IBJA India via goldratetodaylive.in (national rates)
*
* Endpoints:
* GET /api/rates β†’ Prices based on API key's source (SLN or IBJA)
* GET /api/sln-rates β†’ SLN Bullion Chennai rates (for frontend)
* GET /api/ibja-rates β†’ IBJA India rates (for frontend)
* GET /api/history β†’ SLN historical prices
* GET /api/ibja-history β†’ IBJA historical prices
* GET /health β†’ Server health check
*
* Authentication:
* Header: x-api-key: YOUR_API_KEY
* OR Query: ?apikey=YOUR_API_KEY
*/
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const { getGoldRate, getIBJARate } = require('./priceFetcher');
const { authMiddleware } = require('./auth');
const { connect: connectDB, getIsConnected, getClient } = require('./db');
const { initTelegramBot } = require('./telegramBot');
// Middleware to secure public endpoints (allows same-origin referers, custom headers, or valid API keys)
const dashboardAuthMiddleware = (req, res, next) => {
const apiKey = req.headers['x-api-key'] || req.query.apikey;
if (apiKey) {
return authMiddleware(req, res, next);
}
// Allow requests from RapidAPI proxy (strictly authenticated using the proxy secret key)
if (req.headers['x-rapidapi-proxy-secret'] && req.headers['x-rapidapi-proxy-secret'] === process.env.RAPIDAPI_PROXY_SECRET) {
req.priceSource = 'SLN'; // Default to SLN
return next();
}
// Allow requests containing our secure frontend dashboard verification header or query param
if (req.headers['x-app-request'] === 'goldscrape-dashboard' || req.query.dashboard === 'true') {
req.priceSource = 'SLN'; // Default to SLN
return next();
}
const referer = req.headers.referer || req.headers.referrer;
const host = req.headers.host;
// Allow same-origin, Hugging Face Space iframes, Render domains, and localhost
const isAllowedOrigin = referer && (
(host && referer.includes(host)) ||
referer.includes('huggingface.co') ||
referer.includes('onrender.com') ||
referer.includes('localhost')
);
if (isAllowedOrigin) {
req.priceSource = 'SLN'; // Default to SLN
return next();
}
return res.status(401).json({
success: false,
error: 'API key required for external requests.'
});
};
// Connect to MongoDB (if URI present)
connectDB();
const app = express();
const PORT = process.env.PORT || 3000;
// Trust proxy header to allow correct IP extraction behind Hugging Face/Render proxies
app.set('trust proxy', 1);
// ─── Security ────────────────────────────────────────
app.use(helmet({
contentSecurityPolicy: false,
frameguard: false
}));
app.use(cors());
app.use(express.json());
// ─── Serve Frontend ───────────────────────────────────
app.use(express.static('public'));
// Explicitly serve index.html for the root route
app.get('/', (req, res) => {
const path = require('path');
res.sendFile(path.join(__dirname, '../public/index.html'));
});
// ─── Rate Limiting ───────────────────────────────────
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX) || 500,
skip: (req) => {
return req.headers['x-app-request'] === 'goldscrape-dashboard' ||
req.query.dashboard === 'true' ||
(req.headers['x-rapidapi-proxy-secret'] && req.headers['x-rapidapi-proxy-secret'] === process.env.RAPIDAPI_PROXY_SECRET);
},
message: {
success: false,
error: 'Too many requests. Please try again later.'
}
});
app.use('/api', limiter);
// ─── Track server start time ─────────────────────────
const serverStartTime = new Date();
let lastSlnSaveTime = 0;
let lastIbjaSaveTime = 0;
// Warm rate caches in the background every 30 seconds
setInterval(async () => {
try {
await getGoldRate();
await getIBJARate();
} catch (err) {
console.warn('⚠️ Background cache warming failed:', err.message);
}
}, 30000);
// ─── Health Check (No API key needed) ────────────────
app.get('/health', (req, res) => {
const uptime = Math.floor((Date.now() - serverStartTime.getTime()) / 1000);
res.json({
success: true,
status: 'running',
uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`,
serverTime: new Date().toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' }),
sources: ['SLN Bullion Chennai', 'IBJA India'],
version: '2.0.0'
});
});
// ─── Helper: Save price to DB ────────────────────────
const DB_SAVE_INTERVAL = 30 * 60 * 1000; // 30 minutes
async function saveSLNToDb(data) {
if (!getIsConnected() || (Date.now() - lastSlnSaveTime) < DB_SAVE_INTERVAL) return;
try {
const supabase = getClient();
const today = data.rateDate || new Date().toISOString().split('T')[0];
// Explicitly round to 2 decimals
const goldRounded = data.gold24k ? Math.round(data.gold24k * 100) / 100 : null;
const silverPerGram = data.silver999 ? Math.round((data.silver999 / 1000) * 100) / 100 : null;
await supabase
.from('price_records')
.upsert({ date: today, gold24k: goldRounded, silver999: silverPerGram, source: 'SLN' });
lastSlnSaveTime = Date.now();
console.log(`πŸ’Ύ SLN DB saved: β‚Ή${goldRounded}/g`);
} catch (e) {
console.warn('⚠️ SLN DB save failed:', e.message);
}
}
async function saveIBJAToDb(data) {
if (!getIsConnected() || (Date.now() - lastIbjaSaveTime) < DB_SAVE_INTERVAL) return;
try {
const supabase = getClient();
const today = data.rateDate || new Date().toISOString().split('T')[0];
// Explicitly round to 2 decimals
const goldRounded = data.gold24k ? Math.round(data.gold24k * 100) / 100 : null;
const rawSilver = data.silver999;
const silverPerGram = rawSilver
? (rawSilver > 500 ? Math.round((rawSilver / 1000) * 100) / 100 : Math.round(rawSilver * 100) / 100)
: null;
await supabase
.from('ibja_price_records')
.upsert({ date: today, gold24k: goldRounded, silver999: silverPerGram, source: 'IBJA' });
lastIbjaSaveTime = Date.now();
console.log(`πŸ’Ύ IBJA DB saved: β‚Ή${goldRounded}/g`);
} catch (e) {
console.warn('⚠️ IBJA DB save failed:', e.message);
}
}
// ─── Main API: Rates based on API key source ─────────
app.get('/api/rates', authMiddleware, async (req, res) => {
try {
const source = req.priceSource || 'SLN';
// If master key or explicitly configured for BOTH sources
if (req.isMasterKey || source === 'BOTH') {
const slnData = await getGoldRate();
const ibjaData = await getIBJARate();
saveSLNToDb(slnData);
saveIBJAToDb(ibjaData);
const slnSilver = slnData.silver999
? Math.round((slnData.silver999 / 1000) * 100) / 100
: null;
const ibjaSilver = ibjaData.silver999
? (ibjaData.silver999 > 500 ? Math.round((ibjaData.silver999 / 1000) * 100) / 100 : ibjaData.silver999)
: null;
res.set('Cache-Control', 'public, max-age=15');
return res.json({
success: true,
rates: {
sln: {
gold24k_1gram: slnData.gold24k,
silver999_1gram: slnSilver,
currency: 'INR',
source: slnData.source,
session: slnData.session,
date: slnData.rateDate,
updatedAt: slnData.fetchedAt
},
ibja: {
gold24k_1gram: ibjaData.gold24k,
silver999_1gram: ibjaSilver,
currency: 'INR',
source: ibjaData.source,
session: ibjaData.session,
date: ibjaData.rateDate,
updatedAt: ibjaData.fetchedAt
}
}
});
}
// Default: Single source rates based on key configuration
let data;
if (source === 'IBJA') {
data = await getIBJARate();
saveIBJAToDb(data);
} else {
data = await getGoldRate();
saveSLNToDb(data);
}
const silverPerGram = data.silver999
? (data.silver999 > 500 ? Math.round((data.silver999 / 1000) * 100) / 100 : data.silver999)
: null;
res.set('Cache-Control', 'public, max-age=15');
res.json({
success: true,
gold24k_1gram: data.gold24k,
silver999_1gram: silverPerGram,
currency: 'INR',
source: data.source,
session: data.session,
date: data.rateDate,
updatedAt: data.fetchedAt
});
} catch (err) {
console.error('❌ API Error:', err.message);
res.status(503).json({
success: false,
error: 'Unable to fetch prices right now. Please try again.',
details: err.message
});
}
});
// ─── SLN Rates (for frontend) ────────────────────────
app.get('/api/sln-rates', dashboardAuthMiddleware, async (req, res) => {
try {
const data = await getGoldRate();
saveSLNToDb(data);
const silverPerGram = data.silver999
? Math.round((data.silver999 / 1000) * 100) / 100
: null;
res.set('Cache-Control', 'public, max-age=15');
res.json({
success: true,
gold24k_1gram: data.gold24k,
silver999_1gram: silverPerGram,
currency: 'INR',
source: data.source,
session: data.session,
date: data.rateDate,
updatedAt: data.fetchedAt
});
} catch (err) {
res.status(503).json({ success: false, error: err.message });
}
});
// ─── IBJA Rates (for frontend) ───────────────────────
app.get('/api/ibja-rates', dashboardAuthMiddleware, async (req, res) => {
try {
const data = await getIBJARate();
saveIBJAToDb(data);
const silverPerGram = data.silver999
? (data.silver999 > 500 ? Math.round((data.silver999 / 1000) * 100) / 100 : data.silver999)
: null;
res.set('Cache-Control', 'public, max-age=15');
res.json({
success: true,
gold24k_1gram: data.gold24k,
silver999_1gram: silverPerGram,
currency: 'INR',
source: data.source,
session: data.session,
date: data.rateDate,
updatedAt: data.fetchedAt
});
} catch (err) {
res.status(503).json({
success: false,
error: 'IBJA source unavailable',
details: err.message
});
}
});
// Memory cache for historical records
const slnHistoryCache = {};
const ibjaHistoryCache = {};
const HISTORY_CACHE_DURATION = 60 * 60 * 1000; // 1 hour
// ─── Historical Prices (SLN) ─────────────────────────
const RANGE_DAYS = { '1m': 30, '3m': 90, '6m': 180, '1y': 365, '3y': 1095 };
app.get('/api/history', dashboardAuthMiddleware, async (req, res) => {
if (!getIsConnected()) {
return res.status(503).json({
success: false,
error: 'History not available β€” database not configured'
});
}
try {
const range = (req.query.range || '3m').toLowerCase();
const now = Date.now();
// Check memory cache
if (slnHistoryCache[range] && (now - slnHistoryCache[range].time) < HISTORY_CACHE_DURATION) {
return res.json(slnHistoryCache[range].data);
}
const days = RANGE_DAYS[range] || 90;
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const startStr = startDate.toISOString().split('T')[0];
const supabase = getClient();
const { data: records } = await supabase
.from('price_records')
.select('date, gold24k, silver999')
.gte('date', startStr)
.order('date', { ascending: true });
// Calculate performance
let performance = null;
if (records.length >= 2) {
const first = records[0].gold24k;
const last = records[records.length - 1].gold24k;
const change = last - first;
const percent = ((change / first) * 100).toFixed(2);
performance = {
startPrice: first,
endPrice: last,
change: Math.round(change * 100) / 100,
percentChange: parseFloat(percent),
direction: change >= 0 ? 'UP' : 'DOWN'
};
}
const responseJson = {
success: true,
source: 'SLN Bullion Chennai',
range: range,
totalDays: records.length,
performance: performance,
data: records
};
// Save to memory cache
slnHistoryCache[range] = {
time: now,
data: responseJson
};
res.json(responseJson);
} catch (err) {
console.error('❌ History Error:', err.message);
res.status(500).json({ success: false, error: 'Failed to fetch history' });
}
});
// ─── Historical Prices (IBJA) ────────────────────────
app.get('/api/ibja-history', dashboardAuthMiddleware, async (req, res) => {
if (!getIsConnected()) {
return res.status(503).json({
success: false,
error: 'History not available β€” database not configured'
});
}
try {
const range = (req.query.range || '3m').toLowerCase();
const now = Date.now();
// Check memory cache
if (ibjaHistoryCache[range] && (now - ibjaHistoryCache[range].time) < HISTORY_CACHE_DURATION) {
return res.json(ibjaHistoryCache[range].data);
}
const days = RANGE_DAYS[range] || 90;
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const startStr = startDate.toISOString().split('T')[0];
const supabase = getClient();
const { data: records } = await supabase
.from('ibja_price_records')
.select('date, gold24k, silver999')
.gte('date', startStr)
.order('date', { ascending: true });
let performance = null;
if (records && records.length >= 2) {
const first = records[0].gold24k;
const last = records[records.length - 1].gold24k;
const change = last - first;
const percent = ((change / first) * 100).toFixed(2);
performance = {
startPrice: first,
endPrice: last,
change: Math.round(change * 100) / 100,
percentChange: parseFloat(percent),
direction: change >= 0 ? 'UP' : 'DOWN'
};
}
const responseJson = {
success: true,
source: 'IBJA India',
range: range,
totalDays: records ? records.length : 0,
performance: performance,
data: records || []
};
// Save to memory cache
ibjaHistoryCache[range] = {
time: now,
data: responseJson
};
res.json(responseJson);
} catch (err) {
console.error('❌ IBJA History Error:', err.message);
res.status(500).json({ success: false, error: 'Failed to fetch IBJA history' });
}
});
// ─── SEO Landing Pages Engine ────────────────────────
const serveSEOPage = (req, res, seoData) => {
try {
const htmlPath = path.join(__dirname, '../public/index.html');
if (!fs.existsSync(htmlPath)) {
return res.status(404).send('Dashboard template not found');
}
let html = fs.readFileSync(htmlPath, 'utf8');
// Replace Meta Title
if (seoData.title) {
html = html.replace(/<title>[^<]*<\/title>/, `<title>${seoData.title}</title>`);
html = html.replace(/<meta property="og:title" content="[^"]*"/, `<meta property="og:title" content="${seoData.title}"`);
html = html.replace(/<meta name="twitter:title" content="[^"]*"/, `<meta name="twitter:title" content="${seoData.title}"`);
}
// Replace Meta Description
if (seoData.description) {
html = html.replace(/<meta name="description" content="[^"]*"/, `<meta name="description" content="${seoData.description}"`);
html = html.replace(/<meta property="og:description" content="[^"]*"/, `<meta property="og:description" content="${seoData.description}"`);
html = html.replace(/<meta name="twitter:description" content="[^"]*"/, `<meta name="twitter:description" content="${seoData.description}"`);
}
// Replace H1 tag
if (seoData.h1) {
html = html.replace(/<h1 class="hero-title">[\s\S]*?<\/h1>/, `<h1 class="hero-title" style="font-size: 38px; line-height: 1.3; margin-bottom: 16px;">${seoData.h1}</h1>`);
}
res.send(html);
} catch (err) {
console.error('❌ SEO Render Error:', err.message);
res.status(500).send('Error rendering page');
}
};
const seoRoutes = [
{
path: '/gold-rate-api',
title: 'Gold Rate API India | Real-time Gold Price Feed',
description: 'Free and premium Gold Rate API for India. Get live 22K and 24K gold prices and developer documentation.',
h1: 'Gold Rate API India'
},
{
path: '/live-gold-api',
title: 'Live Gold Price API | Real-Time Bullion Prices',
description: 'High-speed live gold price API for developers. Integrate spot gold rates with under 10ms response times.',
h1: 'Live Gold Price API'
},
{
path: '/chennai-gold-rate-api',
title: 'Chennai Gold Rate API | Live SLN Bullion Price Feed',
description: 'Get real-time retail and wholesale gold spot rates in Chennai directly from SLN Bullion Chennai.',
h1: 'Chennai Gold Rate API'
},
{
path: '/india-gold-api',
title: 'India Gold API | Official IBJA Bullion Price Feed',
description: 'Access official gold and silver price feeds for India based on IBJA national bullion standards.',
h1: 'India Gold API & Silver Price API'
},
{
path: '/gold-price-api',
title: 'Gold Price API | Real-Time Gold Price JSON Feed',
description: 'Reliable B2B Gold Price API for jewellery stores, finance apps, and ERP systems.',
h1: 'Gold Price API for Developers'
},
{
path: '/silver-price-api',
title: 'Silver Price API | Live Silver Spot Rates',
description: 'Get live Silver 999 price API feeds for India. Under 10ms cached proxy responses.',
h1: 'Silver Price API'
},
{
path: '/historical-gold-api',
title: 'Historical Gold Price API India | Historical Bullion Data',
description: 'Retrieve historical gold and silver price records for Chennai and India. Includes 1-month to 3-year history.',
h1: 'Historical Gold Price API India'
},
{
path: '/rest-api',
title: 'Gold Rate JSON API | Gold Price REST API',
description: 'Easy integration using our RESTful JSON API. Complete with code examples in Node.js, Python, and cURL.',
h1: 'Gold Rate JSON API'
},
{
path: '/json-api',
title: 'Gold Rate JSON API | Gold Price REST API',
description: 'Easy integration using our RESTful JSON API. Complete with code examples in Node.js, Python, and cURL.',
h1: 'Gold Rate JSON API'
},
{
path: '/docs',
title: 'GoldScrape API Documentation | Developer Guides',
description: 'Developer guides, JSON response templates, and implementation code snippets in Node.js, Python, and PHP.',
h1: 'GoldScrape API Documentation'
},
{
path: '/pricing',
title: 'GoldScrape API Pricing | Gold API Subscription Plans',
description: 'Choose between Monthly License (β‚Ή599/mo) and Yearly License (β‚Ή3,999/yr) plans. Start a 7-day free trial.',
h1: 'GoldScrape API Pricing'
}
];
seoRoutes.forEach(route => {
app.get(route.path, (req, res) => {
serveSEOPage(req, res, route);
});
});
// Dynamic City Specific Landing Pages
app.get('/city/:cityName', (req, res) => {
const citySlug = req.params.cityName;
let cityName = citySlug.split('-')[0];
cityName = cityName.charAt(0).toUpperCase() + cityName.slice(1);
const seoData = {
title: `${cityName} Gold Rate API | Live Gold Price in ${cityName}`,
description: `Get real-time gold and silver spot prices in ${cityName}, Tamil Nadu. Live updates, fallbacks, and developer guides.`,
h1: `${cityName} Gold Rate API`
};
serveSEOPage(req, res, seoData);
});
// ─── Google Ads Conversion Page ──────────────────────
app.get('/thank-you', (req, res) => {
const plan = req.query.plan || 'general';
let waText = 'Hi, I want to inquire about GoldScrape API.';
if (plan === 'monthly') {
waText = 'Hi, I want to subscribe to the Monthly Plan (β‚Ή599/mo) for GoldScrape API.';
} else if (plan === 'yearly') {
waText = 'Hi, I want to subscribe to the Yearly Plan (β‚Ή3,999/yr) for GoldScrape API.';
} else if (plan === 'enterprise') {
waText = 'Hi, I want to inquire about the Enterprise Plan for GoldScrape API.';
}
const waUrl = `https://wa.me/919360345770?text=${encodeURIComponent(waText)}`;
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thank You - GoldScrape API</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&display=swap" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
font-family: 'Outfit', sans-serif;
background: #0B0F19;
color: white;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
}
.container {
max-width: 450px;
padding: 40px 24px;
background: #111827;
border: 1px solid #1f2937;
border-radius: 24px;
box-shadow: 0 20px 40px rgba(0,0,0,0.4);
}
.icon {
font-size: 48px;
margin-bottom: 20px;
}
h1 {
font-size: 28px;
margin-bottom: 12px;
background: linear-gradient(135deg, #F59E0B, #6366F1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
p {
color: #9CA3AF;
font-size: 16px;
margin-bottom: 24px;
line-height: 1.6;
}
.spinner {
border: 4px solid rgba(255,255,255,0.1);
border-top: 4px solid #6366F1;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<script>
setTimeout(() => {
window.location.href = "${waUrl}";
}, 1500);
</script>
</head>
<body>
<div class="container">
<div class="icon">βœ…</div>
<h1>Thank You!</h1>
<p>Your request has been received. Redirecting you to WhatsApp to complete your activation...</p>
<div class="spinner"></div>
</div>
</body>
</html>
`);
});
// ─── 404 Handler ─────────────────────────────────────
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'Endpoint not found',
availableEndpoints: {
rates: 'GET /api/rates?apikey=YOUR_KEY (source based on key)',
slnRates: 'GET /api/sln-rates?apikey=YOUR_KEY',
ibjaRates: 'GET /api/ibja-rates?apikey=YOUR_KEY',
history: 'GET /api/history?range=1m|3m|6m|1y|3y&apikey=YOUR_KEY',
ibjaHistory: 'GET /api/ibja-history?range=1m|3m|6m|1y|3y&apikey=YOUR_KEY',
health: 'GET /health'
}
});
});
// ─── Start Server ────────────────────────────────────
app.listen(PORT, () => {
console.log('');
console.log('╔══════════════════════════════════════════════════╗');
console.log('β•‘ πŸͺ™ GoldRate Engine v2.0.0 πŸͺ™ β•‘');
console.log('β•‘ SLN Bullion Chennai + IBJA India Live API β•‘');
console.log('╠══════════════════════════════════════════════════╣');
console.log(`β•‘ Server: http://localhost:${PORT} β•‘`);
console.log(`β•‘ SLN Rates: /api/sln-rates β•‘`);
console.log(`β•‘ IBJA Rates: /api/ibja-rates β•‘`);
console.log(`β•‘ SLN History: /api/history β•‘`);
console.log(`β•‘ IBJA History: /api/ibja-history β•‘`);
console.log(`β•‘ Health: /health β•‘`);
console.log('╠══════════════════════════════════════════════════╣');
console.log(`β•‘ Master Key: ${process.env.API_KEY ? 'βœ… Loaded' : '❌ Missing!'} β•‘`);
console.log(`β•‘ Sources: SLN Bullion + IBJA β•‘`);
console.log(`β•‘ Key Expiry: 30 days (Supabase api_keys table) β•‘`);
console.log(`β•‘ Telegram: ${process.env.TELEGRAM_BOT_TOKEN ? 'βœ… Bot Active' : '❌ Not configured'} β•‘`);
console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•');
console.log('');
console.log('πŸ“± API Key Management:');
console.log(' πŸ€– Telegram: /create_sln or /create_ibja');
console.log(' πŸ’» CLI: node scripts/manage-keys.js create --name "Shop" --source SLN');
console.log('');
// Start Telegram Bot
initTelegramBot();
// Start background price saver to guarantee historical records without client traffic
startBackgroundSaver();
});
// ─── Background Price Saver (no traffic required) ─────
function startBackgroundSaver() {
console.log('⏰ Starting background price saver (interval: 1 hour)...');
const saveRates = async () => {
try {
const slnData = await getGoldRate();
const slnToday = slnData.rateDate || new Date().toISOString().split('T')[0];
const slnGoldRounded = slnData.gold24k ? Math.round(slnData.gold24k * 100) / 100 : null;
const slnSilverPerGram = slnData.silver999 ? Math.round((slnData.silver999 / 1000) * 100) / 100 : null;
const supabase = getClient();
if (getIsConnected()) {
await supabase
.from('price_records')
.upsert({ date: slnToday, gold24k: slnGoldRounded, silver999: slnSilverPerGram, source: 'SLN' });
console.log(`⏰ Background Auto-Save: SLN Bullion saved (β‚Ή${slnGoldRounded}/g)`);
try {
const ibjaData = await getIBJARate();
const ibjaToday = ibjaData.rateDate || new Date().toISOString().split('T')[0];
const ibjaGoldRounded = ibjaData.gold24k ? Math.round(ibjaData.gold24k * 100) / 100 : null;
const rawSilver = ibjaData.silver999;
const ibjaSilverPerGram = rawSilver
? (rawSilver > 500 ? Math.round((rawSilver / 1000) * 100) / 100 : Math.round(rawSilver * 100) / 100)
: null;
await supabase
.from('ibja_price_records')
.upsert({ date: ibjaToday, gold24k: ibjaGoldRounded, silver999: ibjaSilverPerGram, source: 'IBJA' });
console.log(`⏰ Background Auto-Save: IBJA India saved (β‚Ή${ibjaGoldRounded}/g)`);
} catch (e) {
console.warn('⚠️ Background IBJA save failed:', e.message);
}
}
} catch (err) {
console.error('⚠️ Background price saver failed:', err.message);
}
};
// Run initial save after 10 seconds
setTimeout(saveRates, 10000);
// Repeat every 1 hour
const ONE_HOUR = 60 * 60 * 1000;
setInterval(saveRates, ONE_HOUR);
}
module.exports = app;