Add scheduled tariff synchronization and bulk update functionality
- Introduced `runScheduledSyncTariffs` function to handle tariff synchronization independently from VPS and payment sync. - Updated `syncFromBillmanager` to accept options for skipping tariff and VPS payment syncs. - Added new database columns for `syncTariffsIntervalMinutes` and `customFields` in settings. - Enhanced settings page to configure tariff sync interval. - Implemented bulk update functionality for VPS status and deletion in the VPS management page. - Updated various components to support new features, including sync log display and improved payment handling.
This commit is contained in:
@@ -9,28 +9,33 @@ import { mapVdsToVps, mapPaymentToPayment } from './mappers.js'
|
||||
* Sync BILLmanager data into vps-tracker DB
|
||||
* @param {object} account - provider_account with apiBaseUrl, apiCredentials
|
||||
* @param {object} db - getDb() wrapper
|
||||
* @param {object} [opts] - { paymentDaysBack }
|
||||
* @param {object} [opts] - { paymentDaysBack, skipTariffs, skipVpsPayments }
|
||||
* @returns {{ vpsCount: number, paymentsCount: number, tariffsCount: number, balance?: object }}
|
||||
*/
|
||||
export async function syncFromBillmanager(account, db, _opts = {}) {
|
||||
export async function syncFromBillmanager(account, db, opts = {}) {
|
||||
const { skipTariffs = false, skipVpsPayments = false } = opts
|
||||
const { apiBaseUrl, apiCredentials, providerId, id: accountId } = account
|
||||
if (!apiBaseUrl?.trim() || !apiCredentials?.trim()) {
|
||||
throw new Error('API URL and credentials are required')
|
||||
}
|
||||
const authinfo = apiCredentials.trim()
|
||||
|
||||
const fetchVpsPayments = !skipVpsPayments
|
||||
const fetchTariffs = !skipTariffs
|
||||
|
||||
const [vdsItems, paymentItems, dashboardInfo, tariffResult] = await Promise.all([
|
||||
fetchVds(apiBaseUrl, authinfo),
|
||||
fetchPayments(apiBaseUrl, authinfo, {}),
|
||||
fetchDashboardInfo(apiBaseUrl, authinfo, { fallbackCurrency: account.currency }).catch(() => null),
|
||||
fetchVdsOrderPricelistAllDatacenters(apiBaseUrl, authinfo).catch((err) => {
|
||||
fetchVpsPayments ? fetchVds(apiBaseUrl, authinfo) : [],
|
||||
fetchVpsPayments ? fetchPayments(apiBaseUrl, authinfo, {}) : [],
|
||||
fetchVpsPayments ? fetchDashboardInfo(apiBaseUrl, authinfo, { fallbackCurrency: account.currency }).catch(() => null) : null,
|
||||
fetchTariffs ? fetchVdsOrderPricelistAllDatacenters(apiBaseUrl, authinfo).catch((err) => {
|
||||
console.warn('fetchVdsOrderPricelistAllDatacenters failed:', err.message)
|
||||
return { tariffItems: [], slist: {} }
|
||||
}),
|
||||
}) : { tariffItems: [], slist: {} },
|
||||
])
|
||||
const { tariffItems = [], slist = {} } = tariffResult || {}
|
||||
|
||||
let vpsCount = 0
|
||||
if (fetchVpsPayments) {
|
||||
const vpsInsertSql = `INSERT INTO vps (id, ip, ipv6, additionalIps, dns, providerId, providerAccountId, country, city, datacenter, os, vcpu, ramGb, diskGb, diskType, virtualization, bandwidthTb, sshPort, rootUser, purpose, environment, project, monitoringEnabled, backupEnabled, status, tariffType, currency, dailyRate, monthlyRate, createdAt, paidUntil, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
const vpsUpdateSql = `UPDATE vps SET ip=?, ipv6=?, additionalIps=?, dns=?, country=?, city=?, datacenter=?, os=?, status=?, tariffType=?, currency=?, dailyRate=?, monthlyRate=?, paidUntil=?, notes=?
|
||||
@@ -133,8 +138,10 @@ export async function syncFromBillmanager(account, db, _opts = {}) {
|
||||
}
|
||||
vpsCount++
|
||||
}
|
||||
}
|
||||
|
||||
let paymentsCount = 0
|
||||
if (fetchVpsPayments) {
|
||||
const existingPayments = new Set(
|
||||
db.prepare('SELECT note FROM payments WHERE providerAccountId = ?').all(accountId).map((r) => r.note),
|
||||
)
|
||||
@@ -151,8 +158,9 @@ export async function syncFromBillmanager(account, db, _opts = {}) {
|
||||
existingPayments.add(note)
|
||||
paymentsCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (dashboardInfo) {
|
||||
if (fetchVpsPayments && dashboardInfo) {
|
||||
db.run(
|
||||
'UPDATE provider_accounts SET balance_api=?, balance_currency=?, balance_updated_at=?, enoughmoneyto=? WHERE id=?',
|
||||
dashboardInfo.balance,
|
||||
@@ -164,6 +172,7 @@ export async function syncFromBillmanager(account, db, _opts = {}) {
|
||||
}
|
||||
|
||||
let tariffsCount = 0
|
||||
if (fetchTariffs) {
|
||||
const syncedAt = new Date().toISOString()
|
||||
db.run('DELETE FROM active_tariffs WHERE providerAccountId = ?', accountId)
|
||||
const tariffInsertSql = `INSERT INTO active_tariffs (id, providerAccountId, providerId, externalId, datacenterKey, datacenterName, name, desc, vcpu, ramGb, diskGb, diskType, virtualization, channel, location, country, cpuModel, orderAvailable, price, syncedAt)
|
||||
@@ -209,6 +218,7 @@ export async function syncFromBillmanager(account, db, _opts = {}) {
|
||||
syncedAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return { vpsCount, paymentsCount, tariffsCount, balance: dashboardInfo }
|
||||
}
|
||||
|
||||
@@ -98,6 +98,26 @@ export const MIGRATIONS = [
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'settings_customFields',
|
||||
run(db) {
|
||||
try {
|
||||
db.exec('ALTER TABLE settings ADD COLUMN customFields TEXT')
|
||||
} catch (e) {
|
||||
if (!e.message?.includes('duplicate column')) throw e
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'settings_syncTariffsInterval',
|
||||
run(db) {
|
||||
try {
|
||||
db.exec('ALTER TABLE settings ADD COLUMN syncTariffsIntervalMinutes INTEGER')
|
||||
} catch (e) {
|
||||
if (!e.message?.includes('duplicate column')) throw e
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'active_tariffs_country_datacenter',
|
||||
run(db) {
|
||||
|
||||
+3
-1
@@ -100,7 +100,9 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
autoConvert INTEGER,
|
||||
ratesUpdatedAt TEXT,
|
||||
syncEnabled INTEGER,
|
||||
syncIntervalMinutes INTEGER
|
||||
syncIntervalMinutes INTEGER,
|
||||
syncTariffsIntervalMinutes INTEGER,
|
||||
customFields TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sync_log (
|
||||
|
||||
@@ -6,10 +6,19 @@ const router = Router()
|
||||
|
||||
export function rowToSettings(row) {
|
||||
if (!row) return null
|
||||
let customFields = []
|
||||
if (row.customFields) {
|
||||
try {
|
||||
customFields = JSON.parse(row.customFields)
|
||||
} catch {
|
||||
customFields = []
|
||||
}
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
autoConvert: Boolean(row.autoConvert),
|
||||
syncEnabled: Boolean(row.syncEnabled),
|
||||
customFields: Array.isArray(customFields) ? customFields : [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +32,13 @@ router.get('/', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
function serializeCustomFields(val) {
|
||||
if (val == null) return null
|
||||
if (Array.isArray(val)) return JSON.stringify(val)
|
||||
if (typeof val === 'string') return val || null
|
||||
return null
|
||||
}
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
@@ -31,10 +47,12 @@ router.put('/:id', (req, res) => {
|
||||
const existing = db.prepare('SELECT * FROM settings WHERE id = ?').get(id)
|
||||
const syncEnabled = r.syncEnabled !== undefined ? (r.syncEnabled ? 1 : 0) : (existing?.syncEnabled ? 1 : 0)
|
||||
const syncIntervalMinutes = r.syncIntervalMinutes !== undefined ? Math.max(15, Number(r.syncIntervalMinutes) || 60) : (existing?.syncIntervalMinutes ?? 60)
|
||||
const syncTariffsIntervalMinutes = r.syncTariffsIntervalMinutes !== undefined ? Math.max(60, Number(r.syncTariffsIntervalMinutes) || 1440) : (existing?.syncTariffsIntervalMinutes ?? 1440)
|
||||
const customFields = serializeCustomFields(r.customFields ?? existing?.customFields)
|
||||
if (existing) {
|
||||
db.prepare(`
|
||||
UPDATE settings SET
|
||||
baseCurrency = ?, ratesUrl = ?, autoConvert = ?, ratesUpdatedAt = ?, syncEnabled = ?, syncIntervalMinutes = ?
|
||||
baseCurrency = ?, ratesUrl = ?, autoConvert = ?, ratesUpdatedAt = ?, syncEnabled = ?, syncIntervalMinutes = ?, syncTariffsIntervalMinutes = ?, customFields = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
r.baseCurrency ?? existing.baseCurrency ?? 'RUB',
|
||||
@@ -43,12 +61,14 @@ router.put('/:id', (req, res) => {
|
||||
r.ratesUpdatedAt ?? existing.ratesUpdatedAt ?? '',
|
||||
syncEnabled,
|
||||
syncIntervalMinutes,
|
||||
syncTariffsIntervalMinutes,
|
||||
customFields,
|
||||
id,
|
||||
)
|
||||
} else {
|
||||
db.prepare(`
|
||||
INSERT INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes, syncTariffsIntervalMinutes, customFields)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.baseCurrency ?? 'RUB',
|
||||
@@ -57,6 +77,8 @@ router.put('/:id', (req, res) => {
|
||||
r.ratesUpdatedAt ?? '',
|
||||
syncEnabled,
|
||||
syncIntervalMinutes,
|
||||
syncTariffsIntervalMinutes,
|
||||
customFields,
|
||||
)
|
||||
}
|
||||
startScheduler()
|
||||
@@ -74,9 +96,11 @@ router.post('/', (req, res) => {
|
||||
const id = r.id ?? 'settings-main'
|
||||
const syncEnabled = r.syncEnabled ? 1 : 0
|
||||
const syncIntervalMinutes = Math.max(15, Number(r.syncIntervalMinutes) || 60)
|
||||
const syncTariffsIntervalMinutes = Math.max(60, Number(r.syncTariffsIntervalMinutes) || 1440)
|
||||
const customFields = serializeCustomFields(r.customFields)
|
||||
db.prepare(`
|
||||
INSERT INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes, syncTariffsIntervalMinutes, customFields)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.baseCurrency ?? 'RUB',
|
||||
@@ -85,6 +109,8 @@ router.post('/', (req, res) => {
|
||||
r.ratesUpdatedAt ?? '',
|
||||
syncEnabled,
|
||||
syncIntervalMinutes,
|
||||
syncTariffsIntervalMinutes,
|
||||
customFields,
|
||||
)
|
||||
startScheduler()
|
||||
const row = db.prepare('SELECT * FROM settings WHERE id = ?').get(id)
|
||||
|
||||
@@ -21,7 +21,7 @@ router.get('/status', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db.prepare(`
|
||||
SELECT accountId, startedAt, finishedAt, status, vpsCount, paymentsCount, error
|
||||
SELECT id, accountId, startedAt, finishedAt, status, vpsCount, paymentsCount, error
|
||||
FROM sync_log ORDER BY startedAt DESC LIMIT 50
|
||||
`).all()
|
||||
res.json(rows)
|
||||
|
||||
@@ -194,4 +194,37 @@ router.delete('/:id', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.patch('/bulk', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { ids = [], action, value } = req.body
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({ error: 'ids must be a non-empty array' })
|
||||
}
|
||||
if (action === 'status' && value) {
|
||||
const validStatus = ['active', 'paused', 'archived']
|
||||
if (!validStatus.includes(value)) {
|
||||
return res.status(400).json({ error: 'value must be active, paused, or archived' })
|
||||
}
|
||||
const stmt = db.prepare('UPDATE vps SET status = ? WHERE id = ?')
|
||||
for (const id of ids) {
|
||||
stmt.run(value, id)
|
||||
}
|
||||
return res.json({ updated: ids.length, status: value })
|
||||
}
|
||||
if (action === 'delete') {
|
||||
const stmt = db.prepare('DELETE FROM vps WHERE id = ?')
|
||||
let deleted = 0
|
||||
for (const id of ids) {
|
||||
const result = stmt.run(id)
|
||||
if (result.changes > 0) deleted++
|
||||
}
|
||||
return res.json({ deleted })
|
||||
}
|
||||
return res.status(400).json({ error: 'action must be status or delete' })
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getDb } from './db.js'
|
||||
import { syncFromBillmanager } from './adapters/billmanager/index.js'
|
||||
|
||||
let syncIntervalId = null
|
||||
let syncTariffsIntervalId = null
|
||||
|
||||
export function runScheduledSync() {
|
||||
try {
|
||||
@@ -13,8 +14,8 @@ export function runScheduledSync() {
|
||||
WHERE apiType = 'billmanager' AND apiBaseUrl IS NOT NULL AND apiBaseUrl != '' AND apiCredentials IS NOT NULL AND apiCredentials != ''
|
||||
`).all()
|
||||
for (const account of accounts) {
|
||||
syncFromBillmanager(account, db).catch((err) => {
|
||||
console.warn(`Sync failed for account ${account.id}:`, err.message)
|
||||
syncFromBillmanager(account, db, { skipTariffs: true }).catch((err) => {
|
||||
console.warn(`Sync VPS/payments failed for account ${account.id}:`, err.message)
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -22,16 +23,39 @@ export function runScheduledSync() {
|
||||
}
|
||||
}
|
||||
|
||||
export function runScheduledSyncTariffs() {
|
||||
try {
|
||||
const db = getDb()
|
||||
const settings = db.prepare('SELECT * FROM settings WHERE id = ?').get('settings-main')
|
||||
if (!settings?.syncEnabled) return
|
||||
const accounts = db.prepare(`
|
||||
SELECT * FROM provider_accounts
|
||||
WHERE apiType = 'billmanager' AND apiBaseUrl IS NOT NULL AND apiBaseUrl != '' AND apiCredentials IS NOT NULL AND apiCredentials != ''
|
||||
`).all()
|
||||
for (const account of accounts) {
|
||||
syncFromBillmanager(account, db, { skipVpsPayments: true }).catch((err) => {
|
||||
console.warn(`Sync tariffs failed for account ${account.id}:`, err.message)
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Scheduled sync tariffs error:', err.message)
|
||||
}
|
||||
}
|
||||
|
||||
export function startScheduler() {
|
||||
if (syncIntervalId) clearInterval(syncIntervalId)
|
||||
syncIntervalId = null
|
||||
if (syncTariffsIntervalId) clearInterval(syncTariffsIntervalId)
|
||||
syncTariffsIntervalId = null
|
||||
try {
|
||||
const db = getDb()
|
||||
const settings = db.prepare('SELECT * FROM settings WHERE id = ?').get('settings-main')
|
||||
if (!settings?.syncEnabled) return
|
||||
const interval = Math.max(15, Number(settings.syncIntervalMinutes) || 60)
|
||||
const tariffsInterval = Math.max(60, Number(settings.syncTariffsIntervalMinutes) || 1440)
|
||||
syncIntervalId = setInterval(runScheduledSync, interval * 60 * 1000)
|
||||
console.log(`Scheduled sync enabled: every ${interval} min`)
|
||||
syncTariffsIntervalId = setInterval(runScheduledSyncTariffs, tariffsInterval * 60 * 1000)
|
||||
console.log(`Scheduled sync enabled: VPS/payments every ${interval} min, tariffs every ${tariffsInterval} min`)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user