mirror of
https://github.com/community-scripts/ProxmoxVE-Local.git
synced 2026-08-04 19:25:11 -04:00
* feat: Add Web UI IP:Port tracking and access functionality - Add web_ui_ip and web_ui_port columns to installed_scripts table with migration - Update database CRUD methods to handle new Web UI fields - Add terminal output parsing to auto-detect Web UI URLs during installation - Create autoDetectWebUI mutation that runs hostname -I in containers via SSH - Add Web UI column to desktop table with editable IP and port fields - Add Open UI button that opens http://ip:port in new tab - Add Re-detect button for manual IP detection using script metadata - Update mobile card view with Web UI fields and buttons - Fix nested button hydration error in ContextualHelpIcon - Prioritize script metadata interface_port over existing database values - Use pct exec instead of pct enter for container command execution - Add comprehensive error handling and user feedback - Style auto-detect button with muted colors and Re-detect text Features: - Automatic Web UI detection during script installation - Manual IP detection with port lookup from script metadata - Editable IP and port fields in both desktop and mobile views - Clickable Web UI links that open in new tabs - Support for both local and SSH script executions - Proper port detection from script JSON metadata (e.g., actualbudget:5006) - Clean UI with subtle button styling and clear text labels * feat: Disable Open UI button when container is stopped - Add disabled state to Open UI button in desktop table when container is stopped - Update mobile card Open UI button to be disabled when container is stopped - Apply consistent styling with Shell and Update buttons - Prevent users from accessing Web UI when container is not running - Add cursor-not-allowed styling for disabled clickable IP links * feat: Align Re-detect buttons consistently in Web UI column - Change flex layout from space-x-2 to justify-between for consistent button alignment - Add flex-shrink-0 to prevent IP:port text and buttons from shrinking - Add ml-2 margin to Re-detect button for proper spacing - Apply changes to both desktop table and mobile card views - Buttons now align vertically regardless of IP:port text length * feat: Add actions dropdown menu with conditional Start/Stop colors and update help - Create dropdown-menu.tsx component using Radix UI primitives - Move all action buttons except Edit into dropdown menu - Keep Edit and Save/Cancel buttons always visible - Add conditional styling: Start (green), Stop (red) - Apply changes to both desktop table and mobile card views - Add smart visibility - dropdown only shows when actions available - Auto-close dropdown after clicking any action - Style dropdown to match existing button theme - Fix syntax error in dropdown-menu.tsx component - Update help section with Web UI Access and Actions Dropdown documentation - Add detailed explanations of auto-detection, IP/port tracking, and color coding * Fix TypeScript build error in server.js - Updated parseWebUIUrl JSDoc return type from Object|null to {ip: string, port: number}|null - This fixes the TypeScript error where 'ip' property was not recognized on type 'Object' - Build now completes successfully without errors
337 lines
9.5 KiB
JavaScript
337 lines
9.5 KiB
JavaScript
import Database from 'better-sqlite3';
|
|
import { join } from 'path';
|
|
|
|
class DatabaseService {
|
|
constructor() {
|
|
const dbPath = join(process.cwd(), 'data', 'settings.db');
|
|
this.db = new Database(dbPath);
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Create servers table if it doesn't exist
|
|
this.db.exec(`
|
|
CREATE TABLE IF NOT EXISTS servers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
ip TEXT NOT NULL,
|
|
user TEXT NOT NULL,
|
|
password TEXT,
|
|
auth_type TEXT DEFAULT 'password' CHECK(auth_type IN ('password', 'key', 'both')),
|
|
ssh_key TEXT,
|
|
ssh_key_passphrase TEXT,
|
|
ssh_port INTEGER DEFAULT 22,
|
|
color TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
// Migration: Add new columns to existing servers table
|
|
try {
|
|
this.db.exec(`
|
|
ALTER TABLE servers ADD COLUMN auth_type TEXT DEFAULT 'password' CHECK(auth_type IN ('password', 'key', 'both'))
|
|
`);
|
|
} catch (e) {
|
|
// Column already exists, ignore error
|
|
}
|
|
|
|
try {
|
|
this.db.exec(`
|
|
ALTER TABLE servers ADD COLUMN ssh_key TEXT
|
|
`);
|
|
} catch (e) {
|
|
// Column already exists, ignore error
|
|
}
|
|
|
|
try {
|
|
this.db.exec(`
|
|
ALTER TABLE servers ADD COLUMN ssh_key_passphrase TEXT
|
|
`);
|
|
} catch (e) {
|
|
// Column already exists, ignore error
|
|
}
|
|
|
|
try {
|
|
this.db.exec(`
|
|
ALTER TABLE servers ADD COLUMN ssh_port INTEGER DEFAULT 22
|
|
`);
|
|
} catch (e) {
|
|
// Column already exists, ignore error
|
|
}
|
|
|
|
try {
|
|
this.db.exec(`
|
|
ALTER TABLE servers ADD COLUMN color TEXT
|
|
`);
|
|
} catch (e) {
|
|
// Column already exists, ignore error
|
|
}
|
|
|
|
// Update existing servers to have auth_type='password' if not set
|
|
this.db.exec(`
|
|
UPDATE servers SET auth_type = 'password' WHERE auth_type IS NULL
|
|
`);
|
|
|
|
// Update existing servers to have ssh_port=22 if not set
|
|
this.db.exec(`
|
|
UPDATE servers SET ssh_port = 22 WHERE ssh_port IS NULL
|
|
`);
|
|
|
|
// Migration: Add web_ui_ip column to existing installed_scripts table
|
|
try {
|
|
this.db.exec(`
|
|
ALTER TABLE installed_scripts ADD COLUMN web_ui_ip TEXT
|
|
`);
|
|
} catch (e) {
|
|
// Column already exists, ignore error
|
|
}
|
|
|
|
// Migration: Add web_ui_port column to existing installed_scripts table
|
|
try {
|
|
this.db.exec(`
|
|
ALTER TABLE installed_scripts ADD COLUMN web_ui_port INTEGER
|
|
`);
|
|
} catch (e) {
|
|
// Column already exists, ignore error
|
|
}
|
|
|
|
// Create installed_scripts table if it doesn't exist
|
|
this.db.exec(`
|
|
CREATE TABLE IF NOT EXISTS installed_scripts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
script_name TEXT NOT NULL,
|
|
script_path TEXT NOT NULL,
|
|
container_id TEXT,
|
|
server_id INTEGER,
|
|
execution_mode TEXT NOT NULL CHECK(execution_mode IN ('local', 'ssh')),
|
|
installation_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
status TEXT NOT NULL CHECK(status IN ('in_progress', 'success', 'failed')),
|
|
output_log TEXT,
|
|
web_ui_ip TEXT,
|
|
web_ui_port INTEGER,
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE SET NULL
|
|
)
|
|
`);
|
|
|
|
// Create trigger to update updated_at on row update
|
|
this.db.exec(`
|
|
CREATE TRIGGER IF NOT EXISTS update_servers_timestamp
|
|
AFTER UPDATE ON servers
|
|
BEGIN
|
|
UPDATE servers SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|
END
|
|
`);
|
|
}
|
|
|
|
// Server CRUD operations
|
|
/**
|
|
* @param {import('../types/server').CreateServerData} serverData
|
|
*/
|
|
createServer(serverData) {
|
|
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color } = serverData;
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO servers (name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
return stmt.run(name, ip, user, password, auth_type || 'password', ssh_key, ssh_key_passphrase, ssh_port || 22, color);
|
|
}
|
|
|
|
getAllServers() {
|
|
const stmt = this.db.prepare('SELECT * FROM servers ORDER BY created_at DESC');
|
|
return stmt.all();
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
*/
|
|
getServerById(id) {
|
|
const stmt = this.db.prepare('SELECT * FROM servers WHERE id = ?');
|
|
return stmt.get(id);
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
* @param {import('../types/server').CreateServerData} serverData
|
|
*/
|
|
updateServer(id, serverData) {
|
|
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color } = serverData;
|
|
const stmt = this.db.prepare(`
|
|
UPDATE servers
|
|
SET name = ?, ip = ?, user = ?, password = ?, auth_type = ?, ssh_key = ?, ssh_key_passphrase = ?, ssh_port = ?, color = ?
|
|
WHERE id = ?
|
|
`);
|
|
return stmt.run(name, ip, user, password, auth_type || 'password', ssh_key, ssh_key_passphrase, ssh_port || 22, color, id);
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
*/
|
|
deleteServer(id) {
|
|
const stmt = this.db.prepare('DELETE FROM servers WHERE id = ?');
|
|
return stmt.run(id);
|
|
}
|
|
|
|
// Installed Scripts CRUD operations
|
|
/**
|
|
* @param {Object} scriptData
|
|
* @param {string} scriptData.script_name
|
|
* @param {string} scriptData.script_path
|
|
* @param {string} [scriptData.container_id]
|
|
* @param {number} [scriptData.server_id]
|
|
* @param {string} scriptData.execution_mode
|
|
* @param {string} scriptData.status
|
|
* @param {string} [scriptData.output_log]
|
|
* @param {string} [scriptData.web_ui_ip]
|
|
* @param {number} [scriptData.web_ui_port]
|
|
*/
|
|
createInstalledScript(scriptData) {
|
|
const { script_name, script_path, container_id, server_id, execution_mode, status, output_log, web_ui_ip, web_ui_port } = scriptData;
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO installed_scripts (script_name, script_path, container_id, server_id, execution_mode, status, output_log, web_ui_ip, web_ui_port)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
return stmt.run(script_name, script_path, container_id || null, server_id || null, execution_mode, status, output_log || null, web_ui_ip || null, web_ui_port || null);
|
|
}
|
|
|
|
getAllInstalledScripts() {
|
|
const stmt = this.db.prepare(`
|
|
SELECT
|
|
inst.*,
|
|
s.name as server_name,
|
|
s.ip as server_ip,
|
|
s.user as server_user,
|
|
s.password as server_password,
|
|
s.auth_type as server_auth_type,
|
|
s.ssh_key as server_ssh_key,
|
|
s.ssh_key_passphrase as server_ssh_key_passphrase,
|
|
s.ssh_port as server_ssh_port,
|
|
s.color as server_color
|
|
FROM installed_scripts inst
|
|
LEFT JOIN servers s ON inst.server_id = s.id
|
|
ORDER BY inst.installation_date DESC
|
|
`);
|
|
return stmt.all();
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
*/
|
|
getInstalledScriptById(id) {
|
|
const stmt = this.db.prepare(`
|
|
SELECT
|
|
inst.*,
|
|
s.name as server_name,
|
|
s.ip as server_ip
|
|
FROM installed_scripts inst
|
|
LEFT JOIN servers s ON inst.server_id = s.id
|
|
WHERE inst.id = ?
|
|
`);
|
|
return stmt.get(id);
|
|
}
|
|
|
|
/**
|
|
* @param {number} server_id
|
|
*/
|
|
getInstalledScriptsByServer(server_id) {
|
|
const stmt = this.db.prepare(`
|
|
SELECT
|
|
inst.*,
|
|
s.name as server_name,
|
|
s.ip as server_ip
|
|
FROM installed_scripts inst
|
|
LEFT JOIN servers s ON inst.server_id = s.id
|
|
WHERE inst.server_id = ?
|
|
ORDER BY inst.installation_date DESC
|
|
`);
|
|
return stmt.all(server_id);
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
* @param {Object} updateData
|
|
* @param {string} [updateData.script_name]
|
|
* @param {string} [updateData.container_id]
|
|
* @param {string} [updateData.status]
|
|
* @param {string} [updateData.output_log]
|
|
* @param {string} [updateData.web_ui_ip]
|
|
* @param {number} [updateData.web_ui_port]
|
|
*/
|
|
updateInstalledScript(id, updateData) {
|
|
const { script_name, container_id, status, output_log, web_ui_ip, web_ui_port } = updateData;
|
|
const updates = [];
|
|
const values = [];
|
|
|
|
if (script_name !== undefined) {
|
|
updates.push('script_name = ?');
|
|
values.push(script_name);
|
|
}
|
|
if (container_id !== undefined) {
|
|
updates.push('container_id = ?');
|
|
values.push(container_id);
|
|
}
|
|
if (status !== undefined) {
|
|
updates.push('status = ?');
|
|
values.push(status);
|
|
}
|
|
if (output_log !== undefined) {
|
|
updates.push('output_log = ?');
|
|
values.push(output_log);
|
|
}
|
|
if (web_ui_ip !== undefined) {
|
|
updates.push('web_ui_ip = ?');
|
|
values.push(web_ui_ip);
|
|
}
|
|
if (web_ui_port !== undefined) {
|
|
updates.push('web_ui_port = ?');
|
|
values.push(web_ui_port);
|
|
}
|
|
|
|
if (updates.length === 0) {
|
|
return { changes: 0 };
|
|
}
|
|
|
|
values.push(id);
|
|
const stmt = this.db.prepare(`
|
|
UPDATE installed_scripts
|
|
SET ${updates.join(', ')}
|
|
WHERE id = ?
|
|
`);
|
|
return stmt.run(...values);
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
*/
|
|
deleteInstalledScript(id) {
|
|
const stmt = this.db.prepare('DELETE FROM installed_scripts WHERE id = ?');
|
|
return stmt.run(id);
|
|
}
|
|
|
|
/**
|
|
* @param {number} server_id
|
|
*/
|
|
deleteInstalledScriptsByServer(server_id) {
|
|
const stmt = this.db.prepare('DELETE FROM installed_scripts WHERE server_id = ?');
|
|
return stmt.run(server_id);
|
|
}
|
|
|
|
close() {
|
|
this.db.close();
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
/** @type {DatabaseService | null} */
|
|
let dbInstance = null;
|
|
|
|
export function getDatabase() {
|
|
if (!dbInstance) {
|
|
dbInstance = new DatabaseService();
|
|
}
|
|
return dbInstance;
|
|
}
|
|
|
|
export default DatabaseService;
|
|
|