feat(web): доработка UX/UI — дашборд, навигация и data foundation
Docker / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Добавлены syncLog в snapshot, API статистики дашборда и маппинг цен тарифов; переработаны shell, главная страница, empty states и новые экраны журнала синка и проектов. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -6,6 +6,7 @@ import { balanceLedgerRepository } from './balance-ledger.js'
|
||||
import { settingsRepository } from './settings.js'
|
||||
import { activeTariffsRepository, tariffSyncOptionsRepository } from './tariffs.js'
|
||||
import { projectsRepository } from './projects.js'
|
||||
import { syncLogRepository } from './sync-log.js'
|
||||
|
||||
export interface Snapshot {
|
||||
vps: ReturnType<typeof vpsRepository.list>
|
||||
@@ -17,6 +18,7 @@ export interface Snapshot {
|
||||
settings: ReturnType<typeof settingsRepository.list>
|
||||
activeTariffs: ReturnType<typeof activeTariffsRepository.list>
|
||||
tariffSyncOptions: ReturnType<typeof tariffSyncOptionsRepository.list>
|
||||
syncLog: ReturnType<typeof syncLogRepository.listRecent>
|
||||
}
|
||||
|
||||
export function getSnapshot(): Snapshot {
|
||||
@@ -30,6 +32,7 @@ export function getSnapshot(): Snapshot {
|
||||
settings: settingsRepository.list(),
|
||||
activeTariffs: activeTariffsRepository.list(),
|
||||
tariffSyncOptions: tariffSyncOptionsRepository.list(),
|
||||
syncLog: syncLogRepository.listRecent(50),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,4 +46,5 @@ export {
|
||||
activeTariffsRepository,
|
||||
tariffSyncOptionsRepository,
|
||||
projectsRepository,
|
||||
syncLogRepository,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { desc } from 'drizzle-orm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
|
||||
export interface SyncLogDto {
|
||||
id: string
|
||||
accountId: string
|
||||
status: 'ok' | 'error' | 'running' | string | null
|
||||
startedAt: string
|
||||
finishedAt: string | null
|
||||
vpsCount: number | null
|
||||
paymentsCount: number | null
|
||||
error: string | null
|
||||
summary: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
function toDto(row: typeof schema.syncLog.$inferSelect): SyncLogDto {
|
||||
let summary: Record<string, unknown> | null = null
|
||||
if (row.summary) {
|
||||
try {
|
||||
const parsed = JSON.parse(row.summary) as unknown
|
||||
summary = parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : null
|
||||
} catch {
|
||||
summary = null
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
accountId: row.accountId,
|
||||
status: row.status,
|
||||
startedAt: row.startedAt,
|
||||
finishedAt: row.finishedAt,
|
||||
vpsCount: row.vpsCount,
|
||||
paymentsCount: row.paymentsCount,
|
||||
error: row.error,
|
||||
summary,
|
||||
}
|
||||
}
|
||||
|
||||
export const syncLogRepository = {
|
||||
listRecent(limit = 50): SyncLogDto[] {
|
||||
const rows = getDb()
|
||||
.select()
|
||||
.from(schema.syncLog)
|
||||
.orderBy(desc(schema.syncLog.startedAt))
|
||||
.limit(limit)
|
||||
.all()
|
||||
return rows.map(toDto)
|
||||
},
|
||||
}
|
||||
@@ -3,17 +3,40 @@ import { getDb, schema } from '../index.js'
|
||||
|
||||
type Row = typeof schema.activeTariffs.$inferSelect
|
||||
|
||||
export type ActiveTariffDto = Omit<Row, 'orderAvailable' | 'ramGb'> & {
|
||||
export type ActiveTariffDto = Omit<Row, 'orderAvailable' | 'ramGb' | 'price'> & {
|
||||
orderAvailable: boolean
|
||||
ramGb: number
|
||||
monthlyRate: number | null
|
||||
currency: string | null
|
||||
}
|
||||
|
||||
/** Парсит строку цены BILLmanager: «100.50 RUB», «€12», «12 USD». */
|
||||
export function parseTariffPrice(price: string | null | undefined): {
|
||||
monthlyRate: number | null
|
||||
currency: string | null
|
||||
} {
|
||||
const raw = String(price ?? '').trim()
|
||||
if (!raw) return { monthlyRate: null, currency: null }
|
||||
const match = raw.match(/([\d.,]+)\s*([A-Za-z]{3})?/)
|
||||
if (!match) return { monthlyRate: null, currency: null }
|
||||
const monthlyRate = Number.parseFloat(match[1].replace(',', '.'))
|
||||
const currency = match[2]?.toUpperCase() ?? null
|
||||
return {
|
||||
monthlyRate: Number.isFinite(monthlyRate) ? monthlyRate : null,
|
||||
currency,
|
||||
}
|
||||
}
|
||||
|
||||
function toDto(row: Row | undefined): ActiveTariffDto | undefined {
|
||||
if (!row) return undefined
|
||||
const { monthlyRate, currency } = parseTariffPrice(row.price)
|
||||
const { price: _price, ...rest } = row
|
||||
return {
|
||||
...row,
|
||||
...rest,
|
||||
orderAvailable: Boolean(row.orderAvailable),
|
||||
ramGb: row.ramGb != null ? Number(row.ramGb) : 0,
|
||||
monthlyRate,
|
||||
currency,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user