feat: add instance management to dashboard
This commit is contained in:
+57
-1
@@ -1,5 +1,5 @@
|
||||
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
@@ -47,6 +47,7 @@ export async function loadConfig({ configPath, defaultConfigPath }) {
|
||||
}
|
||||
return {
|
||||
configPath: resolvedPath,
|
||||
raw: { ...raw, instances },
|
||||
server: {
|
||||
host: raw.server?.host || process.env.HOST || '0.0.0.0',
|
||||
port: Number(process.env.PORT || raw.server?.port || 8788),
|
||||
@@ -55,6 +56,61 @@ export async function loadConfig({ configPath, defaultConfigPath }) {
|
||||
}
|
||||
}
|
||||
|
||||
function serializableInstance(instance) {
|
||||
return {
|
||||
id: instance.id,
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
description: instance.description || '',
|
||||
tags: instance.tags || [],
|
||||
auth: instance.auth?.password ? { mode: 'password', password: instance.auth.password } : { mode: 'none' },
|
||||
capabilities: instance.capabilities || [],
|
||||
}
|
||||
}
|
||||
|
||||
async function persistConfig(config) {
|
||||
const raw = {
|
||||
...(config.raw || {}),
|
||||
server: config.raw?.server || config.server,
|
||||
instances: config.instances.map(serializableInstance),
|
||||
}
|
||||
await writeFile(config.configPath, `${JSON.stringify(raw, null, 2)}\n`)
|
||||
config.raw = raw
|
||||
}
|
||||
|
||||
export async function addInstanceToConfig(config, payload) {
|
||||
const next = normalizeInstance(payload, config.instances.length)
|
||||
if (config.instances.some(item => item.id === next.id)) throw new Error(`duplicate instance id: ${next.id}`)
|
||||
config.instances.push(next)
|
||||
await persistConfig(config)
|
||||
return next
|
||||
}
|
||||
|
||||
export async function updateInstanceInConfig(config, id, payload) {
|
||||
const index = config.instances.findIndex(item => item.id === id)
|
||||
if (index < 0) throw new Error(`instance not found: ${id}`)
|
||||
const current = config.instances[index]
|
||||
const merged = {
|
||||
...current,
|
||||
...payload,
|
||||
id: payload.id ? String(payload.id).trim() : current.id,
|
||||
auth: payload.auth !== undefined || payload.password !== undefined ? payload.auth || { password: payload.password || '' } : current.auth,
|
||||
}
|
||||
const next = normalizeInstance(merged, index)
|
||||
if (next.id !== id && config.instances.some(item => item.id === next.id)) throw new Error(`duplicate instance id: ${next.id}`)
|
||||
config.instances[index] = next
|
||||
await persistConfig(config)
|
||||
return next
|
||||
}
|
||||
|
||||
export async function deleteInstanceFromConfig(config, id) {
|
||||
const index = config.instances.findIndex(item => item.id === id)
|
||||
if (index < 0) throw new Error(`instance not found: ${id}`)
|
||||
const [removed] = config.instances.splice(index, 1)
|
||||
await persistConfig(config)
|
||||
return removed
|
||||
}
|
||||
|
||||
export function redactInstance(instance) {
|
||||
return {
|
||||
id: instance.id,
|
||||
|
||||
@@ -3,11 +3,15 @@ import fastifyStatic from '@fastify/static'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import {
|
||||
addInstanceToConfig,
|
||||
buildClients,
|
||||
collectInstanceStatus,
|
||||
createSimAdminClient,
|
||||
deleteInstanceFromConfig,
|
||||
loadConfig,
|
||||
proxyToInstance,
|
||||
redactInstance,
|
||||
updateInstanceInConfig,
|
||||
} from './core.js'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
@@ -29,6 +33,16 @@ function clientById(id, reply) {
|
||||
return client
|
||||
}
|
||||
|
||||
function sendConfigError(reply, error) {
|
||||
const message = String(error?.message || error)
|
||||
const status = /not found/.test(message) ? 404 : /duplicate|required|invalid|URL/.test(message) ? 400 : 500
|
||||
return reply.code(status).send({ error: message })
|
||||
}
|
||||
|
||||
function refreshClientFor(instance) {
|
||||
clients.set(instance.id, createSimAdminClient(instance))
|
||||
}
|
||||
|
||||
app.get('/api/config', async () => ({
|
||||
configPath: config.configPath,
|
||||
instances: config.instances.map(redactInstance),
|
||||
@@ -43,6 +57,38 @@ app.get('/api/config', async () => ({
|
||||
|
||||
app.get('/api/instances', async () => ({ instances: config.instances.map(redactInstance) }))
|
||||
|
||||
app.post('/api/instances', async (request, reply) => {
|
||||
try {
|
||||
const instance = await addInstanceToConfig(config, request.body || {})
|
||||
refreshClientFor(instance)
|
||||
return { instance: redactInstance(instance), configPath: config.configPath }
|
||||
} catch (error) {
|
||||
return sendConfigError(reply, error)
|
||||
}
|
||||
})
|
||||
|
||||
app.put('/api/instances/:id', async (request, reply) => {
|
||||
try {
|
||||
const oldId = request.params.id
|
||||
const instance = await updateInstanceInConfig(config, oldId, request.body || {})
|
||||
if (instance.id !== oldId) clients.delete(oldId)
|
||||
refreshClientFor(instance)
|
||||
return { instance: redactInstance(instance), configPath: config.configPath }
|
||||
} catch (error) {
|
||||
return sendConfigError(reply, error)
|
||||
}
|
||||
})
|
||||
|
||||
app.delete('/api/instances/:id', async (request, reply) => {
|
||||
try {
|
||||
const removed = await deleteInstanceFromConfig(config, request.params.id)
|
||||
clients.delete(removed.id)
|
||||
return { removed: redactInstance(removed), configPath: config.configPath }
|
||||
} catch (error) {
|
||||
return sendConfigError(reply, error)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/status', async () => ({
|
||||
instances: await Promise.all([...clients.values()].map(collectInstanceStatus)),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user