Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
4.0 KiB
JavaScript
121 lines
4.0 KiB
JavaScript
import { Router } from 'express'
|
|
import { getDb } from '../db.js'
|
|
import { fetchDashboardInfo, testConnection } from '../adapters/billmanager/index.js'
|
|
import { runBillmanagerAccountSync } from '../sync-account-job.js'
|
|
import { billmanagerAccountRowForSync } from '../utils/billmanager-context.js'
|
|
|
|
const router = Router()
|
|
|
|
router.post('/test-connection', async (req, res) => {
|
|
try {
|
|
const { apiBaseUrl, apiCredentials } = req.body || {}
|
|
if (!apiBaseUrl?.trim() || !apiCredentials?.trim()) {
|
|
return res.status(400).json({ ok: false, error: 'Укажите URL и учётные данные' })
|
|
}
|
|
const result = await testConnection(apiBaseUrl.trim(), apiCredentials.trim())
|
|
res.json(result)
|
|
} catch (err) {
|
|
res.status(500).json({ ok: false, error: err.message || 'Ошибка проверки' })
|
|
}
|
|
})
|
|
|
|
router.get('/status', (req, res) => {
|
|
try {
|
|
const db = getDb()
|
|
const rows = db.prepare(`
|
|
SELECT id, accountId, startedAt, finishedAt, status, vpsCount, paymentsCount, error, summary
|
|
FROM sync_log ORDER BY startedAt DESC LIMIT 50
|
|
`).all()
|
|
res.json(
|
|
rows.map((row) => {
|
|
let summaryParsed = null
|
|
if (row.summary) {
|
|
try {
|
|
summaryParsed = JSON.parse(row.summary)
|
|
} catch {
|
|
summaryParsed = null
|
|
}
|
|
}
|
|
return { ...row, summary: summaryParsed }
|
|
}),
|
|
)
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message })
|
|
}
|
|
})
|
|
|
|
router.get('/:accountId/balance', async (req, res) => {
|
|
try {
|
|
const db = getDb()
|
|
const { accountId } = req.params
|
|
const row = db.prepare('SELECT * FROM provider_accounts WHERE id = ?').get(accountId)
|
|
if (!row) {
|
|
return res.status(404).json({ error: 'Account not found' })
|
|
}
|
|
const provider = row.providerId
|
|
? db.prepare('SELECT * FROM providers WHERE id = ?').get(row.providerId)
|
|
: null
|
|
const syncRow = billmanagerAccountRowForSync(row, provider)
|
|
if (!syncRow) {
|
|
return res.status(400).json({
|
|
error:
|
|
'Укажите в настройках хостера тип API BILLmanager и URL; в аккаунте — логин и пароль API',
|
|
})
|
|
}
|
|
const info = await fetchDashboardInfo(syncRow.apiBaseUrl, syncRow.apiCredentials.trim(), {
|
|
fallbackCurrency: row.currency,
|
|
})
|
|
db.run(
|
|
'UPDATE provider_accounts SET balance_api=?, balance_currency=?, balance_updated_at=?, enoughmoneyto=? WHERE id=?',
|
|
info.balance,
|
|
info.currency || 'RUB',
|
|
new Date().toISOString(),
|
|
info.enoughmoneyto || '',
|
|
accountId,
|
|
)
|
|
res.json({ ok: true, balance: info })
|
|
} catch (err) {
|
|
console.error('Balance fetch error:', err)
|
|
res.status(500).json({ ok: false, error: err.message || 'Failed to fetch balance' })
|
|
}
|
|
})
|
|
|
|
router.post('/:accountId', async (req, res) => {
|
|
try {
|
|
const db = getDb()
|
|
const { accountId } = req.params
|
|
const { onlyTariffs = false } = req.body || {}
|
|
const row = db.prepare('SELECT * FROM provider_accounts WHERE id = ?').get(accountId)
|
|
if (!row) {
|
|
return res.status(404).json({ error: 'Account not found' })
|
|
}
|
|
const provider = row.providerId
|
|
? db.prepare('SELECT * FROM providers WHERE id = ?').get(row.providerId)
|
|
: null
|
|
const syncRow = billmanagerAccountRowForSync(row, provider)
|
|
if (!syncRow) {
|
|
return res.status(400).json({
|
|
error:
|
|
'Укажите в настройках хостера тип API BILLmanager и URL; в аккаунте — логин и пароль API',
|
|
})
|
|
}
|
|
|
|
const opts = onlyTariffs ? { skipVpsPayments: true } : { skipTariffs: true }
|
|
const result = await runBillmanagerAccountSync(syncRow, opts)
|
|
|
|
res.json({
|
|
ok: true,
|
|
synced: {
|
|
vpsCount: result.vpsCount,
|
|
paymentsCount: result.paymentsCount,
|
|
tariffsCount: result.tariffsCount ?? 0,
|
|
},
|
|
})
|
|
} catch (err) {
|
|
console.error('Sync error:', err)
|
|
res.status(500).json({ ok: false, error: err.message || 'Sync failed' })
|
|
}
|
|
})
|
|
|
|
export default router
|