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:
@@ -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