Add project management features and enhance database schema
- Introduced a new `server_projects` table to manage project details. - Updated the `vps` table to include a foreign key reference to `server_projects`. - Enhanced the API to support project-related data retrieval and manipulation. - Implemented project filtering in the Reports and VPS pages. - Added project selection functionality in the UI for better resource management. - Improved data seeding and migration scripts to accommodate new project structure.
This commit is contained in:
@@ -36,8 +36,8 @@ export async function syncFromBillmanager(account, db, opts = {}) {
|
||||
|
||||
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 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, projectId, monitoringEnabled, backupEnabled, status, tariffType, currency, dailyRate, monthlyRate, createdAt, paidUntil, notes, userOverrides)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
const vpsUpdateSql = `UPDATE vps SET ip=?, ipv6=?, additionalIps=?, dns=?, country=?, city=?, datacenter=?, os=?, status=?, tariffType=?, currency=?, dailyRate=?, monthlyRate=?, paidUntil=?, notes=?
|
||||
WHERE id=?`
|
||||
|
||||
@@ -124,6 +124,7 @@ export async function syncFromBillmanager(account, db, opts = {}) {
|
||||
vps.purpose,
|
||||
vps.environment,
|
||||
vps.project,
|
||||
null,
|
||||
vps.monitoringEnabled ? 1 : 0,
|
||||
vps.backupEnabled ? 1 : 0,
|
||||
vps.status,
|
||||
@@ -134,6 +135,7 @@ export async function syncFromBillmanager(account, db, opts = {}) {
|
||||
vps.createdAt,
|
||||
paidUntil,
|
||||
notes,
|
||||
'[]',
|
||||
)
|
||||
}
|
||||
vpsCount++
|
||||
|
||||
@@ -51,6 +51,9 @@ export async function initDb() {
|
||||
}
|
||||
|
||||
dbInstance = db
|
||||
if (existsSync(DB_PATH)) {
|
||||
saveDb()
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Database migrations — add columns to existing tables
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
export const MIGRATIONS = [
|
||||
{
|
||||
name: 'provider_accounts_api',
|
||||
@@ -173,4 +175,60 @@ export const MIGRATIONS = [
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'server_projects',
|
||||
run(db) {
|
||||
db.run(
|
||||
`CREATE TABLE IF NOT EXISTS server_projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT,
|
||||
sortOrder INTEGER DEFAULT 0,
|
||||
notes TEXT,
|
||||
createdAt TEXT
|
||||
)`,
|
||||
)
|
||||
try {
|
||||
db.run('ALTER TABLE vps ADD COLUMN projectId TEXT')
|
||||
} catch (e) {
|
||||
if (!String(e.message || e).includes('duplicate column')) throw e
|
||||
}
|
||||
|
||||
const distinctStmt = db.prepare(
|
||||
`SELECT DISTINCT trim(project) AS n FROM vps WHERE length(trim(COALESCE(project, ''))) > 0`,
|
||||
)
|
||||
const seenLower = new Set()
|
||||
const findStmt = db.prepare(
|
||||
'SELECT id FROM server_projects WHERE LOWER(name) = LOWER(?) LIMIT 1',
|
||||
)
|
||||
while (distinctStmt.step()) {
|
||||
const row = distinctStmt.getAsObject()
|
||||
const t = String(row.n ?? '').trim()
|
||||
if (!t) continue
|
||||
const lk = t.toLowerCase()
|
||||
if (seenLower.has(lk)) continue
|
||||
seenLower.add(lk)
|
||||
|
||||
findStmt.bind([t])
|
||||
const exists = Boolean(findStmt.step())
|
||||
findStmt.reset()
|
||||
if (!exists) {
|
||||
const id = `proj-${randomUUID()}`
|
||||
const now = new Date().toISOString()
|
||||
db.run(
|
||||
`INSERT INTO server_projects (id, name, color, sortOrder, notes, createdAt) VALUES (?, ?, NULL, 0, NULL, ?)`,
|
||||
[id, t, now],
|
||||
)
|
||||
}
|
||||
}
|
||||
distinctStmt.free()
|
||||
findStmt.free()
|
||||
|
||||
db.run(`UPDATE vps SET projectId = (
|
||||
SELECT sp.id FROM server_projects sp
|
||||
WHERE LOWER(sp.name) = LOWER(trim(COALESCE(vps.project, '')))
|
||||
LIMIT 1
|
||||
) WHERE length(trim(COALESCE(vps.project, ''))) > 0`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
+12
-1
@@ -28,6 +28,15 @@ CREATE TABLE IF NOT EXISTS provider_accounts (
|
||||
FOREIGN KEY (providerId) REFERENCES providers(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS server_projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT,
|
||||
sortOrder INTEGER DEFAULT 0,
|
||||
notes TEXT,
|
||||
createdAt TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vps (
|
||||
id TEXT PRIMARY KEY,
|
||||
ip TEXT,
|
||||
@@ -51,6 +60,7 @@ CREATE TABLE IF NOT EXISTS vps (
|
||||
purpose TEXT,
|
||||
environment TEXT,
|
||||
project TEXT,
|
||||
projectId TEXT,
|
||||
monitoringEnabled INTEGER,
|
||||
backupEnabled INTEGER,
|
||||
status TEXT,
|
||||
@@ -63,7 +73,8 @@ CREATE TABLE IF NOT EXISTS vps (
|
||||
notes TEXT,
|
||||
userOverrides TEXT,
|
||||
FOREIGN KEY (providerId) REFERENCES providers(id),
|
||||
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id)
|
||||
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id),
|
||||
FOREIGN KEY (projectId) REFERENCES server_projects(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
|
||||
+3
-1
@@ -50,7 +50,7 @@ export function seed(db, seedDir) {
|
||||
const dailyRate = r.dailyRate === '' || r.dailyRate == null ? null : Number(r.dailyRate)
|
||||
const monthlyRate = r.monthlyRate === '' || r.monthlyRate == null ? null : Number(r.monthlyRate)
|
||||
run(
|
||||
`INSERT OR IGNORE 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
`INSERT OR IGNORE 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
r.id,
|
||||
r.ip ?? '',
|
||||
@@ -74,6 +74,7 @@ export function seed(db, seedDir) {
|
||||
r.purpose ?? '',
|
||||
r.environment ?? '',
|
||||
r.project ?? '',
|
||||
r.projectId ?? null,
|
||||
r.monitoringEnabled ? 1 : 0,
|
||||
r.backupEnabled ? 1 : 0,
|
||||
r.status ?? 'active',
|
||||
@@ -84,6 +85,7 @@ export function seed(db, seedDir) {
|
||||
r.createdAt ?? '',
|
||||
r.paidUntil ?? '',
|
||||
r.notes ?? '',
|
||||
'[]',
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import paymentsRouter from './routes/payments.js'
|
||||
import balanceLedgerRouter from './routes/balance-ledger.js'
|
||||
import settingsRouter from './routes/settings.js'
|
||||
import syncRouter from './routes/sync.js'
|
||||
import projectsRouter from './routes/projects.js'
|
||||
|
||||
const app = express()
|
||||
const PORT = process.env.PORT || 3001
|
||||
@@ -29,6 +30,7 @@ app.use(express.json())
|
||||
app.use('/api/balance-ledger', balanceLedgerRouter)
|
||||
app.use('/api/settings', settingsRouter)
|
||||
app.use('/api/sync', syncRouter)
|
||||
app.use('/api/projects', projectsRouter)
|
||||
|
||||
const { startScheduler } = await import('./sync-scheduler.js')
|
||||
startScheduler()
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Справочник проектов (пулов): поиск без учёта регистра, автосоздание, подсказки.
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* @param {unknown} name
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeProjectNameInput(name) {
|
||||
if (name == null) return ''
|
||||
return String(name).trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ReturnType<import('./db.js').getDb>} db
|
||||
* @param {string} name — уже нормализованное имя (trim)
|
||||
* @returns {{ id: string, name: string, color?: string, sortOrder?: number, notes?: string, createdAt?: string } | null}
|
||||
*/
|
||||
export function findProjectByNameCaseInsensitive(db, name) {
|
||||
const n = normalizeProjectNameInput(name)
|
||||
if (!n) return null
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT * FROM server_projects WHERE LOWER(name) = LOWER(?) LIMIT 1`,
|
||||
)
|
||||
.get(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти существующий проект или создать новую строку.
|
||||
* @param {ReturnType<import('./db.js').getDb>} db
|
||||
* @param {string} name
|
||||
* @returns {{ id: string | null, name: string }}
|
||||
*/
|
||||
export function resolveOrCreateProject(db, name) {
|
||||
const n = normalizeProjectNameInput(name)
|
||||
if (!n) return { id: null, name: '' }
|
||||
const existing = findProjectByNameCaseInsensitive(db, n)
|
||||
if (existing) {
|
||||
return { id: existing.id, name: existing.name }
|
||||
}
|
||||
const id = `proj-${randomUUID()}`
|
||||
const now = new Date().toISOString()
|
||||
db.prepare(
|
||||
`INSERT INTO server_projects (id, name, color, sortOrder, notes, createdAt) VALUES (?, ?, ?, 0, ?, ?)`,
|
||||
).run(id, n, null, null, now)
|
||||
return { id, name: n }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ReturnType<import('./db.js').getDb>} db
|
||||
* @param {string} q
|
||||
* @param {number} limit
|
||||
* @returns {{ id: string, name: string }[]}
|
||||
*/
|
||||
export function projectSuggestions(db, q, limit = 20) {
|
||||
const term = normalizeProjectNameInput(q)
|
||||
const lim = Math.min(50, Math.max(1, Number(limit) || 20))
|
||||
if (!term) {
|
||||
return db
|
||||
.prepare(`SELECT id, name FROM server_projects ORDER BY name LIMIT ?`)
|
||||
.all(lim)
|
||||
}
|
||||
const esc = term.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')
|
||||
const pattern = `%${esc.toLowerCase()}%`
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT id, name FROM server_projects WHERE LOWER(name) LIKE ? ESCAPE '\\' ORDER BY name LIMIT ?`,
|
||||
)
|
||||
.all(pattern, lim)
|
||||
}
|
||||
@@ -17,9 +17,18 @@ router.get('/', (req, res) => {
|
||||
const settingsRows = db.prepare('SELECT * FROM settings ORDER BY id').all()
|
||||
const activeTariffs = db.prepare('SELECT * FROM active_tariffs ORDER BY name').all()
|
||||
const tariffSyncOptions = db.prepare('SELECT * FROM tariff_sync_options').all()
|
||||
let serverProjects = []
|
||||
try {
|
||||
serverProjects = db
|
||||
.prepare('SELECT id, name, color, sortOrder, notes, createdAt FROM server_projects ORDER BY name')
|
||||
.all()
|
||||
} catch {
|
||||
serverProjects = []
|
||||
}
|
||||
|
||||
res.json({
|
||||
vps: vps.map(rowToVps),
|
||||
serverProjects,
|
||||
providers,
|
||||
providerAccounts: providerAccounts.map(sanitizeAccount),
|
||||
payments,
|
||||
|
||||
@@ -28,14 +28,14 @@ router.post('/', (req, res) => {
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data.vps) && data.vps.length > 0) {
|
||||
const sql = `INSERT OR REPLACE 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, userOverrides) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
const sql = `INSERT OR REPLACE 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for (const r of data.vps) {
|
||||
const v = typeof r === 'object' ? r : {}
|
||||
const additionalIps = Array.isArray(v.additionalIps) ? JSON.stringify(v.additionalIps) : '[]'
|
||||
const dailyRate = v.dailyRate === '' || v.dailyRate == null ? null : Number(v.dailyRate)
|
||||
const monthlyRate = v.monthlyRate === '' || v.monthlyRate == null ? null : Number(v.monthlyRate)
|
||||
const userOverrides = Array.isArray(v.userOverrides) ? JSON.stringify(v.userOverrides) : (v.userOverrides ?? '')
|
||||
db.run(sql, v.id ?? '', v.ip ?? '', v.ipv6 ?? '', additionalIps, v.dns ?? '', v.providerId ?? '', v.providerAccountId ?? '', v.country ?? '', v.city ?? '', v.datacenter ?? '', v.os ?? '', v.vcpu ?? 0, v.ramGb ?? 0, v.diskGb ?? 0, v.diskType ?? '', v.virtualization ?? '', v.bandwidthTb ?? 0, v.sshPort ?? 22, v.rootUser ?? '', v.purpose ?? '', v.environment ?? '', v.project ?? '', v.monitoringEnabled ? 1 : 0, v.backupEnabled ? 1 : 0, v.status ?? 'active', v.tariffType ?? '', v.currency ?? '', dailyRate, monthlyRate, v.createdAt ?? '', v.paidUntil ?? '', v.notes ?? '', userOverrides)
|
||||
const userOverrides = Array.isArray(v.userOverrides) ? JSON.stringify(v.userOverrides) : (v.userOverrides ?? '[]')
|
||||
db.run(sql, v.id ?? '', v.ip ?? '', v.ipv6 ?? '', additionalIps, v.dns ?? '', v.providerId ?? '', v.providerAccountId ?? '', v.country ?? '', v.city ?? '', v.datacenter ?? '', v.os ?? '', v.vcpu ?? 0, v.ramGb ?? 0, v.diskGb ?? 0, v.diskType ?? '', v.virtualization ?? '', v.bandwidthTb ?? 0, v.sshPort ?? 22, v.rootUser ?? '', v.purpose ?? '', v.environment ?? '', v.project ?? '', v.projectId ?? null, v.monitoringEnabled ? 1 : 0, v.backupEnabled ? 1 : 0, v.status ?? 'active', v.tariffType ?? '', v.currency ?? '', dailyRate, monthlyRate, v.createdAt ?? '', v.paidUntil ?? '', v.notes ?? '', userOverrides)
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data.payments) && data.payments.length > 0) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
import {
|
||||
normalizeProjectNameInput,
|
||||
projectSuggestions,
|
||||
resolveOrCreateProject,
|
||||
} from '../projects-service.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db
|
||||
.prepare('SELECT id, name, color, sortOrder, notes, createdAt FROM server_projects ORDER BY name')
|
||||
.all()
|
||||
res.json(rows)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/suggest', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const q = req.query.q ?? ''
|
||||
const limit = req.query.limit != null ? Number(req.query.limit) : 20
|
||||
const rows = projectSuggestions(db, q, limit)
|
||||
res.json(rows)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/resolve-or-create', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const name = req.body?.name
|
||||
const resolved = resolveOrCreateProject(db, name)
|
||||
res.json(resolved)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const name = normalizeProjectNameInput(req.body?.name)
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'name is required' })
|
||||
}
|
||||
const resolved = resolveOrCreateProject(db, name)
|
||||
res.status(201).json(resolved)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
+86
-12
@@ -1,8 +1,17 @@
|
||||
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 = []
|
||||
@@ -21,6 +30,7 @@ export function rowToVps(row) {
|
||||
...row,
|
||||
additionalIps,
|
||||
userOverrides,
|
||||
projectId: row.projectId ?? '',
|
||||
monitoringEnabled: Boolean(row.monitoringEnabled),
|
||||
backupEnabled: Boolean(row.backupEnabled),
|
||||
dailyRate: row.dailyRate != null ? row.dailyRate : '',
|
||||
@@ -46,14 +56,15 @@ router.post('/', (req, res) => {
|
||||
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, monitoringEnabled, backupEnabled, status, tariffType,
|
||||
purpose, environment, project, projectId, monitoringEnabled, backupEnabled, status, tariffType,
|
||||
currency, dailyRate, monthlyRate, createdAt, paidUntil, notes, userOverrides
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.ip ?? '',
|
||||
@@ -76,7 +87,8 @@ router.post('/', (req, res) => {
|
||||
r.rootUser ?? '',
|
||||
r.purpose ?? '',
|
||||
r.environment ?? '',
|
||||
r.project ?? '',
|
||||
project,
|
||||
projectId || null,
|
||||
r.monitoringEnabled ? 1 : 0,
|
||||
r.backupEnabled ? 1 : 0,
|
||||
r.status ?? 'active',
|
||||
@@ -112,10 +124,44 @@ router.put('/:id', (req, res) => {
|
||||
} catch {
|
||||
userOverrides = []
|
||||
}
|
||||
if (r.userOverrides === 'clear' || (Array.isArray(r.userOverrides) && r.userOverrides.length === 0)) {
|
||||
const clearOverrides =
|
||||
r.userOverrides === 'clear' || (Array.isArray(r.userOverrides) && r.userOverrides.length === 0)
|
||||
if (clearOverrides) {
|
||||
userOverrides = []
|
||||
} else {
|
||||
}
|
||||
|
||||
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 ?? '')
|
||||
@@ -126,16 +172,12 @@ router.put('/:id', (req, res) => {
|
||||
}
|
||||
const userOverridesJson = JSON.stringify([...new Set(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)
|
||||
|
||||
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 = ?, monitoringEnabled = ?, backupEnabled = ?, status = ?, tariffType = ?,
|
||||
project = ?, projectId = ?, monitoringEnabled = ?, backupEnabled = ?, status = ?, tariffType = ?,
|
||||
currency = ?, dailyRate = ?, monthlyRate = ?, createdAt = ?, paidUntil = ?, notes = ?,
|
||||
userOverrides = ?
|
||||
WHERE id = ?
|
||||
@@ -160,7 +202,8 @@ router.put('/:id', (req, res) => {
|
||||
r.rootUser ?? '',
|
||||
r.purpose ?? '',
|
||||
r.environment ?? '',
|
||||
r.project ?? '',
|
||||
projectOut,
|
||||
projectIdOut || null,
|
||||
r.monitoringEnabled ? 1 : 0,
|
||||
r.backupEnabled ? 1 : 0,
|
||||
r.status ?? 'active',
|
||||
@@ -221,7 +264,38 @@ router.patch('/bulk', (req, res) => {
|
||||
}
|
||||
return res.json({ deleted })
|
||||
}
|
||||
return res.status(400).json({ error: 'action must be status or delete' })
|
||||
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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user