Files
DenozordecandCursor 6fbd1a9113 refactor(repo): переход на pnpm monorepo с shadcn/ui и Fastify+Drizzle
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>
2026-06-26 13:42:05 +07:00

305 lines
10 KiB
JavaScript

import { Router } from 'express'
import { getDb } from '../db.js'
import { resolveOrCreateProject } from '../projects-service.js'
const router = Router()
function projectColumnsForSave(db, projectInput) {
const resolved = resolveOrCreateProject(db, projectInput)
if (!resolved.id) {
return { project: '', projectId: '' }
}
return { project: resolved.name, projectId: resolved.id }
}
export function rowToVps(row) {
if (!row) return null
let additionalIps = []
try {
additionalIps = row.additionalIps ? JSON.parse(row.additionalIps) : []
} catch {
additionalIps = []
}
let userOverrides = []
try {
userOverrides = row.userOverrides ? JSON.parse(row.userOverrides) : []
} catch {
userOverrides = []
}
return {
...row,
additionalIps,
userOverrides,
projectId: row.projectId ?? '',
monitoringEnabled: Boolean(row.monitoringEnabled),
backupEnabled: Boolean(row.backupEnabled),
dailyRate: row.dailyRate != null ? row.dailyRate : '',
monthlyRate: row.monthlyRate != null ? row.monthlyRate : '',
}
}
router.get('/', (req, res) => {
try {
const db = getDb()
const rows = db.prepare('SELECT * FROM vps ORDER BY createdAt DESC').all()
res.json(rows.map(rowToVps))
} catch (err) {
res.status(500).json({ error: err.message })
}
})
router.post('/', (req, res) => {
try {
const db = getDb()
const r = req.body
const id = r.id || `vps-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
const additionalIps = Array.isArray(r.additionalIps) ? JSON.stringify(r.additionalIps) : '[]'
const dailyRate = r.dailyRate === '' || r.dailyRate == null ? null : Number(r.dailyRate)
const monthlyRate = r.monthlyRate === '' || r.monthlyRate == null ? null : Number(r.monthlyRate)
const { project, projectId } = projectColumnsForSave(db, r.project)
db.prepare(`
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, projectId, monitoringEnabled, backupEnabled, status, tariffType,
currency, dailyRate, monthlyRate, createdAt, paidUntil, notes, userOverrides
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
r.ip ?? '',
r.ipv6 ?? '',
additionalIps,
r.dns ?? '',
r.providerId ?? '',
r.providerAccountId ?? '',
r.country ?? '',
r.city ?? '',
r.datacenter ?? '',
r.os ?? '',
r.vcpu ?? 0,
r.ramGb ?? 0,
r.diskGb ?? 0,
r.diskType ?? '',
r.virtualization ?? '',
r.bandwidthTb ?? 0,
r.sshPort ?? 22,
r.rootUser ?? '',
r.purpose ?? '',
r.environment ?? '',
project,
projectId || null,
r.monitoringEnabled ? 1 : 0,
r.backupEnabled ? 1 : 0,
r.status ?? 'active',
r.tariffType ?? '',
r.currency ?? '',
dailyRate,
monthlyRate,
r.createdAt ?? new Date().toISOString().slice(0, 10),
r.paidUntil ?? '',
r.notes ?? '',
r.userOverrides ? (Array.isArray(r.userOverrides) ? JSON.stringify(r.userOverrides) : r.userOverrides) : '[]',
)
const row = db.prepare('SELECT * FROM vps WHERE id = ?').get(id)
res.status(201).json(rowToVps(row))
} catch (err) {
res.status(500).json({ error: err.message })
}
})
const USER_OVERRIDABLE_FIELDS = ['country', 'city', 'datacenter', 'os', 'vcpu', 'ramGb', 'diskGb', 'diskType', 'virtualization', 'purpose', 'environment', 'project', 'notes', 'sshPort', 'rootUser', 'bandwidthTb', 'monitoringEnabled', 'backupEnabled']
router.put('/:id', (req, res) => {
try {
const db = getDb()
const { id } = req.params
const r = req.body
const existing = db.prepare('SELECT * FROM vps WHERE id = ?').get(id)
if (!existing) return res.status(404).json({ error: 'Not found' })
let userOverrides = []
try {
userOverrides = existing.userOverrides ? JSON.parse(existing.userOverrides) : []
} catch {
userOverrides = []
}
const clearOverrides =
r.userOverrides === 'clear' || (Array.isArray(r.userOverrides) && r.userOverrides.length === 0)
if (clearOverrides) {
userOverrides = []
}
const additionalIps = Array.isArray(r.additionalIps) ? JSON.stringify(r.additionalIps) : '[]'
const dailyRate = r.dailyRate === '' || r.dailyRate == null ? null : Number(r.dailyRate)
const monthlyRate = r.monthlyRate === '' || r.monthlyRate == null ? null : Number(r.monthlyRate)
let projectOut = existing.project ?? ''
let projectIdOut = existing.projectId ?? ''
if (r.project !== undefined) {
const resolved = projectColumnsForSave(db, r.project)
projectOut = resolved.project
projectIdOut = resolved.projectId
} else if (r.projectId !== undefined) {
if (!r.projectId) {
projectOut = ''
projectIdOut = ''
} else {
const prow = db.prepare('SELECT name FROM server_projects WHERE id = ?').get(r.projectId)
projectOut = prow?.name ?? ''
projectIdOut = r.projectId
}
}
if (!clearOverrides) {
for (const f of USER_OVERRIDABLE_FIELDS) {
if (f === 'project') {
const projectChanged =
String(projectOut ?? '') !== String(existing.project ?? '') ||
String(projectIdOut ?? '') !== String(existing.projectId ?? '')
if (projectChanged && !userOverrides.includes('project')) {
userOverrides.push('project')
}
continue
}
const newVal = r[f]
const oldVal = existing[f]
const changed = String(newVal ?? '') !== String(oldVal ?? '')
if (changed && !userOverrides.includes(f)) {
userOverrides.push(f)
}
}
}
const userOverridesJson = JSON.stringify([...new Set(userOverrides)])
db.prepare(`
UPDATE vps SET
ip = ?, ipv6 = ?, additionalIps = ?, dns = ?, providerId = ?, providerAccountId = ?,
country = ?, city = ?, datacenter = ?, os = ?, vcpu = ?, ramGb = ?, diskGb = ?, diskType = ?,
virtualization = ?, bandwidthTb = ?, sshPort = ?, rootUser = ?, purpose = ?, environment = ?,
project = ?, projectId = ?, monitoringEnabled = ?, backupEnabled = ?, status = ?, tariffType = ?,
currency = ?, dailyRate = ?, monthlyRate = ?, createdAt = ?, paidUntil = ?, notes = ?,
userOverrides = ?
WHERE id = ?
`).run(
r.ip ?? '',
r.ipv6 ?? '',
additionalIps,
r.dns ?? '',
r.providerId ?? '',
r.providerAccountId ?? '',
r.country ?? '',
r.city ?? '',
r.datacenter ?? '',
r.os ?? '',
r.vcpu ?? 0,
r.ramGb ?? 0,
r.diskGb ?? 0,
r.diskType ?? '',
r.virtualization ?? '',
r.bandwidthTb ?? 0,
r.sshPort ?? 22,
r.rootUser ?? '',
r.purpose ?? '',
r.environment ?? '',
projectOut,
projectIdOut || null,
r.monitoringEnabled ? 1 : 0,
r.backupEnabled ? 1 : 0,
r.status ?? 'active',
r.tariffType ?? '',
r.currency ?? '',
dailyRate,
monthlyRate,
r.createdAt ?? '',
r.paidUntil ?? '',
r.notes ?? '',
userOverridesJson,
id,
)
const row = db.prepare('SELECT * FROM vps WHERE id = ?').get(id)
if (!row) return res.status(404).json({ error: 'Not found' })
res.json(rowToVps(row))
} catch (err) {
res.status(500).json({ error: err.message })
}
})
router.delete('/:id', (req, res) => {
try {
const db = getDb()
const { id } = req.params
const result = db.prepare('DELETE FROM vps WHERE id = ?').run(id)
if (result.changes === 0) return res.status(404).json({ error: 'Not found' })
res.status(204).send()
} catch (err) {
res.status(500).json({ error: err.message })
}
})
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 })
}
if (action === 'project') {
const projectValue = value == null ? '' : String(value)
const { project: projName, projectId: projId } = projectColumnsForSave(db, projectValue)
const getStmt = db.prepare('SELECT * FROM vps WHERE id = ?')
const updStmt = db.prepare(
'UPDATE vps SET project = ?, projectId = ?, userOverrides = ? WHERE id = ?',
)
let updated = 0
for (const id of ids) {
const existing = getStmt.get(id)
if (!existing) continue
if (
String(existing.project ?? '') === projName &&
String(existing.projectId ?? '') === String(projId ?? '')
) {
continue
}
let userOverrides = []
try {
userOverrides = existing.userOverrides ? JSON.parse(existing.userOverrides) : []
} catch {
userOverrides = []
}
if (!userOverrides.includes('project')) {
userOverrides.push('project')
}
updStmt.run(projName, projId || null, JSON.stringify([...new Set(userOverrides)]), id)
updated++
}
return res.json({ updated, project: projName, projectId: projId })
}
return res.status(400).json({ error: 'action must be status, delete, or project' })
} catch (err) {
res.status(500).json({ error: err.message })
}
})
export default router