- Added new backup and restore functionality for JSON and SQLite database formats. - Implemented notification settings for low balance alerts and sync digests in the settings page. - Updated database schema to include new columns for balance alert thresholds and notification preferences. - Refactored synchronization logic to provide detailed summaries and improved error handling. - Enhanced user interface for managing accounts and settings, including new input fields for balance alerts. - Improved sync log display to include summary information for better tracking of synchronization results.
240 lines
6.5 KiB
JavaScript
240 lines
6.5 KiB
JavaScript
import { uid } from './utils'
|
|
|
|
const COLLECTIONS = {
|
|
vps: 'vps',
|
|
providers: 'providers',
|
|
providerAccounts: 'providerAccounts',
|
|
payments: 'payments',
|
|
balanceLedger: 'balanceLedger',
|
|
settings: 'settings',
|
|
}
|
|
|
|
const API_PATHS = {
|
|
[COLLECTIONS.vps]: '/api/vps',
|
|
[COLLECTIONS.providers]: '/api/providers',
|
|
[COLLECTIONS.providerAccounts]: '/api/provider-accounts',
|
|
[COLLECTIONS.payments]: '/api/payments',
|
|
[COLLECTIONS.balanceLedger]: '/api/balance-ledger',
|
|
[COLLECTIONS.settings]: '/api/settings',
|
|
}
|
|
|
|
const STORAGE_KEY_PREFIX = 'vps-tracker:'
|
|
|
|
const API_BASE = import.meta.env.VITE_API_URL || ''
|
|
|
|
function getLocalStorageData() {
|
|
const data = {}
|
|
for (const name of Object.values(COLLECTIONS)) {
|
|
const key = `${STORAGE_KEY_PREFIX}${name}`
|
|
const raw = localStorage.getItem(key)
|
|
if (raw) {
|
|
try {
|
|
data[name] = JSON.parse(raw)
|
|
} catch {
|
|
data[name] = []
|
|
}
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
function clearLocalStorage() {
|
|
for (const name of Object.values(COLLECTIONS)) {
|
|
localStorage.removeItem(`${STORAGE_KEY_PREFIX}${name}`)
|
|
}
|
|
}
|
|
|
|
async function fetchApi(path, options = {}) {
|
|
const url = `${API_BASE}${path.startsWith('/') ? path : `/${path}`}`
|
|
const res = await fetch(url, {
|
|
headers: { 'Content-Type': 'application/json', ...options.headers },
|
|
...options,
|
|
})
|
|
if (!res.ok) {
|
|
let message = res.statusText || 'API error'
|
|
try {
|
|
const data = await res.json()
|
|
if (data?.error) message = data.error
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
const err = new Error(message)
|
|
err.status = res.status
|
|
err.response = res
|
|
throw err
|
|
}
|
|
if (res.status === 204) return null
|
|
return res.json()
|
|
}
|
|
|
|
export async function initDataStore() {
|
|
const localData = getLocalStorageData()
|
|
const hasLocalData = Object.keys(localData).some((k) => {
|
|
const arr = localData[k]
|
|
return Array.isArray(arr) ? arr.length > 0 : arr && typeof arr === 'object'
|
|
})
|
|
|
|
if (hasLocalData) {
|
|
try {
|
|
await fetchApi('/api/migrate', {
|
|
method: 'POST',
|
|
body: JSON.stringify(localData),
|
|
})
|
|
clearLocalStorage()
|
|
} catch (err) {
|
|
console.warn('Migration from localStorage failed:', err)
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function loadDataSet() {
|
|
const data = await fetchApi('/api/data')
|
|
return {
|
|
[COLLECTIONS.vps]: data.vps ?? [],
|
|
[COLLECTIONS.providers]: data.providers ?? [],
|
|
[COLLECTIONS.providerAccounts]: data.providerAccounts ?? [],
|
|
[COLLECTIONS.payments]: data.payments ?? [],
|
|
[COLLECTIONS.balanceLedger]: data.balanceLedger ?? [],
|
|
[COLLECTIONS.settings]: data.settings ?? [],
|
|
activeTariffs: data.activeTariffs ?? [],
|
|
tariffSyncOptions: data.tariffSyncOptions ?? [],
|
|
serverProjects: data.serverProjects ?? [],
|
|
}
|
|
}
|
|
|
|
export async function fetchProjectSuggestions(q = '', limit = 25) {
|
|
const params = new URLSearchParams()
|
|
if (q) params.set('q', q)
|
|
params.set('limit', String(limit))
|
|
return fetchApi(`/api/projects/suggest?${params.toString()}`)
|
|
}
|
|
|
|
async function fetchCollection(collectionName) {
|
|
const path = API_PATHS[collectionName]
|
|
if (!path) throw new Error(`Unknown collection: ${collectionName}`)
|
|
return fetchApi(path)
|
|
}
|
|
|
|
export async function createRecord(collectionName, record) {
|
|
const path = API_PATHS[collectionName]
|
|
if (!path) throw new Error(`Unknown collection: ${collectionName}`)
|
|
const payload = { ...record, id: record.id || uid() }
|
|
await fetchApi(path, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
})
|
|
return fetchCollection(collectionName)
|
|
}
|
|
|
|
export async function updateRecord(collectionName, id, patch) {
|
|
const path = API_PATHS[collectionName]
|
|
if (!path) throw new Error(`Unknown collection: ${collectionName}`)
|
|
await fetchApi(`${path}/${encodeURIComponent(id)}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(patch),
|
|
})
|
|
return fetchCollection(collectionName)
|
|
}
|
|
|
|
export async function deleteRecord(collectionName, id) {
|
|
const path = API_PATHS[collectionName]
|
|
if (!path) throw new Error(`Unknown collection: ${collectionName}`)
|
|
await fetchApi(`${path}/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
return fetchCollection(collectionName)
|
|
}
|
|
|
|
export async function bulkUpdateVps(ids, action, value) {
|
|
return fetchApi('/api/vps/bulk', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ ids, action, value }),
|
|
})
|
|
}
|
|
|
|
export async function syncAccount(accountId, opts = {}) {
|
|
return fetchApi(`/api/sync/${encodeURIComponent(accountId)}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(opts),
|
|
})
|
|
}
|
|
|
|
export async function fetchAccountBalance(accountId) {
|
|
return fetchApi(`/api/sync/${encodeURIComponent(accountId)}/balance`, { method: 'GET' })
|
|
}
|
|
|
|
export async function testApiConnection(apiBaseUrl, apiCredentials) {
|
|
return fetchApi('/api/sync/test-connection', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ apiBaseUrl, apiCredentials }),
|
|
})
|
|
}
|
|
|
|
export async function fetchSyncStatus() {
|
|
return fetchApi('/api/sync/status')
|
|
}
|
|
|
|
export async function sendTelegramTestNotification() {
|
|
const res = await fetchApi('/api/settings/telegram/test', { method: 'POST' })
|
|
return res
|
|
}
|
|
|
|
export async function downloadBackupJsonBlob() {
|
|
const url = `${API_BASE}/api/backup/json`
|
|
const res = await fetch(url)
|
|
if (!res.ok) {
|
|
let message = res.statusText || 'Ошибка выгрузки'
|
|
try {
|
|
const data = await res.json()
|
|
if (data?.error) message = data.error
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new Error(message)
|
|
}
|
|
return res.blob()
|
|
}
|
|
|
|
export async function downloadBackupDatabaseBlob() {
|
|
const url = `${API_BASE}/api/backup/database`
|
|
const res = await fetch(url)
|
|
if (!res.ok) {
|
|
let message = res.statusText || 'Ошибка выгрузки'
|
|
try {
|
|
const data = await res.json()
|
|
if (data?.error) message = data.error
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new Error(message)
|
|
}
|
|
return res.blob()
|
|
}
|
|
|
|
export async function importBackupJson(payload) {
|
|
return fetchApi('/api/backup/json', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
})
|
|
}
|
|
|
|
export async function importBackupDatabaseBuffer(buffer) {
|
|
const url = `${API_BASE}/api/backup/database`
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/octet-stream' },
|
|
body: buffer,
|
|
})
|
|
if (!res.ok) {
|
|
let message = res.statusText || 'Ошибка восстановления'
|
|
try {
|
|
const data = await res.json()
|
|
if (data?.error) message = data.error
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
const err = new Error(message)
|
|
err.status = res.status
|
|
throw err
|
|
}
|
|
return res.json()
|
|
}
|