fix(rates): серверный прокси /api/rates-proxy и бейдж «Курсы не загружены»

- Добавлен /api/rates-proxy для загрузки курсов без CORS
- Фронтенд загружает курсы через прокси, а не напрямую
- convertWithProviderRate возвращает source=no-rates при отсутствии ratesData
- ConvertedAmount показывает «Курсы не загружены» (жёлтый) вместо ложного «Глобальный курс»

Made-with: Cursor
This commit is contained in:
Denozordec
2026-03-23 21:45:02 +07:00
parent a97ec8c121
commit 92a986c858
5 changed files with 41 additions and 4 deletions
+2
View File
@@ -18,6 +18,7 @@ import settingsRouter from './routes/settings.js'
import syncRouter from './routes/sync.js'
import projectsRouter from './routes/projects.js'
import backupRouter from './routes/backup.js'
import ratesProxyRouter from './routes/rates-proxy.js'
const app = express()
const PORT = process.env.PORT || 3001
@@ -39,6 +40,7 @@ app.use(express.json({ limit: '50mb' }))
app.use('/api/sync', syncRouter)
app.use('/api/projects', projectsRouter)
app.use('/api/backup', backupRouter)
app.use('/api/rates-proxy', ratesProxyRouter)
if (existsSync(distPath)) {
app.use(express.static(distPath))
+22
View File
@@ -0,0 +1,22 @@
import { Router } from 'express'
const router = Router()
router.get('/', async (req, res) => {
const url = req.query.url
if (!url) {
return res.status(400).json({ error: 'Missing url parameter' })
}
try {
const response = await fetch(url, { signal: AbortSignal.timeout(10_000) })
if (!response.ok) {
return res.status(502).json({ error: `Upstream returned ${response.status}` })
}
const data = await response.json()
res.json(data)
} catch (err) {
res.status(502).json({ error: err.message || 'Failed to fetch rates' })
}
})
export default router
+2 -1
View File
@@ -56,7 +56,8 @@ function App() {
if (!settings?.ratesUrl) {
return
}
fetch(settings.ratesUrl)
const proxyUrl = `/api/rates-proxy?url=${encodeURIComponent(settings.ratesUrl)}`
fetch(proxyUrl)
.then((response) => {
if (!response.ok) {
throw new Error('Не удалось получить курсы валют')
+7 -1
View File
@@ -7,12 +7,16 @@ function sourceMeta(source) {
if (source === 'global') {
return { label: 'Глобальный курс', className: 'bg-blue-lt text-blue' }
}
if (source === 'no-rates') {
return { label: 'Курсы не загружены', className: 'bg-yellow-lt text-yellow' }
}
return { label: 'Без конвертации', className: 'bg-secondary-lt text-secondary' }
}
export function ConvertedAmount({ amount, currency, provider, settings, ratesData }) {
const result = convertWithProviderRate(amount, currency, provider, settings, ratesData)
const meta = sourceMeta(result.source)
const showOriginal = result.source !== 'native' && result.source !== 'no-rates'
return (
<div>
@@ -20,7 +24,9 @@ export function ConvertedAmount({ amount, currency, provider, settings, ratesDat
<span>{formatCurrency(result.value, result.currency)}</span>
<span className={`badge ${meta.className}`}>{meta.label}</span>
</div>
<div className="text-secondary small">{formatCurrency(amount, currency)}</div>
{showOriginal ? (
<div className="text-secondary small">{formatCurrency(amount, currency)}</div>
) : null}
</div>
)
}
+8 -2
View File
@@ -337,10 +337,16 @@ export function convertWithProviderRate(amount, currency, provider, appSettings,
return { value: safeAmount * eurRate, currency: appBase, source: 'provider' }
}
if (!ratesData || !ratesData.rates || !ratesData.base) {
return { value: safeAmount, currency: fromCurrency, source: 'no-rates' }
}
const converted = convertCurrency(safeAmount, fromCurrency, appBase, ratesData)
const didConvert = Math.abs(converted - safeAmount) > 0.0001 || fromCurrency === appBase
return {
value: convertCurrency(safeAmount, fromCurrency, appBase, ratesData),
value: converted,
currency: appBase,
source: 'global',
source: didConvert ? 'global' : 'no-rates',
}
}