feat(web): доработка UX/UI — дашборд, навигация и data foundation
Docker / build (push) Has been cancelled

Добавлены syncLog в snapshot, API статистики дашборда и маппинг цен тарифов; переработаны shell, главная страница, empty states и новые экраны журнала синка и проектов.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-28 00:27:09 +07:00
co-authored by Cursor
parent 9df91bf2cc
commit 1e291b3759
35 changed files with 1527 additions and 264 deletions
+4
View File
@@ -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,
}
+49
View File
@@ -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)
},
}
+25 -2
View File
@@ -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,
}
}